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
    }
}