PowerShell, and Process with a Timeout

In this post, I’m going to demonstrate how to start a process in PowerShell through .NET, redirect the StdOut/StdErr streams, and set a fail-safe timeout. I’ve used this method over the years to start a process with a purpose - literally side by side in a lifeboat, trying to save a system from sinking. In other words, starting a process to quickly solve the "problem of the day" on many endpoints.

​​​​​​​

We all work in environments where a little housekeeping needs to be done. There are times when this housekeeping isn’t done as well as it should be (or even at all). There are things such as name resolution, intermittent or inconsistent connectivity, and hardware issues to name a few, that hinder the ability to quickly access endpoints. Some endpoints such as remote, hard to access laptops are really challenging. The timeout is really the key to the efficiencies. There is no need to wait for RPC Errors to timeout, etc. Threading this method becomes incredibly fast.

The example I’m going to show below will chunk through a list of endpoints very quickly, regardless of their state. There are many applications for starting a process and reading the standard output; remote command line administration is just one of them. I’ve recently used this to parse output from the “query /user:<server>” command to gain situational awareness for who is logged into servers. It is really nice not having to redirect standard output to a file to read it.

Here is the code:

Code: function Start-ProcessWaitTimeout { <# .SYNOPSIS Function to start a process and wait until it completes or reaches a specified time limit in seconds .DESCRIPTION Function to start a process and wait until it completes or reaches a specified time limit in seconds #> [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName)] [string]$Computer, [Parameter(ValueFromPipelineByPropertyName)] [string]$CmdLine, [Parameter(ValueFromPipelineByPropertyName)] [array]$CmdLineArgs, [Parameter(Mandatory=$true)] [int]$Timeout ) Begin { # Initialize list to hold the process objects $ProcessList = New-Object System.Collections.Generic.List[PSObject] # Establish Functions to Create and Wait for the Process function New-Process { <# .SYNOPSIS Create a process .DESCRIPTION Create a process #> param ( [Parameter(Mandatory=$true)] [string]$CmdLine, [Parameter(Mandatory=$true)] $CmdLineArgs ) ## SET THE PROCESS INFO $processInfo = New-Object System.Diagnostics.ProcessStartInfo $processInfo.FileName = $cmdLine $processInfo.RedirectStandardError = $true $processInfo.RedirectStandardOutput = $true $processInfo.UseShellExecute = $false ## SET THE PROCESS $processInfo.Arguments = $cmdLineArgs $process = New-Object System.Diagnostics.Process $process.StartInfo = $processInfo # Return the Process Return $process } function Wait-ForProcess { <# .SYNOPSIS Wait for a process to complete. .DESCRIPTION Wait for a process to complete. #> param ( [Parameter(Mandatory=$true)] [System.Diagnostics.Process]$Process, [Parameter(Mandatory=$true)] [int]$TimeoutSeconds ) $ProcessComplete = $Process.WaitForExit($TimeoutSeconds * 1000) if ($ProcessComplete -eq $false) { $Process.Kill() Return -1 } else { Return 0 } } } # End Begin Process { # Create the Process Object $ProcessObj = [PSCustomObject]@{ Computer = $computer CmdLine = $cmdLine CmdLineArgs = $cmdLineArgs Timeout = $timeout ProcessStartTime = $null ProcessEndTime = $null ProcessDuration = $null ProcessStdOut = $null ProcessStdErr = $null ProcessExitCode = $null ProcessResult = $null Result = $null ObjTimeStamp = (get-date) } # Create the process $Process = New-Process -CmdLine $CmdLine -CmdLineArgs $CmdLineArgs # START THE PROCESS $Process.Start() | Out-Null $ProcessObj.ProcessStartTime = $process.StartTime # READ STD OUT AND ERROR TO END ASYNC SO BUFFER DOESNT FILL AND HANG PROCESS $stdOut = $process.StandardOutput.ReadToEndAsync() $stdErr = $process.StandardError.ReadToEndAsync() # WAIT for the Process to Complete $WaitProcess = Wait-ForProcess $Process $Timeout if ($WaitProcess -eq 0) { $processObj.ProcessResult = $Process.ProcessExitCode } if ($WaitProcess -eq -1) { $processObj.ProcessResult = "Timeout" } ## SET THE EXIT CODE STD OUT/ERR, END TIME, DURATION $processObj.ProcessExitCode = $process.ExitCode $processObj.ProcessStdOut = $stdOut.Result $processObj.ProcessStdErr = $stdErr.Result $processObj.ProcessEndTime = $process.ExitTime $processObj.ProcessDuration = $processObj.ProcessEndTime - $processObj.ProcessStartTime ## RETURN THE PROCESS OBJECT $ProcessList.Add($processObj) } End { # Return the Process List Return $ProcessList } } $Obj1 = [PSCustomObject]@{ Computer = "localhost" CmdLine = "C:\Windows\System32\cmd.exe" CmdLineArgs = @("/c","copy","/y","\\Computer1\c$\Windows\SoftwareDistribution\ReportingEvents.Log","$($env:TEMP)\localhost_ReportingEvents.log") } $Obj2 = [PSCustomObject]@{ Computer = "Computer2" CmdLine = "C:\Windows\System32\cmd.exe" CmdLineArgs = @("/c","copy","/y","\\Computer2\c$\Windows\SoftwareDistribution\ReportingEvents.Log","$($env:TEMP)\Computer2_ReportingEvents.log") } $Obj1, $Obj2 | Start-ProcessWaitTimeout -Timeout 2

This is my hammer.

Feedback is welcomed and follow Dustin Higgins on Twitter

DHB