SysJam Powershell Right Click Tool – Part 3 Doing more than 1 thing at once more – Powershell Runspaces and WMI Eventing

I’ll be the first to admit…this is an advanced concept post.  If you think Powershell is cool and you want to learn some pretty crazy things, keep reading.  This post is based loosely on the Process monitoring tab of the SysJam Right Click tool for Configuration manager available at https://github.com/mdedeboer/SysJamRightClickToolForConfigMgr.  I say loosely because I wasn’t satisfied with how much wmi querying it took to get all the information from the remote computer all the way back to you.  I hope to get the code used here into the right click tool sometime soonish…I’m just not sure when yet.

To start, you may want to be somewhat familiar with Powershell runspaces.  I’ve done a previous post on this topic here.  If you aren’t familiar with Powershell runspaces, don’t worry I’ll try break it down so it is easier to understand.  Another concept I am using is this post is WMI alerting.  This is the key area of improvement in comparison to the SysJam Right Click tool.

The code used in this post assumes that you have a form called $form1, a button on that form called $buttonClose and a timer on the form called $timerRunSpaceStatus

Lets do some code

First, we will want to create a synchronized hashtable holding all of the variables we want to modify from multiple threads.  You could put this on the click event of a connect button, or on the loading of the script, or on the load event of the form….it is really up to you.  The only prerequisites is that the form and the computer you are connecting to must be defined before this code.


$strComputer = "ComputerToConnectTo"
#Dynamically create a datagridview within a synchronized hash table so that it is available in another workflow/pool
$hashDGProc = [hashtable]::Synchronized(@{ })
$hashDGProc.DGRunningProcs = New-Object System.Windows.Forms.DataGridView
$hashDGProc.dgRunningProcs.AutoSizeColumnsMode = "None"
$hashDGProc.dgRunningProcs.AutoSizeRowsMode = "None"
$hashDGProc.dgRunningProcs.Dock = "None"
$hashDGProc.dgRunningProcs.Location = New-Object System.Drawing.Point(18, 17)
$hashDGProc.dgRunningProcs.Margin = New-Object System.Windows.Forms.Padding(3, 3, 3, 3)
$hashDGProc.dgRunningProcs.ScrollBars = 'Both'
$hashDGProc.dgRunningProcs.ScrollBars = 'Both'
$hashDGProc.dgRunningProcs.Size = New-Object Drawing.Size(622, 309)
$hashDGProc.dgRunningProcs.ColumnCount = 7
$hashDGProc.dgRunningProcs.Columns[0].HeaderText = "Process ID"
$hashDGProc.dgRunningProcs.Columns[1].HeaderText = "Name"
$hashDGProc.dgRunningProcs.Columns[2].HeaderText = "Username"
$hashDGProc.dgRunningProcs.Columns[3].HeaderText = "CreationDate"
$hashDGProc.dgRunningProcs.Columns[4].HeaderText = "PercentCPU"
$hashDGProc.dgRunningProcs.Columns[5].HeaderText = "MemoryUsage"
$hashDGProc.dgRunningProcs.Columns[6].HeaderText = "CommandLine"
$hashDGProc.dgRunningProcs.SelectionMode = 'FullRowSelect'
$hashDGProc.labelStatus = New-Object System.Windows.Forms.Label
$hashDGProc.labelStatus.Location = New-Object System.Drawing.Point(18, 361)
$hashDGProc.labelStatus.Size = New-Object System.Drawing.Size(622, 25)
$hashDGProc.labelStatus.Margin = New-Object System.Windows.Forms.Padding(3, 0, 3, 0)
$hashDGProc.labelStatus.Visible = $true
$hashDGProc.labelStatus.Text = "Loading..."
$hashDGProc.PSSession = New-PSSession $strComputer #This allows us to kill the session easily when the form closes
$hashDGProc.Stop = [boolean]$false
$form1.Controls.Add($hashDGProc.dgRunningProcs)
$form1.Controls.Add($hashDGProc.labelStatus)

This is a decent amount of code, but it is fairly self-explainable.  Simply we are creating a datagridview object, a label, a powershell session and a Boolean variable inside of our synchronized hash table.  Keep in mind that this allows us to modify these variables from other threads.

This can be done like so:

	$hashDGProc.labelStatus.Invoke([action]{
		$hashDGProc.labelStatus.text = "Getting processes...."
	})

Simply, any property in the synchronized hashtable has an invoke method. This allows us to pause the main thread while we make changes to the contained object. Since the code we are running is really short, it runs so quickly the user doesn’t notice. If you are making multiple changes to the form at once in this Invoke method the form can appear to freeze for short periods of time. Also, if you nest Invoke methods inside each other the result is that the same thread asks for reserved time to access the object simultaneously. This results in a collision which freezes your form. So, never do this:

	$hashDGProc.labelStatus.Invoke([action]{
                $hashDGProc.labelStatus.Invoke([action]{
		        $hashDGProc.labelStatus.text = "Getting processes...."
                })
	})

