How to send input to [Console]::In.ReadLine() from parent process? - powershell

Starter is used for starting target script process:
# STARTING PS (TARGET) SCRIPT COMPILED TO EXE
$processStartInfo = New-Object System.Diagnostics.ProcessStartInfo
$processStartInfo.FileName = $somePath
$processStartInfo.WorkingDirectory = (Get-Location).Path
$processStartInfo.RedirectStandardInput = $true
$processStartInfo.RedirectStandardError = $true
$processStartInfo.UseShellExecute = $false
$process = [System.Diagnostics.Process]::Start($processStartInfo)
# SOME OTHER CODE ...
# HERE I'M SENDING "EXIT" TO RUNSPACE RUNNING INSIDE TARGET SCRIPT
$process.StandardInput.WriteLineAsync("exit") | Out-Null
Target script (compiled to *.exe) creates runspace that synchonously waits for ReadLine data from starter
function main {
. createRunspace
while ($true) {
# PARENT LOOP RUNS IN PARALLEL TO RUNSPACE LOOP
sleep -s 1
try {
if ($hash.flags.exit) {
# CLEAN UP AND BREAK
} else {
# RUN OTHER CODE
}
} catch {
# CAN NOT NOTIFY RUNSPACE ABOUT ERROR USING SYNCHRONIZED HASTABLE,
# BECAUSE RUNSPACE IS STUCK ON `ReadLine`.
# ALSO CAN NOT WRITE TO STANDRAD INPUT (DON'T KNOW HOW).
}
}
}
function createRunspace {
#CREATING RUNSPACE WITH SYNCHRONIZED HASTABLE
$hash = [hashtable]::Synchronized(#{ flags: #{} })
$runspace= [runspacefactory]::CreateRunspace()
$runspace.Open()
$runspace.SessionStateProxy.SetVariable('hash', $hash)
$powershell= [powershell]::Create()
$powershell.Runspace = $runspace
$powershell.AddScript({
# RUNSPACE LOOP
while ($true) {
$value = [Console]::In.ReadLine()
if ($value -eq "exit") {
$hash.flags.exit = $true
break
} elseif ($value -eq "valueFromParent") {
# DO STUFF
}
}
}) | Out
}
# OTHER CODE
. main
Is there a way to send standard input data from parent to runspace?

The PowerShell-script-packaged-as-an-*.exe packaging script you're using for some reason doesn't pass stdin input through to the wrapped script, so your script never receives the "exit" line you send from the caller.
I don't know your exact requirements, but here's a much simplified solution that shows that your approach works in principle:
# The code to execute in the background.
$backgroundScript = {
while ($true) {
$value = [Console]::In.ReadLine()
if ($value -eq "exit") {
"Background: Exiting."
break
}
else {
"Background: Performing task: $value"
}
}
}
# Start the background script.
$processStartInfo = [System.Diagnostics.ProcessStartInfo] #{
FileName = "powershell.exe"
Arguments = '-NoProfile', '-Command', $backgroundScript -replace '"', '\"'
WorkingDirectory = $PWD.ProviderPath
RedirectStandardInput = $true
RedirectStandardError = $true
RedirectStandardOutput = $true
UseShellExecute = $false
}
$process = [System.Diagnostics.Process]::Start($processStartInfo)
# Ask the background script to perform a task.
"Submitting task 'doStuff'"
$process.StandardInput.WriteLine("doStuff")
# Ask the background script to exit.
"Submitting exit request."
$process.StandardInput.WriteLine("exit")
# Wait for the background script's process to exit,
# then print its stdout.
$process.WaitForExit()
$process.StandardOutput.ReadToEnd()
The above yields:
Submitting task 'doStuff'
Submitting exit request.
Background: Performing task: doStuff
Background: Exiting.

Related

Running multiple commands in a if statement

