Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@

<!-- external -->
<ItemGroup>
<PackageVersion Include="Azure.Core" Version="1.51.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="Azure.Core" Version="1.53.0" />
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.19.1" />
<PackageVersion Include="CommandLineParser" Version="2.5.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
Expand All @@ -79,8 +79,9 @@
<PackageVersion Include="Microsoft.ApplicationInsights" Version="2.23.0" />
<PackageVersion Include="Microsoft.Data.OData" Version="5.8.4" />
<PackageVersion Include="Microsoft.Data.Services.Client" Version="$(MicrosoftDataServicesClientVersion)" />
<PackageVersion Include="Microsoft.Diagnostics.Runtime" Version="1.0.5" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.80.0" />
<PackageVersion Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.721401" />
<PackageVersion Include="Microsoft.Diagnostics.Runtime" Version="4.0.725801" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.83.1" />
<PackageVersion Include="Microsoft.OpenApi" Version="1.3.2" />
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.3.2" />
<PackageVersion Include="Microsoft.Signed.Wix" Version="$(MicrosoftSignedWixVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ private static TokenCredential CreateAvailableTokenCredential(DefaultIdentityTok
}

// Add Managed Identity credential
tokenCredentials.Add(new ManagedIdentityCredential(options.ManagedIdentityClientId));
ManagedIdentityId managedIdentityId = string.IsNullOrEmpty(options.ManagedIdentityClientId)
? ManagedIdentityId.SystemAssigned
: ManagedIdentityId.FromUserAssignedClientId(options.ManagedIdentityClientId);
tokenCredentials.Add(new ManagedIdentityCredential(managedIdentityId));

// Add work load identity credential if the environment variables are set
TokenCredential? workloadIdentityCredential = GetWorkloadIdentityCredentialForAzurePipelineTask();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" />
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Compile Remove="MiniDump.cs" />
</ItemGroup>

<ItemGroup>
<None Include="build\$(MSBuildProjectName).targets" PackagePath="build\$(MSBuildProjectName).targets" Pack="true" />
<None Include="$(RepoRoot)LICENSE.txt" PackagePath="LICENSE.txt" Pack="true" />
Expand Down
22 changes: 22 additions & 0 deletions src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,28 @@ private static RemoteInvokeHandle Invoke(MethodInfo method, string[] args,
psi.Environment.Remove("CoreClr_Enable_Profiling");
}

if (options.CrashDumpCollectionType is CrashDumpCollectionType.None)
{
psi.Environment.Remove("DOTNET_DbgEnableMiniDump");
psi.Environment.Remove("DOTNET_DbgMiniDumpType");
psi.Environment.Remove("DOTNET_DbgMiniDumpName");
}
else if (options.CrashDumpCollectionType.HasValue)
{
psi.Environment["DOTNET_DbgEnableMiniDump"] = "1";
psi.Environment["DOTNET_DbgMiniDumpType"] = ((int)options.CrashDumpCollectionType.Value).ToString();
if (!string.IsNullOrWhiteSpace(options.CrashDumpPath))
{
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");
}
}

// 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);
Expand Down
158 changes: 83 additions & 75 deletions src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeHandle.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -146,87 +149,16 @@ 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))
{
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}");
}
catch (Exception exc)
{
description.AppendLine($"Failed to create mini dump: {exc.Message}");
}
}
}

// Gather additional details about the process if possible
try
if (Options.EnableTimeoutDumpCollection)
Comment thread
hoyosjs marked this conversation as resolved.
{
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.
{
try
{
using (DataTarget dt = DataTarget.AttachToProcess(Process.Id, msecTimeout: 20_000)) // arbitrary timeout
{
ClrRuntime runtime = dt.ClrVersions.FirstOrDefault()?.CreateRuntime();
if (runtime != 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]" :
thread.IsFinalizer ? "[Finalizer]" :
thread.IsGC ? "[GC]" :
"";

string isBackground = thread.IsBackground ? "[Background]" : "";
string apartmentModel = thread.IsMTA ? "[MTA]" :
thread.IsSTA ? "[STA]" :
"";

description.AppendLine($"\t\tThread #{thread.ManagedThreadId} (OS 0x{thread.OSThreadId:X}) {threadKind} {isBackground} {apartmentModel}");
foreach (ClrStackFrame frame in thread.StackTrace)
{
description.AppendLine($"\t\t\t{frame}");
}
}
}
}
}
finally
{
Interlocked.Exchange(ref s_clrMdLock, 0);
}
}
CollectTimeoutDiagnostics(description);
}
catch { }