Ok. Not to bad. Now before we get into the code we are using to feed our datagridview with all the processes, lets take a look how we execute the code we are going to write. You’ll want to put this code in your form load event or on a button click event as this is the code that kicks off all the code we are going to write. You could put this into a function or just create a new copy for each runspace you want.

        #Make an array to store all of the runspaces we start....this way we can check in on them later
        #If there isn't a pool of runspaces, create one
	If (!($runspacepool))
	{
		$Script:runspaces = New-Object System.Collections.ArrayList
		$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
		$runspacepool = [runspacefactory]::CreateRunspacePool(1, 10, $sessionstate, $host)
		$runspacepool.Open()
	}

	#Execute Powershell Runspace for All Processes
	$powershellRunSpace = [powershell]::Create()
        #Add the script we want to execute....we'll define $sbGetAllProcesses later in the blogpost.  In the script it will be defined earlier.  Pass the synched hashtable and the computername to the script block as arguments
	$powershellRunSpace.AddScript($sbGetAllProcesses).AddArgument($hashDGProc).AddArgument($strComputer)
	$powershellRunSpace.RunspacePool = $runspacepool

        #Create a custom object with specified properties....set the other properties
	$InstRunSpace = "" | Select-Object name, powershell, runspace, Computer, CompletedScript
	$InstRunSpace.Name = (Get-Random)
	$InstRunSpace.Computer = $strComputer
	$instRunSpace.Powershell = $powershellRunSpace
	$InstRunSpace.CompletedScript = $sbProcComplete #not being used in this example
	$InstRunSpace.RunSpace = $powershellRunSpace.BeginInvoke() #Kick off the runspace

        #Add the custom object to our array of runspaces we are keeping track of.
	$runspaces.Add($InstRunSpace) | Out-Null 

I’ve added comments to most of the code. Again, you will want to repeat this for each runspace you create. I’ll add the complete code at the end of the post.

You may notice that I am passing $sbGetAllProcesses as the script block to run inside of my runspace. I will be adding two more. Their uses are as follows:

  • $sbGetAllProcesses – Does the initial fill in of the form. Doesn’t worry about processor usage.
  • $sbGetProcessPerf – Continual refresh of the processor usage of the processes
  • $$sbGetProcessChanges- Continual refresh of new processes and ended processes (using WMI events)

Lets look at $sbGetAllProcesses first:

$sbGetAllProcesses = {
	Param ($hashDGProc, $strComputer)

	#Get the Current Running Processes

	$hashDGProc.labelStatus.Invoke([action]{
		$hashDGProc.labelStatus.text = "Getting processes...."
	})

	$arrProc = Invoke-Command -Session $hashDGProc.PSSession -ScriptBlock {
		#Function to convert WMI date to DateTime
		Function WMIDateStringToDate($crdate)
		{
			If ($crdate -match ".\d*-\d*")
			{
				$crdate = $crdate -replace $matches[0], " "
				$idate = [System.Int64]$crdate
				$date = [DateTime]::ParseExact($idate, 'yyyyMMddHHmmss', $null)
				return $date
			}
		}

		#Get all the existing Processes | For each existing process
		gwmi -class win32_process | ForEach{
			$objProcess = $_
			If ($objProcess.ProcessID -ne "0")
			{
				#Add the owner as a note property
				Try { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name UserName -Value ($objProcess.GetOwner().User) -PassThru }
				Catch [Exception] { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name UserName -Value " " }
				Finally { }

				#Add the reformated date as a note property
				Try { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value (WMIDateStringToDate($objProcess.CreationDate)) -PassThru }
				Catch [Exception] { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value " " -PassThru }
				Finally { }

				#Add the CPU Usage as a note property
				$strCPUUsage = "0" #Assume no cpu usage....this will get updated by the cpu usage thread
				$objProcess = Add-Member -MemberType NoteProperty -Name PercentCPU -Value $strCPUUsage -InputObject $objProcess -PassThru

				$objProcess #Return the custom object
			}
		} | Select-Object ProcessID, Name, Username, refCreationDate, PercentCPU, WorkingSetSize, CommandLine
	} -ErrorAction SilentlyContinue

	#Take all of the process and add them to the datagrid view
	$hashDGProc.DGRunningProcs.Invoke([action]{
		#Clear any previous rows
		$hashDGProc.DGRunningProcs.Rows.Clear()

		#For Each Process
		ForEach ($objProcess in $arrProc)
		{
			$dgIndex = $hashDGProc.dgRunningProcs.Rows.Add($objProcess.ProcessID, $objProcess.Name, $objProcess.Username, $objProcess.refCreationDate, $objProcess.PercentCPU, ($objProcess.WorkingSetSize)/1024, $objProcess.CommandLine)
		}
	})

	#Done existing processes
	$hashDGProc.labelStatus.Invoke([action]{
		$hashDGProc.labelStatus.text = " "
	})
}

Again, the code is fairly commented. I’m guessing if you are reading this post you understand powershell enough to figure it out.

Up next, processor usage for given processes…this caused me a bit of pain. I really wanted to use WMI eventing here. The truth is that the shear number of events that come through mean that WMI tends to missing eventing on some of them. Additionally, the form tends to update much to often…this can be troublesome as the gui doesn’t get enough time to refresh properly. Instead I used a loop that utilizes the Get-Counter powershell cmdlet.

