Why does a simple Powershell Interrupter with Start-Job, Wait-Job not return results? - powershell

We have a jenkins git job that hangs on remote checking every so often. So I want to abort the checking with a timeout.
I understand that wait-job will return null if the timer had to stop the background job being awaited. So when a code block is long-running, it should return a null. That works for me in the command line.
However, when a job is short, the below code still getting null when I run the function in the ISE. When I debug it, it works fine. Help?
thank you!
Anne
function Test-TimeoutJob {
<#
.EXAMPLE
BUG:
> Test-TimeoutJob -theCodeBlock {write-output 'hi'}
Test-TimeoutJob : Type of theCodeBlock= scriptblock ; Text= write-output 'hi' .
Test-TimeoutJob : Return code from starting the job = , True .
Id Name PSJobTypeName State HasMoreData Location
-- ---- ------------- ----- ----------- --------
16 Job16 BackgroundJob Running True localhost
Test-TimeoutJob : Got null output of wait-job on id# 16 .
End : Now= 01/25/2016 17:12:02
#>
param (
$theCodeBlock , # infinite; $i=0; do {$i++; echo $i; } while ($true)
$theTimeoutSeconds = 1 # Beware; default=-1sec means wait infinitely #3
)
$thisFcn = 'Test-TimeoutJob'
# If null input, then set it.
if ( ! $theCodeBlock ) {
$theCodeBlock = {
#Show-TimeNow -theMessage "CodeBlock"
#Start-Sleep -Seconds 10
# your commands here, e.g.
Get-ChildItem *.cs | select name
}
}
$theCodeType = $theCodeBlock.GetType()
$theCodeStr = $theCodeBlock.ToString()
Write-Host "$thisFcn : Type of theCodeBlock= $theCodeType ; Text= $theCodeStr ."
$theJob = Start-Job -ScriptBlock $theCodeBlock
write-host "$thisFcn : Return code from starting the job = $LASTEXITCODE , $? ."
$jobid = $theJob.Id
Get-Job $jobid
$answaitobj = Wait-Job $theJob.Id -Timeout $theTimeoutSeconds
if ( $answaitobj -eq $null ) {
Write-Host "$thisFcn : Got null output of wait-job on id# $jobid . "
}
elseif ( $answaitobj ) {
$jobStatus = $theJob.State
$anstype = $answaitobj.GetType()
Write-Host "$thisFcn : the answer, supposed to be job, has type= $anstype ; status= $jobStatus ."
Stop-Job $theJob
$ansId = $theJob.Id
Write-Host "Job $ansId has been ended, with status= $jobStatus ; Thus it has finished or been stopped due to timeout."
# For our purposes of abending a script, we do not need to
# either get its data, which is null, or cleanup, which automatically occurs
# once jenkins finishes the call to posh.
# Get first element of output iterable
#$ans = Receive-Job -Job $theJob -Keep
#Remove-Job -force $theJob
}
Show-TimeNow -theMessage 'End'
}
function Show-TimeNow {
param (
$theMessage = 'Hello from Show-TimeNow'
)
$now = Get-Date
Write-Output "$theMessage : Now= $now"
}

The documentation never promises that Wait-Job would return anything if the timeout expires, so why would you expect it to?
-Timeout<Int32>
Determines the maximum wait time for each background job, in seconds. The default, -1, waits until the job completes, no matter how long it runs. The timing starts when you submit the Wait-Job command, not the Start-Job command.
If this time is exceeded, the wait ends and the command prompt returns, even if the job is still running. No error message is displayed.
Check the state of the job after Wait-Job returned to determine if the job is completed or not:
if ($job.State -eq 'Completed') {
Write-Host 'Finished.'
} else {
Write-Host 'Timeout expired. Stopping job.'
Stop-Job -Id $job.Id
}

The answer turned out that, on my system, one second was too short of a timeout, even for the simplest program. Setting the timeout to a longer number of seconds returned the expected results.

Related

How to Ignore lines with StreamWriter WriteLine

