Private/Invoke-ColorScriptChildProcess.ps1
|
function Invoke-ColorScriptChildProcess { <# .SYNOPSIS Executes an unknown or custom colorscript in a separate PowerShell process. .DESCRIPTION A process boundary prevents colorscript functions, variables, aliases, and location changes from leaking into the caller. It is not a security sandbox: the child process has the same operating-system permissions as the current user. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ScriptPath, [switch]$ForCache ) $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($ScriptPath) $result = [pscustomobject]@{ ScriptName = $scriptName StdOut = '' StdErr = '' ExitCode = $null Success = $false } if (-not (Test-Path -LiteralPath $ScriptPath -PathType Leaf)) { $result.StdErr = $script:Messages.ScriptPathNotFound return $result } $executable = Get-PowerShellExecutable $scriptDirectory = [System.IO.Path]::GetDirectoryName($ScriptPath) $process = $null try { $escapedScriptPath = $ScriptPath.Replace("'", "''") $escapedScriptDirectory = if ($scriptDirectory) { $scriptDirectory.Replace("'", "''") } else { '' } $commandBuilder = New-Object System.Text.StringBuilder $null = $commandBuilder.AppendLine('[Console]::OutputEncoding = [System.Text.Encoding]::UTF8') $null = $commandBuilder.AppendLine('& {') $null = $commandBuilder.AppendLine((Get-ColorScriptCaptureCommand)) $null = $commandBuilder.Append("} -__cseScriptPath '$escapedScriptPath' ") $null = $commandBuilder.Append("-__cseScriptDirectory '$escapedScriptDirectory' ") $null = $commandBuilder.Append('| ForEach-Object { [Console]::Out.Write([string]$_) }') $commandBytes = [System.Text.Encoding]::Unicode.GetBytes($commandBuilder.ToString()) $encodedCommand = [Convert]::ToBase64String($commandBytes) $startInfo = New-Object System.Diagnostics.ProcessStartInfo $startInfo.FileName = $executable $startInfo.Arguments = "-NoLogo -NoProfile -NonInteractive -EncodedCommand $encodedCommand" $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardOutput = $true $startInfo.RedirectStandardError = $true $startInfo.StandardOutputEncoding = [System.Text.Encoding]::UTF8 $startInfo.StandardErrorEncoding = [System.Text.Encoding]::UTF8 if ($ForCache) { try { $startInfo.EnvironmentVariables['COLOR_SCRIPTS_ENHANCED_CACHE_BUILD'] = '1' } catch { Write-Verbose ("Unable to set cache build environment variable: {0}" -f $_.Exception.Message) } } if ($scriptDirectory) { $startInfo.WorkingDirectory = $scriptDirectory } $process = New-Object System.Diagnostics.Process $process.StartInfo = $startInfo $null = $process.Start() # Drain both redirected streams concurrently. Reading one stream to completion before the # other can deadlock when a renderer fills the second process pipe. $supportsAsyncRead = ( $process.StandardOutput.PSObject.Methods['ReadToEndAsync'] -and $process.StandardError.PSObject.Methods['ReadToEndAsync']) if ($supportsAsyncRead) { $outputTask = $process.StandardOutput.ReadToEndAsync() $errorTask = $process.StandardError.ReadToEndAsync() } $process.WaitForExit() if ($supportsAsyncRead) { $output = $outputTask.GetAwaiter().GetResult() $errorOutput = $errorTask.GetAwaiter().GetResult() } else { # Test doubles and older custom stream implementations may expose only ReadToEnd. $output = $process.StandardOutput.ReadToEnd() $errorOutput = $process.StandardError.ReadToEnd() } $result.ExitCode = $process.ExitCode $result.StdOut = $output $result.StdErr = $errorOutput $result.Success = ($process.ExitCode -eq 0) } catch { $result.StdErr = $_.Exception.Message } finally { if ($process) { $process.Dispose() } } return $result } |