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

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)

Related

In Windows Powershell, how can I wait for an event to be true before proceeding?

I'm writing a Windows Powershell script that stops a service, then I want to print the service's status when it's finally stopped. I've tried the following, but it just hangs
$service = Get-Service | Where-Object {$_.name -like "*MyService*"}
Stop-Service -Name $service.name
$wait=true
while ($wait) {
if($service.Status -eq "Running") {
Start-Sleep -Seconds 1
}
else {
$wait=$false
}
}
I know I can probably write a for{} loop instead that counts 0-9, and breaks when my condition is met, but is there a better way?
tanstaafl's helpful answer addresses your immediate problem:
The .Status property value of a [System.ServiceProcess.ServiceController] instance (as returned by Get-Service) is a static value that only reflects the status of the service at the time of the Get-Service call.[1]
To update the value to reflect the then-current status, call the .Refresh() method.
However, there is no need to explicitly wait for a service to stop, because Stop-Service is synchronous, i.e.:
It waits for the service to finish stopping before returning, unless you explicitly pass -NoWait.
If that doesn't happen within a fixed, 2-second timeout:[2]
A warning is issued if the service last reported that stopping is pending - potentially, stopping will eventually finish.
Otherwise, a non-terminating error occurs - this suggests that the service is stuck.
Thus, you can simplify your code as follows:
# Report a script-terminating error if stopping doesn't finish
# within the timeout period.
Stop-Service -Name *MyService* -ErrorAction Stop -WarningAction Stop
More work is needed if you want to implement a retry mechanism.
[1] There is one exception, although the behavior is undocumented and should be considered an implementation detail: If you pipe a preexisting ServiceController instance to Stop-Service / Start-Service, these cmdlets refresh the instance for you; e.g., after executing ($service = Get-Service Bits) | Stop-Service, $service.Status is current (reflects Stopped).
[2] As of PowerShell Core 7.3.0-preview.2 - see the source code.
You need to re-check your service within the loop.
$service = Get-Service | Where-Object {$_.name -like "*MyService*"}
Stop-Service -Name $service.name
$wait=true
while ($wait) {
if($service.Status -eq "Running") {
Start-Sleep -Seconds 1
# ADD THIS BELOW. Need to re-check service in loop.
$service = Get-Service | Where-Object {$_.name -like "*MyService*"}
}
else {
$wait=$false
}
}

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

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.

echo in while loop get's added to my return value

I wasn't sure how to describe this problem in the title so here goes.
I call a function from a script in another script. In that function i have a while loop that basically keeps looping through a set of ip's and looks up their hostname. when the while loop times out or we have all the host names.
it returns the hostnames.
My problem is that the return value contains every single Write-Host i'm doing in that function.
i know it's because Write-Host puts stuff on the pipeline and the return just returns whatever it has.
How do i go about fixing this?
The entire script i run get's logged in a log file which is why i want to have some verbose logging.
| out-null on write-host fixes the issue but it doesn't print the write-host values in the script.
in main.psm1 i have a function like so:
$nodes = #("ip1", "ip2", "ip3", "ip4")
$nodesnames = DoStuff -nodes $nodes
then in functions.psm1 i have functions like:
Function DoStuff
{
param($nodes)
$timeout = 300
$timetaken = 0
$sleepseconds = 5
$nodenames = #("$env:COMPUTERNAME")
while(($nodenames.count -lt $nodes.count) -and ($timetaken -lt $timeout))
{
try
{
Write-Host "Stuff"
foreach($node in $nodes)
{
$nodename = SuperawesomeFunction $node
Write-Host "$nodename"
if($nodenames -notcontains $nodename)
{
$nodenames += #($nodename)
}
}
}
catch
{
Write-Host "DoStuff Failed because $_"
}
Start-Sleep $sleepseconds
$timetaken += $sleepseconds
}
return $nodenames
}
Function SuperawesomeFunction
{
param($node)
$nodename = [System.Net.Dns]::GetHostEntry("$node")
return $nodename
}
Thanks.
So the answer is, your function is working like it is by design. In PowerShell a function will return output in general to the pipeline, unless specifically directed otherwise.
You used Echo before, which is an alias of Write-Output, and output is passed down the pipe as I mentioned before. As such it would be collected along with the returned $nodenames array.
Replacing Echo with Write-Host changes everything because Write-Host specifically tells PowerShell to send the information to the host application (usually the PowerShell Console or PowerShell ISE).
How do you avoid this? You could add a parameter specifying a path for a logfile, and have your function update the logfile directly, and only output the relevant data.
Or you can make an object with a pair of properties that gets passed back down the pipe which has the DNS results in one property, and the errors in another.
You could use Write-Error in the function, and set it up as an advanced function to support -errorvariable and capture the errors in a separate variable. To be honest, I'm not sure how to do that, I've never done it, but I'm 90% sure that it can be done.