trying to figure out how to ignore or stop specific lines from being written to file with StreamWriter. Here is the code I'm working with from How to pass arguments to program when using variable as path :
$LogDir = "c:\users\user" # Log file output directory
$PlinkDir = "C:" # plink.exe directory
$SerialIP = "1.1.1.1" # serial device IP address
$SerialPort = 10000 # port to log
function CaptureWeight {
Start-Job -Name WeightLog -ScriptBlock {
filter timestamp {
$sw.WriteLine("$(Get-Date -Format MM/dd/yyyy_HH:mm:ss) $_")
}
try {
$sw = [System.IO.StreamWriter]::new("$using:LogDir\WeightLog_$(Get-Date -f MM-dd-yyyy).txt")
& "$using:PlinkDir\plink.exe" -telnet $using:SerialIP -P $using:SerialPort | TimeStamp
}
finally {
$sw.ForEach('Flush')
$sw.ForEach('Dispose')
}
}
}
$job = CaptureWeight # For testing, save the job
Start-Sleep -Seconds 60 # wait 1 minute
$job | Stop-Job # kill the job
Get-Content "$LogDir\WeightLog_$(Get-Date -f MM-dd-yyyy).txt" # Did it work?
And the output is this:
05/09/2022_14:34:19 G+027800 lb
05/09/2022_14:34:20
05/09/2022_14:34:20 G+027820 lb
05/09/2022_14:34:21
05/09/2022_14:34:21 G+027820 lb
05/09/2022_14:34:22
05/09/2022_14:34:22 G+027820 lb
Without the TimeStamp, every other line is blank. I have a couple lines to cleanup the logs, one removes every other line one removes lines with zero weights:
Set-Content -Path "$LogDir\WeightLog_$(get-date -f MM-dd-yyyy).txt" -Value (get-content -Path "$LogDir\WeightLog_$(get-date -f MM-dd-yyyy).txt" | Where-Object { $i % 2 -eq 0; $i++ })
Set-Content -Path "$LogDir\WeightLog_$(get-date -f MM-dd-yyyy).txt" -Value (get-content -Path "$LogDir\WeightLog_$(get-date -f MM-dd-yyyy).txt" | Select-String -Pattern '00000' -NotMatch)
If files get to be too large these can take a while to run, would be nice to not have them written to start with.
Thanks!
Edit, This is what I ended up with:
#****************Serial Scale Weight Logger********************
$LogDir = "c:\ScaleWeightLogger\Logs" # Log File Output Directory
$PlinkDir = "c:\ScaleWeightLogger" # plink.exe Directory
$SerialIP = "1.1.1.1" # Serial Device IP Address
$SerialPort = "10000" # Serial Device Port to Log
$MakeWeight = "000\d\d\d" # Minimum weight to log
[datetime]$JobEndTime = '23:58' # "WeightLog" Job End Time
[datetime]$JobStartTime = '00:02' #Use '8/24/2024 03:00' for a date in the future
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_regular_expressions
function StartWeightCapture {
Start-Job -Name WeightLog -ScriptBlock {
filter timestamp {
# Set Output Filter, Do Not Write Blank Lines or Weight Matching...
if([string]::IsNullOrWhiteSpace($_) -or $_ -match $using:MakeWeight) {
# skip it
return
}
# Set TimeStamp Format Filter
$sw.WriteLine("$(Get-Date -Format MM/dd/yyyy_HH:mm:ss) $_")
}
try {
# Set File Path, Set $true to Append
$sw = [System.IO.StreamWriter]::new("$using:LogDir\WeightLog_$(Get-Date -f MM-dd-yyyy).txt", $true)
# Keep Memory Buffer Clear After Writting
$sw.AutoFlush = $true
# Start plink, Filter Output, Append TimeStamp
& "$using:PlinkDir\plink.exe" -telnet $using:SerialIP -P $using:SerialPort | TimeStamp
}
finally {
# Discard Data After Writing
$sw.ForEach('Flush')
$sw.ForEach('Dispose')
}
}
}
function WeightCaptureEndTime {
[datetime]$CurrentTime = Get-Date
[int]$WaitSeconds = ( $JobEndTime - $CurrentTime ).TotalSeconds
Start-Sleep -Seconds $WaitSeconds
}
function StopWeightCapture {
Stop-Job WeightLog
$AddDaysWhenInPast = 1
[datetime]$CurrentTime = Get-Date
If ($JobStartTime -lt $CurrentTime) { $JobStartTime = $JobStartTime.AddDays($AddDaysWhenInPast) }
[int]$WaitSeconds = ( $JobStartTime - $CurrentTime ).TotalSeconds
Start-Sleep -Seconds $WaitSeconds
}
while ($true) {
StartWeightCapture
WeightCaptureEndTime
StopWeightCapture
}
I'm launching it at boot with:
powershell -windowstyle hidden -ExecutionPolicy bypass "& "C:\ScaleWeightLogger\ScaleWeightLogger.ps1"" & exit
And got this to end it manually since it's in the background. It only grabs the PID of the main powershell process and not the job:
#echo off
for /F "tokens=2" %%K in ('
tasklist /FI "ImageName eq powershell.exe" /FI "Status eq Running" /FO LIST ^| findstr /B "PID:"
') do (
echo "PID is %%K, Ending process..."
taskkill /F /PID %%K
)
pause
exit
If I understand correctly, adding this condition should avoid you the trouble of having to read the logs and skip the unwanted lines.
See String.IsNullOrWhiteSpace(String) Method and -match matching operator for details.
filter timestamp {
# if this output is purely whitespace or it matches `00000`
if([string]::IsNullOrWhiteSpace($_) -or $_ -match '00000') {
# skip it
return
}
$sw.WriteLine("$(Get-Date -Format MM/dd/yyyy_HH:mm:ss) $_")
}
Regarding the observation noted in previous question:
...when trying view the file while it's running, it seems like it updates (for viewing) about every 2 minutes, you get one 2 minute chunk of data that is about 2 minutes behind, the 2 minutes of data is there...
For this, you can enable the AutoFlush property from your StreamWriter.
Remarks has an excellent explanation of when it's worth enabling this property as well as the performance implications:
When AutoFlush is set to false, StreamWriter will do a limited amount of buffering, both internally and potentially in the encoder from the encoding you passed in. You can get better performance by setting AutoFlush to false, assuming that you always call Close (or at least Flush) when you're done writing with a StreamWriter.
For example, set AutoFlush to true when you are writing to a device where the user expects immediate feedback. Console.Out is one of these cases: The StreamWriter used internally for writing to Console flushes all its internal state except the encoder state after every call to StreamWriter.Write.
$sw = [System.IO.StreamWriter]::new("$using:LogDir\WeightLog_$(Get-Date -f MM-dd-yyyy).txt")
$sw.AutoFlush = $true