I'm looking to modify the below powershell script to do two things on an if statement:
if ($PsCmdlet.ParameterSetName -ieq "DumpCreds")
{
$ExeArgs = "privilege::debug"
}
elseif ($PsCmdlet.ParameterSetName -ieq "DumpCerts")
{
$ExeArgs = "crypto::cng crypto::capi `"crypto::certificates /export`" `"crypto::certificates /export /systemstore:CERT_SYSTEM_STORE_LOCAL_MACHINE`" exit"
}
else
{
$ExeArgs = $Command
}`
The line where it reads - $exeargs = "privilege::debug", I need run that and I also need to run - $ExeArgs = "sekurlsa::logonpasswords" The privilege one needs to run first followed by the logonpasswords one.
How do I run 2 commands in 1 if statement in a powershell script?
A if statement has no restriction about how much commands you execute in it, so just execute it...
if ($PsCmdlet.ParameterSetName -ieq "DumpCreds")
{
$ExeArgs = "privilege::debug"
$ExeArgs = "sekurlsa::logonpasswords"
}
elseif ($PsCmdlet.ParameterSetName -ieq "DumpCerts")
{
$ExeArgs = "crypto::cng crypto::capi `"crypto::certificates /export`" `"crypto::certificates /export /systemstore:CERT_SYSTEM_STORE_LOCAL_MACHINE`" exit"
}
else
{
$ExeArgs = $Command
}

PowerShell's pipe adds linefeed

