Private/Invoke-RaidinessBrowserProcess.ps1

<#
    Running the browser once, and the single seam the tests mock.

    Deliberately System.Diagnostics.Process and not Start-Process: this needs
    stdout and a hard deadline in one place, and Start-Process gives neither
    cleanly. It is also the read-only lint's least favourite cmdlet, and a
    local browser does not need an allow marker to justify itself.

    Standard output is read asynchronously *before* WaitForExit. A megabyte of
    DOM will fill the pipe buffer and deadlock a process that is only waited
    on -- which is the classic way this kind of bridge hangs forever.
#>

function Invoke-RaidinessBrowserProcess {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)]
        [string] $Browser,

        [Parameter(Mandatory = $true)]
        [string[]] $ArgumentList,

        [int] $TimeoutSeconds = 180
    )

    $info = [System.Diagnostics.ProcessStartInfo]::new()
    $info.FileName = $Browser
    foreach ($argument in $ArgumentList) { $info.ArgumentList.Add($argument) }
    $info.RedirectStandardOutput = $true
    $info.RedirectStandardError = $true
    $info.UseShellExecute = $false
    $info.CreateNoWindow = $true

    $process = [System.Diagnostics.Process]::Start($info)
    $stdout = $process.StandardOutput.ReadToEndAsync()
    $stderr = $process.StandardError.ReadToEndAsync()

    $finished = $process.WaitForExit($TimeoutSeconds * 1000)
    if (-not $finished) {
        try { $process.Kill($true) } catch { Write-Verbose "browser did not stop: $($_.Exception.Message)" }
        return [pscustomobject]@{ TimedOut = $true; ExitCode = -1; Output = ''; Error = '' }
    }

    [pscustomobject]@{
        TimedOut = $false
        ExitCode = $process.ExitCode
        Output   = $stdout.GetAwaiter().GetResult()
        Error    = $stderr.GetAwaiter().GetResult()
    }
}