Start Process and read StandardOutput - powershell

I am looking for a way to start a process in a new console window or the same window and catch its output, I can open process in new window using:
[Diagnostics.Process]::Start("C:\test.exe","-verbose -page")
This will open new window witch I can interact with but I cannot redirect output for it (output I mean whole interaction with window like key press and messages)
So I thought I can try with:
Start-Transcript -path "C:\test.txt" -append
$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "C:\test.exe"
$ps.StartInfo.Arguments = " -verbose -page"
$ps.StartInfo.RedirectStandardOutput = $True
$ps.StartInfo.UseShellExecute = $false
$ps.start()
while ( ! $ps.HasExited ) {
write-host = $ps.StandardOutput.ReadToEnd();
}
Now here I get get output but without interaction, so I need some sort of option or procedure to start this process in same or different console and catch my interaction with it to file.
It is important as application sometimes asks for press any key and if Ill launch it in background it will never ask it because this app measures console window and checks id output will fit?
Is such thing possible?

You should be able to do
myprocess.StandardInput.WriteLine("some input");
or
myprocess.StandardInput.Write(" ");

As I was having big problem with this kind of interactivity in powershell, I mean run console app and press key, I finally got it right and I am shearing my solution.
Press button to continue function:
Function Press-Button
{
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('~');
}
Start process, log whole transaction and press to continue:
Function Run-Tool
{
Start-Transcript -path "C:\test.txt" -append
$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "C:\test.exe"
$ps.StartInfo.Arguments = " -verbose -page"
$ps.StartInfo.RedirectStandardInput = $true;
$ps.StartInfo.UseShellExecute = $false
$ps.start()
while ( ! $ps.HasExited )
{
Start-Sleep -s 5
write-host "I will press button now..."
Press-Button
}
Stop-Transcript
}

No matter how you do this it will be unreliable in an active system:
Function Run-Tool {
Start-Transcript -path "C:\test.txt" -append
Add-Type -AssemblyName System.Windows.Forms
$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "C:\test.exe"
$ps.StartInfo.Arguments = " -verbose -page"
$ps.StartInfo.RedirectStandardInput = $true;
$ps.StartInfo.UseShellExecute = $false
$ps.start()
do{
Start-Sleep -s 5
write-host "I will press button now..."
[System.Windows.Forms.SendKeys]::SendWait('~');
}
until($ps.HasExited)
Stop-Transcript
}

Related

How to close infinite loop on button event

One of my admins wanted a little script, that visualizes if a certain process is running. I managed to accomplish this by creating a systray icon which visually changes according to the status. Now my problem is that I don't have a lot of experience working with GUIs and I don't seem to be able to exit the program when a certain button is clicked.
Sourcecode is here:
#Declare Assemblies
[System.Reflection.Assembly]::LoadWithPartialName('System.Window.Forms') | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName('WindowsFormsIntegration') | Out-Null
#Add an icon to the systray
$bitmap_green = [System.Drawing.Bitmap]::FromFile('C:\Temp\green.png')
$hicon_green= $bitmap_green.GetHicon()
$icon_green = [system.drawing.icon]::FromHandle($hicon_green)
$bitmap_red = [System.Drawing.Bitmap]::FromFile('C:\Temp\red.png')
$hicon_red= $bitmap_red.GetHicon()
$icon_red = [system.drawing.icon]::FromHandle($hicon_red)
#Create Systray object
$Systray_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
#Mouseover text
$Systray_Tool_Icon.Text = "MOBS läuft"
$Systray_Tool_Icon.Icon = $icon_green
$Systray_Tool_Icon.Visible = $true
$Menu_Exit = New-Object System.Windows.Forms.MenuItem
$Menu_Exit.Text = "Exit"
$exit = $true
$contextmenu = New-Object System.Windows.Forms.ContextMenu
$Systray_Tool_Icon.ContextMenu = $contextmenu
$Systray_Tool_Icon.ContextMenu.MenuItems.AddRange($Menu_Exit)
$Menu_Exit.add_click({
$Systray_Tool_Icon.Visible = $false
$exit = $false
})
do {
$running = Get-Process mobsync.exe -ErrorAction SilentlyContinue
if (!$running)
{
$Systray_Tool_Icon.Icon = $icon_red
$Systray_Tool_Icon.Text = "MOBS läuft nicht"
}
else {
$Systray_Tool_Icon.Icon = $icon_green
$Systray_Tool_Icon.Text = "MOBS läuft "
}
[System.Windows.Forms.Application]::DoEvents()
} while ( $exit -eq $true)
My idea was to create a do while loop which continuesly runs until the admin presses a button in the contextmenu of the icon. So in essence I built an infinite loop.
My problem is, that the add_click isn't working as I expected. Can somebody give me some advice or some hints on how to properly close my loop on the click event?
Thanks in advance.