$sbGetProcessPerf = {
	Param ($hashDGProc, $strComputer)
	Do
	{
		Start-Sleep 5 #Give form some cycles to make the user experience better

                #Get the processor ids
		$objCounters = Invoke-Command -Session $hashDGProc.PSSession { Get-Counter '\Process(*)\ID Process' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty CounterSamples }

                #Get the CPU Usage
		$objPerCPU = Invoke-Command -Session $hashDGProc.PSSession { Get-Counter '\Process(*)\% Processor Time' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty CounterSamples }

		#Measure the total CPU usage returned by counters....since this includes the System Idle process it totals 100% of cpu usage...except when it doesn't...so we're normalizing it here (add it all up and divide by 100) this gives us a number close to the number of cores available
		$ProcCount = (($objPerCPU | Where-Object -Property Path -NotMatch "(_total)" | Measure-Object 'CookedValue' -Sum).Sum)/100

		#Commit the changes to the datagrid view
		$arrProcIDs = @() #array to keep track of all the proc IDs ....so we can remove duplicates
		$arrRowsToRemove = @() #array to keep track of rows we need to remove.  Some times duplicates can occur when a process starts at the same time as us getting the processes from wmi.  We'll get one process from the WMI alert and one process from the Win32_Process wmi query.

                #Reserve processor time for us to modify the datagridview
		$hashDGProc.DGRunningProcs.Invoke([action]{
                        #Check each row in the datagridview
			ForEach ($objRow in $hashDGProc.dgRunningProcs.Rows)
			{
				$ProcID = $objRow.Cells[0].Value #get the processid
				$arrProcIDs += $ProcID #add the process id to our array of process ids

				If ($ProcID -and ($ProcID -notin $arrProcIDs)) #if not a blank processid and not a duplicate
				{
					$objProcCounters = $objCounters | Where-Object -Property Path -NotMatch "_Total" | Where-Object -Property CookedValue -eq $ProcID

                                        #Get the Process path since the % Processor Time counter doesn't reference the process id
					ForEach ($objProcCounter in $objProcCounters) #For each should only run once...we're expecting only one counter.
					{
						Try
						{
							$ProcPath = $objProcCounter.Path.TrimEnd("\id process")
							$ProcPath = $ProcPath + "\% Processor Time"
						}
						Catch [Exception]{ }
					}
					$PercCPU = 0 #Default CPU usage

					#If there isn't a counter for the proc....check if the proc really is around
					If ($objProcCounters.Count -eq 0)
					{
						If ((!(Get-Process $ProcID)) -and ($objRow -notin $arrRowsToRemove))
						{
							$arrRowsToRemove += $objRow
						}
					}
					else
					{
						#Get the percentcpu counter and devide it by the number of cores...to normalize it
						Try { $PercCPU = (($objPerCPU | Where-Object -Property Path -eq $ProcPath).CookedValue)/$ProcCount }
						Catch [Exception]{ }
						If ($PercCPU -ne $objRow.Cells[4].Value)
						{
                                                        #If there is a change, add it to the datagridview.
							$objRow.Cells[4].Value = ([system.Math]::Round($PercCPU, 2)).ToString() #by converting it to a string, we make it sortable.
						}
					}
				}
				else
				{
                                        #If the process isn't in our array of duplicates, add it
					If ($objRow -notin $arrRowsToRemove)
					{
						$arrRowsToRemove += $objRow
					}
				}
			}

			#Remove duplicate rows
			ForEach ($objRow in $arrRowsToRemove) { $hashDGProc.DGRunningProcs.Rows.Remove($objRow) }

			#Update the sorting
			Try
			{
				$dgProcSortColumn = $hashDGProc.dgRunningProcs.sortedcolumn
				$dgProcSortOrder = $hashDGProc.dgRunningProcs.sortorder
				Switch ($dgProcSortOrder)
				{
					"Ascending" {
						$hashDGProc.dgrunningProcs.Sort($dgProcSortColumn, "Ascending")
					}
					"Descending" {
						$hashDGProc.dgrunningProcs.Sort($dgProcSortColumn, "Descending")
					}
					Default { }
				}
				$hashDGProc.dgrunningProcs.Refresh()
			}
			Catch [Exception]{ }

		})
	}While ($hashDGProc.Stop -eq $false) #Keep doing this until $hashDGProc.Stop is true. This is an object inside a synched hash table, so we can tell this thread to stop by setting this to true anywhere in the script.
}

Not to bad. Now we come to the even more fun part 🙂

WMI eventing.

Here is a really good blog series on learning WMI eventing:
http://blogs.technet.com/b/heyscriptingguy/archive/2012/06/08/an-insider-s-guide-to-using-wmi-events-and-powershell.aspx

I’ll break this into smaller chunks.