How to receive progress data from a running job?

I have a need to run a bunch of tasks in the background in parallel.
These jobs can run for hours .
But I need all the jobs to give progress while they are running , that the host can read and give a summary of all the jobs at some time interval . For example last task performed and percent completed.
I did a bunch of searching but I didn't find anything on how the host and all the jobs can communicate at run time to achieve my goal.
How to do this with jobs , or are their other way to run things in parallel that facilitate this ?
There is a sample project here: https://key2consulting.com/powershell-how-to-display-job-progress/
It uses Write-Progress in the job to output the status and a loop that monitors the job and displays a progress bar.
Your jobs would need to be designed to output % complete. I.e. Your job would need to know when it is 20% complete.
Here's another solution that does not use progress bars. Your jobs still need to report their progress with periodic writes. If the jobs return data, the data should be stored to a file or other destination. This code reads and discards the returned data as the returned text is used in the update messages.
A better solution could let each job write the status messages to a SQL table and a simple looping screen reads and displays that data. The job's default response is still ready for Receive-Job
For demo purposes only...
#load your jobs here... Note that they periodically report their status.
Start-Job -ScriptBlock {$i = 0; 1..10 | foreach { Start-Sleep -Seconds 1; $i++; "LoopA $($i*10)% complete" }; Start-Sleep -Seconds 1; }
Start-Job -ScriptBlock {$i = 0; 1..10 | foreach { Start-Sleep -Seconds 2; $i++; "LoopB $($i*10)% complete" }; Start-Sleep -Seconds 1; }
Start-Job -ScriptBlock {$i = 0; 1..10 | foreach { Start-Sleep -Seconds 3; $i++; "LoopC $($i*10)% complete" }; Start-Sleep -Seconds 1; }
$joblist = #{} #empty hash table
# get initial list of jobs...
Get-Job | foreach { $joblist.Add( $_.Name, #{ "State"=$_.State;
"HasMoreData"=$_.HasMoreData;
"LastMsg"="" } ) }
# loop and report status
do
{
$done = $true
Get-Job | foreach {
Start-Sleep -Milliseconds 200; # change for you application
# should be more frequent than job messages
# collect data from job
$joblist[$_.Name]["State"] = $_.State;
$joblist[$_.Name]["HasMoreData"] = $_.HasMoreData;
if ($_.State -eq "Running") {
if ($_.HasMoreData) {
# get the message from the job
$txt = (Receive-Job -Name $_.Name)
if ($txt.Count -gt 0) {
$joblist[$_.Name]["LastMsg"] = $txt;
}
}
$done = $false #still have Running jobs
}
}
# report status
cls
$joblist.GetEnumerator() | select Name, {$_.value.State}, {$_.value.HasMoreData}, {$_.value.LastMsg} | ft
} until ($done)
# final status...
cls
$joblist.GetEnumerator() | select Name, {$_.value.State}, {$_.value.HasMoreData}, {$_.value.LastMsg} | ft
# demo clean up.
#Get-Job | Stop-Job
#Get-Job | Remove-Job

