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..c5f9053c9323ef 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 @@ -1295,7 +1298,8 @@ public void StartInfo_BadVerb(bool useShellExecute) public void StartInfo_BadExe(bool useShellExecute) { string tempFile = GetTestFilePath() + ".exe"; - File.Create(tempFile).Dispose(); + // 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 { @@ -1359,6 +1363,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 +1377,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..b8750f61441d6b --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTestHangDiagnostics.cs @@ -0,0 +1,113 @@ +// 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(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 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? uploadFolder = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + string? werDumpFolder = uploadFolder ?? dumpFolder; + string? processPath = Environment.ProcessPath; + if (string.IsNullOrEmpty(werDumpFolder) || string.IsNullOrEmpty(processPath)) + { + Log($"WER LocalDumps not configured; HELIX_WORKITEM_UPLOAD_ROOT={uploadFolder ?? ""}; 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", werDumpFolder, RegistryValueKind.ExpandString); + key.SetValue("DumpType", 2, RegistryValueKind.DWord); + Log($"WER LocalDumps configured for {executableName} in {werDumpFolder}."); + } + 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 @@ +