I'm trying to pipe a string into a program's STDIN without any trailing linefeeds (unless that string itself actually ends in a linefeed). I tried googling around, but I only found people trying to print to the console without a trailing linefeed, in which case Write-Host takes a parameter -NoNewLine. However, to pipe it on to another program, I need Write-Output or similar which doesn't have such a parameter. Now it seems like Write-Output isn't even the problem:
Z:\> (Write-Output "abc").Length
3
But as soon as I pipe it to another program and read the string there, I get an additional linefeed. For instance, I tried this Ruby snippet:
Z:\> Write-Output "abc" | ruby -e "p ARGF.read"
"abc\n"
I checked that the actual string received is abc\n. The same happens in several other languages (at least C#, Java and Python), so I believe it's an issue with PowerShell, not the language doing the reading.
As a further test, I replaced Write-Output itself with another Ruby script:
Z:\> ruby -e "$> << 'abc'"
abcZ:\>
(That is, there is definitely no \n on the script's STDOUT.)
But again, when I pipe it into another script:
Z:\> ruby -e "$> << 'abc'" | ruby -e "p ARGF.read"
"abc\n"
I'm fairly convinced that it's the pipe which adds the linefeed. How do I avoid that? I actually want to be able to control whether the input ends in a linefeed or not (by including it in the input or omitting it).
(For reference, I also tested strings which already contain a trailing linefeed, and in that case the pipe doesn't add another one, so I guess it just ensures a trailing linefeed.)
I originally encountered this in PowerShell v3, but I'm now using v5 and still have the same issue.
Introduction
Here is my Invoke-RawPipeline function (get latest version from this Gist).
Use it to pipe binary data between processes' Standard Output and Standard Input streams. It can read input stream from file/pipeline and save resulting output stream to file.
It requires PsAsync module to be able to launch and pipe data in multiple processes.
In case of issues use -Verbose switch to see debug output.
Examples
Redirecting to file
Batch:
findstr.exe /C:"Warning" /I C:\Windows\WindowsUpdate.log > C:\WU_Warnings.txt
PowerShell:
Invoke-RawPipeline -Command #{Path = 'findstr.exe' ; Arguments = '/C:"Warning" /I C:\Windows\WindowsUpdate.log'} -OutFile 'C:\WU_Warnings.txt'
Redirecting from file
Batch:
svnadmin load < C:\RepoDumps\MyRepo.dump
PowerShell:
Invoke-RawPipeline -InFile 'C:\RepoDumps\MyRepo.dump' -Command #{Path = 'svnadmin.exe' ; Arguments = 'load'}
Piping strings
Batch:
echo TestString | find /I "test" > C:\SearchResult.log
PowerShell:
'TestString' | Invoke-RawPipeline -Command #{Path = 'find.exe' ; Arguments = '/I "test"'} -OutFile 'C:\SearchResult.log'
Piping between multiple processes
Batch:
ipconfig | findstr /C:"IPv4 Address" /I
PowerShell:
Invoke-RawPipeline -Command #{Path = 'ipconfig'}, #{Path = 'findstr' ; Arguments = '/C:"IPv4 Address" /I'} -RawData
Code:
<#
.Synopsis
Pipe binary data between processes' Standard Output and Standard Input streams.
Can read input stream from file and save resulting output stream to file.
.Description
Pipe binary data between processes' Standard Output and Standard Input streams.
Can read input stream from file/pipeline and save resulting output stream to file.
Requires PsAsync module: http://psasync.codeplex.com
.Notes
Author: beatcracker (https://beatcracker.wordpress.com, https://github.com/beatcracker)
License: Microsoft Public License (http://opensource.org/licenses/MS-PL)
.Component
Requires PsAsync module: http://psasync.codeplex.com
.Parameter Command
An array of hashtables, each containing Command Name, Working Directory and Arguments
.Parameter InFile
This parameter is optional.
A string representing path to file, to read input stream from.
.Parameter OutFile
This parameter is optional.
A string representing path to file, to save resulting output stream to.
.Parameter Append
This parameter is optional. Default is false.
A switch controlling wheither ovewrite or append output file if it already exists. Default is to overwrite.
.Parameter IoTimeout
This parameter is optional. Default is 0.
A number of seconds to wait if Input/Output streams are blocked. Default is to wait indefinetely.
.Parameter ProcessTimeout
This parameter is optional. Default is 0.
A number of seconds to wait for process to exit after finishing all pipeline operations. Default is to wait indefinetely.
Details: https://msdn.microsoft.com/en-us/library/ty0d8k56.aspx
.Parameter BufferSize
This parameter is optional. Default is 4096.
Size of buffer in bytes for read\write operations. Supports standard Powershell multipliers: KB, MB, GB, TB, and PB.
Total number of buffers is: Command.Count * 2 + InFile + OutFile.
.Parameter ForceGC
This parameter is optional.
A switch, that if specified will force .Net garbage collection.
Use to immediately release memory on function exit, if large buffer size was used.
.Parameter RawData
This parameter is optional.
By default function returns object with StdOut/StdErr streams and process' exit codes.
If this switch is specified, function will return raw Standard Output stream.
.Example
Invoke-RawPipeline -Command #{Path = 'findstr.exe' ; Arguments = '/C:"Warning" /I C:\Windows\WindowsUpdate.log'} -OutFile 'C:\WU_Warnings.txt'
Batch analog: findstr.exe /C:"Warning" /I C:\Windows\WindowsUpdate.log' > C:\WU_Warnings.txt
.Example
Invoke-RawPipeline -Command #{Path = 'findstr.exe' ; WorkingDirectory = 'C:\Windows' ; Arguments = '/C:"Warning" /I .\WindowsUpdate.log'} -RawData
Batch analog: cd /D C:\Windows && findstr.exe /C:"Warning" /I .\WindowsUpdate.log
.Example
'TestString' | Invoke-RawPipeline -Command #{Path = 'find.exe' ; Arguments = '/I "test"'} -OutFile 'C:\SearchResult.log'
Batch analog: echo TestString | find /I "test" > C:\SearchResult.log
.Example
Invoke-RawPipeline -Command #{Path = 'ipconfig'}, #{Path = 'findstr' ; Arguments = '/C:"IPv4 Address" /I'} -RawData
Batch analog: ipconfig | findstr /C:"IPv4 Address" /I
.Example
Invoke-RawPipeline -InFile 'C:\RepoDumps\Repo.svn' -Command #{Path = 'svnadmin.exe' ; Arguments = 'load'}
Batch analog: svnadmin load < C:\RepoDumps\MyRepo.dump
#>
function Invoke-RawPipeline
{
[CmdletBinding()]
Param
(
[Parameter(ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[ValidateScript({
if($_.psobject.Methods.Match.('ToString'))
{
$true
}
else
{
throw 'Can''t convert pipeline object to string!'
}
})]
$InVariable,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[ValidateScript({
$_ | ForEach-Object {
$Path = $_.Path
$WorkingDirectory = $_.WorkingDirectory
if(!(Get-Command -Name $Path -CommandType Application -ErrorAction SilentlyContinue))
{
throw "Command not found: $Path"
}
if($WorkingDirectory)
{
if(!(Test-Path -LiteralPath $WorkingDirectory -PathType Container -ErrorAction SilentlyContinue))
{
throw "Working directory not found: $WorkingDirectory"
}
}
}
$true
})]
[ValidateNotNullOrEmpty()]
[array]$Command,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[ValidateScript({
if(!(Test-Path -LiteralPath $_))
{
throw "File not found: $_"
}
$true
})]
[ValidateNotNullOrEmpty()]
[string]$InFile,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[ValidateScript({
if(!(Test-Path -LiteralPath (Split-Path $_)))
{
throw "Folder not found: $_"
}
$true
})]
[ValidateNotNullOrEmpty()]
[string]$OutFile,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[switch]$Append,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[ValidateRange(0, 2147483)]
[int]$IoTimeout = 0,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[ValidateRange(0, 2147483)]
[int]$ProcessTimeout = 0,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[long]$BufferSize = 4096,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[switch]$RawData,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[switch]$ForceGC
)
Begin
{
$Modules = #{PsAsync = 'http://psasync.codeplex.com'}
'Loading modules:', ($Modules | Format-Table -HideTableHeaders -AutoSize | Out-String) | Write-Verbose
foreach($module in $Modules.GetEnumerator())
{
if(!(Get-Module -Name $module.Key))
{
Try
{
Import-Module -Name $module.Key -ErrorAction Stop
}
Catch
{
throw "$($module.Key) module not available. Get it here: $($module.Value)"
}
}
}
function New-ConsoleProcess
{
Param
(
[string]$Path,
[string]$Arguments,
[string]$WorkingDirectory,
[switch]$CreateNoWindow = $true,
[switch]$RedirectStdIn = $true,
[switch]$RedirectStdOut = $true,
[switch]$RedirectStdErr = $true
)
if(!$WorkingDirectory)
{
if(!$script:MyInvocation.MyCommand.Path)
{
$WorkingDirectory = [System.AppDomain]::CurrentDomain.BaseDirectory
}
else
{
$WorkingDirectory = Split-Path $script:MyInvocation.MyCommand.Path
}
}
Try
{
$ps = New-Object -TypeName System.Diagnostics.Process -ErrorAction Stop
$ps.StartInfo.Filename = $Path
$ps.StartInfo.Arguments = $Arguments
$ps.StartInfo.UseShellExecute = $false
$ps.StartInfo.RedirectStandardInput = $RedirectStdIn
$ps.StartInfo.RedirectStandardOutput = $RedirectStdOut
$ps.StartInfo.RedirectStandardError = $RedirectStdErr
$ps.StartInfo.CreateNoWindow = $CreateNoWindow
$ps.StartInfo.WorkingDirectory = $WorkingDirectory
}
Catch
{
throw $_
}
return $ps
}
function Invoke-GarbageCollection
{
[gc]::Collect()
[gc]::WaitForPendingFinalizers()
}
$CleanUp = {
$IoWorkers + $StdErrWorkers |
ForEach-Object {
$_.Src, $_.Dst |
ForEach-Object {
if(!($_ -is [System.Diagnostics.Process]))
{
Try
{
$_.Close()
}
Catch
{
Write-Error "Failed to close $_"
}
$_.Dispose()
}
}
}
}
$PumpData = {
Param
(
[hashtable]$Cfg
)
# Fail hard, we don't want stuck threads
$Private:ErrorActionPreference = 'Stop'
$Src = $Cfg.Src
$SrcEndpoint = $Cfg.SrcEndpoint
$Dst = $Cfg.Dst
$DstEndpoint = $Cfg.DstEndpoint
$BufferSize = $Cfg.BufferSize
$SyncHash = $Cfg.SyncHash
$RunspaceId = $Cfg.Id
# Setup Input and Output streams
if($Src -is [System.Diagnostics.Process])
{
switch ($SrcEndpoint)
{
'StdOut' {$InStream = $Src.StandardOutput.BaseStream}
'StdIn' {$InStream = $Src.StandardInput.BaseStream}
'StdErr' {$InStream = $Src.StandardError.BaseStream}
default {throw "Not valid source endpoint: $_"}
}
}
else
{
$InStream = $Src
}
if($Dst -is [System.Diagnostics.Process])
{
switch ($DstEndpoint)
{
'StdOut' {$OutStream = $Dst.StandardOutput.BaseStream}
'StdIn' {$OutStream = $Dst.StandardInput.BaseStream}
'StdErr' {$OutStream = $Dst.StandardError.BaseStream}
default {throw "Not valid destination endpoint: $_"}
}
}
else
{
$OutStream = $Dst
}
$InStream | Out-String | ForEach-Object {$SyncHash.$RunspaceId.Status += "InStream: $_"}
$OutStream | Out-String | ForEach-Object {$SyncHash.$RunspaceId.Status += "OutStream: $_"}
# Main data copy loop
$Buffer = New-Object -TypeName byte[] $BufferSize
$BytesThru = 0
Try
{
Do
{
$SyncHash.$RunspaceId.IoStartTime = [DateTime]::UtcNow.Ticks
$ReadCount = $InStream.Read($Buffer, 0, $Buffer.Length)
$OutStream.Write($Buffer, 0, $ReadCount)
$OutStream.Flush()
$BytesThru += $ReadCount
}
While($readCount -gt 0)
}
Catch
{
$SyncHash.$RunspaceId.Status += $_
}
Finally
{
$OutStream.Close()
$InStream.Close()
}
}
}
Process
{
$PsCommand = #()
if($Command.Length)
{
Write-Verbose 'Creating new process objects'
$i = 0
foreach($cmd in $Command.GetEnumerator())
{
$PsCommand += New-ConsoleProcess #cmd
$i++
}
}
Write-Verbose 'Building I\O pipeline'
$PipeLine = #()
if($InVariable)
{
[Byte[]]$InVarBytes = [Text.Encoding]::UTF8.GetBytes($InVariable.ToString())
$PipeLine += New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize -ErrorAction Stop
$PipeLine[-1].Write($InVarBytes, 0, $InVarBytes.Length)
[Void]$PipeLine[-1].Seek(0, 'Begin')
}
elseif($InFile)
{
$PipeLine += New-Object -TypeName System.IO.FileStream -ArgumentList ($InFile, [IO.FileMode]::Open) -ErrorAction Stop
if($PsCommand.Length)
{
$PsCommand[0].StartInfo.RedirectStandardInput = $true
}
}
else
{
if($PsCommand.Length)
{
$PsCommand[0].StartInfo.RedirectStandardInput = $false
}
}
$PipeLine += $PsCommand
if($OutFile)
{
if($PsCommand.Length)
{
$PsCommand[-1].StartInfo.RedirectStandardOutput = $true
}
if($Append)
{
$FileMode = [System.IO.FileMode]::Append
}
else
{
$FileMode = [System.IO.FileMode]::Create
}
$PipeLine += New-Object -TypeName System.IO.FileStream -ArgumentList ($OutFile, $FileMode, [System.IO.FileAccess]::Write) -ErrorAction Stop
}
else
{
if($PsCommand.Length)
{
$PipeLine += New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize -ErrorAction Stop
}
}
Write-Verbose 'Creating I\O threads'
$IoWorkers = #()
for($i=0 ; $i -lt ($PipeLine.Length-1) ; $i++)
{
$SrcEndpoint = $DstEndpoint = $null
if($PipeLine[$i] -is [System.Diagnostics.Process])
{
$SrcEndpoint = 'StdOut'
}
if($PipeLine[$i+1] -is [System.Diagnostics.Process])
{
$DstEndpoint = 'StdIn'
}
$IoWorkers += #{
Src = $PipeLine[$i]
SrcEndpoint = $SrcEndpoint
Dst = $PipeLine[$i+1]
DstEndpoint = $DstEndpoint
}
}
Write-Verbose "Created $($IoWorkers.Length) I\O worker objects"
Write-Verbose 'Creating StdErr readers'
$StdErrWorkers = #()
for($i=0 ; $i -lt $PsCommand.Length ; $i++)
{
$StdErrWorkers += #{
Src = $PsCommand[$i]
SrcEndpoint = 'StdErr'
Dst = New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize -ErrorAction Stop
}
}
Write-Verbose "Created $($StdErrWorkers.Length) StdErr reader objects"
Write-Verbose 'Starting processes'
$PsCommand |
ForEach-Object {
$ps = $_
Try
{
[void]$ps.Start()
}
Catch
{
Write-Error "Failed to start process: $($ps.StartInfo.FileName)"
Write-Verbose "Can't launch process, killing and disposing all"
if($PsCommand)
{
$PsCommand |
ForEach-Object {
Try{$_.Kill()}Catch{} # Can't do much if kill fails...
$_.Dispose()
}
}
Write-Verbose 'Closing and disposing I\O streams'
. $CleanUp
}
Write-Verbose "Started new process: Name=$($ps.Name), Id=$($ps.Id)"
}
$WorkersCount = $IoWorkers.Length + $StdErrWorkers.Length
Write-Verbose 'Creating sync hashtable'
$sync = #{}
for($i=0 ; $i -lt $WorkersCount ; $i++)
{
$sync += #{$i = #{IoStartTime = $nul ; Status = $null}}
}
$SyncHash = [hashtable]::Synchronized($sync)
Write-Verbose 'Creating runspace pool'
$RunspacePool = Get-RunspacePool $WorkersCount
Write-Verbose 'Loading workers on the runspace pool'
$AsyncPipelines = #()
$i = 0
$IoWorkers + $StdErrWorkers |
ForEach-Object {
$Param = #{
BufferSize = $BufferSize
Id = $i
SyncHash = $SyncHash
} + $_
$AsyncPipelines += Invoke-Async -RunspacePool $RunspacePool -ScriptBlock $PumpData -Parameters $Param
$i++
Write-Verbose 'Started working thread'
$Param | Format-Table -HideTableHeaders -AutoSize | Out-String | Write-Debug
}
Write-Verbose 'Waiting for I\O to complete...'
if($IoTimeout){Write-Verbose "Timeout is $IoTimeout seconds"}
Do
{
# Check for pipelines with errors
[array]$FailedPipelines = Receive-AsyncStatus -Pipelines $AsyncPipelines | Where-Object {$_.Completed -and $_.Error}
if($FailedPipelines)
{
"$($FailedPipelines.Length) pipeline(s) failed!",
($FailedPipelines | Select-Object -ExpandProperty Error | Format-Table -AutoSize | Out-String) | Write-Debug
}
if($IoTimeout)
{
# Compare I\O start time of thread with current time
[array]$LockedReaders = $SyncHash.Keys | Where-Object {[TimeSpan]::FromTicks([DateTime]::UtcNow.Ticks - $SyncHash.$_.IoStartTime).TotalSeconds -gt $IoTimeout}
if($LockedReaders)
{
# Yikes, someone is stuck
"$($LockedReaders.Length) I\O operations reached timeout!" | Write-Verbose
$SyncHash.GetEnumerator() | ForEach-Object {"$($_.Key) = $($_.Value.Status)"} | Sort-Object | Out-String | Write-Debug
$PsCommand | ForEach-Object {
Write-Verbose "Killing process: Name=$($_.Name), Id=$($_.Id)"
Try
{
$_.Kill()
}
Catch
{
Write-Error 'Failed to kill process!'
}
}
break
}
}
Start-Sleep 1
}
While(Receive-AsyncStatus -Pipelines $AsyncPipelines | Where-Object {!$_.Completed}) # Loop until all pipelines are finished
Write-Verbose 'Waiting for all pipelines to finish...'
$IoStats = Receive-AsyncResults -Pipelines $AsyncPipelines
Write-Verbose 'All pipelines are finished'
Write-Verbose 'Collecting StdErr for all processes'
$PipeStdErr = $StdErrWorkers |
ForEach-Object {
$Encoding = $_.Src.StartInfo.StandardOutputEncoding
if(!$Encoding)
{
$Encoding = [System.Text.Encoding]::Default
}
#{
FileName = $_.Src.StartInfo.FileName
StdErr = $Encoding.GetString($_.Dst.ToArray())
ExitCode = $_.Src.ExitCode
}
} |
Select-Object #{Name = 'FileName' ; Expression = {$_.FileName}},
#{Name = 'StdErr' ; Expression = {$_.StdErr}},
#{Name = 'ExitCode' ; Expression = {$_.ExitCode}}
if($IoWorkers[-1].Dst -is [System.IO.MemoryStream])
{
Write-Verbose 'Collecting final pipeline output'
if($IoWorkers[-1].Src -is [System.Diagnostics.Process])
{
$Encoding = $IoWorkers[-1].Src.StartInfo.StandardOutputEncoding
}
if(!$Encoding)
{
$Encoding = [System.Text.Encoding]::Default
}
$PipeResult = $Encoding.GetString($IoWorkers[-1].Dst.ToArray())
}
Write-Verbose 'Closing and disposing I\O streams'
. $CleanUp
$PsCommand |
ForEach-Object {
$_.Refresh()
if(!$_.HasExited)
{
Write-Verbose "Process is still active: Name=$($_.Name), Id=$($_.Id)"
if(!$ProcessTimeout)
{
$ProcessTimeout = -1
}
else
{
$WaitForExitProcessTimeout = $ProcessTimeout * 1000
}
Write-Verbose "Waiting for process to exit (Process Timeout = $ProcessTimeout)"
if(!$_.WaitForExit($WaitForExitProcessTimeout))
{
Try
{
Write-Verbose 'Trying to kill it'
$_.Kill()
}
Catch
{
Write-Error "Failed to kill process $_"
}
}
}
Write-Verbose "Disposing process object: Name=$($_.StartInfo.FileName)"
$_.Dispose()
}
Write-Verbose 'Disposing runspace pool'
# http://stackoverflow.com/questions/21454252/how-to-cleanup-resources-in-a-dll-when-powershell-ise-exits-like-new-pssession
$RunspacePool.Dispose()
if($ForceGC)
{
Write-Verbose 'Forcing garbage collection'
Invoke-GarbageCollection
}
if(!$RawData)
{
New-Object -TypeName psobject -Property #{Result = $PipeResult ; Status = $PipeStdErr}
}
else
{
$PipeResult
}
}
}
Bruteforce approach: feed binary data to process' stdin. I've tested this code on the cat.exe from UnixUtils and it seems to do what you want:
# Text to send
$InputVar = "No Newline, No NewLine,`nNewLine, No NewLine,`nNewLine, No NewLine"
# Buffer & initial size of MemoryStream
$BufferSize = 4096
# Convert text to bytes and write to MemoryStream
[byte[]]$InputBytes = [Text.Encoding]::UTF8.GetBytes($InputVar)
$MemStream = New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize
$MemStream.Write($InputBytes, 0, $InputBytes.Length)
[Void]$MemStream.Seek(0, 'Begin')
# Setup stdin\stdout redirection for our process
$StartInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo -Property #{
FileName = 'MyLittle.exe'
UseShellExecute = $false
RedirectStandardInput = $true
}
# Create new process
$Process = New-Object -TypeName System.Diagnostics.Process
# Assign previously created StartInfo properties
$Process.StartInfo = $StartInfo
# Start process
[void]$Process.Start()
# Pipe data
$Buffer = New-Object -TypeName byte[] -ArgumentList $BufferSize
$StdinStream = $Process.StandardInput.BaseStream
try
{
do
{
$ReadCount = $MemStream.Read($Buffer, 0, $Buffer.Length)
$StdinStream.Write($Buffer, 0, $ReadCount)
$StdinStream.Flush()
}
while($ReadCount -gt 0)
}
catch
{
throw 'Houston, we have a problem!'
}
finally
{
# Close streams
$StdinStream.Close()
$MemStream.Close()
}
# Cleanup
'Process', 'StdinStream', 'MemStream' |
ForEach-Object {
(Get-Variable $_ -ValueOnly).Dispose()
Remove-Variable $_ -Force
}
Do it a simple way create a cmd process and execute it
$cmdArgs = #('/c','something.exe','arg1', .. , 'arg2' , $anotherArg , '<', '$somefile.txt' )
&'cmd.exe' $cmdArgs
Worked perfect for piping information into stdin that I wanted,
To clear up a fundamental misconception in some of the comments: the "powershell commands" in a pipeline are cmdlets and each one runs within the process space of the single powershell. Thus, objects are being passed as is within the same process (on multiple threads) UNLESS you invoke an external command. Then the passed objects are converted to strings by the appropriate formatting cmdlet (if not already string objects). These strings are then converted to a stream of characters with each string having an appended \n. So it is not "the pipeline" adding the \n but the implicit conversion to text for input to the "legacy" command.
The basic problem in the question is that the asker is trying to get object like behaviour (e.g. a string with no trailing \n) on a character (byte) stream input. The standard input stream of a (console) process supplies characters (bytes) one at a time. The input routines collect these individual characters into a single string (typically) terminated when a \n is received. Whether the \n is returned as part of the string is up to the input routine. When the standard input stream is redirected to a file or pipe, the input routines mostly have no knowledge of this. So there is no way to determine the difference between a complete string with no \n and an incomplete string with more characters and the \n still to come.
Possible solutions (to the string delimiting problem, not the powershell added \n problem) would be to have some sort of time-out on the standard input reads. The end of a string could be signaled by no received characters for a certain time. Alternatively, if you had low enough level access to the pipe you could try to have atomic reads and writes. In this way a blocked read would return exactly what was written. Unfortunately, both of these methods have timing problems when running within a multitasking environment. If the delay is long then efficiency falls but if it is too short then it can be fooled by delays caused by process priority scheduling. Scheduling priorities can also interfere with atomic reads and writes if the writing process writes another line before the reading process has read the current one. It would need some sort of synchronizing system.
The only other way to signal that there are no more characters coming on the current line would be to close the pipe (EOF) but this is a once only method so you can only send one string (trailing \n or not). (This is how Ruby knows when the input is finished both in the initial example and in the Invoke-RawPipeline example.) It is possible that this is actually your intention (send only one string with or without trailing \n) in which case you could simply concatenate all the input (retaining or re-inserting any embedded \n) and throw away the last \n.
A possible solution to the powershell added \n problem for multiple strings would be to redefine your encoding of "a string" by terminating each string object with an otherwise invalid character sequence. \0 could be used if you had per character input (no good for C-like line input) otherwise maybe \377 (0xff). This would allow the input routines of your legacy command to "know" when a string ended. The sequence \0\n (or \377\n) would be the "end" of the string and everything before that (including a trailing \n or not, possibly using multiple reads) would be the string. I am assuming that you have some control over the input routines (e.g. you wrote the program) since any off the shelf program reading from standard input would typically be expecting a \n (or EOF) to delimit its input.
I will admit to having zero experience with the ruby -e "puts ARGF.read" command you are using after the pipe, but I think I can prove that the pipe doesn't adding a newline.
# check length of string without newline after pipe
Write-Output "abc" | %{Write-Host "$_ has a length of: $($_.Length)" }
#check of string with newline length after pipe
Write-Output "def`n" | %{Write-Host "$($_.Length) is the length of $_" -NoNewline }
#write a string without newline (suppressing newline on Write-Host)
Write-Output 'abc' | %{ Write-Host $_ -NoNewline; }
#write a string with newline (suppressing newline on Write-Host)
Write-Output "def`n" | %{ Write-Host $_ -NoNewline; }
#write a final string without newline (suppressing newline on Write-Host)
Write-Output 'ghi' | %{ Write-Host $_ -NoNewline; }
This gives me an output of:
abc has a length of: 3
4 is the length of def
abcdef
ghi
I think you might want to start looking at the ruby -e "put AGRF.read" command and see if it is adding a newline after each read.

Powershell: Capture output from cmd at run time

is it possible to capture the Output from a commandline while a cmd is running? I´ve a small exe, which displays various Messages that should be processed with a script and displays the user some informations while the script is running.
My script starts the program (the exe) with some Parameters und checks if the process is still running. While the program is running i want to capture all messages to a variable to process it. I can´t find any solution, some tests with "run.exe 2>&1" etc... fails.
Any ideas?
$oInfo = New-Object System.Diagnostics.ProcessStartInfo
$oInfo.FileName = "ping"
$oInfo.Arguments = "localhost"
$oInfo.UseShellExecute = $False
$oInfo.RedirectStandardOutput = $True
$oProcess = New-Object System.Diagnostics.Process
$oProcess.StartInfo = $oInfo
[Void]$oProcess.Start()
$bDone = $False
while (!$bDone)
{
$char = $oProcess.StandardOutput.Read()
if ($char -eq -1)
{
if ($oProcess.HasExited)
{
$bDone = $True
}
else
{
Wait-Event 1
}
}
else
{
Write-Host -NoNewline "".PadLeft(1, $char)
}
}

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"