From 406f0a3f358f473b214b51941d4fc7188e1b6943 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Thu, 30 Jul 2026 15:34:41 -0700 Subject: [PATCH] Bound post-kill WaitForExit to prevent scenario-test hangs The scenario-test harness kills long-running processes (e.g. `dotnet run` on web templates) via Kill(true) followed by a parameterless WaitForExit(). With stdout/stderr redirected and read asynchronously, WaitForExit() blocks until the pipes reach EOF, not merely until the process exits. A grandchild that inherited the pipe handles and survives the tree-kill race keeps the write end open, so WaitForExit() never returns and the leg runs until the pipeline task timeout (observed as a 4h hang with no .trx). This is most frequent on the newest distro image (Fedora 43) in the source-build offline validation matrix. Bound every post-kill wait with a grace timeout so the run fails fast instead of hanging. Sites: ExecuteHelper.ExecuteProcess and DotNetSdkHelper ExecuteRunWeb/ExecuteRunUIApp. Investigation: https://github.com/dotnet/source-build/issues/5624 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 367a8152-b3c0-447a-99f1-ea5d9a9d1d79 --- .../ExecuteHelper.cs | 13 ++++++++++++- .../DotNetSdkHelper.cs | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.DotNet.ScenarioTests.Common/ExecuteHelper.cs b/src/Microsoft.DotNet.ScenarioTests.Common/ExecuteHelper.cs index cfa888ee0..58ad33255 100644 --- a/src/Microsoft.DotNet.ScenarioTests.Common/ExecuteHelper.cs +++ b/src/Microsoft.DotNet.ScenarioTests.Common/ExecuteHelper.cs @@ -10,6 +10,13 @@ namespace Microsoft.DotNet.ScenarioTests.Common; public static class ExecuteHelper { + // Grace period to wait for a killed process tree to fully exit. A parameterless + // WaitForExit() after Kill(true) blocks until the redirected stdout/stderr pipes + // reach EOF; a surviving grandchild (e.g. the web app spawned by `dotnet run`) that + // inherited those handles can keep them open indefinitely, hanging the test until the + // pipeline task timeout. Bounding the wait lets the run fail fast instead. + public const int KillGraceMilliseconds = 30_000; + public static (Process Process, string StdOut, string StdErr) ExecuteProcess( string fileName, string args, @@ -63,7 +70,11 @@ public static (Process Process, string StdOut, string StdErr) ExecuteProcess( { outputHelper.WriteLine($"Killing: {fileName} {args}"); process.Kill(true); - process.WaitForExit(); + if (!process.WaitForExit(KillGraceMilliseconds)) + { + outputHelper.WriteLine("Process tree did not fully exit within the kill grace period; " + + "abandoning wait to avoid hanging on inherited stdout/stderr handles."); + } ProcessStartInfo startInfo = process.StartInfo; string msg = $" {startInfo.FileName} {startInfo.Arguments} timed out after " + $"{millisecondTimeout} milliseconds" + diff --git a/src/Microsoft.DotNet.ScenarioTests.SdkTemplateTests/DotNetSdkHelper.cs b/src/Microsoft.DotNet.ScenarioTests.SdkTemplateTests/DotNetSdkHelper.cs index 16437ec3a..e3aac55fc 100644 --- a/src/Microsoft.DotNet.ScenarioTests.SdkTemplateTests/DotNetSdkHelper.cs +++ b/src/Microsoft.DotNet.ScenarioTests.SdkTemplateTests/DotNetSdkHelper.cs @@ -238,7 +238,7 @@ void processConfigCallback(Process process) if (e.Data?.Contains("Application started. Press Ctrl+C to shut down.") ?? false) { process.Kill(true); - process.WaitForExit(); + process.WaitForExit(ExecuteHelper.KillGraceMilliseconds); } }); } @@ -276,7 +276,7 @@ async void processConfigCallback(Process process) await Task.Delay(5000); } TerminateProcess(process.Handle, 0); - process.WaitForExit(); + process.WaitForExit(ExecuteHelper.KillGraceMilliseconds); } bool checkProcess(string projectDirectory, Process process)