$sbGetProcessChanges = {
	Param ($hashDGProc, $strComputer)

	#Register a new WMI event trace for two types of process:
	#1. Newly started processes
	#2. Old Processes
	Invoke-Command{
		#New Processes
		Register-WmiEvent -Class win32_ProcessStartTrace -SourceIdentifier processStarted

		#Old Process
		$strDeleteQuery = "Select * from __instanceDeletionEvent within 0.5 where targetInstance isa 'win32_Process'"
		Register-WmiEvent -Query $strDeleteQuery -SourceIdentifier ProcDelete
	} -Session $hashDGProc.PSSession

As you see there are multiple types of traces you can create. Some are defined as classes, others are not. The new process event I’ve created is an example of a trace that is defined as a class. You can relatively easily see these in a wmi browser such as wbemtest. The second type I am using is a default __instanceDeletionEvent. This traces all deletions of instances in wmi. We narrow it down by setting the target instance to the win32_process class. In English, this query is simply: “Select everything from stuff that gets deleted within a 1/2 second timespan if it is coming from the Win32_process class”

Another option, which I am not using is when an item is modified. This is handled by the default __InstanceModificationEvent. Again, the query will look similar to the delete event (note: I am not using this query in the process monitor for reasons mentioned above)

$strQuery = "Select * From __InstanceModificationEvent WITHIN 0.5 Where TargetInstance ISA 'Win32_PerfFormattedData_PerfProc_Process' AND PreviousInstance.PercentProcessorTime != TargetInstance.PercentProcessorTime"

In English: “Select everything that changes within a 1/2 second timespan where the class is from the formated perfomance data class of the wmi AND only if the previous value of the percent cpu usage is different than the current cpu usage”. This gives us an alert whenever any process changes cpu usage (generating a ton of wmi alerts)

Lets keep going with the actual script:

	

	#Get the events generated by WMI while the stop button has not been pressed
	Do
	{
		#Check the pssession status
		Write-Host $hashDGProc.PSsession.State
		If ($hashDGProc.PSSession.State -notmatch "Opened")
		{
			Try { $hashDGProc.PSSession | remove-pssession }
			Catch { }
			$hashDGProc.PSSession = New-PSSession $strComputer #This allows us to kill the session easily when the form closes
		}

Every once in a while you may find your powershell session die. This can happen if the wsmprovhost process dies on the remote computer. This checks that. If the powershell session is broken…recreate it.

		#Sleep just long enough to give the gui thread a chance to refresh
		Start-Sleep 1

		#Get all the New Processes
		$newProcs = Invoke-Command{ Get-Event -SourceIdentifier processStarted -ErrorAction SilentlyContinue } -Session $hashDGProc.PSSession

                #If there are any new processes
		If ($newProcs)
		{
			$strNewProcs = $newprocs.SourceArgs.NewEvent.ProcessName
			$hashDGProc.labelStatus.Invoke([action]{
				$hashDGProc.labelStatus.text = "New Proc: $strNewProcs"
			})
		}

		#For Each new Process
		$arrNewProc = @() #Keep track of all the new processes...I like to update all of process into the datagridview all at once.
		ForEach ($newProc in $newProcs)
		{
			#Get the ProcessID from the alert
			$strProcessID = $newproc.SourceArgs.NewEvent.ProcessID

			#Go get the specific process from wmi
			$arrNewProc += Invoke-Command -Session $hashDGProc.PSSession -ArgumentList $strProcessID {
				Param ($strProcessID)

				#Function to convert wmi date to datetime
				Function WMIDateStringToDate($crdate)
				{
					If ($crdate -match ".\d*-\d*")
					{
						$crdate = $crdate -replace $matches[0], " "
						$idate = [System.Int64]$crdate
						$date = [DateTime]::ParseExact($idate, 'yyyyMMddHHmmss', $null)
						return $date
					}
				}

				#Get the process from wmi
				#We could use get-process, but then we can't get ownership information for the process
				gwmi -class win32_process -Filter "ProcessID=${strProcessID}" | ForEach{
					#Add the owner as a note property
					$objProcess = $_
					Try { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name UserName -Value ($objProcess.GetOwner().User) -PassThru }
					Catch [Exception] { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name UserName -Value " " }
					Finally { }

					#Add the reformated date as a note property
					Try { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value (WMIDateStringToDate($objProcess.CreationDate)) -PassThru }
					Catch [Exception] { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value " " -PassThru }
					Finally { }

					#Add the CPU Usage as a note property
					$strCPUUsage = "0" #Assume no cpu usage....the GetProcessPerf thread will update this
					$objProcess = Add-Member -MemberType NoteProperty -Name PercentCPU -Value $strCPUUsage -InputObject $objProcess -PassThru

					$objProcess #Return the custom object
				} | Select-Object ProcessID, Name, Username, refCreationDate, PercentCPU, WorkingSetSize, CommandLine
			} -ErrorAction SilentlyContinue

			#Remove the proc from the existing events (so it doesn't show up the next time we get the events)
			Invoke-Command -Session $hashDGProc.PSSession -ArgumentList $newProc{
				Param ($newProc)
				$newProc | Remove-Event
			}
		}

		#Add the new procs to the datagridview
		$hashDGProc.DGRunningProcs.Invoke([action]{
			#For Each Process
			ForEach ($objProcess in $arrNewProc)
			{
				$dgIndex = $hashDGProc.dgRunningProcs.Rows.Add($objProcess.ProcessID, $objProcess.Name, $objProcess.Username, $objProcess.refCreationDate, $objProcess.PercentCPU, ($objProcess.WorkingSetSize)/1024, $objProcess.CommandLine)
			}
		})

		#Get the old ended Processes
		$arrWMIEvents = Invoke-Command{
			$oldProcs = Get-Event -SourceIdentifier ProcDelete -ErrorAction SilentlyContinue
			$arrWMIEvents = @()
			ForEach ($oldProc in $oldProcs)
			{
				$oldProcWMI = $oldProc.SourceArgs.NewEvent.TargetInstance
				$hashWMIEvent = New-Object Hashtable
				$hashWMIEvent = @{
					"WMIObject" = $oldProcWMI
					"WMIEvent" = $oldProcs
				}
				$arrWMIEvents += $hashWMIEvent
			}
			$arrWMIEVents
		} -Session $hashDGProc.PSSession

		If ($arrWMIEvents.wmiobject.Name)
		{
			$strOldProcs = $arrWMIEvents.wmiobject.Name
			$hashDGProc.labelStatus.Invoke([action]{
				$hashDGProc.labelStatus.text = "Ended Proc: ${strOldProcs}"
			})
		}

		#For Each old process
		$arrDeadProcs = @()
		ForEach ($hashWMIEvents in $arrWMIEvents)
		{
			$objWMIProc = $hashWMIEvents.wmiobject
			$objWMIEvent = $hashWMIEvents.wmiEvent
			$stroldProcID = $objWMIProc.ProcessId

			#Keep track of the dead proc
			$arrDeadProcs += $strOldProcID

			#Remove the proc from the existing events (so it doesn't show up the next time we get the events)
			Invoke-Command -Session $hashDGProc.PSSession -ArgumentList $objWMIEvent{
				Param ($objWMIEvent)
				$objWMIEvent | Remove-Event
			}
		}

		#Update the datagrid view
		$hashDGProc.DGRunningProcs.Invoke([action]{
			ForEach ($objRow in $hashDGProc.dgrunningProcs.Rows)
			{
				If ($objRow.Cells[0].Value -in $arrDeadProcs)
				{
					$arrDeadProcsRows += $objRow
				}
			}

			#Remove dead procs
			ForEach ($objRow in $arrDeadProcsRows) { $hashDGProc.DGRunningProcs.Rows.Remove($objRow) }
		})

	}
	While ($hashDGProc.Stop -eq $false)
}

All in all, WMI eventing is pretty easy.

1. Register the event
2. Watch for the event with wait-event or get-event in a loop.
3. Do stuff with that event

