1

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

Community
  • 1
  • 1
Graham
  • 21
  • 6
  • Try removing the comma from `return ,$job_result` – Jan Chrbolka Feb 10 '16 at 03:23
  • The comma is attempt to force an array to be returned (see: http://stackoverflow.com/questions/31514102/count-property-of-array-in-powershell-with-pscustomobjects ) – Graham Feb 10 '16 at 03:37
  • Possible duplicate of [Function return value in PowerShell](http://stackoverflow.com/questions/10286164/function-return-value-in-powershell) – Eris Feb 10 '16 at 08:09
  • The problematic/unexpected data type being encountered is in the variable populated by Receive-Job, so it happens within the function before any external code receives the returned data. – Graham Feb 10 '16 at 12:14
  • Try using `$var.psbase` rather than `$var`. `PSobject` is, in my understanding, supposed to be a wrapper around a normal .NET object. So under the covers most everything in a script would be a `PSObject`, but PS would normally hide that. However in some cases PS seems to get fooled (confused?) about an object's PSObject wrapper. I think it may be related to an object crossing Runspaces, because of your case here and similar behavior when receiving objects created via a script run in a Powershell created by `[system.management.automation.powershell]::Create()`. – Χpẘ Feb 10 '16 at 23:47

1 Answers1

1

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)
Graham
  • 21
  • 6