Powershell popup to close applications

I want to create powershell file that will create a popup to warn someone that an application will close automatically in 15 mins unless they extend a session. It should then kill the application unless they extend the session.
I’m completely new to this and this is all I have mustered trying it with the notepad application.
Add-Type -AssemblyName PresentationCore,PresentationFramework
$ButtonType = [System.Windows.MessageBoxButton]::YesNoCancel
$MessageIcon = [System.Windows.MessageBoxImage]::Error
$MessageBody = "Can we close notepad?"
$MessageTitle = "Using Notepad"
$Result = [System.Windows.MessageBox]::Show($MessageBody, $MessageTitle, $ButtonType, $MessageIcon)
Write-Host "Your choice is $Result"
Get-Process Notepad | Foreach-Object { $_.CloseMainWindow() | Out-Null }
Mathias, if you change this to a switch it works like you want where it takes Yes to close the window, but No and cancel leave it open
Add-Type -AssemblyName PresentationCore,PresentationFramework
$ButtonType = [System.Windows.MessageBoxButton]::YesNoCancel
$MessageIcon = [System.Windows.MessageBoxImage]::Error
$MessageBody = "Can we close notepad?"
$MessageTitle = "Using Notepad"
$Result = [System.Windows.MessageBox]::Show($MessageBody, $MessageTitle, $ButtonType, $MessageIcon)
switch($Result){
"Yes" {
write-host "You selected $Result"
Get-Process notepad | Foreach-Object { $_.CloseMainWindow() | Out-Null } | Stop-Process -Force
}
"No" {
Write-Host "You selected $Result"
}
"Cancel" {
Write-Host "You selected $Result"
}
}

Listen for a key press with Powershell, but don't wait for it