A couple of cautionary notes:
1. WMI eventing is relatively CPU and memory intensive.
2. WMI eventing is tied to your powershell session. Loose the session and loose the eventing.
3. Since wmi eventing is tied to your powershell session, don’t leave sessions open. This will cause memory issues on your remote computer. If you have this problem, kill all the wsmprovhost processes.

I found the hardest part in controlling my powershell sessions, wmi events and runspaces was when the user closes the form. On the one hand you want the form to respond…on the other hand you want the runspaces to close nicely.

As a result, you’ll want to add the following to your script:

function fnNiceFormTearDown
{
	#Unregister event subscriptions
	If ($hashDGProc.PSSession)
	{
		If ($hashDGProc.PSSession.STate -eq "Open")
		{
			Invoke-Command -Session $hashDGProc.PSSession {
				Try
				{
					#Unregister wmi event subscribers
					Get-eventsubscriber | Unregister-Event
				}
				Catch [Exception]
				{ }
			}
		}
		#Remove our PSSession
		Try { $hashDGProc.PSSession | Remove-PSSession }Catch [Exception]{ }
	}
}

$buttonClose_Click={
	#TODO: Place custom script here
	$hashDGProc.Stop = $true
	$form1.Visible = $false
	fnNiceFormTearDown
	$form1.Close()
}

$form1_FormClosing=[System.Windows.Forms.FormClosingEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.FormClosingEventArgs]
	#TODO: Place custom script here
	$hashDGProc.Stop = $true
	$form1.Visible = $false
	fnNiceFormTearDown
	$form1.Close()
}

$form1_FormClosed=[System.Windows.Forms.FormClosedEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.FormClosedEventArgs]
	#TODO: Place custom script here
	$hashDGProc.Stop = $true
	$form1.Visible = $false
	fnNiceFormTearDown
	$form1.Close()
}

This nicely hides the form as soon as the user clicks the button, then it calls the function to clean up our wmi events and finally closes the powershell session. I also hid the X button on my form to make it harder for the user to break it. You can do this by setting the ControlBox property to false

You’ll also want to ensure that when your runspace completes you remove it from your main powershell session and free up the resources it is taking. To this you’ll want to use the following function. Call this function from the tick event of your timer.

Function fnGet-RunspaceData
{
	$more = $false
	Foreach ($runspace in $runspaces)
	{
		If ($runspace.Runspace.isCompleted)
		{
			Try { $runspace.powershell.EndInvoke($runspace.Runspace) }
			Catch [Exception]{ }
			$runspace.powershell.dispose()
			$runspace.Runspace = $null
			$runspace.powershell = $null
			If ($runspace.CompletedScript)
			{
				& $runspace.completedScript
			}
		}
		ElseIf ($runspace.Runspace -ne $null)
		{
			$more = $true
		}
	}
	#Clean out unused runspace jobs
	$temphash = $runspaces.clone()
	$temphash | Where-Object -Property Runspace -eq $Null | ForEach-Object{ $Runspaces.remove($_) }
}

$timerRunSpaceStatus_Tick={
	#TODO: Place custom script here
	fnGet-RunspaceData
}

And thus concludes a really long post. I hope I didn’t loose you in the midst of all the code.


If you are interested, here is the entire script from beginning to end:


$strComputer = "ComputerToConnectTo"
#Dynamically create a datagridview within a synchronized hash table so that it is available in another workflow/pool
$hashDGProc = [hashtable]::Synchronized(@{ })
$hashDGProc.DGRunningProcs = New-Object System.Windows.Forms.DataGridView
$hashDGProc.dgRunningProcs.AutoSizeColumnsMode = "None"
$hashDGProc.dgRunningProcs.AutoSizeRowsMode = "None"
$hashDGProc.dgRunningProcs.Dock = "None"
$hashDGProc.dgRunningProcs.Location = New-Object System.Drawing.Point(18, 17)
$hashDGProc.dgRunningProcs.Margin = New-Object System.Windows.Forms.Padding(3, 3, 3, 3)
$hashDGProc.dgRunningProcs.ScrollBars = 'Both'
$hashDGProc.dgRunningProcs.ScrollBars = 'Both'
$hashDGProc.dgRunningProcs.Size = New-Object Drawing.Size(622, 309)
$hashDGProc.dgRunningProcs.ColumnCount = 7
$hashDGProc.dgRunningProcs.Columns[0].HeaderText = "Process ID"
$hashDGProc.dgRunningProcs.Columns[1].HeaderText = "Name"
$hashDGProc.dgRunningProcs.Columns[2].HeaderText = "Username"
$hashDGProc.dgRunningProcs.Columns[3].HeaderText = "CreationDate"
$hashDGProc.dgRunningProcs.Columns[4].HeaderText = "PercentCPU"
$hashDGProc.dgRunningProcs.Columns[5].HeaderText = "MemoryUsage"
$hashDGProc.dgRunningProcs.Columns[6].HeaderText = "CommandLine"
$hashDGProc.dgRunningProcs.SelectionMode = 'FullRowSelect'
$hashDGProc.labelStatus = New-Object System.Windows.Forms.Label
$hashDGProc.labelStatus.Location = New-Object System.Drawing.Point(18, 361)
$hashDGProc.labelStatus.Size = New-Object System.Drawing.Size(622, 25)
$hashDGProc.labelStatus.Margin = New-Object System.Windows.Forms.Padding(3, 0, 3, 0)
$hashDGProc.labelStatus.Visible = $true
$hashDGProc.labelStatus.Text = "Loading..."
$hashDGProc.PSSession = New-PSSession $strComputer #This allows us to kill the session easily when the form closes
$hashDGProc.Stop = [boolean]$false
$form1.Controls.Add($hashDGProc.dgRunningProcs)
$form1.Controls.Add($hashDGProc.labelStatus)

