From a5f3616d8d88dc4ece1220476ff5afac726a617a Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Tue, 14 Apr 2026 01:36:42 -0700 Subject: [PATCH 1/3] Enhance crash dump collection options in RemoteExecutor - Update package references for Microsoft.Diagnostics.Runtime and Microsoft.Diagnostics.NETCore.Client. - Introduce CrashDumpCollectionType enum to specify types of crash dumps. - Add options to enable/disable crash dump collection and configure related environment variables. --- Directory.Packages.props | 3 +- .../Microsoft.DotNet.RemoteExecutor.csproj | 1 + .../src/MiniDump.cs | 4 + .../src/RemoteExecutor.cs | 17 ++++ .../src/RemoteInvokeHandle.cs | 82 +++++++++---------- .../src/RemoteInvokeOptions.cs | 32 ++++++++ 6 files changed, 97 insertions(+), 42 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dc3da890ce4..ab9cd155010 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,8 @@ - + + diff --git a/src/Microsoft.DotNet.RemoteExecutor/src/Microsoft.DotNet.RemoteExecutor.csproj b/src/Microsoft.DotNet.RemoteExecutor/src/Microsoft.DotNet.RemoteExecutor.csproj index 8a76b50f650..fd7c0ef6ad6 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/src/Microsoft.DotNet.RemoteExecutor.csproj +++ b/src/Microsoft.DotNet.RemoteExecutor/src/Microsoft.DotNet.RemoteExecutor.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Microsoft.DotNet.RemoteExecutor/src/MiniDump.cs b/src/Microsoft.DotNet.RemoteExecutor/src/MiniDump.cs index e8ff482ef3d..bc871cd211d 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/src/MiniDump.cs +++ b/src/Microsoft.DotNet.RemoteExecutor/src/MiniDump.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if !NETCOREAPP + using System; using System.ComponentModel; using System.Diagnostics; @@ -73,3 +75,5 @@ private enum MINIDUMP_TYPE : int } } } + +#endif diff --git a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs index 17e38b1008d..4c622d08514 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs +++ b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs @@ -453,6 +453,23 @@ private static RemoteInvokeHandle Invoke(MethodInfo method, string[] args, psi.Environment.Remove("CoreClr_Enable_Profiling"); } + if (options.DisableCrashDumpCollection) + { + psi.Environment.Remove("DOTNET_DbgEnableMiniDump"); + psi.Environment.Remove("DOTNET_DbgMiniDumpType"); + psi.Environment.Remove("DOTNET_DbgMiniDumpName"); + } + else if (options.CrashDumpCollectionType.HasValue) + { + string uploadPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + psi.Environment["DOTNET_DbgEnableMiniDump"] = "1"; + psi.Environment["DOTNET_DbgMiniDumpType"] = ((int)options.CrashDumpCollectionType.Value).ToString(); + if (!string.IsNullOrWhiteSpace(uploadPath)) + { + psi.Environment["DOTNET_DbgMiniDumpName"] = IOPath.Combine(uploadPath, "%e.%p.%t.dmp"); + } + } + // If we need the host (if it exists), use it, otherwise target the console app directly. string metadataArgs = PasteArguments.Paste(new string[] { a.FullName, t.FullName, method.Name, options.ExceptionFile }, pasteFirstArgumentUsingArgV0Rules: false); string passedArgs = pasteArguments ? PasteArguments.Paste(args, pasteFirstArgumentUsingArgV0Rules: false) : string.Join(" ", args); diff --git a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeHandle.cs b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeHandle.cs index 70254c07553..53991506320 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeHandle.cs +++ b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeHandle.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Diagnostics.Runtime; using System; using System.Collections.Generic; using System.Diagnostics; @@ -10,6 +9,10 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; +#if NETCOREAPP +using Microsoft.Diagnostics.NETCore.Client; +#endif +using Microsoft.Diagnostics.Runtime; namespace Microsoft.DotNet.RemoteExecutor { @@ -146,81 +149,78 @@ private void Dispose(bool disposing) { description.AppendLine($"Timed out at {DateTime.Now} after {Options.TimeOut}ms waiting for remote process."); - // Create a dump if possible - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (Options.EnableTimeoutDumpCollection) { string uploadPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); if (!string.IsNullOrWhiteSpace(uploadPath)) { try { - string miniDmpPath = Path.Combine(uploadPath, $"{Process.Id}.{Path.GetRandomFileName()}.dmp"); - MiniDump.Create(Process, miniDmpPath); - description.AppendLine($"Wrote mini dump to: {miniDmpPath}"); + string dumpPath = Path.Combine(uploadPath, $"{Process.Id}.{Path.GetRandomFileName()}.dmp"); + #if NETCOREAPP + // These define guards assume that harness running on .NET Framework implies test process runs on .NET Framework. + var client = new DiagnosticsClient(Process.Id); + client.WriteDump(DumpType.Full, dumpPath, logDumpGeneration: false); + #else + MiniDump.Create(Process, dumpPath); + #endif + description.AppendLine($"Wrote dump to: {dumpPath}"); } catch (Exception exc) { - description.AppendLine($"Failed to create mini dump: {exc.Message}"); + description.AppendLine($"Failed to create dump: {exc.Message}"); } } - } - // Gather additional details about the process if possible - try - { - description.AppendLine($"\tProcess ID: {Process.Id}"); - description.AppendLine($"\tHandle: {Process.Handle}"); - description.AppendLine($"\tName: {Process.ProcessName}"); - description.AppendLine($"\tMainModule: {Process.MainModule?.FileName}"); - description.AppendLine($"\tStartTime: {Process.StartTime}"); - description.AppendLine($"\tTotalProcessorTime: {Process.TotalProcessorTime}"); - - // Attach ClrMD to gather some additional details. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && // As of Microsoft.Diagnostics.Runtime v1.0.5, process attach only works on Windows. - Interlocked.CompareExchange(ref s_clrMdLock, 1, 0) == 0) // Make sure we only attach to one process at a time. + // Gather additional details about the process if possible + try { - try + description.AppendLine($"\tProcess ID: {Process.Id}"); + description.AppendLine($"\tHandle: {Process.Handle}"); + description.AppendLine($"\tName: {Process.ProcessName}"); + description.AppendLine($"\tMainModule: {Process.MainModule?.FileName}"); + description.AppendLine($"\tStartTime: {Process.StartTime}"); + description.AppendLine($"\tTotalProcessorTime: {Process.TotalProcessorTime}"); + + // Attach ClrMD to gather some additional details. + if (Interlocked.CompareExchange(ref s_clrMdLock, 1, 0) == 0) // Make sure we only attach to one process at a time. { - using (DataTarget dt = DataTarget.AttachToProcess(Process.Id, msecTimeout: 20_000)) // arbitrary timeout + try { + using DataTarget dt = DataTarget.CreateSnapshotAndAttach(Process.Id); ClrRuntime runtime = dt.ClrVersions.FirstOrDefault()?.CreateRuntime(); - if (runtime != null) + if (runtime is not null) { // Dump the threads in the remote process. description.AppendLine("\tThreads:"); foreach (ClrThread thread in runtime.Threads.Where(t => t.IsAlive)) { - string threadKind = - thread.IsThreadpoolCompletionPort ? "[Thread pool completion port]" : - thread.IsThreadpoolGate ? "[Thread pool gate]" : - thread.IsThreadpoolTimer ? "[Thread pool timer]" : - thread.IsThreadpoolWait ? "[Thread pool wait]" : - thread.IsThreadpoolWorker ? "[Thread pool worker]" : + string threadKind = + thread.IsGc ? "[Thread that started suspension]" : thread.IsFinalizer ? "[Finalizer]" : - thread.IsGC ? "[GC]" : - ""; + "Unknown"; - string isBackground = thread.IsBackground ? "[Background]" : ""; - string apartmentModel = thread.IsMTA ? "[MTA]" : - thread.IsSTA ? "[STA]" : + string isBackground = thread.State.HasFlag(ClrThreadState.TS_Background) ? "[Background]" : ""; + string apartmentModel = thread.State.HasFlag(ClrThreadState.TS_InMTA) ? "[MTA]" : + thread.State.HasFlag(ClrThreadState.TS_InSTA) ? "[STA]" : ""; description.AppendLine($"\t\tThread #{thread.ManagedThreadId} (OS 0x{thread.OSThreadId:X}) {threadKind} {isBackground} {apartmentModel}"); - foreach (ClrStackFrame frame in thread.StackTrace) + foreach (ClrStackFrame frame in thread.EnumerateStackTrace()) { description.AppendLine($"\t\t\t{frame}"); } } } } - } - finally - { - Interlocked.Exchange(ref s_clrMdLock, 0); + finally + { + Interlocked.Exchange(ref s_clrMdLock, 0); + } } } + catch { } } - catch { } throw new RemoteExecutionException(description.ToString()); } diff --git a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs index 932b7c807d9..43d5fdde48b 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs +++ b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs @@ -9,6 +9,18 @@ namespace Microsoft.DotNet.RemoteExecutor { + /// + /// The type of crash dump to collect. Maps to DOTNET_DbgMiniDumpType values + /// as documented in https://learn.microsoft.com/en-us/dotnet/core/diagnostics/collect-dumps-crash#types-of-mini-dumps. Only applies to .NET Core subprocesses. + /// + public enum CrashDumpCollectionType + { + Mini = 1, + Heap = 2, + Triage = 3, + Full = 4 + } + /// /// Options used with RemoteInvoke. /// @@ -22,6 +34,8 @@ public sealed class RemoteInvokeOptions public bool EnableProfiling { get; set; } = true; + public bool EnableTimeoutDumpCollection { get; set; } = true; + public bool CheckExitCode { get; set; } = true; /// @@ -62,5 +76,23 @@ public bool RunAsSudo /// Specifies the roll-forward policy for dotnet cli to use. Only applies when running .NET Core /// public string RollForward { get; set; } + + /// + /// Gets or sets whether to configure crash dump collection on the subprocess via + /// DOTNET_DbgEnableMiniDump / DOTNET_DbgMiniDumpType / DOTNET_DbgMiniDumpName. + /// When set to a value, crash dump collection is enabled + /// with that dump type. When set to null (default), the environment variables are left as-is. + /// To explicitly disable crash dumps (removing any inherited env vars), set to true. + /// + /// + /// Only applies to .NET Core subprocesses. + /// + public CrashDumpCollectionType? CrashDumpCollectionType { get; set; } + + /// + /// When true, explicitly removes the DOTNET_DbgEnableMiniDump, DOTNET_DbgMiniDumpType, and + /// DOTNET_DbgMiniDumpName environment variables from the subprocess, disabling any inherited crash dump configuration. + /// + public bool DisableCrashDumpCollection { get; set; } } } From ebe2de0a5654e8f66516336656e50c9f886a729c Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:27:09 -0700 Subject: [PATCH 2/3] Add remote executor dump tests --- .../tests/RemoteExecutorTests.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/Microsoft.DotNet.RemoteExecutor/tests/RemoteExecutorTests.cs b/src/Microsoft.DotNet.RemoteExecutor/tests/RemoteExecutorTests.cs index 466c1807290..8bb75495e1c 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/tests/RemoteExecutorTests.cs +++ b/src/Microsoft.DotNet.RemoteExecutor/tests/RemoteExecutorTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.IO; using System.Threading.Tasks; using Xunit; using Xunit.Sdk; @@ -158,5 +159,91 @@ public static void IgnoreExitCode() Assert.Equal(exitCode, h.ExitCode); } } + + [Theory] + [InlineData(CrashDumpCollectionType.Mini, "1")] + [InlineData(CrashDumpCollectionType.Heap, "2")] + [InlineData(CrashDumpCollectionType.Triage, "3")] + [InlineData(CrashDumpCollectionType.Full, "4")] + public void CrashDumpCollection_SetsEnvVars(CrashDumpCollectionType dumpType, string expectedTypeValue) + { + using RemoteInvokeHandle h = RemoteExecutor.Invoke(expectedType => + { + Assert.Equal("1", Environment.GetEnvironmentVariable("DOTNET_DbgEnableMiniDump")); + Assert.Equal(expectedType, Environment.GetEnvironmentVariable("DOTNET_DbgMiniDumpType")); + return RemoteExecutor.SuccessExitCode; + }, expectedTypeValue, new RemoteInvokeOptions + { + RollForward = "Major", + CrashDumpCollectionType = dumpType + }); + } + + [Fact] + public void DisableCrashDumpCollection_RemovesEnvVars() + { + // Pre-set the env vars on the StartInfo to simulate inherited values + var options = new RemoteInvokeOptions + { + RollForward = "Major", + DisableCrashDumpCollection = true + }; + options.StartInfo.Environment["DOTNET_DbgEnableMiniDump"] = "1"; + options.StartInfo.Environment["DOTNET_DbgMiniDumpType"] = "4"; + options.StartInfo.Environment["DOTNET_DbgMiniDumpName"] = "/tmp/test.dmp"; + + using RemoteInvokeHandle h = RemoteExecutor.Invoke(() => + { + Assert.Null(Environment.GetEnvironmentVariable("DOTNET_DbgEnableMiniDump")); + Assert.Null(Environment.GetEnvironmentVariable("DOTNET_DbgMiniDumpType")); + Assert.Null(Environment.GetEnvironmentVariable("DOTNET_DbgMiniDumpName")); + }, options); + } + + [Fact] + public void CrashDumpCollection_DefaultLeavesEnvVarsUntouched() + { + // When neither option is set, env vars should pass through from the parent unchanged + using RemoteInvokeHandle h = RemoteExecutor.Invoke(() => + { + // Without explicit config, the child inherits whatever the parent has. + // The parent test process shouldn't have DOTNET_DbgEnableMiniDump set, + // so the child shouldn't either. + string parentValue = Environment.GetEnvironmentVariable("DOTNET_DbgEnableMiniDump"); + Assert.Null(parentValue); + }, new RemoteInvokeOptions { RollForward = "Major" }); + } + + [Fact] + public static unsafe void CrashDumpCollection_CreatesDumpOnCrash() + { + string dumpDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dumpDir); + try + { + var options = new RemoteInvokeOptions + { + RollForward = "Major", + CrashDumpCollectionType = CrashDumpCollectionType.Mini, + CheckExitCode = false + }; + // Point the dump path to our temp directory so we can verify the file is created. + // Use %p so the filename includes the PID and is unique. + options.StartInfo.Environment["DOTNET_DbgMiniDumpName"] = Path.Combine(dumpDir, "crashdump.%p.dmp"); + + RemoteExecutor.Invoke(() => + { + // Trigger an access violation to crash the process + *(int*)0x10000 = 0; + }, options).Dispose(); + + string[] dumpFiles = Directory.GetFiles(dumpDir, "*.dmp"); + Assert.NotEmpty(dumpFiles); + } + finally + { + Directory.Delete(dumpDir, recursive: true); + } + } } } From a599345d917f48ca0126b65f30852cdb7d664c18 Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:28:27 -0700 Subject: [PATCH 3/3] Add CrashDumpPath option for customizable crash dump file paths --- .../src/RemoteExecutor.cs | 11 ++++++++--- .../src/RemoteInvokeOptions.cs | 9 +++++++++ .../tests/RemoteExecutorTests.cs | 8 ++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs index 4c622d08514..e81f282cb1d 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs +++ b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs @@ -461,12 +461,17 @@ private static RemoteInvokeHandle Invoke(MethodInfo method, string[] args, } else if (options.CrashDumpCollectionType.HasValue) { - string uploadPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); psi.Environment["DOTNET_DbgEnableMiniDump"] = "1"; psi.Environment["DOTNET_DbgMiniDumpType"] = ((int)options.CrashDumpCollectionType.Value).ToString(); - if (!string.IsNullOrWhiteSpace(uploadPath)) + if (!string.IsNullOrWhiteSpace(options.CrashDumpPath)) { - psi.Environment["DOTNET_DbgMiniDumpName"] = IOPath.Combine(uploadPath, "%e.%p.%t.dmp"); + psi.Environment["DOTNET_DbgMiniDumpName"] = options.CrashDumpPath; + } + else + { + string uploadPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + string dumpDir = !string.IsNullOrWhiteSpace(uploadPath) ? uploadPath : IOPath.GetTempPath(); + psi.Environment["DOTNET_DbgMiniDumpName"] = IOPath.Combine(dumpDir, "%e.%p.%t.dmp"); } } diff --git a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs index 43d5fdde48b..3183949e338 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs +++ b/src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs @@ -89,6 +89,15 @@ public bool RunAsSudo /// public CrashDumpCollectionType? CrashDumpCollectionType { get; set; } + /// + /// Gets or sets the path template for crash dump files. When is set, + /// this value is used for DOTNET_DbgMiniDumpName. Supports the same placeholders as createdump: + /// %p (PID), %e (process name), %t (timestamp), etc. + /// When null (default), defaults to HELIX_WORKITEM_UPLOAD_ROOT/%e.%p.%t.dmp if running in Helix, + /// or the system temp directory otherwise. + /// + public string CrashDumpPath { get; set; } + /// /// When true, explicitly removes the DOTNET_DbgEnableMiniDump, DOTNET_DbgMiniDumpType, and /// DOTNET_DbgMiniDumpName environment variables from the subprocess, disabling any inherited crash dump configuration. diff --git a/src/Microsoft.DotNet.RemoteExecutor/tests/RemoteExecutorTests.cs b/src/Microsoft.DotNet.RemoteExecutor/tests/RemoteExecutorTests.cs index 8bb75495e1c..def42446b44 100644 --- a/src/Microsoft.DotNet.RemoteExecutor/tests/RemoteExecutorTests.cs +++ b/src/Microsoft.DotNet.RemoteExecutor/tests/RemoteExecutorTests.cs @@ -225,11 +225,11 @@ public static unsafe void CrashDumpCollection_CreatesDumpOnCrash() { RollForward = "Major", CrashDumpCollectionType = CrashDumpCollectionType.Mini, - CheckExitCode = false + CheckExitCode = false, + // Point the dump path to our temp directory so we can verify the file is created. + // Use %p so the filename includes the PID and is unique. + CrashDumpPath = Path.Combine(dumpDir, "crashdump.%p.dmp") }; - // Point the dump path to our temp directory so we can verify the file is created. - // Use %p so the filename includes the PID and is unique. - options.StartInfo.Environment["DOTNET_DbgMiniDumpName"] = Path.Combine(dumpDir, "crashdump.%p.dmp"); RemoteExecutor.Invoke(() => {