throw new RemoteExecutionException(description.ToString());
}
}

FileInfo exceptionFileInfo = new FileInfo(Options.ExceptionFile);
FileInfo exceptionFileInfo = new(Options.ExceptionFile);
if (exceptionFileInfo.Exists && exceptionFileInfo.Length != 0)
{
throw new RemoteExecutionException("Remote process failed with an unhandled exception.", File.ReadAllText(Options.ExceptionFile));
Expand Down Expand Up @@ -271,6 +203,82 @@ private void Dispose(bool disposing)
}
}

/// <summary>
/// Collects diagnostic information (dump + thread stacks) from the timed-out remote process.
/// </summary>
private void CollectTimeoutDiagnostics(StringBuilder description)
{
string uploadPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT");
if (!string.IsNullOrWhiteSpace(uploadPath))
{
try
{
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 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 (Interlocked.CompareExchange(ref s_clrMdLock, 1, 0) == 0) // Make sure we only attach to one process at a time.
{
try
{
using DataTarget dt = DataTarget.CreateSnapshotAndAttach(Process.Id);
ClrRuntime runtime = dt.ClrVersions.FirstOrDefault()?.CreateRuntime();
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.IsGc ? "[Thread that started suspension]" :
thread.IsFinalizer ? "[Finalizer]" :
"Unknown";

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.EnumerateStackTrace())
{
description.AppendLine($"\t\t\t{frame}");
}
}
}
}
finally
{
Interlocked.Exchange(ref s_clrMdLock, 0);
}
}
}
catch { }
}

~RemoteInvokeHandle()
{
// Finalizer flags tests that omitted the explicit Dispose() call; they must have it, or they aren't
Expand Down
42 changes: 42 additions & 0 deletions src/Microsoft.DotNet.RemoteExecutor/src/RemoteInvokeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@

namespace Microsoft.DotNet.RemoteExecutor
{
/// <summary>
/// The type of crash dump to collect. Maps to DOTNET_DbgMiniDumpType values
/// as documented in <see href="https://learn.microsoft.com/dotnet/core/diagnostics/collect-dumps-crash#types-of-mini-dumps">the docs</see>. Only applies to .NET Core subprocesses.
/// </summary>
public enum CrashDumpCollectionType
{
/// <summary>
/// Explicitly disables crash dump collection, removing any inherited DOTNET_DbgEnableMiniDump,
/// DOTNET_DbgMiniDumpType, and DOTNET_DbgMiniDumpName environment variables from the subprocess.
/// </summary>
None = 0,
Mini = 1,
Heap = 2,
Triage = 3,
Full = 4
}

/// <summary>
/// Options used with RemoteInvoke.
/// </summary>
Expand All @@ -22,6 +39,8 @@ public sealed class RemoteInvokeOptions

public bool EnableProfiling { get; set; } = true;

public bool EnableTimeoutDumpCollection { get; set; } = true;
Comment thread
hoyosjs marked this conversation as resolved.

public bool CheckExitCode { get; set; } = true;

/// <summary>
Expand Down Expand Up @@ -62,5 +81,28 @@ public bool RunAsSudo
/// Specifies the roll-forward policy for dotnet cli to use. Only applies when running .NET Core
/// </summary>
public string RollForward { get; set; }

/// <summary>
/// Gets or sets the type of crash dump to collect on the subprocess via
/// DOTNET_DbgEnableMiniDump / DOTNET_DbgMiniDumpType / DOTNET_DbgMiniDumpName.
/// When set to a value other than <see cref="CrashDumpCollectionType.None"/>,
/// crash dump collection is enabled with that dump type.
/// When set to <see cref="CrashDumpCollectionType.None"/>, crash dump collection is
/// explicitly disabled (removing any inherited env vars).
/// When null (default), the environment variables are left as-is.
/// </summary>
/// <remarks>
/// Only applies to .NET Core subprocesses.
/// </remarks>
public CrashDumpCollectionType? CrashDumpCollectionType { get; set; }

/// <summary>
/// Gets or sets the path template for crash dump files. When <see cref="CrashDumpCollectionType"/> 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.
/// </summary>
public string CrashDumpPath { get; set; }
}
}
Loading
Loading