Background Jobs (PowerShell): Difference between revisions

From vwiki
Jump to navigation Jump to search
(Initial creation - content from depreciated PowerShell page)
 
(Initial creation - content from depreciated PowerShell page)
Line 1: Line 1:
== Overview ==
== Action on Error ==
This feature allows Cmdlets to run as background jobs.  Its fairly easy to work out how to make individual Cmdlet's or PowerShell scripts to run as jobs (see [http://technet.microsoft.com/en-us/library/dd315273.aspx about_Jobs], but running functions are a bit more of a pain as the job runs in a new scope (so any functions, variables etc that are defined in your scripts scope, have no meaning in the background job's scope).  Therefore functions, etc, have to explicitly included as a script block in the background job...
To control how a script behaves as a result of an exception, modify the <code> $ErrorActionPreference </code> variable, if required.


<source lang="powershell">
In the default continue mode, an error will output to the screen, then the script will continue.  By outputting the error to the screen PowerShell CmdLets consider the exception to have been handled!  If you want the opportunity to <code>catch</code> or <code>trap</code> you must append <code>-ErrorAction:Stop</code> to the CmdLet you expect might fail.
# First define the script block and function
 
$funky = {
{|class="vwikitable"
    function TestJob {
|-
        $processList = Get-Process
! Value            !! Effect
        Return $processList                          # Yes could be achieved in one line, but wouldn't be much of a function!
|-
    }
| Continue          || [Default] Outputs error, but keeps processing
}
|-
| SilentlyContinue  || No output and it keeps going
|-
| Inquire          || Prompt user for action
|-
| Stop              || Outputs error and halts processing
|}


# Start job
== $error ==
$job = Start-Job -ScriptBlock {TestJob} -InitializationScript $funky
Provides a list of recent errors experienced - which can be invaluable for properly identifying and investigating errors.  The object is the same as found in a pipeline when an exception has occurred.


$job | Format-List *                                # Displays created $job object
{|class="vwikitable"
|-
! Property                                    !! Description                              !! Example (VC login error)
|-
| <code>$error[1].Exception.GetType().FullName</code> || Error class for specific error || <code>[VMware.VimAutomation.ViCore.Types.V1.ErrorHandling.InvalidLogin]</code>
|-
| <code> $error[1].Exception.Message </code>  || User friendly error message  || <code>16/11/2010 10:32:52  Connect-VIServer  Login failed due to a bad username or password.</code>
|-
| <code> $error[1].ErrorDetails.Message </code> || Shorter friendly error message  || <code> Login failed due to a bad username or password </code>
|-
| <code> $error[1].CategoryInfo.Reason </code> || Short error message        || <code> InvalidLogin </code>
|-
| <code> $error[1].InvocationInfo </code>    || Invocation info (what triggered exception) || Various info including script command
|}


Wait-Job -job $job                                  # Wait on completion of job
== Basic Error Handler ==
Receive-Job -job $job                                # Gets result of $job (ie result of Get-Process)
If you know where the error is likely to occur, then just place an error catcher immediately after it.  This doesn't stop the exception appearing on the console, but does allow you to take some action as a result.
Get-Job                                              # Shows list of jobs (current and completed)
<source lang="powershell">
if (-not $?) {
    # Handle error here
  }
</source>
</source>


Arguments have to be passed through to the job through the <code> -InputObject </code> parameter, which isn't particularly pretty. For further info see http://robertrobelo.wordpress.com/2010/03/14/background-jobs-input-gotcha/ for a decent explanation, though I do kind of cover this below.
== Try...Catch ==
 