Powershell job unexpectedly returning 'System.Management.Automation.PSObject' object instead of System.Object

I run a maintenance Powershell script which remotely checks Windows server event logs for various entries and then takes appropriate corrective/alerting actions.
The script runs every 5 minutes, but will occasionally run too long due to Get-WinEvent calls timing out with an RPC unavailable error while attempting to query unreachable/unresponsive servers.
To avoid this issue, I am working on wrapping the Get-WinEvent calls in Jobs so that I can apply a configurable timeout to them.
For Get-WinEvent jobs finding multiple events, Receive-Job properly returns a 'System.Object[]' array containing 'System.Diagnostics.Eventing.Reader.EventLogRecord' objects. If only a single event is found, Receive-Job returns a 'System.Management.Automation.PSObject' object instead.
Without the Job-related code, a Get-WinEvent call finding one event returns a non-array 'System.Diagnostics.Eventing.Reader.EventLogRecord' object which can easily be wrapped with an array for downstream consumption.
Anyone have a better way to add a timeout to a remote Get-WinEvent call or an explanation/fix for the 'System.Management.Automation.PSObject' being returned instead of a non-array 'System.Diagnostics.Eventing.Reader.EventLogRecord' object?
The function and some sample calls are shown below:
Function CollectRemoteEvents($the_server,$event_log,$events_to_find,$event_label,$search_start,$search_timeout,$max_event_count){
Try{
$job_info = Start-Job -name GetEvents -scriptblock {param($server,$logname,$eventID,$StartTime,$MaxEvents) Get-WinEvent -ComputerName $server -FilterHashtable #{"logname"=$logname;"id"=$eventID;StartTime=$StartTime} -MaxEvents $MaxEvents} -Arg $the_server,$event_log,$events_to_find,$search_start,$max_event_count
#if the provided timeout value is greater than 0, use it
if($search_timeout -gt 0){
#if the job takes a while, tell it to timeout after ## seconds
$wait_result = Wait-Job -id $job_info.id -timeout $search_timeout
}Else{
#if the timeout was specified as 0, let the job run to completion
$wait_result = Wait-Job -id $job_info.id
}
$current_job_state = Get-Job -id ($job_info.id)
#check if the job has completed before time runs out
if($current_job_state.State -eq "Completed"){
#capture the job object
$job = Get-Job -id ($job_info.id)
#retrieve the output of the job; if the job raises errors, exceptions will be populated into the $joberror variable
#NOTE: the $ is *intentionally* left out of the 'joberror' variable name in the command below
$job_result = $job | Receive-Job -ErrorVariable joberror -ErrorAction Stop
If($joberror -ne "" -And $joberror -ne $null){
#if joberror is not empty, the job failed; log it
# write-host "JobError: '$joberror'" #used for debugging, this would log to file in a production capacity
}Else{
# write-host $job_result.gettype() #used for debugging
return ,$job_result
}
}else{
#the search timed out
# write-host "The event log search timed out." #used for debugging, this would log to file in a production capacity
return $null
}
}Catch [Exception]{
If($_.FullyQualifiedErrorID -eq "NoMatchingEventsFound,Microsoft.PowerShell.Commands.GetWinEventCommand"){
#No logon timeout events were registered since $search_start
write-host "$the_server : No $event_label events were found."
return #()
}Elseif($_.FullyQualifiedErrorID -eq "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.GetWinEventCommand"){
#"argument validation error", exit the function with a return value indicating failure
write-host "$the_server : Event log retrieval failed, can't check for $event_label events (Argument validation error);"
return $null
}Elseif($_.FullyQualifiedErrorID -eq "System.Diagnostics.Eventing.Reader.EventLogException,Microsoft.PowerShell.Commands.GetWinEventCommand"){
#"the RPC server is unavailable", exit the function with a return value indicating failure
write-host "$the_server : Event log retrieval failed, can't check for $event_label events (RPC server unavailable);"
return $null
}Else{
#if the server logs cannot be retrieved, exit the function with a return value indicating failure
write-host "$the_server : Event log retrieval failed, can't check for $event_label events (Check access/permissions)($($_.FullyQualifiedErrorID));"
return $null
}
}
}
$server_name = "localhost"
$system_event_ID = #(6013)
$app_event_ID = #(1033)
$timeout_check_timespan = (Get-Date).AddMonths(-2)
$WinEvent_timeout = 10 #how long to let the Job run before timing out
$returns_array = CollectRemoteEvents $server_name 'System' $system_event_ID "Label One" $timeout_check_timespan $WinEvent_timeout 5
$returns_non_array = CollectRemoteEvents $server_name 'Application' $app_event_ID "Label Two" $timeout_check_timespan $WinEvent_timeout 1
write-host ""
write-host $returns_array
write-host $returns_array.count
write-host ""
write-host $returns_non_array
write-host $returns_non_array.count
The comma on the main return line is attempt to force an array to be returned (see: Count property of array in PowerShell with pscustomobjects )
I have also tried instantiating an array and then adding the result set to it:
$var = #()
$var += $results
return $var
casting the result set as an array:
return [Array]($results)
and returning the result set as part of an array:
return #($results)
I believe that this is a different issue than the one covered in the 'Function return value in Powershell' proposed solution - in my issue the problem of the object types is present before the function returns.
Uncommenting the following line for debugging purposes
# write-host $job_result.gettype() #used for debugging
Results in the following output being printed:
System.Object[]
System.Management.Automation.PSObject
The System.Object[] line is returned by a Job running a Get-WinEvent query that finds multiple events.
The 'System.Management.Automation.PSObject' line is returned by a Job running a Get-WinEvent query that finds a single event
After lots of googling based upon a suggestion from a Reddit user, it appears that you effectively have to double-wrap the single-object return content to have it end up as an array:
#this *does not* work
return #(#($job_result))
#This works
return , #($job_result)

Memory leak in Powershell 3.0 script to monitor log file

I've written a script in Powershell 3.0 to monitor a log file for specific errors. The script starts a background process, which monitors the file. When anything gets written to the file, the background process simply passes it to the foreground process, if it matches the proper format (a datestamped line). The foreground process then counts the number of errors.
Everything works correctly with no errors. The issue is that, as the source logfile grows in size, the memory consumed by Powershell increases dramatically. These logs are capped at ~24M before they are rotated, which amounts to ~250K lines. In my tests, by the time the log size reaches ~80K lines or so, the monitor process is consuming 250M RAM (foreground and background processes combined. They're consuming ~70M combined when they first start. This type of growth is unacceptable in our environment. What can I do to decrease this?
Here's the script:
# Constants.
$F_IN = "C:\Temp\test.log"
$RE = "^\d+-\d+-\d+ \d+:\d+:\d+,.+ERROR.+Foo$"
$MAX_RESTARTS = 3 # Max restarts for failed background job.
$SLEEP_DELAY = 60 # In seconds.
# Background job.
$SCRIPT_BLOCK = { param($f, $r)
Get-Content -Path $f -Tail 0 -Wait -EA SilentlyContinue `
| Where { $_ -match $r }
}
function Start-FileMonitor {
Param([parameter(Mandatory=$true,Position=0)][alias("f")]
[String]$file,
[parameter(Mandatory=$true,Position=1)][alias("b")]
[ScriptBlock]$SCRIPT_BLOCK,
[parameter(Mandatory=$true,Position=2)][alias("re","r")]
[String]$regex)
$j = Start-Job -ScriptBlock $SCRIPT_BLOCK -Arg $file,$regex
return $j
}
function main {
# Tail log file in the background, return any errors.
$job = Start-FileMonitor -b $SCRIPT_BLOCK -f $F_IN -r $RE
$restarts = 0 # Current number of restarts.
# Poll background $job every $SLEEP_DELAY seconds.
While ($true) {
$a = (Receive-Job $job | Measure-Object)
If ($job.JobStateInfo.State -eq "Running") {
$restarts = 0
If ($a.Count -gt 0) {
$t0 = $a.Count
Write-Host "Error Count: ${t0}"
}
}
Else {
If ($restarts -lt $MAX_RESTARTS) {
$job = Start-FileMonitor -b $SCRIPT_BLOCK -f $F_IN -r $RE
$restarts++
Write-Host "Background job not running. Attempted restart ${restarts}."
}
Else {
Write-Host "`$MAX_RESTARTS (${MAX_RESTARTS}) exceeded. Exiting."
Break
}
}
# Sleep for $SLEEP_DELAY.
Start-Sleep -Seconds $SLEEP_DELAY
}
Write-Host "Done."
}
# Execute script.
main
...and here's the sample data:
2015-11-19 00:00:00, WARN Foo
2015-11-19 00:00:00, ERROR Foo
In order to replicate this issue:
Paste the sample data lines into the file C:\Temp\test.log. Save.
Start the monitoring script.
Paste additional sample data lines into the log and save. Wait for the Error Count: line to confirm that everything is working correctly.
Continue to paste additional lines and watch the memory consumption for powershell.exe in Task Manager. Note how much it increases at 400 lines...800 lines...8,000 lines...80,000 lines...

