diff --git a/tools/run-with-console.ps1 b/tools/run-with-console.ps1 index bb5e0bb..9c74f79 100644 --- a/tools/run-with-console.ps1 +++ b/tools/run-with-console.ps1 @@ -20,72 +20,99 @@ param( ) $TempBatFile = Join-Path ([System.IO.Path]::GetTempPath()) "run_bash_$(Get-Random).bat" +# conhost.exe adds VT100 escape codes which GitHub Actions doesn't handle well, +# so pipe output through a file. +$ConsoleOutputFile = Join-Path ([System.IO.Path]::GetTempPath()) "exit_code_$(Get-Random).txt" # conhost.exe does not propagate the exit code, so write it to a file. $ExitCodeFile = Join-Path ([System.IO.Path]::GetTempPath()) "exit_code_$(Get-Random).txt" $CurrentWorkingDirectory = Get-Location $BatchContent = @" @echo off cd "$CurrentWorkingDirectory" -$Command +$Command >"$ConsoleOutputFile" 2>&1 echo %ERRORLEVEL% > "$ExitCodeFile" exit /b %ERRORLEVEL% "@ -try { - $BatchContent | Out-File -FilePath $TempBatFile -Encoding ASCII +$asyncConhost = @" +using System; +using System.Diagnostics; +using System.IO; - $ProcessStartInfo = New-Object System.Diagnostics.ProcessStartInfo - $ProcessStartInfo.FileName = "conhost.exe" - $ProcessStartInfo.Arguments = "`"$TempBatFile`"" - $ProcessStartInfo.RedirectStandardOutput = $true - $ProcessStartInfo.RedirectStandardError = $true - $ProcessStartInfo.UseShellExecute = $false - $ProcessStartInfo.CreateNoWindow = $false +namespace AsyncConhost { + public static class exec { + public static int runCommand(string batFile, string consoleOutputFile, int timeoutInSeconds) { + using (var stdoutStream = Console.OpenStandardOutput()) + using (var conhostOutputStream = File.Open(consoleOutputFile, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) { + Process process = new Process(); + process.StartInfo.FileName = "conhost.exe"; + process.StartInfo.Arguments = '"' + batFile + '"'; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; - $Process = New-Object System.Diagnostics.Process - $Process.StartInfo = $ProcessStartInfo - $OutputAction = { - if ($Event.SourceEventArgs.Data -ne $null) { - Write-Host $Event.SourceEventArgs.Data - } - } - $ErrorAction = { - if ($Event.SourceEventArgs.Data -ne $null) { - Write-Error $Event.SourceEventArgs.Data - } - } - $OutputEvent = Register-ObjectEvent -InputObject $Process -EventName OutputDataReceived -Action $OutputAction - $ErrorEvent = Register-ObjectEvent -InputObject $Process -EventName ErrorDataReceived -Action $ErrorAction + var outErrReceivedHandler = new DataReceivedEventHandler( + (sender, e) => Console.WriteLine(e.Data) + ); + process.OutputDataReceived += outErrReceivedHandler; + process.ErrorDataReceived += outErrReceivedHandler; - try { - $Process.Start() | Out-Null - $Process.BeginOutputReadLine() - $Process.BeginErrorReadLine() - Write-Host "Started conhost process with PID: $($Process.Id)" + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + Console.WriteLine("Started conhost.exe " + batFile + " with PID " + process.Id); - $ProcessCompleted = $Process.WaitForExit($TimeoutMinutes * 60 * 1000) + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + while (!process.WaitForExit(100)) { + ForwardConsoleOutput(stdoutStream, conhostOutputStream); + if (stopWatch.ElapsedMilliseconds > 1000 * timeoutInSeconds) { + Console.WriteLine("Process exceeded timeout, force killing..."); + // process.Kill(/*entireProcessTree =*/ true); should work. + process.Kill(); + Console.WriteLine("Force killed process and its children"); + return 42; + } + } + stopWatch.Stop(); + ForwardConsoleOutput(stdoutStream, conhostOutputStream); + return process.ExitCode; + } + } - if (!$ProcessCompleted) { - Write-Error "Process exceeded timeout, force killing..." - $Process.Kill($true) - Write-Error "Force killed process and its children" - exit -1 + private static void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) { + Console.WriteLine(outLine.Data); } - - if (!(Test-Path $ExitCodeFile)) { - Write-Error "Exit code file not found" - exit 1 + + private static void ForwardConsoleOutput(Stream stdoutStream, FileStream consoleOutputStream) { + int bufSize = 65536; + byte[] bytes = new byte[bufSize]; + while (true) { + int numBytesRead = consoleOutputStream.Read(bytes, 0, bufSize); + if (numBytesRead == 0) { + // End of file for now. + break; + } + stdoutStream.Write(bytes, 0, numBytesRead); + } } - - $ActualExitCode = [int](Get-Content $ExitCodeFile -Raw).Trim() - Write-Host "Process completed normally with exit code: $ActualExitCode" - exit $ActualExitCode } - finally { - Unregister-Event -SourceIdentifier $OutputEvent.Name - Unregister-Event -SourceIdentifier $ErrorEvent.Name - $Process.Dispose() +} +"@ + +try { + $BatchContent | Out-File -FilePath $TempBatFile -Encoding ASCII + + Add-Type -TypeDefinition $asyncConhost -Language CSharp + [AsyncConhost.exec]::runCommand($TempBatFile, $ConsoleOutputFile, $TimeoutMinutes * 60) + if (!(Test-Path $ExitCodeFile)) { + Write-Error "Exit code file not found" + exit 1 } + $ActualExitCode = [int](Get-Content $ExitCodeFile -Raw).Trim() + Write-Host "Process completed normally with exit code: $ActualExitCode" + exit $ActualExitCode } catch { Write-Error "Error running process: $_" @@ -93,5 +120,6 @@ catch { } finally { Remove-Item $TempBatFile -Force -ErrorAction SilentlyContinue + Remove-Item $ConsoleOutputFile -Force -ErrorAction SilentlyContinue Remove-Item $ExitCodeFile -Force -ErrorAction SilentlyContinue }