Used to catch an exception in a script block where an exception may be likelyStops the exception being shown on the console and gives you a chance to do something about it (as long as you've set <code>$ErrorActionPreference</code> to Stop or used <code>-ErrorAction:Stop</code>).
'''Script Block''' or '''Script File'''...?
Your background task can either take the form of a script block, or a script file.  Personally I prefer to keep everything in one script as it makes organisation easier, up to an extentThere is a limit to size of a script block, no idea what it is, as the script I as trying to mangle into running as jobs was large (20 KB, nearly 1000 lines) and I didn't have a convenient way to test.  But your jobs will fail if they're too big.
 
== Job Control ==
Below is a fuller example of using background jobs to manage multiple work streams
 
Further reading...
* http://ryan.witschger.net/?p=22 - Multi-Threading in PowerShell V2
 
<source lang="powershell">
<source lang="powershell">
$funky = {
try
     function TestJob {
{
        $var = $Input.‘<>4__this’.Read()
     # Something in which an exception is likely
        Write-Host "This is job " $var[0]
}
        Start-Sleep $var[1]
catch
     }
{
    Write-Host "FAILED: $_"
     Exit
}
}
</source>


$jobs = @()
The error returned by the CmdLet can be found in <code> $_ </code>, so this can be tested to ensure the error is as expected (just because you though a command might fail, doesn't mean it failed in the way you expected).  For example, <code> $_ </code> will contain the bold bit of the following error.
$job = "" | Select Name, Vars, State, Obj
Get-Log : '''Cannot validate argument on parameter 'StartLineNum'. The 0 argument is less than the minimum allowed range of 1. Supply an argument that is greater than 1 and then try the command again.'''
$job.Name = "Job1"
At C:\Users\Simon\Documents\Scripts\ESX-LogTail.ps1:20 char:57
$job.Vars = ($job.Name, 10)
+    $ESXLog = Get-Log $logKey -VMHost $ESX -StartLineNum <<<<  $LineNo
$jobs += $job
    + CategoryInfo          : InvalidData: (:) [Get-Log], ParameterBindingValidationException
$job = "" | Select Name, Vars, State, Obj
    + FullyQualifiedErrorId : ParameterArgumentValidationError,VMware.VimAutomation.Commands.GetLog
$job.Name = "Job2"
$job.Vars = ($job.Name, 5)
$jobs += $job


foreach ($job in $jobs) {
This can be elaborated on to make the catch handling more specific, by making the catch block executed depend on the error class.  The error class can be determined by making causing the error to be thrown, in which case the class can be found at <code>  $_.Exception.GetType().FullName </code>.
     Write-Host ("Starting " + $job.Name)
<source lang="powershell">
     $job.Obj = Start-Job -ScriptBlock {TestJob} -InitializationScript $funky -Name $job.Name -InputObject $job.Vars
try {
     $job.State = "Running"
  $proxy = New-WebServiceProxy -uri $endpoint -cred $credential -ErrorAction:Stop
} catch [System.Net.WebException] {
     Write-Host "ERROR: Unable to connect to SOAP server"
    Write-Host $_
} catch {
     Write-Host "ERROR: Unexpected error"
    Write-Host $_
    Write-Host $_.Exception.Message
     Write-Host $_.Exception.GetType().FullName
}
}
# Idle loop
While (1) {
    $JobsRunning = 0
    foreach ($job in $jobs) {
        if ($job.State -ne $job.Obj.JobStateInfo.state) {
            Write-Host ($job.Name + " state now " + $job.Obj.JobStateInfo.state)
            $job.State = $job.Obj.JobStateInfo.state
        }
        if ($job.State -eq "Running") {
            $JobsRunning += 1
        }
  }
  Write-Host ("$JobsRunning of " + $jobs.count + " jobs still running")
  if ($JobsRunning -eq 0) {
      Break
  }
  Start-Sleep 1
}
   
Write-Host "All finished...!"
# To see output from jobs
# Get-Jobs              - shows list of jobs
# Receive-Job -Id x    - shows the data returned from the job
</source>
</source>


== Transcripts / Logging ==
== Trap ==
I haven't been able to get transcripts working properly with background jobs, at least, not to my likingIt is possible to capture the console output from a background job into the transcript of the parent or master script.  But if you're running a large background job script and want to capture the transcript/logging from job separately you have to faff around - you can't just start and stop transcribing from within the (child) background job script, it won't write anything to disk.
Used to capture any unhanded exception that occurs anywhereI tend to consider this a last resort catch-all, though really it depends on the nature of your script.  For example if your script is reliant on a connection to a server that can go down, you can design a trap to recover from that specific occurrence rather than having to put a Try...Catch around every operation that could fail.


Similarly, you can't redirect the output from <code> Receive-Job </code> to a file, you'll lose some of the output (I think this may only capture StdErr and/or explicitly returned objects, standard <code> Write-Host </code> output is dropped).
The key to an effective <code>trap</code> is allowing for the fact that ''anything'' might go wrong, therefore you have to set your traps up to handle only specific cases and in all likelihood stop on anything else.


One way around this is to stop the transcript for your master/parent script, then start a temporary trasncript to capture then return from your child job once its finished, so so something like...
<source lang="powershell">
<source lang="powershell">
if ($job.State.ToString() -eq "Completed") {
trap {
    Write-Host ($job.Name + " writing log to job-" + $job.Name + ".log")
     # Handle the exception
     # Nasty logging handling (Receive-Job StdOut to console only, can't redirect to file, can only catch StdErr to file)
     Continue
    Stop-Transcript
    Start-Transcript -Path ("job-" + $job.Name + ".log")
    Receive-Job -Id $job.job.Id
    Stop-Transcript
     Start-Transcript -Path $Logfile -Append
}
}
</source>
</source>


== Gotchas ==
== Further Reading ==
* '''Working Directory'''
* Exception trapping
** The background job script runs in a new context, therefore it runs in the default path. If you tend to run your scripts from a non-default path, and need to read/write files, get the current directory using <code>Get-Location</code> and pass it to your job as a parameter, then use <code>Set-Location</code> in your job.
** http://huddledmasses.org/trap-exception-in-powershell/
* '''VMware PowerCLI 64 bit'''
* Error handling
** Due to a bug you can't launch background jobs against VMware vSphere (VI4) hosts in a 64-bit environment, PowerShell will crash. Its probably a bug in PowerCLI, and will hopefully be fixed in a future release (bug exists in v4.1u1 and possibly other versions).  You can run scripts in 32-bit (see [[Getting_Started_(PowerShell)#Useful_One-Liners|Useful One-Liners]] on how to detect in your script).
** http://www.pluralsight.com/community/blogs/keith/archive/2007/01/22/45814.aspx


[[Category:PowerShell]]
[[Category:PowerShell]]

Revision as of 14:21, 20 April 2012

Action on Error

To control how a script behaves as a result of an exception, modify the $ErrorActionPreference variable, if required.

In the default continue mode, an error will output to the screen, then the script will continue. By outputting the error to the screen PowerShell CmdLets consider the exception to have been handled! If you want the opportunity to catch or trap you must append -ErrorAction:Stop to the CmdLet you expect might fail.

Value Effect
Continue [Default] Outputs error, but keeps processing
SilentlyContinue No output and it keeps going
Inquire Prompt user for action
Stop Outputs error and halts processing

$error

Provides a list of recent errors experienced - which can be invaluable for properly identifying and investigating errors. The object is the same as found in a pipeline when an exception has occurred.

Property Description Example (VC login error)
$error[1].Exception.GetType().FullName Error class for specific error [VMware.VimAutomation.ViCore.Types.V1.ErrorHandling.InvalidLogin]
$error[1].Exception.Message User friendly error message 16/11/2010 10:32:52 Connect-VIServer Login failed due to a bad username or password.
$error[1].ErrorDetails.Message Shorter friendly error message Login failed due to a bad username or password
$error[1].CategoryInfo.Reason Short error message InvalidLogin
$error[1].InvocationInfo Invocation info (what triggered exception) Various info including script command

Basic Error Handler

If you know where the error is likely to occur, then just place an error catcher immediately after it. This doesn't stop the exception appearing on the console, but does allow you to take some action as a result.

if (-not $?) {
    # Handle error here
  }

Try...Catch

Used to catch an exception in a script block where an exception may be likely. Stops the exception being shown on the console and gives you a chance to do something about it (as long as you've set $ErrorActionPreference to Stop or used -ErrorAction:Stop).

try
{
    # Something in which an exception is likely
}
catch
{
    Write-Host "FAILED: $_"
    Exit
}

The error returned by the CmdLet can be found in $_ , so this can be tested to ensure the error is as expected (just because you though a command might fail, doesn't mean it failed in the way you expected). For example, $_ will contain the bold bit of the following error.

Get-Log : Cannot validate argument on parameter 'StartLineNum'. The 0 argument is less than the minimum allowed range of 1. Supply an argument that is greater than 1 and then try the command again.
At C:\Users\Simon\Documents\Scripts\ESX-LogTail.ps1:20 char:57
+     $ESXLog = Get-Log $logKey -VMHost $ESX -StartLineNum <<<<  $LineNo
    + CategoryInfo          : InvalidData: (:) [Get-Log], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,VMware.VimAutomation.Commands.GetLog 

This can be elaborated on to make the catch handling more specific, by making the catch block executed depend on the error class. The error class can be determined by making causing the error to be thrown, in which case the class can be found at $_.Exception.GetType().FullName .

try {
   $proxy = New-WebServiceProxy -uri $endpoint -cred $credential -ErrorAction:Stop
} catch [System.Net.WebException] {
    Write-Host "ERROR: Unable to connect to SOAP server"
    Write-Host $_
} catch {
    Write-Host "ERROR: Unexpected error"
    Write-Host $_
    Write-Host $_.Exception.Message
    Write-Host $_.Exception.GetType().FullName
}

Trap

Used to capture any unhanded exception that occurs anywhere. I tend to consider this a last resort catch-all, though really it depends on the nature of your script. For example if your script is reliant on a connection to a server that can go down, you can design a trap to recover from that specific occurrence rather than having to put a Try...Catch around every operation that could fail.

The key to an effective trap is allowing for the fact that anything might go wrong, therefore you have to set your traps up to handle only specific cases and in all likelihood stop on anything else.

trap {
    # Handle the exception
    Continue
}

Further Reading