How to run a command against multiple servers simultaneously in Powershell

I am looking for a way to restart three services on multiple servers simultaneously. I know how to restart services against a list of servers by using a loop but as I have many servers it would take a long time to wait for each service on each server to restart in a sequential order. Is there a way to send restart service command to all servers at once instead of waiting for each server?
You could try to work with jobs. Jobs are run in the background and you have to retrieve them with Get-Job to see their status. Please read the information to Powershell jobs on these two sites:
http://msdn.microsoft.com/en-us/library/dd878288%28v=vs.85%29.aspx
http://technet.microsoft.com/de-DE/library/hh847783.aspx
Your code would look something like this:
$servernames | ForEach-Object {Start-Job -Name "Job-$_" -Scriptblock {"Enter your code here -Computername $_"}}
This will create a background job for each servername. As already mentioned you can see the status using the cmdlet Get-Job. To get the result use the cmdlet Receive-Job.
you can use the invoke-command cmdlet
invoke-command -computername computer1,computer2,computer3 {restart-service servicename}
I use and improove a multi-thread Function, you can use it like :
$Script = {
param($Computername)
restart-service servicename -Computername $Computername
}
#('Srv1','Srv2') | Run-Parallel -ScriptBlock $Script
include this code in your script
function Run-Parallel {
<#
.Synopsis
This is a quick and open-ended script multi-threader searcher
http://www.get-blog.com/?p=189#comment-28834
Improove by Alban LOPEZ 2016
.Description
This script will allow any general, external script to be multithreaded by providing a single
argument to that script and opening it in a seperate thread. It works as a filter in the
pipeline, or as a standalone script. It will read the argument either from the pipeline
or from a filename provided. It will send the results of the child script down the pipeline,
so it is best to use a script that returns some sort of object.
.PARAMETER ScriptBlock
This is where you provide the PowerShell ScriptBlock that you want to multithread.
.PARAMETER ItemObj
The ItemObj represents the arguments that are provided to the child script. This is an open ended
argument and can take a single object from the pipeline, an array, a collection, or a file name. The
multithreading script does it's best to find out which you have provided and handle it as such.
If you would like to provide a file, then the file is read with one object on each line and will
be provided as is to the script you are running as a string. If this is not desired, then use an array.
.PARAMETER InputParam
This allows you to specify the parameter for which your input objects are to be evaluated. As an example,
if you were to provide a computer name to the Get-Process cmdlet as just an argument, it would attempt to
find all processes where the name was the provided computername and fail. You need to specify that the
parameter that you are providing is the "ComputerName".
.PARAMETER AddParam
This allows you to specify additional parameters to the running command. For instance, if you are trying
to find the status of the "BITS" service on all servers in your list, you will need to specify the "Name"
parameter. This command takes a hash pair formatted as follows:
#{"key" = "Value"}
#{"key1" = "Value"; "key2" = 321; "key3" = 1..9}
.PARAMETER AddSwitch
This allows you to add additional switches to the command you are running. For instance, you may want
to include "RequiredServices" to the "Get-Service" cmdlet. This parameter will take a single string, or
an aray of strings as follows:
"RequiredServices"
#("RequiredServices", "DependentServices")
.PARAMETER MaxThreads
This is the maximum number of threads to run at any given time. If ressources are too congested try lowering
this number. The default value is 20.
.PARAMETER SleepTimer_ms
This is the time between cycles of the child process detection cycle. The default value is 200ms. If CPU
utilization is high then you can consider increasing this delay. If the child script takes a long time to
run, then you might increase this value to around 1000 (or 1 second in the detection cycle).
.PARAMETER TimeOutGlobal
this is the TimeOut in second for listen the last thread, after this timeOut All thread are closed, only each other are returned
.PARAMETER TimeOutThread
this is the TimeOut in second for each thread, the thread are aborted at this time
.PARAMETER PSModules
List of PSModule name to include for use in ScriptBlock
.PARAMETER PSSapins
List of PSSapin name to include for use in ScriptBlock
.EXAMPLE
1..20 | Run-Parallel -ScriptBlock {param($i) Start-Sleep $i; "> $i sec <"} -TimeOutGlobal 15 -TimeOutThread 5
.EXAMPLE
Both of these will execute the scriptBlock and provide each of the server names in AllServers.txt
while providing the results to GridView. The results will be the output of the child script.
gc AllServers.txt | Run-Parallel $ScriptBlock_GetTSUsers -MaxThreads $findOut_AD.ActiveDirectory.Servers.count -PSModules 'PSTerminalServices' | out-gridview
#>
Param(
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
$ItemObj,
[ScriptBlock]$ScriptBlock = $null,
$InputParam = $Null,
[HashTable] $AddParam = #{},
[Array] $AddSwitch = #(),
$MaxThreads = 20,
$SleepTimer_ms = 100,
$TimeOutGlobal = 300,
$TimeOutThread = 100,
[string[]]$PSSapins = $null,
[string[]]$PSModules = $null,
$Modedebug = $true
)
Begin{
$ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
ForEach ($Snapin in $PSSapins){
[void]$ISS.ImportPSSnapIn($Snapin, [ref]$null)
}
ForEach ($Module in $PSModules){
[void]$ISS.ImportPSModule($Module)
}
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host)
$RunspacePool.CleanupInterval=1000
$RunspacePool.Open()
$Jobs = #()
}
Process{
#ForEach ($Object in $ItemObj){
if ($ItemObj){
Write-Host $ItemObj -ForegroundColor Yellow
$PowershellThread = [powershell]::Create().AddScript($ScriptBlock)
If ($InputParam -ne $Null){
$PowershellThread.AddParameter($InputParam, $ItemObj.ToString()) | out-null
}Else{
$PowershellThread.AddArgument($ItemObj.ToString()) | out-null
}
ForEach($Key in $AddParam.Keys){
$PowershellThread.AddParameter($Key, $AddParam.$key) | out-null
}
ForEach($Switch in $AddSwitch){
$PowershellThread.AddParameter($Switch) | out-null
}
$PowershellThread.RunspacePool = $RunspacePool
$Handle = $PowershellThread.BeginInvoke()
$Job = [pscustomobject][ordered]#{
Handle = $Handle
Thread = $PowershellThread
object = $ItemObj.ToString()
Started = Get-Date
}
$Jobs += $Job
}
#}
}
End{
$GlobalStartTime = Get-Date
$continue = $true
While (#($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0 -and $continue) {
ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){
$out = $Job.Thread.EndInvoke($Job.Handle)
$out # return vers la sortie srandard
#Write-Host $out -ForegroundColor green
$Job.Thread.Dispose() | Out-Null
$Job.Thread = $Null
$Job.Handle = $Null
}
foreach ($InProgress in $($Jobs | Where-Object {$_.Handle})) {
if ($TimeOutGlobal -and (($(Get-Date) - $GlobalStartTime).totalseconds -gt $TimeOutGlobal)){
$Continue = $false
#Write-Host $InProgress -ForegroundColor magenta
}
if (!$Continue -or ($TimeOutThread -and (($(Get-Date) - $InProgress.Started).totalseconds -gt $TimeOutThread))) {
$InProgress.thread.Stop() | Out-Null
$InProgress.thread.Dispose() | Out-Null
$InProgress.Thread = $Null
$InProgress.Handle = $Null
#Write-Host $InProgress -ForegroundColor red
}
}
Start-Sleep -Milliseconds $SleepTimer_ms
}
$RunspacePool.Close() | Out-Null
$RunspacePool.Dispose() | Out-Null
}
}

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.