Powershell: How do I get the exit code returned from a process run inside a PsJob?

I have the following job in powershell:
$job = start-job {
...
c:\utils\MyToolReturningSomeExitCode.cmd
} -ArgumentList $JobFile
How do I access the exit code returned by c:\utils\MyToolReturningSomeExitCode.cmd ? I have tried several options, but the only one I could find that works is this:
$job = start-job {
...
c:\utils\MyToolReturningSomeExitCode.cmd
$LASTEXITCODE
} -ArgumentList $JobFile
...
# collect the output
$exitCode = $job | Wait-Job | Receive-Job -ErrorAction SilentlyContinue
# output all, except the last line
$exitCode[0..($exitCode.Length - 2)]
# the last line is the exit code
exit $exitCode[-1]
I find this approach too wry to my delicate taste. Can anyone suggest a nicer solution?
Important, I have read in the documentation that powershell must be run as administrator in order for the job related remoting stuff to work. I cannot run it as administrator, hence -ErrorAction SilentlyContinue. So, I am looking for solutions not requiring admin privileges.
Thanks.
If all you need is to do something in background while the main script does something else then PowerShell class is enough (and it is normally faster). Besides it allows passing in a live object in order to return something in addition to output via parameters.
$code = #{}
$job = [PowerShell]::Create().AddScript({
param($JobFile, $Result)
cmd /c exit 42
$Result.Value = $LASTEXITCODE
'some output'
}).AddArgument($JobFile).AddArgument($code)
# start thee job
$async = $job.BeginInvoke()
# do some other work while $job is working
#.....
# end the job, get results
$job.EndInvoke($async)
# the exit code is $code.Value
"Code = $($code.Value)"
UPDATE
The original code was with [ref] object. It works in PS V3 CTP2 but does not work in V2. So I corrected it, we can use other objects instead, a hashtable, for example, in order to return some data via parameters.
One way you can detect if the background job failed or not based on an exit code is to evaluate the exit code inside the background job itself and throw an exception if the exit code indicates an error occurred. For instance, consider the following example:
$job = start-job {
# ...
$output = & C:\utils\MyToolReturningSomeExitCode.cmd 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Job failed. The error was: {0}." -f ([string] $output)
}
} -ArgumentList $JobFile
$myJob = Start-Job -ScriptBlock $job | Wait-Job
if ($myJob.State -eq 'Failed') {
Receive-Job -Job $myJob
}
A couple things of note in this example. I am redirecting the standard error output stream to the standard output stream to capture all textual output from the batch script and returning it if the exit code is non-zero indicating it failed to run. By throwing an exception this way the background job object State property will let us know the result of the job.