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(() =>
{