BGProcess.psm1
using namespace System.Management.Automation using namespace System.Management.Automation.Runspaces using namespace System.Threading.Tasks class AsyncShellOutput { [PSDataCollection[psobject]] $Result [PSDataCollection[ErrorRecord]] $ErrorStream } class AsyncShell { [powershell] $Shell [IAsyncResult] $AsyncResult AsyncShell() {} AsyncShell([powershell]$powershell) { $this.Shell = $powershell } AsyncShell([runspace]$runspace) { $this.Shell = [powershell]::Create($runspace) } AsyncShell([RunspacePool]$runspacepool) { $this.Shell = [powershell]::Create() $this.Shell.RunspacePool = $runspacepool } [AsyncShellOutput] CollectResults() { if ($this.AsyncResult.IsCompleted) { try { return ([AsyncShellOutput]@{ Result = $this.Shell.EndInvoke($this.AsyncResult) ErrorStream = $this.Shell.Streams.Error }) } finally { $this.Shell.Streams.ClearStreams() $this.AsyncResult = $null } } return $null } } class BGProcessData { [string] $StandardOutput [string] $StandardError [ErrorRecord[]] $ErrorRecords [bool] $HasData } class BGProcessDataBuilder { hidden [text.stringbuilder] $stdOut hidden [text.stringbuilder] $stdErr hidden [collections.generic.list[errorrecord]] $errors hidden [bool] $hasData BGProcessDataBuilder() { $this.stdOut = [text.stringbuilder]::new() $this.stdErr = [text.stringbuilder]::new() $this.errors = [collections.generic.list[errorrecord]]::new() } [void] AddStandardOutput([string]$data) { $this.stdOut.Append($data) $this.hasData = $true } [void] AddStandardError([string]$data) { $this.stdErr.Append($data) $this.hasData = $true } [void] AddErrorRecord([errorrecord]$errorrecord) { $this.errors.Add($errorrecord) $this.hasData = $true } [BGProcessData] ToBGProcessData() { return ([BGProcessData]@{ StandardOutput = $this.stdOut.ToString() StandardError = $this.stdErr.ToString() ErrorRecords = $this.errors.ToArray() HasData = $this.hasData }) } } class BGProcess { [System.Diagnostics.Process] $Process [int] $Id [string] $Name [nullable[int]] $ExitCode [bool] $HasExited hidden [AsyncShell]$stdOut hidden [AsyncShell]$StdErr hidden static [hashtable] $Instances = @{} BGProcess ([diagnostics.process]$process) { $this.Process = $process $this.Id = $process.Id $this.Name = $process.Name [BGProcess]::Instances[$process.Id] = $this $pool = [runspacefactory]::CreateRunspacePool(2, 2) $pool.Open() $readStream = (Get-Command -Name ReadStream).ScriptBlock $stdOutShell = [powershell]::Create().AddScript($readStream).AddArgument($process.StandardOutput.BaseStream) $stdErrShell = [powershell]::Create().AddScript($readStream).AddArgument($process.StandardError.BaseStream) $stdOutShell.RunspacePool = $pool $stdErrShell.RunspacePool = $pool $this.stdOut = [AsyncShell]::new($stdOutShell) $this.stdErr = [AsyncShell]::new($stdErrShell) $this.stdOut.AsyncResult = $this.stdOut.Shell.BeginInvoke() $this.StdErr.AsyncResult = $this.StdErr.Shell.BeginInvoke() Register-ObjectEvent -InputObject $this.Process -EventName Exited -Action { $process = $Sender -as [diagnostics.process] [BGProcess]::Instances[$process.Id].ExitCode = $process.ExitCode [BGProcess]::Instances[$process.Id].HasExited = $true } } [void] Write([string] $data) { $this.Process.StandardInput.Write($data) } [BGProcessData] Read() { $dataBuilder = [BGProcessDataBuilder]::new() while ($this.stdOut.AsyncResult.IsCompleted) { $results = $this.stdOut.CollectResults() foreach ($record in $results.Result) { $dataBuilder.AddStandardOutput($record) } foreach ($record in $results.ErrorStream) { $dataBuilder.AddErrorRecord($record) } $this.stdOut.AsyncResult = $this.stdOut.Shell.BeginInvoke() } while ($this.StdErr.AsyncResult.IsCompleted) { $results = $this.StdErr.CollectResults() foreach ($record in $results.Result) { $dataBuilder.AddStandardError($record) } foreach ($record in $results.ErrorStream) { $dataBuilder.AddErrorRecord($record) } $this.StdErr.AsyncResult = $this.StdErr.Shell.BeginInvoke() } return $dataBuilder.ToBGProcessData() } } function ReadStream { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNull()] [System.IO.Stream] $Stream, [Parameter(Position = 1)] [System.Text.Encoding] $Encoding = [System.Text.Encoding]::UTF8 ) process { $sb = [text.stringbuilder]::new() $buffer = [byte[]]::new(1KB) $read = 0 do { $read = $Stream.Read($buffer, 0, $buffer.Length) if ($read -gt 0) { $null = $sb.Append($Encoding.GetString($buffer, 0, $read)) } } while ($read -eq $buffer.Length) $sb.ToString() } } function Read-BGProcess { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [BGProcess] $Process, [Parameter()] [switch] $MapErrorsToStdOut, [Parameter()] [switch] $Wait, [Parameter()] [timespan] $Timeout = (New-TimeSpan -Seconds 1) ) process { $stopwatch = [diagnostics.stopwatch]::StartNew() do { $data = $Process.Read() foreach ($record in $data.ErrorRecords) { $stopwatch.Restart() Write-Error -ErrorRecord $record } if (-not [string]::IsNullOrEmpty($data.StandardError)) { $stopwatch.Restart() if ($MapErrorsToStdOut) { $data.StandardError } else { Write-Error -Message $data.StandardError } } if (-not [string]::IsNullOrEmpty($data.StandardOutput)) { $stopwatch.Restart() $data.StandardOutput } if ($Wait -and $stopwatch.Elapsed -lt $Timeout) { Start-Sleep -Milliseconds 100 } } while ($Wait -and $stopwatch.Elapsed -lt $Timeout) } } function Start-BGProcess { [CmdletBinding(SupportsShouldProcess)] [OutputType([BGProcess])] param( [Parameter(Mandatory, Position = 0)] [Alias('Path')] [string] $FileName, [Parameter(ValueFromRemainingArguments, Position = 1)] [string[]] $Arguments ) process { try { $startInfo = [System.Diagnostics.ProcessStartInfo]@{ FileName = $FileName Arguments = $Arguments -join ' ' CreateNoWindow = $true RedirectStandardInput = $true RedirectStandardOutput = $true RedirectStandardError = $true UseShellExecute = $false } if ($PSCmdlet.ShouldProcess($FileName, "Start process with arguments: $($startInfo.Arguments)")) { $p = [System.Diagnostics.Process]::Start($startInfo) $p.EnableRaisingEvents = $true [BGProcess]::new($p) } } catch { throw } } } function Stop-BGProcess { [CmdletBinding(SupportsShouldProcess)] [OutputType([BGProcess])] param ( [Parameter(Mandatory, ValueFromPipeline)] [BGProcess[]] $Process, [Parameter()] [switch] $PassThru ) process { foreach ($p in $Process) { if ($PSCmdlet.ShouldProcess(("{0} ({1})" -f $p.Process.Name, $p.Id), "Stop-BGProcess")) { $p.process | Stop-Process if ($PassThru) { $p } } } } } function Wait-BGProcess { [CmdletBinding()] [OutputType([BGProcess])] param ( [Parameter(Mandatory, ValueFromPipeline)] [BGProcess[]] $Process, [Parameter()] [switch] $PassThru ) process { foreach ($p in $Process) { while (-not $p.HasExited) { Start-Sleep -Milliseconds 100 } if ($PassThru) { $p } } } } function Write-BGProcess { [CmdletBinding()] [OutputType([BGProcess])] param( [Parameter(Mandatory, ValueFromPipeline)] [BGProcess] $Process, [Parameter(Mandatory, Position = 0)] [string] $Text, [Parameter()] [string] $LineTerminator = ([System.Environment]::NewLine), [Parameter()] [switch] $NoLineTerminator, [Parameter()] [switch] $PassThru ) process { $Process.Write($Text) if (-not $NoLineTerminator) { $Process.Write($LineTerminator) } if ($Passthru) { $Process } } } |