SysJam Powershell Right Click Tool – Part 2 Doing more than 1 thing at once – Powershell Jobs

When you build powershell forms for the first time you will notice that whenever you execute code while your form is open everything appears to freeze. What is happening is that you are utilizing the same powershell thread is used to update your form (including when a user tries to move it/scroll etc…) and to also run your code.

To get around this we have two options. The most straight forward one of the two is a little slower running but much easier to code. This is using Powershell jobs. The second option is much quicker to execute and more flexible….it will also require you learn gymnastics within Powershell. This is using synchronized objects within Powershell runspaces.

This post will cover the first of the two options. My examples will come from version 1.1 of the SysJam Right click tool for SCCM available at https://github.com/mdedeboer/SysJamRightClickToolForConfigMgr. Note that some of the code may be slightly different depending on the release. Also of note, I have used some code from Sapien Powershell Studio as well as from the blog available here: http://www.sapien.com/blog/2012/05/16/powershell-studio-creating-responsive-forms/
Please understand that in steps 2-5 I heavily use code from Sapien. Even if you do not use Sapien’s toolset their blog contains a lot of good information.

Lets break this up into pieces.

1. Build a code block of stuff we want to run synchronously. This code block is similar to a function. It will need parameters (input), body (what its doing) and a return (output).

2. Add a timer to check when the jobs complete

3. Create a function to start the script block, log the properties of the job to an arraylist and start the timer.

4. Create a function triggered by the timer to check every job logged in the arraylist to see if it is completed. If the job is completed or running. Trigger an optional “Update Script” or “Completed Script” to gather the output from your script block.

If you are following along in the Sysjam right click tool, you’ll notice that this jumps around a bit in the source code…but I hope you can follow me 🙂

Let’s start at the beginning.

1. Build a code block of stuff we want to run synchronously.
For this example, I’ll use a small piece of code from SysJam rightclick tool. The SysJam tool contains many of these code blocks. In order to do each one of these justice, I’ll tackle each one separately. This one you will find in the source code under “function fnTabToolsScriptBlocks”.

$sbTools = {
	Param ($strComputer)
	[hashtable]$hashReturn = Invoke-Command{
	      $hashReturn = @{ }
	      $objReturn = Gwmi -class Win32_OperatingSystem -Property @('LastBootUpTime', 'caption', 'installdate')
	      ForEach ($strProperty in $objReturn.Properties.Name){
			$hashReturn.Add($strProperty, ($objReturn.Properties | Where-Object -Property Name -eq $strProperty).Value)
	      }
	      Return $hashReturn
	} -ComputerName $strComputer
	Return $hashReturn
}

On the surface this looks a little complicated. In truth, it is probably one of the most simple script blocks in the SysJam tool. So what are we doing here?

$sbTools = {  #Define the variable sbTools as a script block (denoted by the {)
       Param ($strComputer) #This is what the script block takes as input.
       [hashtable]$hashReturn = Invoke-Command{  #We will be running a command (Invoke-Command{)....we'll be storing it in a hashtable.  A hashtable is a paring of properties and values.  We store it as a hashtable so we can pass it back easier later.
       $hashReturn = @{ } #What is this? We are reusing the same variable?  Keep in mind thought that this is in the Invoke-Command scope.  Inside the {} of the invoke command, we know nothing of any variables outside of the {} unless the are explicitly passed.
       $objReturn = Gwmi -class Win32_OperatingSystem -Property @('LastBootUpTime', 'caption', 'installdate') #Get the last boot time, caption and install date of the OS from wmi. If you are unfamiliar with WMI it is helpful to spend a lot of time in wbemtest and google.
       ForEach ($strProperty in $objReturn.Properties.Name){ #For each property returned in the wmi object (we are assuming only one object gets returned here....a safe assumption in this case)
             $hashReturn.Add($strProperty, ($objReturn.Properties | Where-Object -Property Name -eq $strProperty).Value) #Take that hash table we created.  Add a key with the name stored in strProperty.(ie 'LastBootUpTime', or 'caption', or 'installdate').  This key has the same value as it does in wmi.  ($objReturn.Properties | Where-Object -Property Name -eq $strProperty).Value essentially means take the return from WMI ($objReturn) and get all the properties (.Properties).  Filter all these properties and only take the one with the property value stored in strProperty (Where-Object -Property Name -eq $strProperty).  Then take all of that (wrapped in ()) and get the value of that property .Value
       } #end the for each.  We now should have a hashtable containing all of the properties we requested from WMI and their values....like this:
#Name                           Value
#----                           -----
#Caption                        Microsoft Windows 8.1 Pro
#InstallDate                    20131120160225.000000-420
#LastBootUpTime                 20141219080039.490054-420
      Return $hashReturn #Take the hashtable we build and return it back to the beginning of the invoke-command (see line 3 where it is assigned to $hashReturn outside of the invoke-command.
    } -ComputerName $strComputer #I mentioned earlier that the code inside invoke-command knows nothing of the variables outside of invoke-command.  Another reason for using it is to target other computers.  This is essentially saying....take everything wrapped in the {} of the invoke command and execute it on another computer.  see also https://technet.microsoft.com/en-us/magazine/ff700227.aspx
    Return $hashReturn #take the hashtable returned from the invoke command and return out of the script block.
} #End the script block