$sbGetAllProcesses = {
	Param ($hashDGProc, $strComputer)

	#Get the Current Running Processes

	$hashDGProc.labelStatus.Invoke([action]{
		$hashDGProc.labelStatus.text = "Getting processes...."
	})

	$arrProc = Invoke-Command -Session $hashDGProc.PSSession -ScriptBlock {
		#Function to convert WMI date to DateTime
		Function WMIDateStringToDate($crdate)
		{
			If ($crdate -match ".\d*-\d*")
			{
				$crdate = $crdate -replace $matches[0], " "
				$idate = [System.Int64]$crdate
				$date = [DateTime]::ParseExact($idate, 'yyyyMMddHHmmss', $null)
				return $date
			}
		}

		#Get all the existing Processes | For each existing process
		gwmi -class win32_process | ForEach{
			$objProcess = $_
			If ($objProcess.ProcessID -ne "0")
			{
				#Add the owner as a note property
				Try { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name UserName -Value ($objProcess.GetOwner().User) -PassThru }
				Catch [Exception] { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name UserName -Value " " }
				Finally { }

				#Add the reformated date as a note property
				Try { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value (WMIDateStringToDate($objProcess.CreationDate)) -PassThru }
				Catch [Exception] { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value " " -PassThru }
				Finally { }

				#Add the CPU Usage as a note property
				$strCPUUsage = "0" #Assume no cpu usage....this will get updated by the cpu usage thread
				$objProcess = Add-Member -MemberType NoteProperty -Name PercentCPU -Value $strCPUUsage -InputObject $objProcess -PassThru

				$objProcess #Return the custom object
			}
		} | Select-Object ProcessID, Name, Username, refCreationDate, PercentCPU, WorkingSetSize, CommandLine
	} -ErrorAction SilentlyContinue

	#Take all of the process and add them to the datagrid view
	$hashDGProc.DGRunningProcs.Invoke([action]{
		#Clear any previous rows
		$hashDGProc.DGRunningProcs.Rows.Clear()

		#For Each Process
		ForEach ($objProcess in $arrProc)
		{
			$dgIndex = $hashDGProc.dgRunningProcs.Rows.Add($objProcess.ProcessID, $objProcess.Name, $objProcess.Username, $objProcess.refCreationDate, $objProcess.PercentCPU, ($objProcess.WorkingSetSize)/1024, $objProcess.CommandLine)
		}
	})

	#Done existing processes
	$hashDGProc.labelStatus.Invoke([action]{
		$hashDGProc.labelStatus.text = " "
	})
}

$sbGetProcessChanges = {
	Param ($hashDGProc, $strComputer)

	#Register a new WMI event trace for three types of process:
	#1. Newly started processes
	#2. Old Processes
	Invoke-Command{
		#New Processes
		Register-WmiEvent -Class win32_ProcessStartTrace -SourceIdentifier processStarted

		#Old Process
		$strDeleteQuery = "Select * from __instanceDeletionEvent within 0.5 where targetInstance isa 'win32_Process'"
		Register-WmiEvent -Query $strDeleteQuery -SourceIdentifier ProcDelete
	} -Session $hashDGProc.PSSession

	#Get the events generated by WMI while the stop button has not been pressed
	Do
	{
		#Check the pssession status
		Write-Host $hashDGProc.PSsession.State
		If ($hashDGProc.PSSession.State -notmatch "Opened")
		{
			Try { $hashDGProc.PSSession | remove-pssession }
			Catch { }
			$hashDGProc.PSSession = New-PSSession $strComputer #This allows us to kill the session easily when the form closes
		}

		#Sleep just long enough to give the gui thread a chance to refresh
		Start-Sleep 1

		#Get all the New Processes
		$newProcs = Invoke-Command{ Get-Event -SourceIdentifier processStarted -ErrorAction SilentlyContinue } -Session $hashDGProc.PSSession

		If ($newProcs)
		{
			$strNewProcs = $newprocs.SourceArgs.NewEvent.ProcessName
			$hashDGProc.labelStatus.Invoke([action]{
				$hashDGProc.labelStatus.text = "New Proc: $strNewProcs"
			})
		}

		#For Each new Process
		$arrNewProc = @()
		ForEach ($newProc in $newProcs)
		{
			#Get the ProcessID from the alert
			$strProcessID = $newproc.SourceArgs.NewEvent.ProcessID

			#Go get the specific process from wmi
			$arrNewProc += Invoke-Command -Session $hashDGProc.PSSession -ArgumentList $strProcessID {
				Param ($strProcessID)

				#Function to convert wmi date to datetime
				Function WMIDateStringToDate($crdate)
				{
					If ($crdate -match ".\d*-\d*")
					{
						$crdate = $crdate -replace $matches[0], " "
						$idate = [System.Int64]$crdate
						$date = [DateTime]::ParseExact($idate, 'yyyyMMddHHmmss', $null)
						return $date
					}
				}

				#Get the process from wmi
				#We could use get-process, but then we can't get ownership information for the process
				gwmi -class win32_process -Filter "ProcessID=${strProcessID}" | ForEach{
					#Add the owner as a note property
					$objProcess = $_
					Try { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name UserName -Value ($objProcess.GetOwner().User) -PassThru }
					Catch [Exception] { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name UserName -Value " " }
					Finally { }

					#Add the reformated date as a note property
					Try { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value (WMIDateStringToDate($objProcess.CreationDate)) -PassThru }
					Catch [Exception] { $objProcess = Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value " " -PassThru }
					Finally { }

					#Add the CPU Usage as a note property
					$strCPUUsage = "0" #Assume no cpu usage....the GetProcessPerf thread will update this
					$objProcess = Add-Member -MemberType NoteProperty -Name PercentCPU -Value $strCPUUsage -InputObject $objProcess -PassThru

					$objProcess #Return the custom object
				} | Select-Object ProcessID, Name, Username, refCreationDate, PercentCPU, WorkingSetSize, CommandLine
			} -ErrorAction SilentlyContinue

			#Remove the proc from the existing events (so it doesn't show up the next time we get the events)
			Invoke-Command -Session $hashDGProc.PSSession -ArgumentList $newProc{
				Param ($newProc)
				$newProc | Remove-Event
			}
		}

		#Add the new procs to the datagridview
		$hashDGProc.DGRunningProcs.Invoke([action]{
			#For Each Process
			ForEach ($objProcess in $arrNewProc)
			{
				$dgIndex = $hashDGProc.dgRunningProcs.Rows.Add($objProcess.ProcessID, $objProcess.Name, $objProcess.Username, $objProcess.refCreationDate, $objProcess.PercentCPU, ($objProcess.WorkingSetSize)/1024, $objProcess.CommandLine)
			}
		})

		#Get the old ended Processes
		$arrWMIEvents = Invoke-Command{
			$oldProcs = Get-Event -SourceIdentifier ProcDelete -ErrorAction SilentlyContinue
			$arrWMIEvents = @()
			ForEach ($oldProc in $oldProcs)
			{
				$oldProcWMI = $oldProc.SourceArgs.NewEvent.TargetInstance
				$hashWMIEvent = New-Object Hashtable
				$hashWMIEvent = @{
					"WMIObject" = $oldProcWMI
					"WMIEvent" = $oldProcs
				}
				$arrWMIEvents += $hashWMIEvent
			}
			$arrWMIEVents
		} -Session $hashDGProc.PSSession

		If ($arrWMIEvents.wmiobject.Name)
		{
			$strOldProcs = $arrWMIEvents.wmiobject.Name
			$hashDGProc.labelStatus.Invoke([action]{
				$hashDGProc.labelStatus.text = "Ended Proc: ${strOldProcs}"
			})
		}

		#For Each old process
		$arrDeadProcs = @()
		ForEach ($hashWMIEvents in $arrWMIEvents)
		{
			$objWMIProc = $hashWMIEvents.wmiobject
			$objWMIEvent = $hashWMIEvents.wmiEvent
			$stroldProcID = $objWMIProc.ProcessId

			#Keep track of the dead proc
			$arrDeadProcs += $strOldProcID

			#Remove the proc from the existing events (so it doesn't show up the next time we get the events)
			Invoke-Command -Session $hashDGProc.PSSession -ArgumentList $objWMIEvent{
				Param ($objWMIEvent)
				$objWMIEvent | Remove-Event
			}
		}

		#Update the datagrid view
		$hashDGProc.DGRunningProcs.Invoke([action]{
			ForEach ($objRow in $hashDGProc.dgrunningProcs.Rows)
			{
				If ($objRow.Cells[0].Value -in $arrDeadProcs)
				{
					$arrDeadProcsRows += $objRow
				}
			}

			#Remove dead procs
			ForEach ($objRow in $arrDeadProcsRows) { $hashDGProc.DGRunningProcs.Rows.Remove($objRow) }
		})

	}
	While ($hashDGProc.Stop -eq $false)
}

