From af90ed713db2f31009c96de54d685c8f28dde3da Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Wed, 5 Aug 2026 14:04:15 -0400 Subject: [PATCH 1/4] Add diagnostics for Process test hangs Focus and serialize Windows ProcessStartInfo tests, log startup and test ordering, and capture a WER dump before the Helix timeout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54c890f9-b409-4b7d-b810-3701e125fd96 --- .../tests/AssemblyInfo.cs | 1 + .../tests/ProcessStartInfoTests.cs | 6 + .../tests/ProcessTestHangDiagnostics.cs | 111 ++++++++++++++++++ .../System.Diagnostics.Process.Tests.csproj | 6 + 4 files changed, 124 insertions(+) create mode 100644 src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs diff --git a/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs b/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs index 80471364814581..607f83dab3569b 100644 --- a/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs +++ b/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs @@ -6,5 +6,6 @@ // Process tests can conflict with each other, as they modify ambient state // like the console code page and environment variables [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] +[assembly: System.Diagnostics.Tests.ProcessTestHangDiagnosticsAttribute] [assembly: SkipOnPlatform(TestPlatforms.Browser, "System.Diagnostics.Process is not supported on Browser.")] diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs index ba1eb8445aa33c..c23a708278c42d 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs @@ -1238,6 +1238,7 @@ private static string GetAssociationDetails() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindowsNanoServer))] public void ShellExecute_Nano_Fails_Start() { + ProcessTestHangDiagnostics.Log("ShellExecute_Nano_Fails_Start started."); string tempFile = GetTestFilePath() + ".txt"; File.Create(tempFile).Dispose(); @@ -1250,7 +1251,9 @@ public void ShellExecute_Nano_Fails_Start() // Nano does not support either the STA apartment or ShellExecute. // Since we try to start an STA thread for ShellExecute, we hit a ThreadStartException // before we get to the PlatformNotSupportedException. + ProcessTestHangDiagnostics.Log("ShellExecute_Nano_Fails_Start calling Process.Start."); Assert.Throws(() => Process.Start(info)); + ProcessTestHangDiagnostics.Log("ShellExecute_Nano_Fails_Start completed Process.Start."); } public static TheoryData UseShellExecute @@ -1359,6 +1362,7 @@ public void InitializeWithArgumentList_ThrowsArgumentNullException() [ActiveIssue("https://github.com/dotnet/runtime/issues/34685", TestRuntimes.Mono)] public void StartInfo_NotepadWithContent_withArgumentList(bool useShellExecute) { + ProcessTestHangDiagnostics.Log($"StartInfo_NotepadWithContent_withArgumentList started; UseShellExecute={useShellExecute}."); string tempFile = GetTestFilePath() + ".txt"; File.WriteAllText(tempFile, $"StartInfo_NotepadWithContent({useShellExecute})"); @@ -1372,8 +1376,10 @@ public void StartInfo_NotepadWithContent_withArgumentList(bool useShellExecute) info.ArgumentList.Add(tempFile); + ProcessTestHangDiagnostics.Log($"StartInfo_NotepadWithContent_withArgumentList calling Process.Start; UseShellExecute={useShellExecute}."); using (var process = Process.Start(info)) { + ProcessTestHangDiagnostics.Log($"StartInfo_NotepadWithContent_withArgumentList completed Process.Start; UseShellExecute={useShellExecute}; ProcessId={process?.Id}."); Assert.True(process != null, $"Could not start {info.FileName} {info.Arguments} UseShellExecute={info.UseShellExecute}"); try diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs new file mode 100644 index 00000000000000..11df9e018cc10e --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Threading; +using Microsoft.Win32; +using Xunit.Sdk; + +namespace System.Diagnostics.Tests +{ + internal sealed class ProcessTestHangDiagnosticsAttribute : BeforeAfterTestAttribute + { + public override void Before(MethodInfo methodUnderTest) + { + ProcessTestHangDiagnostics.Log($"Starting {methodUnderTest.DeclaringType?.FullName}.{methodUnderTest.Name}."); + } + + public override void After(MethodInfo methodUnderTest) + { + ProcessTestHangDiagnostics.Log($"Finished {methodUnderTest.DeclaringType?.FullName}.{methodUnderTest.Name}."); + } + } + + internal static class ProcessTestHangDiagnostics + { +#if TargetsWindows + private const string InstallationTypeKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion"; + private static readonly TimeSpan WatchdogTimeout = TimeSpan.FromMinutes(3); + private static readonly TextWriter s_log = TextWriter.Synchronized( + new StreamWriter(Console.OpenStandardError(), Encoding.UTF8, bufferSize: 1024, leaveOpen: true) { AutoFlush = true }); + + [ModuleInitializer] + internal static void Initialize() + { + Log($"ProcessPath={Environment.ProcessPath}; OSVersion={Environment.OSVersion.Version}; Framework={RuntimeInformation.FrameworkDescription}"); + ConfigureWindowsErrorReporting(); + + var watchdog = new Thread(Watchdog) + { + IsBackground = true, + Name = "Process tests hang watchdog" + }; + watchdog.Start(); + + Log("Reading Windows InstallationType."); + object? installationType = Registry.GetValue(InstallationTypeKey, "InstallationType", defaultValue: null); + Log($"InstallationType={installationType ?? ""}"); + + Log("Evaluating PlatformDetection.IsWindowsNanoServer and IsWindowsServerCore."); + bool isWindowsNanoServer = PlatformDetection.IsWindowsNanoServer; + bool isWindowsServerCore = PlatformDetection.IsWindowsServerCore; + Log($"IsWindowsNanoServer={isWindowsNanoServer}; IsWindowsServerCore={isWindowsServerCore}"); + } + + internal static void Log(string message) + { + s_log.WriteLine($"[Process test hang diagnostics] {message}"); + } + + private static void ConfigureWindowsErrorReporting() + { + string? dumpFolder = Environment.GetEnvironmentVariable("HELIX_DUMP_FOLDER"); + string? processPath = Environment.ProcessPath; + if (string.IsNullOrEmpty(dumpFolder) || string.IsNullOrEmpty(processPath)) + { + Log($"WER LocalDumps not configured; HELIX_DUMP_FOLDER={dumpFolder ?? ""}."); + return; + } + + string executableName = Path.GetFileName(processPath); + string keyPath = $@"SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\{executableName}"; + + try + { + using RegistryKey? key = Registry.LocalMachine.CreateSubKey(keyPath); + if (key is null) + { + Log($"Unable to create WER LocalDumps key HKLM\\{keyPath}."); + return; + } + + key.SetValue("DumpCount", 2, RegistryValueKind.DWord); + key.SetValue("DumpFolder", dumpFolder, RegistryValueKind.ExpandString); + key.SetValue("DumpType", 2, RegistryValueKind.DWord); + Log($"WER LocalDumps configured for {executableName} in {dumpFolder}."); + } + catch (Exception e) when (e is IOException or SecurityException or UnauthorizedAccessException) + { + Log($"WER LocalDumps configuration failed: {e}"); + } + } + + private static void Watchdog() + { + Thread.Sleep(WatchdogTimeout); + const string message = "System.Diagnostics.Process.Tests exceeded the diagnostic watchdog timeout."; + Log(message); + Environment.FailFast(message); + } +#else + internal static void Log(string message) + { + } +#endif + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index 59d512bf16abde..05ea8d989c23b1 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -11,6 +11,11 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) $(DefineConstants);TargetsWindows + + true + $(XUnitOptions) -class System.Diagnostics.Tests.ProcessStartInfoTests -parallel none + true + @@ -33,6 +38,7 @@ + From 83813f663e8152a54175db14dc9dc4bfc13be573 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 6 Aug 2026 10:30:40 -0400 Subject: [PATCH 2/4] Preserve Process hang diagnostics in Helix Write BOM-less UTF-8 console output and place WER dumps in the Helix work-item upload directory. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54c890f9-b409-4b7d-b810-3701e125fd96 --- .../tests/ProcessTestHangDiagnostics.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs index 11df9e018cc10e..b8750f61441d6b 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs @@ -32,7 +32,7 @@ internal static class ProcessTestHangDiagnostics private const string InstallationTypeKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion"; private static readonly TimeSpan WatchdogTimeout = TimeSpan.FromMinutes(3); private static readonly TextWriter s_log = TextWriter.Synchronized( - new StreamWriter(Console.OpenStandardError(), Encoding.UTF8, bufferSize: 1024, leaveOpen: true) { AutoFlush = true }); + new StreamWriter(Console.OpenStandardError(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 1024, leaveOpen: true) { AutoFlush = true }); [ModuleInitializer] internal static void Initialize() @@ -65,10 +65,12 @@ internal static void Log(string message) private static void ConfigureWindowsErrorReporting() { string? dumpFolder = Environment.GetEnvironmentVariable("HELIX_DUMP_FOLDER"); + string? uploadFolder = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + string? werDumpFolder = uploadFolder ?? dumpFolder; string? processPath = Environment.ProcessPath; - if (string.IsNullOrEmpty(dumpFolder) || string.IsNullOrEmpty(processPath)) + if (string.IsNullOrEmpty(werDumpFolder) || string.IsNullOrEmpty(processPath)) { - Log($"WER LocalDumps not configured; HELIX_DUMP_FOLDER={dumpFolder ?? ""}."); + Log($"WER LocalDumps not configured; HELIX_WORKITEM_UPLOAD_ROOT={uploadFolder ?? ""}; HELIX_DUMP_FOLDER={dumpFolder ?? ""}."); return; } @@ -85,9 +87,9 @@ private static void ConfigureWindowsErrorReporting() } key.SetValue("DumpCount", 2, RegistryValueKind.DWord); - key.SetValue("DumpFolder", dumpFolder, RegistryValueKind.ExpandString); + key.SetValue("DumpFolder", werDumpFolder, RegistryValueKind.ExpandString); key.SetValue("DumpType", 2, RegistryValueKind.DWord); - Log($"WER LocalDumps configured for {executableName} in {dumpFolder}."); + Log($"WER LocalDumps configured for {executableName} in {werDumpFolder}."); } catch (Exception e) when (e is IOException or SecurityException or UnauthorizedAccessException) { From 38282ce8f58f763dd8daa4ce6a2903647fab5a33 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 6 Aug 2026 16:00:57 -0400 Subject: [PATCH 3/4] Use malformed PE for bad executable test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/ProcessStartInfoTests.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs index c23a708278c42d..18f569dcc840c2 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs @@ -1298,7 +1298,20 @@ public void StartInfo_BadVerb(bool useShellExecute) public void StartInfo_BadExe(bool useShellExecute) { string tempFile = GetTestFilePath() + ".exe"; - File.Create(tempFile).Dispose(); + const int MalformedExecutableSize = 512; + const int PeHeaderOffset = 0x80; + const int PeOptionalHeaderSizeOffset = PeHeaderOffset + 20; + const byte PeOptionalHeaderSize = 0xF0; + + // A truncated PE avoids special shell handling for an empty executable while still producing ERROR_BAD_EXE_FORMAT. + byte[] malformedExecutable = new byte[MalformedExecutableSize]; + malformedExecutable[0] = (byte)'M'; + malformedExecutable[1] = (byte)'Z'; + malformedExecutable[0x3C] = PeHeaderOffset; + malformedExecutable[PeHeaderOffset] = (byte)'P'; + malformedExecutable[PeHeaderOffset + 1] = (byte)'E'; + malformedExecutable[PeOptionalHeaderSizeOffset] = PeOptionalHeaderSize; + File.WriteAllBytes(tempFile, malformedExecutable); ProcessStartInfo info = new ProcessStartInfo { From 0cae6d674170fe18705e72517b0a74f81602e74f Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 6 Aug 2026 19:57:24 -0400 Subject: [PATCH 4/4] Use system DLL for bad executable test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6a078136-b226-4ce5-94ff-1732ebcdfe79 --- .../tests/ProcessStartInfoTests.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs index 18f569dcc840c2..c5f9053c9323ef 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs @@ -1298,20 +1298,8 @@ public void StartInfo_BadVerb(bool useShellExecute) public void StartInfo_BadExe(bool useShellExecute) { string tempFile = GetTestFilePath() + ".exe"; - const int MalformedExecutableSize = 512; - const int PeHeaderOffset = 0x80; - const int PeOptionalHeaderSizeOffset = PeHeaderOffset + 20; - const byte PeOptionalHeaderSize = 0xF0; - - // A truncated PE avoids special shell handling for an empty executable while still producing ERROR_BAD_EXE_FORMAT. - byte[] malformedExecutable = new byte[MalformedExecutableSize]; - malformedExecutable[0] = (byte)'M'; - malformedExecutable[1] = (byte)'Z'; - malformedExecutable[0x3C] = PeHeaderOffset; - malformedExecutable[PeHeaderOffset] = (byte)'P'; - malformedExecutable[PeHeaderOffset + 1] = (byte)'E'; - malformedExecutable[PeOptionalHeaderSizeOffset] = PeOptionalHeaderSize; - File.WriteAllBytes(tempFile, malformedExecutable); + // A DLL is a valid PE that cannot be executed, avoiding malformed-image shell recovery paths. + File.Copy(Path.Combine(Environment.SystemDirectory, "kernel32.dll"), tempFile); ProcessStartInfo info = new ProcessStartInfo {