I'm trying to write a script that toggles the caps lock key periodically, but I also want to be able to toggle the script because it causes some issues with other functionality (like alt tabbing, and rolling over windows in the task bar to see a preview)
The script I have so far is
:outer while($true){
do{
echo "Toggle with F12";
$x = [System.Console]::ReadKey()
}while( $x.Key -ne "F12" )
while($true){
$wsh = New-Object -ComObject WScript.Shell
$wsh.SendKeys('{CAPSLOCK}')
sleep 0.3
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($wsh)
Remove-Variable wsh
if ($Host.UI.RawUI.KeyAvailable) {
$key = $Host.UI.RawUI.ReadKey()
if($key.Key -ne "F12"){
break outer
}
}
}
}
The script waits for the user to press F12, and then once they press F12 I want it to start toggling the caps lock key ever 0.3 seconds until the user presses F12 again. Ideally, I want this to happen in the background, but I don't know if that's possible. I don't want the user to have to have the console window open to toggle the script.
The way this runs now, after the user presses F12 the script will toggle capslock once and then exit. If I remove the conditional after Remove-Variable, the script will run as I want it to except the only way it can be stopped is if the console window is closed.
Here is a try. BTW I changed the sleep to 1 second so it doesn't flash capslock too much while testing:
$continue = $true
while($continue)
{
if ([console]::KeyAvailable)
{
echo "Toggle with F12";
$x = [System.Console]::ReadKey()
switch ( $x.key)
{
F12 { $continue = $false }
}
}
else
{
$wsh = New-Object -ComObject WScript.Shell
$wsh.SendKeys('{CAPSLOCK}')
sleep 1
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($wsh)| out-null
Remove-Variable wsh
}
}
add-type -Path '.\documents\WindowsPowerShell\VISE_WinKeyboardHook.dll'
$KeyboardInterceptor = new-object VISE.WinKeyboardHook.KeyboardInterceptor
function HandleKeyDown($keyId)
{
write-host $keyID.KeyCode
if($keyID.KeyCode -eq "Escape"){
$KeyboardInterceptor.StopCapturing()
}
}
Unregister-Event -SourceIdentifier KeyDown -ErrorAction SilentlyContinue
$keyevent = Register-ObjectEvent -InputObject $KeyboardInterceptor -EventName KeyDown -SourceIdentifier KeyDown -Action {HandleKeyDown($event.sourceEventArgs)}
$KeyboardInterceptor.StartCapturing()
Here is a C# assembly which provides event for global keyboard events.
https://ianmorrish.wordpress.com/v-ise/keyboard-hook/
Advantage of this is that it is non-blocking and also works in ISE.

Reading exit code from a process in PowerShell