$sbGetProcessPerf = {
	Param ($hashDGProc, $strComputer)
	Do
	{
		Start-Sleep 5 #Give the computer a break!
		$objCounters = Invoke-Command -Session $hashDGProc.PSSession { Get-Counter '\Process(*)\ID Process' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty CounterSamples }
		$objPerCPU = Invoke-Command -Session $hashDGProc.PSSession { Get-Counter '\Process(*)\% Processor Time' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty CounterSamples }

		#Measure the total CPU usage returned by counters....since this inclues the System Idle process it totals 100% of cpu usage...except when it doesn't...so we're normalizing it here based on the number of CPUs
		$ProcCount = (($objPerCPU | Where-Object -Property Path -NotMatch "(_total)" | Measure-Object 'CookedValue' -Sum).Sum)/100

		#Commit the changes to the datagrid view
		$arrProcIDs = @() #array to keep track of all the proc IDs ....so we can remove duplicates
		$arrRowsToRemove = @()
		$hashDGProc.DGRunningProcs.Invoke([action]{
			ForEach ($objRow in $hashDGProc.dgRunningProcs.Rows)
			{

				$ProcID = $objRow.Cells[0].Value
				$arrProcIDs += $ProcID

				If ($ProcID -and ($ProcID -notin $arrProcIDs)) #if not a blank processid
				{
					$objProcCounters = $objCounters | Where-Object -Property Path -NotMatch "_Total" | Where-Object -Property CookedValue -eq $ProcID
					ForEach ($objProcCounter in $objProcCounters)
					{
						Try
						{
							$ProcPath = $objProcCounter.Path.TrimEnd("\id process")
							$ProcPath = $ProcPath + "\% Processor Time"
						}
						Catch [Exception]{ }
					}
					$PercCPU = 0

					#If there isn't a counter for the proc....check if the proc really is around
					If ($objProcCounters.Count -eq 0)
					{
						If ((!(Get-Process $ProcID)) -and ($objRow -notin $arrRowsToRemove))
						{
							$arrRowsToRemove += $objRow
						}
					}
					else
					{

						Try { $PercCPU = (($objPerCPU | Where-Object -Property Path -eq $ProcPath).CookedValue)/$ProcCount }
						Catch [Exception]{ }
						If ($PercCPU -ne $objRow.Cells[4].Value)
						{
							$objRow.Cells[4].Value = ([system.Math]::Round($PercCPU, 2)).ToString()
						}
					}
				}
				else
				{
					If ($objRow -notin $arrRowsToRemove)
					{
						$arrRowsToRemove += $objRow
					}
				}
			}

			#Remove dead rows
			ForEach ($objRow in $arrRowsToRemove) { $hashDGProc.DGRunningProcs.Rows.Remove($objRow) }

			#Update the sorting
			Try
			{
				$dgProcSortColumn = $hashDGProc.dgRunningProcs.sortedcolumn
				$dgProcSortOrder = $hashDGProc.dgRunningProcs.sortorder
				Switch ($dgProcSortOrder)
				{
					"Ascending" {
						$hashDGProc.dgrunningProcs.Sort($dgProcSortColumn, "Ascending")
					}
					"Descending" {
						$hashDGProc.dgrunningProcs.Sort($dgProcSortColumn, "Descending")
					}
					Default { }
				}
				$hashDGProc.dgrunningProcs.Refresh()
			}
			Catch [Exception]{ }

		})#>
	}While ($hashDGProc.Stop -eq $false)
}