2. Add a timer to check when the jobs complete
This should be pretty self explanatory if you are using a form builder…..its pretty much drag and drop. I personally use Sapien Powershell studio for this. As a result, I can’t really explain it very well, but I’ll still provide the code:

$timerJobTracker = New-Object 'System.Windows.Forms.Timer'
$timerJobTracker.Interval = 2500
$timerJobTracker.add_Tick($timerJobTracker_Tick)
$timerJobTracker_Tick={
	Update-JobTracker
}

All in all it is probably the easiest form object you can add to a form. You can see that the timer has an interval of 2.5 seconds. This means that the timer will fire every 2.5 seconds. What happens when it fires? $timerJobTracker_Tick, which refers to the script block { Update-JobTracker } We’ll get back to this in a moment…suffice it to say at the moment that the function “Update-JobTracker” is the function we build in step 4.

3. Create a function to start the script block, log the properties of the job to an arraylist and start the timer.

For my purposes, I used the Add-JobTracker function from Sapien’s blog post available here:
http://www.sapien.com/blog/2012/05/16/powershell-studio-creating-responsive-forms/

In order to honor their work, I will not be posting this code here. However it is fairly simple:
– Start the job.
– Check if the job started
– Log the job information into an array list so we can reference it later when the job completes
– Start a timer so we come back every so often to check the status of the job.

4. Create a function triggered by the timer to check every job logged in the arraylist to see if it is completed. If the job is completed or running. Trigger an optional “Update Script” or “Completed Script” to gather the output from your script block.

Once again, see the Sapien blog for their explanation as well. Basically this is what we do:

Every time this function is called (remember this is whenever the timer tick event occurs) we will
-Stop the timer
-Go through the list of jobs in our array list
-Check if object in the list is an actual object
-Check the referenced ps job to see if it is still running
-If the job is stopped, take the output from the scriptblock and pass it to the completed script and remove the job from the list
-If the job has not stopped, run the update script and keep going.

This code is available in the Update-JobTracker function on the Sapien blog.

So you can see then that our flow looks like this when we put it all together:

1. Regular script defines script block of things we want to do.
2. Regular script calls function to run script block as a powershell job with the provided parameters.
3. Timer function checks if job is complete
4. When job is complete timer function executes complete script with output from script block as the input.

Lets see how we call the function in our regular script. (Note this example doesn’t use the UpdateScript option…this is typically used for progress bars etc….)

Add-JobTracker -JobScript $sbTools -ArgumentList $strComputer -Name (Get-Random -Minimum 1000 -Maximum 9999) `
						   -CompletedScript {
				Param ($Job)
				[hashtable]$hashReturn = Receive-Job -Job $Job
				$dgClientStatus.Rows.Clear()

				ForEach ($strProperty in $hashReturn.keys)
				{
					If ($strProperty -in @("InstallDate", "LastBootUpTime"))
					{
						If ($hashReturn.Item($strProperty))
						{
							$dgClientSTatus.Rows.Add($strProperty, $hashReturn.Item($strProperty).SubString(0, 8))
						}
					}
					else
					{
						$dgClientSTatus.Rows.Add($strProperty, $hashReturn.Item($strProperty))
					}
				}
			}

All in all actually pretty easy. But lets go through it anyways.
Remember at the beginning our $sbTools script block took an input for a computername? This is included in “-ArgumentList $strComputer”. “-JobScript $sbTools” supplies our script block as the input to run. “-Name (Get-Random -Minimum 1000 -Maximum 9999)” is basically a random name for our job. The key thing is that the name should be unique. Truth be told, it would be better if we would test if the job already exists in our array. Then comes the completedScript.

Param ($Job)
	[hashtable]$hashReturn = Receive-Job -Job $Job #Get the return data from the job....since we wrote the input script block, we know the output is a hashtable.

	$dgClientStatus.Rows.Clear() #This is my datagrid view (Building this is outside the scope of this post)

	ForEach ($strProperty in $hashReturn.keys) #ForEach key in our hashtable that was returned from our script block
	{
		If ($strProperty -in @("InstallDate", "LastBootUpTime")) #If the property is Installdate or LastBootTime
		{
			If ($hashReturn.Item($strProperty)) #If there is a value
			{
			        $dgClientSTatus.Rows.Add($strProperty, $hashReturn.Item($strProperty).SubString(0, 8)) #Add the property name and first 8 characters of the value to a new row in the datagrid view.
			}
		}
		else #Not Installdate or LastBootTime
		{
			$dgClientSTatus.Rows.Add($strProperty, $hashReturn.Item($strProperty)) #Add the property name and the value to a new row in the datagrid view.
		}
	}

And we are done!

We have successfully performed a remote WMI query in separate powershell job and returned the databack to our main script and populated our form (in this case a datagridview).

Advertisements

2 thoughts on “SysJam Powershell Right Click Tool – Part 2 Doing more than 1 thing at once – Powershell Jobs

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