I'm working on a build environment for our JavaScript project. We use require.js (r.js) to combine different js modules into one output js file. We use TeamCity and I wanted to configure Powershell build step that would call r.js, read it's standard output and exit code and pass that exit code back to TeamCity (by exiting from Powershell with this exit code) so that if the tasks fails (r.js comes back with exit code 1) it won't proceed with other build steps. Also I wanted the standard output of r.js to be saved in the TeamCity log to allow developers to quickly see the error causing r.js to stop.
This is the way how I can start r.js process with it's arguments, read it's exit code and use it to exit from Powershell:
$process = start-process r.js.cmd -ArgumentList "-o build-dev.js" -PassThru -Wait
exit $process.ExitCode
If I try to read standard output in this way before exiting:
Write-Host $process.StandardOutput.ReadToEnd();
I get this error, which probably suggests that I can't read StandardOutput in this way as it is a stream:
You cannot call a method on a null-valued expression.
At line:1 char:45
+ Write-Host $process.StandardOutput.ReadToEnd <<<< ();
+ CategoryInfo : InvalidOperation: (ReadToEnd:String) [], Runtime
Exception
+ FullyQualifiedErrorId : InvokeMethodOnNull
Process exited with code 1
Now I found a way of running the process so that I can read the standard output but I'm not able to read the exit code:
$psi = New-object System.Diagnostics.ProcessStartInfo
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
#$psi.FileName = "r.js.cmd"
$psi.FileName = "C:\Users\Administrator\AppData\Roaming\npm\r.js.cmd"
$psi.Arguments = #("-o build-dev.js")
#$psi.WorkingDirectory = (Get-Location).Path;
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $psi
$process.Start() | Out-Host
$process.WaitForExit()
$output = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
sleep(10)
$exit_code = $process.ExitCode
write-host "========== OUTPUT =========="
write-host $output
write-host "========== ERROR =========="
write-host $stderr
write-host "========== EXIT CODE =========="
write-host $exit_code
write-host "========== $LastExitCode =========="
#write-host $LastExitCode
#Exit $exit_code
But again this returns the console output but the exit code is always 0 even if r.js returns 1 because I have error in my js scripts.
Could anyone advise how can I read standard output with start-process or how can I read exit code with New-object System.Diadnostics.ProcessStartInfo
I can attach screenshots with more details of my TC build step configuration and output saved to the build log if that would help answering the question.
I usually tackle this situation by using a variation of the start-process command mentioned in the question.
$outputLog = "outputFile.log"
$errLog = "errorFile.log"
$process = Start-Process r.js.cmd -ArgumentList "-o build-dev.js" -PassThru -RedirectStandardOutput $outputLog -RedirectStandardError $errLog -Wait
$exitCode = $process.ExitCode
Now the log files $outputLog and $errorLog will have the standard output and error contents.
This is how you can do it. Cmd files are however not applications, they are associated with cmd so you might have to target cmd.exe and put the path to the cmd file as a parameter. Also UseShellExecute for process start info stops output and errors from being recorded so dont enable it if you need output. UseShellExecute is as if you put the $Path into the Win+R window. If its a png for example, windows will find an app that is supposed to open it and opens that file with it.
$Path = "C:\whatever.exe"
$WorkingDirectory = "C:\"
$CreateNoWindow = $true #$true to not create another window for console application
$Parameters = "-paremeters for -the app"
$WindowStyle = [Diagnostics.ProcessWindowStyle]::Normal
# prepare process start info
$processStartInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' -ErrorAction 'Stop'
$processStartInfo.FileName = $Path
$processStartInfo.WorkingDirectory = $WorkingDirectory
$processStartInfo.ErrorDialog = $false
$processStartInfo.RedirectStandardOutput = $true
$processStartInfo.RedirectStandardError = $true
$processStartInfo.CreateNoWindow = $CreateNoWindow
If ($Parameters) { $processStartInfo.Arguments = $Parameters }
$processStartInfo.WindowStyle = $WindowStyle
#create a process and assign the process start info object to it
$process = New-Object -TypeName 'System.Diagnostics.Process' -ErrorAction 'Stop'
$process.StartInfo = $processStartInfo
#Add event handler to capture process's standard output redirection
[scriptblock]$processEventHandler = { If (-not [string]::IsNullOrEmpty($EventArgs.Data)) { $Event.MessageData.AppendLine($EventArgs.Data) } }
#create string builders to store the output and errors in
$stdOutBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList ''
$stdOutEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'OutputDataReceived' -MessageData $stdOutBuilder -ErrorAction 'Stop'
$stdErrBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList ''
$stdErrEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'ErrorDataReceived' -MessageData $stdErrBuilder -ErrorAction 'Stop'
#start the process
$null = $process.Start()
#begin reading the output and errors
$process.BeginOutputReadLine()
$process.BeginErrorReadLine()
#Instructs the Process component to wait indefinitely for the associated process to exit.
$process.WaitForExit()
#HasExited indicates that the associated process has terminated, either normally or abnormally. Wait until HasExited returns $true.
While (-not ($process.HasExited)) { $process.Refresh(); Start-Sleep -Seconds 1 }
## Get the exit code for the process
Try {
[int32]$returnCode = $process.ExitCode
}
Catch [System.Management.Automation.PSInvalidCastException] {
# Catch exit codes that are out of int32 range
[int32]$returnCode = 1
}
#unregister the events
If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null }
If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null }
$stdOut = $stdOutBuilder.ToString() -replace $null,''
$stdErr = $stdErrBuilder.ToString() -replace $null,''
## Free resources associated with the process, this does not cause process to exit
If ($process) { $process.Dispose() }
# check if we have output and return it
If ($stdOut) {
Write-Output "Process output:$stdOut"
}
If ($stdErr) {
Write-Output "Process errors:$stdErr"
}
# return exit code
Write-Output "Exit code:$returnCode"
Start-Sleep -Seconds 5
Exit $returnCode

Captured Output of command run by PowerShell Is Sometimes Incomplete

