Public/TMD.PowerShell.ps1
using namespace System.Collections.Generic function Test-TMDIsRunningAction { $Global:TmdRunningAction ?? $false } function Invoke-WindowsPowerShell { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [scriptblock]$ScriptBlock, [Parameter(Mandatory = $false)] [psobject]$Params, [Parameter(Mandatory = $false)] [switch]$Console, [Parameter(Mandatory = $false)] [ValidateSet("Text", "XML")] [psobject]$Format = 'Text' ) begin { ## If Windows if (-not $IsWindows) { throw "Running Invoke-Windows PowerShell is only supported on a Windows computer." } ## Get the PowerShell Path $PowerShellExeFilePath = 'C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe' If (-not (Test-Path $PowerShellExeFilePath)) { throw 'PowerShell is not installed.' } ## Setup Standard Process Options $PowerShellProcInfo = New-Object System.Diagnostics.ProcessStartInfo $PowerShellProcInfo.FileName = $PowerShellExeFilePath $PowerShellProcInfo.UseShellExecute = $false $PowerShellProcInfo.CreateNoWindow = $true $PowerShellProcInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden ## Redirect Standard Streams $PowerShellProcInfo.RedirectStandardOutput = $true $PowerShellProcInfo.RedirectStandardInput = $true $PowerShellProcInfo.RedirectStandardError = $true ## Assmble Powershell Params and Arguments $PowerShellProcInfo.Arguments = '-NoLogo ' $PowerShellProcInfo.Arguments += '-ExecutionPolicy Unrestricted ' $PowerShellProcInfo.Arguments += '-NonInteractive ' $PowerShellProcInfo.Arguments += '-WindowStyle Hidden ' $PowerShellProcInfo.Arguments += "-OutputFormat $Format " ## Build a ScriptBlock that encompasses the $Params block, and converts the data safely $ProcessScriptBlockText = @() $ParamsJSON = $Params | ConvertTo-Json -Depth 10 -Compress -ErrorAction SilentlyContinue $ProcessScriptBlockText += "`$Params = ('$ParamsJSON' | ConvertFrom-Json -ErrorAction SilentlyContinue)" $ProcessScriptBlockText += $ScriptBlock.ToString() ## Use a temporary file to store the script to be invoked $tempps1FilePath = "$(New-TemporaryFile).ps1" $PowerShellProcInfo.Arguments += "-File $tempps1FilePath" Set-Content -Value $ProcessScriptBlockText -Path $tempps1FilePath ## Open a new Process to run PowerShell in $PowerShellProc = New-Object System.Diagnostics.Process $PowerShellProc.StartInfo = $PowerShellProcInfo ## Create an Event Handler for Standard Out # $global:PowerShellStdErr = [System.Collections.ArrayList]::New() # $global:PowerShellStdOut = [System.Collections.ArrayList]::New() $PowerShellStdErr = [List[string]]::new() $PowerShellStdOut = [List[string]]::new() } process { try { if (-not $PowerShellProc.Start()) { throw "Error starting the Windows Powershell process: $_" } ## Keep reading Standard Out until the stream has ended while ( -not $PowerShellProc.StandardOutput.EndOfStream ) { $SerializedStandardOutData = $PowerShellProc.StandardOutput.ReadToEnd() if ($Format -eq 'Text') { foreach ($DataItem in $SerializedStandardOutData) { if (-not [string]::IsNullOrEmpty($DataItem)) { $PowerShellStdOut.Add($DataItem) if ($Console) { Write-Host $DataItem } } } } else { $Data = [System.Management.Automation.PSSerializer]::Deserialize(($SerializedStandardOutData -replace '#< CLIXML', '')) foreach ($DataItem in $Data) { if (-Not [string]::IsNullOrEmpty($DataItem)) { $PowerShellStdOut.Add($DataItem) if ($Console) { Write-Host $DataItem } } } } } ## Then read all of Standard Error while ($PowerShellProc.StandardError -and -not $PowerShellProc.StandardError.EndOfStream) { if ($Format -eq 'Text') { $SerializedStandardError = $PowerShellProc.StandardError.ReadToEnd() foreach ($DataItem in $SerializedStandardError) { if (-not [string]::IsNullOrEmpty($DataItem)) { $PowerShellStdErr.Add($DataItem) Write-Host "ERROR: $DataItem" -ForegroundColor Red } } } else { $SerializedStandardError = $PowerShellProc.StandardError.ReadToEnd() $Data = [System.Management.Automation.PSSerializer]::Deserialize(( $SerializedStandardError -replace '#< CLIXML', '')) foreach ($DataItem in $Data) { if (-not [string]::IsNullOrEmpty($DataItem)) { <# TONY - This is set up now to return native objects, but they seem to populated with far more crap than we want. I'm going to implement the optional format switch and simply use Text as the default. #> $PowerShellStdErr.Add($DataItem) Write-Host "ERROR: $DataItem" -ForegroundColor Red } } } if ($PowerShellStdErr) { throw $PowerShellStdErr } } $PowerShellProc.WaitForExit() } catch { throw $_ } finally { $PowerShellProc.Close() $PowerShellProc.Dispose() } ## Convert the output to a String if ($PowerShellStdErr) { throw $PowerShellStdErr } try { $PassThruTempFile = New-TemporaryFile Set-Content -Path $PassThruTempFile -Value $PowerShellStdOut -Force $ResponseData = Import-Clixml $PassThruTempFile.FullName return $ResponseData } catch { return $PowerShellStdOut } } end { Remove-Item $tempps1FilePath -Force Remove-Item $PassThruTempFile -Force } } |