Rank: Advanced Member
Groups: Registered
Joined: 7/1/2018(UTC) Posts: 64  Thanks: 1 times Was thanked: 6 time(s) in 6 post(s)
|
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 Edited by user Tuesday, December 10, 2019 3:45:23 PM(UTC)
| Reason: Not specified
|