$form1_Load = {
	If (!($runspacepool))
	{
		$Script:runspaces = New-Object System.Collections.ArrayList
		$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
		$runspacepool = [runspacefactory]::CreateRunspacePool(1, 10, $sessionstate, $host)
		$runspacepool.Open()
	}

	#Execute Powershell Runspace for All Processes
	$powershellRunSpace = [powershell]::Create()
	$powershellRunSpace.AddScript($sbGetAllProcesses).AddArgument($hashDGProc).AddArgument($strComputer)
	$powershellRunSpace.RunspacePool = $runspacepool

	$InstRunSpace = "" | Select-Object name, powershell, runspace, Computer, CompletedScript
	$InstRunSpace.Name = (Get-Random)

	$InstRunSpace.Computer = $strComputer
	$instRunSpace.Powershell = $powershellRunSpace
	$InstRunSpace.CompletedScript = $sbProcComplete #not being used in this test
	$InstRunSpace.RunSpace = $powershellRunSpace.BeginInvoke()
	$runspaces.Add($InstRunSpace) | Out-Null

	#Execute Powershell Runspace for Changed Processes
	$powershellRunSpace = [powershell]::Create()
	$powershellRunSpace.AddScript($sbGetProcessChanges).AddArgument($hashDGProc).AddArgument($strComputer)
	$powershellRunSpace.RunspacePool = $runspacepool

	$InstRunSpace = "" | Select-Object name, powershell, runspace, Computer, CompletedScript
	$InstRunSpace.Name = (Get-Random)

	$InstRunSpace.Computer = $strComputer
	$instRunSpace.Powershell = $powershellRunSpace
	$InstRunSpace.CompletedScript = $sbProcComplete #not being used in this test
	$InstRunSpace.RunSpace = $powershellRunSpace.BeginInvoke()
	$runspaces.Add($InstRunSpace) | Out-Null

	#Execute Powershell Runspace for Processes Performance
	$powershellRunSpace = [powershell]::Create()
	$powershellRunSpace.AddScript($sbGetProcessPerf).AddArgument($hashDGProc).AddArgument($strComputer)
	$powershellRunSpace.RunspacePool = $runspacepool

	$InstRunSpace = "" | Select-Object name, powershell, runspace, Computer, CompletedScript
	$InstRunSpace.Name = (Get-Random)

	$InstRunSpace.Computer = $strComputer
	$instRunSpace.Powershell = $powershellRunSpace
	$InstRunSpace.CompletedScript = $sbProcComplete #not being used in this test
	$InstRunSpace.RunSpace = $powershellRunSpace.BeginInvoke()
	$runspaces.Add($InstRunSpace) | Out-Null
}

Function fnGet-RunspaceData
{
	$more = $false
	Foreach ($runspace in $runspaces)
	{
		If ($runspace.Runspace.isCompleted)
		{
			Try { $runspace.powershell.EndInvoke($runspace.Runspace) }
			Catch [Exception]{ }
			$runspace.powershell.dispose()
			$runspace.Runspace = $null
			$runspace.powershell = $null
			If ($runspace.CompletedScript)
			{
				& $runspace.completedScript
			}
		}
		ElseIf ($runspace.Runspace -ne $null)
		{
			$more = $true
		}
	}
	#Clean out unused runspace jobs
	$temphash = $runspaces.clone()
	$temphash | Where-Object -Property Runspace -eq $Null | ForEach-Object{ $Runspaces.remove($_) }
}

$timerRunSpaceStatus_Tick={
	#TODO: Place custom script here
	fnGet-RunspaceData
}

$buttonClose_Click={
	#TODO: Place custom script here
	$hashDGProc.Stop = $true
	$form1.Visible = $false
	fnNiceFormTearDown
	$form1.Close()
}

function fnNiceFormTearDown
{
	#Unregister event subscriptions
	If ($hashDGProc.PSSession)
	{
		If ($hashDGProc.PSSession.STate -eq "Open")
		{
			Invoke-Command -Session $hashDGProc.PSSession {
				Try
				{
					#Unregister wmi event subscribers
					Get-eventsubscriber | Unregister-Event
				}
				Catch [Exception]
				{ }
			}
		}
		#Remove our PSSession
		Try { $hashDGProc.PSSession | Remove-PSSession }Catch [Exception]{ }
	}
}

$form1_FormClosing=[System.Windows.Forms.FormClosingEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.FormClosingEventArgs]
	#TODO: Place custom script here
	$hashDGProc.Stop = $true
	$form1.Visible = $false
	fnNiceFormTearDown
	$form1.Close()
}

$form1_FormClosed = [System.Windows.Forms.FormClosedEventHandler]{
	#Event Argument: $_ = [System.Windows.Forms.FormClosedEventArgs]
	#TODO: Place custom script here
	$hashDGProc.Stop = $true
	$form1.Visible = $false
	fnNiceFormTearDown
	$form1.Close()
}
Advertisements

One thought on “SysJam Powershell Right Click Tool – Part 3 Doing more than 1 thing at once more – Powershell Runspaces and WMI Eventing

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s