I'm new to Powershell. I need to capture the output from a command-line call in Powershell continuously and preferably send it to another function. My code currently waits until the external program is completely finished. Here's what I've got:
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = "DataSet.exe"
$startInfo.CreateNoWindow = $true
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardError = $true
$startInfo.RedirectStandartOutput = $true
$startInfo.Arguments = "1 off"
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
$process.Start() | Out-Null
$stdOut = $process.StandardOutput.ReadToEnd()
$stdErr = $process.StandardError.ReadToEnd()
$process.WaitForExit()
process($stdOut)
process($stdErr)
# Do stuff with $process.ExitCode
Here's what I need
...
while (readOutput)
{
process($stdOut)
process($stdErr)
}
...
I would recommend using a background job for this kind of thing, e.g. like this:
$job = Start-Job -ScriptBlock { & "some.exe" }
while ($job.State -eq 'Running') {
Receive-Job $job -OutVariable $outValue -ErrorVariable $errValue
if ($outValue) { Do-SomethingWith $outValue }
ir ($errValue) { Do-OtherWith $errValue }
Start-Sleep -Milliseconds 200
}
Related
Help me please figure out how asynchronous loop execution works.
In many forums, examples are very cumbersome and not understandable
During execution, the program window freezes, and at the end of the cycle it shows the final number
for example
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
[xml]$XAML = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="Рассылка интернет-трафика" Height="200" Width="300" ResizeMode="NoResize">
<Grid Width="300">
<TextBox Name="text_result" TextWrapping="Wrap" Margin="0,0,0,0" VerticalAlignment="Top" Height="31" Width="200" />
<Button Name="button_start" Content="Start" Height="31" Margin="0,50,0,0" VerticalAlignment="Top" Width="200" />
<Button Name="button_close" Content="Close" Height="31" Margin="0,100,0,0" VerticalAlignment="Top" Width="200"/>
</Grid>
</Window>
"#
#Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
catch{Write-Host "Unable to load Windows.Markup.XamlReader"; exit}
# Store Form Objects In PowerShell
$xaml.SelectNodes("//*[#Name]") | ForEach-Object {Set-Variable -Name ($_.Name) -Value $Form.FindName($_.Name)}
$button_start.Add_Click({
for ($row = 2; $row -le 1000; $row++) {
$text_result.Text = $row
}
})
#Загружаем главную форму
$Form.ShowDialog() | out-null
It took me quite a while to figure this one out. Let me preface by saying that Adam the Automator has a fantastic breakdown:
https://adamtheautomator.com/building-asynchronous-powershell-functions/
The easiest way to think about executing an asynchronous loop is to think about tricking powershell. In short, you just want it to do something in the background while your script is running. So, you just need to turn the loop you would like into a script block and use start-job, a.k.a. &.
Let's use a common task, such as looking through a bunch of files. Here, we'll use gci.
$scriptblock = {
param ($folder)
Get-ChildItem -Path $folder}
Start-job -ScriptBlock $scriptblock -ArgumentList "C:\Windows" -name "stackexample"
The name is important so that you can reference it later. Now you have a job in the background, and you're good to continue with your script. But what happens if you want to see if the job is done? There's a command for checking on the status of your job (code below).
Get-Job -Name stackexample
One thing to note, make sure to CLOSE your jobs once they're done.
$status = (Get-Job -Name stackexample).state
if ($status -eq "completed")
{
Remove-Job -Name stackexample
}
Wait, I need to know what those folders were! No problem. We just need to tell the scriptblock to output data (write-host, for example). Then, we receive the job before we close it. So a full circuit might look like this:
$scriptblock = {
param ($folder)
$folders = Get-ChildItem -Path $folder
write-host $folders
}
Start-job -ScriptBlock $scriptblock -ArgumentList "C:\Windows" -name "stackexample"
Start-Sleep -Seconds 5
$status = (Get-Job -Name stackexample).state
if ($status -eq "completed")
{
Receive-Job -Name StackExample -Wait
Remove-Job -Name stackexample
}
You can get so much fancier from here, but that is the core of what 'asynchronous code execution' is - telling powershell to do something time consuming while you're doing other stuff.
As applied to your code, you can use the button click to set off a job and run in the background, then receive the final results.
$xaml.SelectNodes("//*[#Name]") | ForEach-Object {Set-Variable -Name ($_.Name) -Value $Form.FindName($_.Name)}
$scriptblock = {
for ($row = 2; $row -le 1000; $row++)
{
$result = $row
}
return $result
}
$button_start.Add_Click({
Start-job -ScriptBlock $scriptblock -Name scriptblock
While ((Get-Job -Name scriptblock).State -ne "Completed")
{
start-sleep 1
}
$results = receive-job -Name scriptblock -keep
remove-job -Name scriptblock
$text_result.Text = $results
})
Above the button click we've defined the script block. When the button is clicked, it kicks off the job and then watches it with the while loop. Once the job is finished, we can extract the result (done here with return $result in the scriptblock). Receive the job and you have your final variable which you can now display for the user.
With no cycle.
The form is just a name in this case. You can remove all GUI elements from it if you want.
$Script:SyncHashTable = [Hashtable]::Synchronized(#{})
$RunSpace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$RunSpace.ApartmentState = "STA"
$RunSpace.ThreadOptions = "ReuseThread"
$RunSpace.Open()
$RunSpace.SessionStateProxy.SetVariable("SyncHashTable", $Script:SyncHashTable)
$PowerShellCmd = [Management.Automation.PowerShell]::Create().AddScript({
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Script:SyncHashTable.objForm = New-Object System.Windows.Forms.Form
$Script:SyncHashTable.InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
$Script:SyncHashTable.objForm.Size = New-Object System.Drawing.Size(340, 200)
$Script:SyncHashTable.objForm.Name = "objForm"
$Script:SyncHashTable.objForm.Text = "Test form"
$Script:SyncHashTable.objLabel = New-Object System.Windows.Forms.Label
$Script:SyncHashTable.objLabel.Location = New-Object System.Drawing.Size(10,20)
$Script:SyncHashTable.objLabel.Size = New-Object System.Drawing.Size(320,20)
$Script:SyncHashTable.objLabel.Text = "Enter the data and press the OK button or key Enter"
$Script:SyncHashTable.objForm.Controls.Add($Script:SyncHashTable.objLabel)
$Script:SyncHashTable.objTextBox = New-Object System.Windows.Forms.TextBox
$Script:SyncHashTable.objTextBox.Location = New-Object System.Drawing.Size(10,40)
$Script:SyncHashTable.objTextBox.Size = New-Object System.Drawing.Size(300,20)
$Script:SyncHashTable.objForm.Controls.Add($Script:SyncHashTable.objTextBox)
$Script:SyncHashTable.objForm.KeyPreview = $True
$Script:SyncHashTable.objForm.Add_KeyDown( { if ($_.KeyCode -eq "Enter") { $Script:SyncHashTable.X = $Script:SyncHashTable.objTextBox.Text;$Script:SyncHashTable.objForm.Close() } })
$Script:SyncHashTable.objForm.Add_KeyDown( { if ($_.KeyCode -eq "Escape") { $objForm.Close() } })
$Script:SyncHashTable.OKButton = New-Object System.Windows.Forms.Button
$Script:SyncHashTable.OKButton.Location = New-Object System.Drawing.Size(75,120)
$Script:SyncHashTable.OKButton.Size = New-Object System.Drawing.Size(75,23)
$Script:SyncHashTable.OKButton.Text = "OK"
$Script:SyncHashTable.OKButton.Add_Click( { $Script:SyncHashTable.X = $Script:SyncHashTable.objTextBox.Text;$Script:SyncHashTable.objForm.Close() } )
$Script:SyncHashTable.objForm.Controls.Add($Script:SyncHashTable.OKButton)
$Script:SyncHashTable.InitialFormWindowState = $Script:SyncHashTable.objForm.WindowState
$Script:SyncHashTable.objForm.add_Load($Script:SyncHashTable.OnLoadForm_StateCorrection)
[void] $Script:SyncHashTable.objForm.ShowDialog()
})
$PowerShellCmd.Runspace = $RunSpace
$obj=$PowerShellCmd.BeginInvoke()
#Time to retrieve our missing object $AsyncObject
$BindingFlags = [Reflection.BindingFlags]'nonpublic','instance'
$Field = $PowerShellCmd.GetType().GetField('invokeAsyncResult',$BindingFlags)
$AsyncObject = $Field.GetValue($PowerShellCmd)
Write-Host 'Here is your code that will be executed in parallel with the form code'
Write-Host '(any length and execution time)'
Write-Host "`$PowerShellCmd.EndInvoke(`$AsyncObject) will wait for results from the form,"
Write-Host "or pick them up if they are ready."
Write-Host "=================================="
#Closing the execution space when getting the result
$PowerShellCmd.EndInvoke($AsyncObject)
'This is the result of executing the form code: {0}' -f $Script:SyncHashTable.X
Is there a bug in PowerShell's Start-Process command when accessing the StandardError and StandardOutput properties?
If I run the following I get no output:
$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.StandardOutput
$process.StandardError
But if I redirect the output to a file I get the expected result:
$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait -RedirectStandardOutput stdout.txt -RedirectStandardError stderr.txt
That's how Start-Process was designed for some reason. Here's a way to get it without sending to file:
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
In the code given in the question, I think that reading the ExitCode property of the initiation variable should work.
$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.ExitCode
Note that (as in your example) you need to add the -PassThru and -Wait parameters (this caught me out for a while).
IMPORTANT:
We have been using the function as provided above by LPG.
However, this contains a bug you might encounter when you start a process that generates a lot of output. Due to this you might end up with a deadlock when using this function. Instead use the adapted version below:
Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
Try {
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $commandArguments
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
[pscustomobject]#{
commandTitle = $commandTitle
stdout = $p.StandardOutput.ReadToEnd()
stderr = $p.StandardError.ReadToEnd()
ExitCode = $p.ExitCode
}
$p.WaitForExit()
}
Catch {
exit
}
}
Further information on this issue can be found at MSDN:
A deadlock condition can result if the parent process calls p.WaitForExit before p.StandardError.ReadToEnd and the child process writes enough text to fill the redirected stream. The parent process would wait indefinitely for the child process to exit. The child process would wait indefinitely for the parent to read from the full StandardError stream.
I also had this issue and ended up using Andy's code to create a function to clean things up when multiple commands need to be run.
It'll return stderr, stdout, and exit codes as objects. One thing to note: the function won't accept .\ in the path; full paths must be used.
Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $commandArguments
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
[pscustomobject]#{
commandTitle = $commandTitle
stdout = $p.StandardOutput.ReadToEnd()
stderr = $p.StandardError.ReadToEnd()
ExitCode = $p.ExitCode
}
}
Here's how to use it:
$DisableACMonitorTimeOut = Execute-Command -commandTitle "Disable Monitor Timeout" -commandPath "C:\Windows\System32\powercfg.exe" -commandArguments " -x monitor-timeout-ac 0"
I really had troubles with those examples from Andy Arismendi and from LPG. You should always use:
$stdout = $p.StandardOutput.ReadToEnd()
before calling
$p.WaitForExit()
A full example is:
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$p.WaitForExit()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
Here's a kludgy way to get the output from another powershell process (serialized):
start-process -wait -nonewwindow powershell 'ps | Export-Clixml out.xml'
import-clixml out.xml
Let me emphasize -nonewwindow to get the standardoutput and standarderror, at least on the local screen:
start-process -wait cmd '/c dir' -nonewwindow
Volume in drive C is Windows
Volume Serial Number is 2AC6-626F
Directory of C:\users\me\foo
11/24/2022 11:40 AM <DIR> .
11/24/2022 11:40 AM <DIR> ..
11/24/2022 11:40 AM 330 file.json
1 File(s) 330 bytes
2 Dir(s) 25,042,915,328 bytes free
start-process -wait cmd '/c dir foo' -nonewwindow
Volume in drive C is Windows
Volume Serial Number is 2AC6-626F
Directory of C:\users\me\foo
File Not Found
Here's what I cooked up based on the examples posted by others on this thread. This version will hide the console window and provided options for output display.
function Invoke-Process {
[CmdletBinding(SupportsShouldProcess)]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$FilePath,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$ArgumentList,
[ValidateSet("Full","StdOut","StdErr","ExitCode","None")]
[string]$DisplayLevel
)
$ErrorActionPreference = 'Stop'
try {
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $FilePath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.WindowStyle = 'Hidden'
$pinfo.CreateNoWindow = $true
$pinfo.Arguments = $ArgumentList
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$result = [pscustomobject]#{
Title = ($MyInvocation.MyCommand).Name
Command = $FilePath
Arguments = $ArgumentList
StdOut = $p.StandardOutput.ReadToEnd()
StdErr = $p.StandardError.ReadToEnd()
ExitCode = $p.ExitCode
}
$p.WaitForExit()
if (-not([string]::IsNullOrEmpty($DisplayLevel))) {
switch($DisplayLevel) {
"Full" { return $result; break }
"StdOut" { return $result.StdOut; break }
"StdErr" { return $result.StdErr; break }
"ExitCode" { return $result.ExitCode; break }
}
}
}
catch {
exit
}
}
Example: Invoke-Process -FilePath "FQPN" -ArgumentList "ARGS" -DisplayLevel Full
To get both stdout and stderr, I use:
Function GetProgramOutput([string]$exe, [string]$arguments)
{
$process = New-Object -TypeName System.Diagnostics.Process
$process.StartInfo.FileName = $exe
$process.StartInfo.Arguments = $arguments
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
$process.Start()
$output = $process.StandardOutput.ReadToEnd()
$err = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$output
$err
}
$exe = "cmd"
$arguments = '/c echo hello 1>&2' #this writes 'hello' to stderr
$runResult = (GetProgramOutput $exe $arguments)
$stdout = $runResult[-2]
$stderr = $runResult[-1]
[System.Console]::WriteLine("Standard out: " + $stdout)
[System.Console]::WriteLine("Standard error: " + $stderr)
Improved Answer - as long as you're OK with Start-Job instead of Start-Process
It turns out that the STDOUT and STDERR are accumulated in string arrays $job.ChildJobs[0].Output and $job.ChildJobs[0].Erroras the script runs. So you can poll these values and write them out periodically. Somewhat of a hack maybe, but it works.
It's not a stream though, so you have to manually keep track of the starting index into the array.
This code is simpler than my original answer, and at the end you have the entire STDOUT in $job.ChildJobs[0].Output. And as a little bonus for this demo, the calling script is PS7 and the background job is PS5.
$scriptBlock = {
Param ([int]$param1, [int]$param2)
$PSVersionTable
Start-Sleep -Seconds 1
$param1 + $param2
}
$parameters = #{
ScriptBlock = $scriptBlock
ArgumentList = 1, 2
PSVersion = 5.1 # <-- remove this line for PS7
}
$timeoutSec = 5
$job = Start-Job #parameters
$job.ChildJobs[0].Output
$index = $job.ChildJobs[0].Output.Count
while ($job.JobStateInfo.State -eq [System.Management.Automation.JobState]::Running) {
Start-Sleep -Milliseconds 200
$job.ChildJobs[0].Output[$index]
$index = $job.ChildJobs[0].Output.Count
if (([DateTime]::Now - $job.PSBeginTime).TotalSeconds -gt $timeoutSec) {
throw "Job timed out."
}
}
As pointed out, my original answer can interleave the output. This is a limitation of event handling in PowerShell. It's not a fixable problem.
Original Answer, don't use - just leaving it here for interest
If there's a timeout, ReadToEnd() is not an option. You could do some fancy looping, but IMO the 'cleanest' way to do this is to ignore the streams. Hook the OutputDataReceived/ErrorDataReceived events instead, collecting the output. This approach also avoids the threading issues mentioned by others.
This is straightforward in C#, but it's tricky and verbose in Powershell. In particular, add_OutputDataReceived is not available for some reason. (Not sure if this is a bug or a feature, at least this seems to be the case in PowerShell 5.1.) To work around it you can use Register-ObjectEvent.
$stdout = New-Object System.Text.StringBuilder
$stderr = New-Object System.Text.StringBuilder
$proc = [System.Diagnostics.Process]#{
StartInfo = #{
FileName = 'ping.exe'
Arguments = 'google.com'
RedirectStandardOutput = $true
RedirectStandardError = $true
UseShellExecute = $false
WorkingDirectory = $PSScriptRoot
}
}
$stdoutEvent = Register-ObjectEvent $proc -EventName OutputDataReceived -MessageData $stdout -Action {
$Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
}
$stderrEvent = Register-ObjectEvent $proc -EventName ErrorDataReceived -MessageData $stderr -Action {
$Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
}
$proc.Start() | Out-Null
$proc.BeginOutputReadLine()
$proc.BeginErrorReadLine()
Wait-Process -Id $proc.Id -TimeoutSec 5
if ($proc.HasExited) {
$exitCode = $proc.ExitCode
}
else {
Stop-Process -Force -Id $proc.Id
$exitCode = -1
}
# Be sure to unregister. You have been warned.
Unregister-Event $stdoutEvent.Id
Unregister-Event $stderrEvent.Id
Write-Output $stdout.ToString()
Write-Output $stderr.ToString()
Write-Output "Exit code: $exitCode"
The code shown is the happy path (stderr is empty)
To test the timeout path, set -TimeoutSec to .5
To test the sad path (stderr has content), set FileName to 'cmd' and Arguments to /C asdf
Here is my version of function that is returning standard System.Diagnostics.Process with 3 new properties
Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
Try {
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.WindowStyle = 'Hidden'
$pinfo.CreateNoWindow = $True
$pinfo.Arguments = $commandArguments
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$p.WaitForExit()
$p | Add-Member "commandTitle" $commandTitle
$p | Add-Member "stdout" $stdout
$p | Add-Member "stderr" $stderr
}
Catch {
}
$p
}
You may want to also consider using the & operator combined with --% instead of start-process - that lets you easily pipe and process the command and/or error output.
put the escape parameter into a variable
put the arguments into a variable
$deploy= "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
$esc = '--%'
$arguments ="-source:package='c:\temp\pkg.zip' -verb:sync"
$output = & $deploy $esc $arguments
That passes the parameters to the executable without interference and let me get around the issues with start-process.
Combine Stderr and Stdout into one variable:
$output = & $deploy $esc $arguments 2>&1
Get separate variables for Stderr and Stdout
$err = $( $output = & $deploy $esc $arguments) 2>&1
I need to start a long-running process in a PowerShell script and capture the StandardOutput to a file as it is being generated. I'm currently using an asynchronous approach and appending the output to a text file each time an OutputDataReceived event is fired. The following code excerpt illustrates:
$PSI = New-Object System.Diagnostics.ProcessStartInfo
$PSI.CreateNoWindow = $true
$PSI.RedirectStandardOutput = $true
$PSI.RedirectStandardError = $true
$PSI.UseShellExecute = $false
$PSI.FileName = $EXECUTE
$PSI.RedirectStandardInput = $true
$PSI.WorkingDirectory = $PWD
$PSI.EnvironmentVariables["TMP"] = $TMPDIR
$PSI.EnvironmentVariables["TEMP"] = $TMPDIR
$PSI.EnvironmentVariables["TMPDIR"] = $TMPDIR
$PROCESS = New-Object System.Diagnostics.Process
$PROCESS.StartInfo = $PSI
[void]$PROCESS.Start()
# asynchronously listen for OutputDataReceived events on the process
$sb = [scriptblock]::Create("`$text = `$Event.SourceEventArgs.Data; `$OUTFIL = `$Event.MessageData; Add-Content $OUTFIL.out `$text")
$EVENT_OBJ = Register-ObjectEvent -InputObject $PROCESS -EventName OutputDataReceived -Action $sb -MessageData "$OUTFIL.out"
# asynchronously listen for ErrorDataReceived events on the process
$sb2 = [scriptblock]::Create("`$text = `$Event.SourceEventArgs.Data; Write-Host `$text;")
$EVENT_OBJ2 = Register-ObjectEvent -InputObject $PROCESS -EventName ErrorDataReceived -Action $sb2
# begin asynchronous read operations on the redirected StandardOutput
$PROCESS.BeginOutputReadLine();
# begin asynchronous read operations on the redirected StandardError
$PROCESS.BeginErrorReadLine();
# write the input file contents to standard input
$WRITER = $PROCESS.StandardInput
$READER = [System.IO.File]::OpenText("$IN_FILE")
try
{
while (($line = $READER.ReadLine()) -ne $null)
{
$WRITER.WriteLine($line)
}
}
finally
{
$WRITER.close()
$READER.close()
}
$Process.WaitForExit() | Out-Null
# end asynchronous read operations on the redirected StandardOutput
$PROCESS.CancelOutputRead();
# end asynchronous read operations on the redirected StandardError
$PROCESS.CancelErrorRead();
Unregister-Event -SubscriptionId $EVENT_OBJ.Id
Unregister-Event -SubscriptionId $EVENT_OBJ2.Id
The issue with this approach is the large amount of output being generated and the latency caused by having to open and close the text file each event (i.e., Add-Content $OUTFIL.out $text) is called each time the event is fired).
$sb = [scriptblock]::Create("`$text = `$Event.SourceEventArgs.Data; `$OUTFIL = `$Event.MessageData; Add-Content $OUTFIL.out `$text")
In each case the actual process will complete minutes before all of the output data has been written to the text file.
Is there a better approach to doing this? A faster way to append text to the file?
Long story short, we are experiencing issues with some of our servers that cause crippling effects on them and I am looking for a way to monitor them, now I have a script that will check the RDP port to make sure that it is open and I am thinking that I want to use get-service and then I will return if it pulled any data or not.
Here is the issue I don't know how to limit the time it will wait for a response before returning false.
[bool](Get-process -ComputerName MYSERVER)
Although I like Ansgars answer with a time-limited job, I think a separate Runspace and async invocation fits this task better.
The major difference here being that a Runspace reuses the in-process thread pool, whereas the PSJob method launches a new process, with the overhead that that entails, such as OS/kernel resources spawning and managing a child process, serializing and deserializing data etc.
Something like this:
function Timeout-Statement {
param(
[scriptblock[]]$ScriptBlock,
[object[]]$ArgumentList,
[int]$Timeout
)
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.Open()
$PS = [powershell]::Create()
$PS.Runspace = $Runspace
$PS = $PS.AddScript($ScriptBlock)
foreach($Arg in $ArgumentList){
$PS = $PS.AddArgument($Arg)
}
$IAR = $PS.BeginInvoke()
if($IAR.AsyncWaitHandle.WaitOne($Timeout)){
$PS.EndInvoke($IAR)
}
return $false
}
Then use that to do:
$ScriptBlock = {
param($ComputerName)
Get-Process #PSBoundParameters
}
$Timeout = 2500 # 2 and a half seconds (2500 milliseconds)
Timeout-Statement $ScriptBlock -ArgumentList "mycomputer.fqdn" -Timeout $Timeout
You could run your check as a background job:
$sb = { Get-Process -ComputerName $args[0] }
$end = (Get-Date).AddSeconds(5)
$job = Start-Job -ScriptBlock $sb -ArgumentList 'MYSERVER'
do {
Start-Sleep 100
$finished = (Get-Job -Id $job.Id).State -eq 'Completed'
} until ($finished -or (Get-Date) -gt $end)
if (-not $finished) {
Stop-Job -Id $job.Id
}
Receive-Job $job.Id
Remove-Job $job.Id
This is a known issue: https://connect.microsoft.com/PowerShell/feedback/details/645165/add-timeout-parameter-to-get-wmiobject
There is a workaround provided Here : https://connect.microsoft.com/PowerShell/feedback/details/645165/add-timeout-parameter-to-get-wmiobject
Function Get-WmiCustom([string]$computername,[string]$namespace,[string]$class,[int]$timeout=15)
{
$ConnectionOptions = new-object System.Management.ConnectionOptions
$EnumerationOptions = new-object System.Management.EnumerationOptions
$timeoutseconds = new-timespan -seconds $timeout
$EnumerationOptions.set_timeout($timeoutseconds)
$assembledpath = "\\" + $computername + "\" + $namespace
#write-host $assembledpath -foregroundcolor yellow
$Scope = new-object System.Management.ManagementScope $assembledpath, $ConnectionOptions
$Scope.Connect()
$querystring = "SELECT * FROM " + $class
#write-host $querystring
$query = new-object System.Management.ObjectQuery $querystring
$searcher = new-object System.Management.ManagementObjectSearcher
$searcher.set_options($EnumerationOptions)
$searcher.Query = $querystring
$searcher.Scope = $Scope
trap { $_ } $result = $searcher.get()
return $result
}
You can call the function like this:
get-wmicustom -class Win32_Process -namespace "root\cimv2" -computername MYSERVER –timeout 1
Before I was using a piece of code like this $prog="cmd.exe";
$params=#('/C','"D:\EmailConnector-Disc Optimus\run.bat"','connector.log')
$rc=start-process $prog $params -WorkingDirectory "D:\EmailConnector-Disc Optimus" -RedirectStandardOutput $emailconnecter_log -PassThru -wait The issue was with this code is not returing error code. Later I usedd a code like here below
$errorlog = "D:\EmailConnector-Disc Optimus\logs\error.log"
$emailconnecter_log = "D:\EmailConnector-Disc Optimus\logs\connector.log"
$prog="cmd.exe"
$params=#('/C','"D:\EmailConnector-Disc Optimus\run.bat"','connector.log')
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $prog
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $emailconnecter_log
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $params
$pinfo.WorkingDirectory = "D:\EmailConnector-Disc Optimus"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
if ($p.ExitCode -gt 0)
{Some code is here }
Now the issue is $p.WaitForExit() not exiting, i was debuggin through PowershellISE. I dont know Why the code is not exiting. Also I want to write the console output to $emailconnecter_log. Previously everything working except the return code. Pleae help me.