Skip to content

Commit a5b388d

Browse files
tonyredondoandrewlock
authored andcommitted
Dump process and children processes when a test hangups (#6401)
## Summary of changes This PR adds the mechanism to create a memory dump not only on the process id but also the children processes ids of a samples app. ## Reason for change This is useful for a `dotnet test` scenario where the CLI spawn multiple processes (testhost, datacollectorhost) Ticket: SDTEST-1309 --------- Co-authored-by: Andrew Lock <andrew.lock@datadoghq.com>
1 parent 2b58510 commit a5b388d

File tree

4 files changed

+228
-12
lines changed

4 files changed

+228
-12
lines changed

tracer/test/Datadog.Trace.TestHelpers.AutoInstrumentation/TestHelper.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public async Task<ProcessResult> RunDotnetTestSampleAndWaitForExit(MockTracerAge
115115
var process = await StartDotnetTestSample(agent, arguments, packageVersion, aspNetCorePort: 5000, framework: framework, forceVsTestParam: forceVsTestParam);
116116

117117
using var helper = new ProcessHelper(process);
118-
return WaitForProcessResult(helper, expectedExitCode);
118+
return WaitForProcessResult(helper, expectedExitCode, dumpChildProcesses: true);
119119
}
120120

121121
public async Task<Process> StartSample(MockTracerAgent agent, string arguments, string packageVersion, int aspNetCorePort, string framework = "", bool? enableSecurity = null, string externalRulesFile = null, bool usePublishWithRID = false, string dotnetRuntimeArgs = null)
@@ -166,7 +166,7 @@ public async Task<ProcessResult> RunSampleAndWaitForExit(MockTracerAgent agent,
166166
return WaitForProcessResult(helper);
167167
}
168168

169-
public ProcessResult WaitForProcessResult(ProcessHelper helper, int expectedExitCode = 0)
169+
public ProcessResult WaitForProcessResult(ProcessHelper helper, int expectedExitCode = 0, bool dumpChildProcesses = false)
170170
{
171171
// this is _way_ too long, but we want to be v. safe
172172
// the goal is just to make sure we kill the test before

tracer/test/Datadog.Trace.TestHelpers/MemoryDumpHelper.cs

+23-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// </copyright>
55

66
using System;
7+
using System.Collections.Generic;
78
using System.Diagnostics;
89
using System.IO;
910
using System.Linq;
@@ -126,24 +127,37 @@ void OnDataReceived(string output)
126127
return tcs.Task;
127128
}
128129

129-
public static bool CaptureMemoryDump(Process process, IProgress<string> output = null)
130+
public static bool CaptureMemoryDump(Process process, IProgress<string> output = null, bool includeChildProcesses = false)
131+
{
132+
return CaptureMemoryDump(process.Id, output);
133+
}
134+
135+
private static bool CaptureMemoryDump(int pid, IProgress<string> output = null, bool includeChildProcesses = false)
130136
{
131137
if (!IsAvailable)
132138
{
133139
_output?.Report("Memory dumps not enabled");
134140
return false;
135141
}
136142

137-
try
138-
{
139-
var args = EnvironmentTools.IsWindows() ? $"-ma -accepteula {process.Id} {Path.GetTempPath()}" : process.Id.ToString();
140-
return CaptureMemoryDump(args, output ?? _output);
141-
}
142-
catch (Exception ex)
143+
// children first and then the parent process last
144+
IEnumerable<int> pids = includeChildProcesses ? [..ProcessHelper.GetChildrenIds(pid), pid] : [pid];
145+
var atLeastOneDump = false;
146+
foreach (var cPid in pids)
143147
{
144-
_output?.Report("Error taking memory dump: " + ex);
145-
return false;
148+
try
149+
{
150+
var args = EnvironmentTools.IsWindows() ? $"-ma -accepteula {cPid} {Path.GetTempPath()}" : cPid.ToString();
151+
atLeastOneDump |= CaptureMemoryDump(args, output ?? _output);
152+
}
153+
catch (Exception ex)
154+
{
155+
_output?.Report("Error taking memory dump: " + ex);
156+
return false;
157+
}
146158
}
159+
160+
return atLeastOneDump;
147161
}
148162

149163
private static bool CaptureMemoryDump(string args, IProgress<string> output)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// <copyright file="ProcessHelper.Children.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Diagnostics;
9+
using System.Diagnostics.CodeAnalysis;
10+
using System.IO;
11+
using System.Runtime.InteropServices;
12+
13+
namespace Datadog.Trace.TestHelpers;
14+
15+
/// <summary>
16+
/// Add methods to get the children of a process
17+
/// </summary>
18+
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "PInvokes are grouped at the bottom of the class")]
19+
public partial class ProcessHelper
20+
{
21+
public static IReadOnlyList<int> GetChildrenIds(int parentId)
22+
{
23+
var childPids = new List<int>();
24+
25+
try
26+
{
27+
var processes = Process.GetProcesses();
28+
foreach (var process in processes)
29+
{
30+
int ppid;
31+
try
32+
{
33+
ppid = GetParentProcessId(process);
34+
}
35+
catch
36+
{
37+
continue; // Skip processes that can't be accessed
38+
}
39+
40+
var id = process.Id;
41+
if (ppid == parentId && id != parentId)
42+
{
43+
childPids.Add(id);
44+
}
45+
}
46+
}
47+
catch (Exception ex)
48+
{
49+
Console.WriteLine($"Error retrieving child processes: {ex.Message}");
50+
}
51+
52+
return childPids;
53+
}
54+
55+
private static int GetParentProcessId(Process process)
56+
{
57+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
58+
{
59+
return GetParentProcessIdWindows(process.Id);
60+
}
61+
62+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
63+
{
64+
return GetParentProcessIdLinux(process.Id);
65+
}
66+
67+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
68+
{
69+
return GetParentProcessIdMacOS(process.Id);
70+
}
71+
72+
throw new PlatformNotSupportedException("Unsupported platform.");
73+
}
74+
75+
private static int GetParentProcessIdWindows(int pid)
76+
{
77+
try
78+
{
79+
var pbi = new PROCESS_BASIC_INFORMATION();
80+
uint returnLength;
81+
82+
var hProcess = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, pid);
83+
if (hProcess == IntPtr.Zero)
84+
{
85+
throw new Exception("Could not open process.");
86+
}
87+
88+
var status = NtQueryInformationProcess(
89+
hProcess, 0, ref pbi, (uint)Marshal.SizeOf(pbi), out returnLength);
90+
91+
CloseHandle(hProcess);
92+
93+
if (status != 0)
94+
{
95+
throw new Exception("NtQueryInformationProcess failed.");
96+
}
97+
98+
return pbi.InheritedFromUniqueProcessId.ToInt32();
99+
}
100+
catch (Exception ex)
101+
{
102+
throw new Exception($"Error getting parent PID for process {pid}: {ex.Message}");
103+
}
104+
}
105+
106+
private static int GetParentProcessIdLinux(int pid)
107+
{
108+
try
109+
{
110+
var statusPath = $"/proc/{pid}/status";
111+
if (!File.Exists(statusPath))
112+
{
113+
throw new Exception("PPid not found.");
114+
}
115+
116+
foreach (var line in File.ReadLines(statusPath))
117+
{
118+
if (!line.StartsWith("PPid:"))
119+
{
120+
continue;
121+
}
122+
123+
if (int.TryParse(line.Substring(5).Trim(), out var ppid))
124+
{
125+
return ppid;
126+
}
127+
}
128+
129+
throw new Exception("PPid not found.");
130+
}
131+
catch (Exception ex)
132+
{
133+
throw new Exception($"Error reading /proc/{pid}/status: {ex.Message}");
134+
}
135+
}
136+
137+
private static int GetParentProcessIdMacOS(int pid)
138+
{
139+
try
140+
{
141+
var startInfo = new ProcessStartInfo
142+
{
143+
FileName = "ps",
144+
Arguments = $"-o ppid= -p {pid}",
145+
RedirectStandardOutput = true,
146+
UseShellExecute = false,
147+
CreateNoWindow = true
148+
};
149+
150+
using var proc = Process.Start(startInfo);
151+
var output = proc!.StandardOutput.ReadToEnd();
152+
proc.WaitForExit();
153+
154+
if (int.TryParse(output.Trim(), out var ppid))
155+
{
156+
return ppid;
157+
}
158+
159+
throw new Exception("Failed to parse PPid.");
160+
}
161+
catch (Exception ex)
162+
{
163+
throw new Exception($"Error executing ps command: {ex.Message}");
164+
}
165+
}
166+
167+
// P/Invoke declarations for Windows
168+
[Flags]
169+
private enum ProcessAccessFlags : uint
170+
{
171+
QueryLimitedInformation = 0x1000
172+
}
173+
174+
[DllImport("ntdll.dll")]
175+
private static extern int NtQueryInformationProcess(
176+
IntPtr processHandle,
177+
int processInformationClass,
178+
ref PROCESS_BASIC_INFORMATION processInformation,
179+
uint processInformationLength,
180+
out uint returnLength);
181+
182+
[DllImport("kernel32.dll")]
183+
private static extern IntPtr OpenProcess(
184+
ProcessAccessFlags processAccess,
185+
bool bInheritHandle,
186+
int processId);
187+
188+
[DllImport("kernel32.dll")]
189+
private static extern bool CloseHandle(IntPtr hObject);
190+
191+
[StructLayout(LayoutKind.Sequential)]
192+
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Keeping the original windows struct name")]
193+
private struct PROCESS_BASIC_INFORMATION
194+
{
195+
public IntPtr Reserved1;
196+
public IntPtr PebBaseAddress;
197+
public IntPtr Reserved20;
198+
public IntPtr Reserved21;
199+
public IntPtr UniqueProcessId;
200+
public IntPtr InheritedFromUniqueProcessId;
201+
}
202+
}

tracer/test/Datadog.Trace.TestHelpers/ProcessHelper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Datadog.Trace.TestHelpers
1616
/// <summary>
1717
/// Drains the standard and error output of a process
1818
/// </summary>
19-
public class ProcessHelper : IDisposable
19+
public partial class ProcessHelper : IDisposable
2020
{
2121
private readonly TaskCompletionSource<bool> _errorTask = new(TaskCreationOptions.RunContinuationsAsynchronously);
2222
private readonly TaskCompletionSource<bool> _outputTask = new(TaskCreationOptions.RunContinuationsAsynchronously);

0 commit comments

Comments
 (0)