I'm running the DTEXEC.exe command from within a PowerShell script, trying to capture and log the output to a file. Sometimes the output is incomplete and I'm trying to figure out why this the case and what might be done about it. The lines that never seem to get logged are the most interesting:
DTEXEC: The package execution returned DTSER_SUCCESS(0)
Started: 10:58:43 a.m.
Finished: 10:59:24 a.m.
Elapsed: 41.484 seconds
The output always seems incomplete on packages that execute in less than ~ 8 seconds and this might be a clue (there isn't much output or they finish quickly).
I'm using .NETs System.Diagnostics.Process and ProcessStartInfo to setup and run the command, and I'm redirecting stdout and stderror to event handlers that each append to a StringBuilder which is subsequently written to disk.
The problem feels like a timing issue or a buffering issue. To solve the timing issue, I've attempted to use Monitor.Enter/Exit. If it's a buffering issue, I'm not sure how to force the Process to not buffer stdout and stderror.
The environment is
- PowerShell 2 running CLR version 2
- SQL 2008 32-bit DTEXEC.exe
- Host Operating System: XP Service Pack 3.
Here's the code:
function Execute-SSIS-Package
{
param([String]$fileName)
$cmd = GetDTExecPath
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo.FileName = $cmd
$proc.StartInfo.Arguments = "/FILE ""$fileName"" /CHECKPOINTING OFF /REPORTING ""EWP"""
$proc.StartInfo.RedirectStandardOutput = $True
$proc.StartInfo.RedirectStandardError = $True
$proc.StartInfo.WorkingDirectory = Get-Location
$proc.StartInfo.UseShellExecute = $False
$proc.StartInfo.CreateNoWindow = $False
Write-Host $proc.StartInfo.FileName $proc.StartInfo.Arguments
$cmdOut = New-Object System.Text.StringBuilder
$errorEvent = Register-ObjectEvent -InputObj $proc `
-Event "ErrorDataReceived" `
-MessageData $cmdOut `
-Action `
{
param
(
[System.Object] $sender,
[System.Diagnostics.DataReceivedEventArgs] $e
)
try
{
[System.Threading.Monitor]::Enter($Event.MessageData)
Write-Host -ForegroundColor "DarkRed" $e.Data
[void](($Event.MessageData).AppendLine($e.Data))
}
catch
{
Write-Host -ForegroundColor "Red" "Error capturing processes std error" $Error
}
finally
{
[System.Threading.Monitor]::Exit($Event.MessageData)
}
}
$outEvent = Register-ObjectEvent -InputObj $proc `
-Event "OutputDataReceived" `
-MessageData $cmdOut `
-Action `
{
param
(
[System.Object] $sender,
[System.Diagnostics.DataReceivedEventArgs] $e
)
try
{
[System.Threading.Monitor]::Enter($Event.MessageData)
#Write-Host $e.Data
[void](($Event.MessageData).AppendLine($e.Data))
}
catch
{
Write-Host -ForegroundColor "Red" "Error capturing processes std output" $Error
}
finally
{
[System.Threading.Monitor]::Exit($Event.MessageData)
}
}
$isStarted = $proc.Start()
$proc.BeginOutputReadLine()
$proc.BeginErrorReadLine()
while (!$proc.HasExited)
{
Start-Sleep -Milliseconds 100
}
Start-Sleep -Milliseconds 1000
$procExitCode = $proc.ExitCode
$procStartTime = $proc.StartTime
$procFinishTime = Get-Date
$proc.Close()
$proc.CancelOutputRead()
$proc.CancelErrorRead()
$result = New-Object PsObject -Property #{
ExitCode = $procExitCode
StartTime = $procStartTime
FinishTime = $procFinishTime
ElapsedTime = $procFinishTime.Subtract($procStartTime)
StdErr = ""
StdOut = $cmdOut.ToString()
}
return $result
}
The reason that your output is truncated is that Powershell returns from WaitForExit() and sets the HasExited property before it has processed all the output events in the queue.
One solution it to loop an arbitrary amount of time with short sleeps to allow the events to be processed; Powershell event processing appear to not be pre-emptive so a single long sleep does not allow events to process.
A much better solution is to also register for the Exited event (in addition to Output and Error events) on the Process. This event is the last in the queue so if you set a flag when this event occurs then you can loop with short sleeps until this flag is set and know that you have processed all the output events.
I have written up a full solution on my blog but the core snippet is:
# Set up a pair of stringbuilders to which we can stream the process output
$global:outputSB = New-Object -TypeName "System.Text.StringBuilder";
$global:errorSB = New-Object -TypeName "System.Text.StringBuilder";
# Flag that shows that final process exit event has not yet been processed
$global:myprocessrunning = $true
$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = $target
$ps.StartInfo.WorkingDirectory = Split-Path $target -Parent
$ps.StartInfo.UseShellExecute = $false
$ps.StartInfo.RedirectStandardOutput = $true
$ps.StartInfo.RedirectStandardError = $true
$ps.StartInfo.CreateNoWindow = $true
# Register Asynchronous event handlers for Standard and Error Output
Register-ObjectEvent -InputObject $ps -EventName OutputDataReceived -action {
if(-not [string]::IsNullOrEmpty($EventArgs.data)) {
$global:outputSB.AppendLine(((get-date).toString('yyyyMMddHHmm')) + " " + $EventArgs.data)
}
} | Out-Null
Register-ObjectEvent -InputObject $ps -EventName ErrorDataReceived -action {
if(-not [string]::IsNullOrEmpty($EventArgs.data)) {
$global:errorSB.AppendLine(((get-date).toString('yyyyMMddHHmm')) + " " + $EventArgs.data)
}
} | Out-Null
Register-ObjectEvent -InputObject $ps -EventName Exited -action {
$global:myprocessrunning = $false
} | Out-Null
$ps.start() | Out-Null
$ps.BeginOutputReadLine();
$ps.BeginErrorReadLine();
# We set a timeout after which time the process will be forceably terminated
$processTimeout = $timeoutseconds * 1000
while (($global:myprocessrunning -eq $true) -and ($processTimeout -gt 0)) {
# We must use lots of shorts sleeps rather than a single long one otherwise events are not processed
$processTimeout -= 50
Start-Sleep -m 50
}
if ($processTimeout -le 0) {
Add-Content -Path $logFile -Value (((get-date).toString('yyyyMMddHHmm')) + " PROCESS EXCEEDED EXECUTION ALLOWANCE AND WAS ABENDED!")
$ps.Kill()
}
# Append the Standard and Error Output to log file, we don't use Add-Content as it appends a carriage return that is not required
[System.IO.File]::AppendAllText($logFile, $global:outputSB)
[System.IO.File]::AppendAllText($logFile, $global:errorSB)
My 2 cents...its not a powershell issue but an issue/bug in the System.Diagnostics.Process class and underlying shell. I've seen times when wrapping the StdError and StdOut does not catch everything, and other times when the 'listening' wrapper application will hang indefinitly because of HOW the underlying application writes to the console. (in the c/c++ world there are MANY different ways to do this, [e.g. WriteFile, fprintf, cout, etc])
In addition there are more than 2 outputs that may need to be captured, but the .net framework only shows you those two (given they are the two primary ones) [see this article about command redirection here as it starts to give hints).
My guess (for both your issue as well as mine) is that it has to do with some low-level buffer flushing and/or ref counting. (If you want to get deep, you can start here)
One (very hacky) way to get around this is instead of executing the program directly to actually execute wrap it in a call to cmd.exe with 2>&1, but this method has its own pitfalls and issues.
The most ideal solution is for the executable to have a logging parameter, and then go parse the log file after the process exits...but most of the time you don't have that option.
But wait, we're using powershell...why are you using System.Diagnositics.Process in the first place? you can just call the command directly:
$output = & (GetDTExecPath) /FILE "$fileName" /CHECKPOINTING OFF /REPORTING "EWP"