Skip to content

Commit 00967c5

Browse files
nohwndEvangelink
andauthored
[rel/17.6] Fix hangdump running into crashdump (#4480)
Co-authored-by: Amaury Levé <amaury.leve@gmail.com>
1 parent 90efc14 commit 00967c5

File tree

4 files changed

+107
-29
lines changed

4 files changed

+107
-29
lines changed

src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -245,22 +245,21 @@ private void CollectDumpAndAbortTesthost()
245245

246246
if (_collectProcessDumpOnCrash)
247247
{
248-
// Detach procdump from the testhost process to prevent testhost process from crashing
249-
// if/when we try to kill the existing proc dump process.
250-
// And also prevent collecting dump on exit of the process.
248+
// Detach the dumper from the testhost process to prevent crashing testhost process. When the dumper is procdump.exe
249+
// it must be detached before we try to dump the process, and simply killing it would take down the testhost process.
250+
//
251+
// Detaching also prevents creating an extra dump at the exit of the testhost process.
251252
_processDumpUtility.DetachFromTargetProcess(_testHostProcessId);
252253
}
253254

254255
// Skip creating the dump if the option is set to none, and just kill the process.
255256
if ((_hangDumpType ?? HangDumpType.Full) != HangDumpType.None)
256257
{
257-
var hangDumpSuccess = false;
258258
try
259259
{
260260
Action<string> logWarning = m => _logger.LogWarning(_context.SessionDataCollectionContext, m);
261261
var dumpDirectory = GetDumpDirectory();
262262
_processDumpUtility.StartHangBasedProcessDump(_testHostProcessId, dumpDirectory, _hangDumpType == HangDumpType.Full, _targetFramework!, logWarning);
263-
hangDumpSuccess = true;
264263
}
265264
catch (Exception ex)
266265
{
@@ -271,7 +270,8 @@ private void CollectDumpAndAbortTesthost()
271270
{
272271
try
273272
{
274-
var dumpFiles = _processDumpUtility.GetDumpFiles(true, /* if we killed it by hang dumper, we already have our dump, otherwise it might have crashed, and we want all dumps */ !hangDumpSuccess);
273+
var dumpFiles = _processDumpUtility.GetDumpFiles(true,
274+
true /* Get all dumps that there are, if crashdump was created before we hangdumped, get it. It probably has interesting info. */);
275275
foreach (var dumpFile in dumpFiles)
276276
{
277277
try

src/Microsoft.TestPlatform.Extensions.BlameDataCollector/NetClientCrashDumper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void DetachFromTargetProcess(int processId)
3131
// here we might consider renaming the files to have timestamp
3232
}
3333

34-
public IEnumerable<string> GetDumpFiles(bool processCrashed)
34+
public IEnumerable<string> GetDumpFiles(bool _)
3535
{
3636
return _fileHelper.DirectoryExists(_outputDirectory)
3737
? _fileHelper.GetFiles(_outputDirectory, "*_crashdump*.dmp", SearchOption.AllDirectories)

src/Microsoft.TestPlatform.Extensions.BlameDataCollector/ProcDumpDumper.cs

+94-16
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7-
using System.Diagnostics.CodeAnalysis;
87
using System.Globalization;
98
using System.IO;
109
using System.Linq;
10+
using System.Threading;
11+
using System.Threading.Tasks;
1112

1213
using Microsoft.VisualStudio.TestPlatform.CoreUtilities.Helpers;
1314
using Microsoft.VisualStudio.TestPlatform.Execution;
@@ -32,13 +33,16 @@ public class ProcDumpDumper : ICrashDumper, IHangDumper
3233
private readonly IFileHelper _fileHelper;
3334
private readonly IEnvironment _environment;
3435
private readonly IEnvironmentVariableHelper _environmentVariableHelper;
36+
private string? _procDumpPath;
3537
private Process? _procDumpProcess;
3638
private string? _tempDirectory;
3739
private string? _dumpFileName;
3840
private bool _collectAlways;
3941
private string? _outputDirectory;
4042
private Process? _process;
4143
private string? _outputFilePrefix;
44+
private bool _isCrashDumpInProgress;
45+
private readonly int _timeout = EnvironmentHelper.GetConnectionTimeout() * 1000;
4246
private readonly ProcDumpExecutableHelper _procDumpExecutableHelper;
4347

4448
public ProcDumpDumper()
@@ -63,25 +67,47 @@ internal ProcDumpDumper(IProcessHelper processHelper, IFileHelper fileHelper, IE
6367
_procDumpExecutableHelper = new ProcDumpExecutableHelper(processHelper, fileHelper, environment, environmentVariableHelper);
6468
}
6569

66-
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Part of the public API")]
6770
protected Action<object?, string?> OutputReceivedCallback => (process, data) =>
68-
// useful for visibility when debugging this tool
69-
// Console.ForegroundColor = ConsoleColor.Cyan;
70-
// Console.WriteLine(data);
71-
// Console.ForegroundColor = ConsoleColor.White;
72-
// Log all standard output message of procdump in diag files.
73-
// Otherwise they end up coming on console in pipleine.
71+
{
7472
EqtTrace.Info($"ProcDumpDumper.OutputReceivedCallback: Output received from procdump process: {data ?? "<null>"}");
7573

74+
// This is what procdump writes to the output when it is creating a crash dump. When hangdump triggers while we are writing a crash dump
75+
// we probably don't want to cancel, because that crashdump probably has the more interesting info.
76+
// [16:06:59] Dump 1 initiated: <path>
77+
// [16:07:00] Dump 1 writing: Estimated dump file size is 11034 MB.
78+
// [16:07:09] Dump 1 complete: 11034 MB written in 10.1 seconds
79+
// We also want to know when we completed writing a dump (and not just set _isCrashDumpInProgress once), because dumpcount larger than 1
80+
// can be provided externally and then the first dump would prevent hangdump forever from stopping the process, but the not every dump is crashing the process
81+
// so we would run forever.
82+
//
83+
// Yes the two ifs below depend on the content being in english, and containg those words (which is the case for procdump from 2017 till 2023 at least),
84+
// if we get different language it should not break us, we will just cancel more aggressively (unfortunately).
85+
if (data != null && data.Contains("initiated"))
86+
{
87+
EqtTrace.Info($"ProcDumpDumper.OutputReceivedCallback: Output received from procdump process contains 'initiated', crashdump is being written. Don't cancel procdump right now.");
88+
_isCrashDumpInProgress = true;
89+
}
90+
91+
if (data != null && data.Contains("complete"))
92+
{
93+
EqtTrace.Info($"ProcDumpDumper.OutputReceivedCallback: Output received from procdump process contains 'complete' dump is finished, you can cancel procdump if you need.");
94+
_isCrashDumpInProgress = false;
95+
}
96+
};
97+
98+
internal static Action<object?, string?> ErrorReceivedCallback => (process, data) =>
99+
EqtTrace.Error($"ProcDumpDumper.ErrorReceivedCallback: Error received from procdump process: {data ?? "<null>"}");
100+
76101
/// <inheritdoc/>
77102
public void WaitForDumpToFinish()
78103
{
79-
if (_processHelper == null)
104+
if (_procDumpProcess == null)
80105
{
81106
EqtTrace.Info($"ProcDumpDumper.WaitForDumpToFinish: ProcDump was not previously attached, this might indicate error during setup, look for ProcDumpDumper.AttachToTargetProcess.");
107+
return;
82108
}
83109

84-
_processHelper?.WaitForProcessExit(_procDumpProcess);
110+
_processHelper.WaitForProcessExit(_procDumpProcess);
85111
}
86112

87113
/// <inheritdoc/>
@@ -118,12 +144,13 @@ public void AttachToTargetProcess(int processId, string outputDirectory, DumpTyp
118144
isFullDump: dumpType == DumpTypeOption.Full);
119145

120146
EqtTrace.Info($"ProcDumpDumper.AttachToTargetProcess: Running ProcDump with arguments: '{procDumpArgs}'.");
147+
_procDumpPath = procDumpPath;
121148
_procDumpProcess = (Process)_processHelper.LaunchProcess(
122149
procDumpPath,
123150
procDumpArgs,
124151
_tempDirectory,
125152
null,
126-
null,
153+
ErrorReceivedCallback,
127154
null,
128155
OutputReceivedCallback);
129156

@@ -133,27 +160,78 @@ public void AttachToTargetProcess(int processId, string outputDirectory, DumpTyp
133160
/// <inheritdoc/>
134161
public void DetachFromTargetProcess(int targetProcessId)
135162
{
136-
if (_procDumpProcess == null)
163+
if (_procDumpProcess == null || _procDumpPath == null)
137164
{
138165
EqtTrace.Info($"ProcDumpDumper.DetachFromTargetProcess: ProcDump was not previously attached, this might indicate error during setup, look for ProcDumpDumper.AttachToTargetProcess.");
139166
return;
140167
}
141168

169+
if (_procDumpProcess.HasExited)
170+
{
171+
EqtTrace.Info($"ProcDumpDumper.DetachFromTargetProcess: ProcDump already exited, returning.");
172+
return;
173+
}
174+
142175
try
143176
{
177+
if (_isCrashDumpInProgress)
178+
{
179+
EqtTrace.Info($"ProcDumpDumper.DetachFromTargetProcess: ProcDump is currently dumping process '{targetProcessId}', wait at most {_timeout} ms for it to finish so we get the crashdump.");
180+
var procDumpExit = Task.Run(() => _procDumpProcess.WaitForExit(_timeout));
181+
// Could do this better with completion source, but we have nothing better to do in this process anyway,
182+
// than wait for the crashdump to finish.
183+
while (_isCrashDumpInProgress && !procDumpExit.IsCompleted)
184+
{
185+
// The timeout is driven by VSTEST_CONNECTION_TIMEOUT which is specified in seconds so it cannot be less than 1000ms.
186+
// (Technically it can be 0, but that will fail way before we ever reach here.)
187+
Thread.Sleep(500);
188+
EqtTrace.Verbose($"ProcDumpDumper.DetachFromTargetProcess: Waiting for procdump to finish dumping the process.");
189+
}
190+
191+
if (procDumpExit.IsCompleted && procDumpExit.Result == false)
192+
{
193+
EqtTrace.Verbose($"ProcDumpDumper.DetachFromTargetProcess: Procdump did not exit after {_timeout} ms.");
194+
}
195+
}
196+
197+
if (_procDumpProcess.HasExited)
198+
{
199+
EqtTrace.Info($"ProcDumpDumper.DetachFromTargetProcess: ProcDump already exited, returning.");
200+
return;
201+
}
202+
144203
EqtTrace.Info($"ProcDumpDumper.DetachFromTargetProcess: ProcDump detaching from target process '{targetProcessId}'.");
204+
// Alternative to sending this event is calling Procdump -cancel <targetProcessId> (the dumped process id, not the existing Procdump.exe process id).
205+
// But not all versions of procdump have that parameter (definitely not the one we are getting from the Procdump 0.0.1 nuget package), and it works reliably.
206+
// What was not reliable before was that we sent the message and immediately killed procdump, that caused testhost to crash occasionally, because procdump was not detached,
207+
// and killing the process when it is not detached takes the observed process with it.
145208
new Win32NamedEvent($"Procdump-{targetProcessId}").Set();
209+
EqtTrace.Info($"ProcDumpDumper.DetachFromTargetProcess: Cancel event was sent to Procdump.");
210+
211+
var sw = Stopwatch.StartNew();
212+
var exited = _procDumpProcess.WaitForExit(_timeout);
213+
if (exited)
214+
{
215+
EqtTrace.Info($"ProcDumpDumper.DetachFromTargetProcess: ProcDump cancelled after {sw.ElapsedMilliseconds} ms.");
216+
}
217+
else
218+
{
219+
EqtTrace.Info($"ProcDumpDumper.DetachFromTargetProcess: ProcDump cancellation timed out, after {sw.ElapsedMilliseconds} ms.");
220+
}
146221
}
147222
finally
148223
{
149224
try
150225
{
151-
EqtTrace.Info("ProcDumpDumper.DetachFromTargetProcess: Attempting to kill proc dump process.");
152-
_processHelper.TerminateProcess(_procDumpProcess);
226+
if (!_procDumpProcess.HasExited)
227+
{
228+
EqtTrace.Info("ProcDumpDumper.DetachFromTargetProcess: Procdump process is still running after cancellation, force killing it. This will probably take down the process it is attached to as well.");
229+
_processHelper.TerminateProcess(_procDumpProcess);
230+
}
153231
}
154232
catch (Exception e)
155233
{
156-
EqtTrace.Warning($"ProcDumpDumper.DetachFromTargetProcess: Failed to kill proc dump process with exception {e}");
234+
EqtTrace.Warning($"ProcDumpDumper.DetachFromTargetProcess: Failed to kill procdump process with exception {e}");
157235
}
158236
}
159237
}
@@ -241,7 +319,7 @@ public void Dump(int processId, string outputDirectory, DumpTypeOption dumpType)
241319
procDumpArgs,
242320
tempDirectory,
243321
null,
244-
null,
322+
ErrorReceivedCallback,
245323
null,
246324
OutputReceivedCallback);
247325

test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public void InitializeWithDumpForHangShouldCaptureADumpOnTimeout()
175175
_mockFileHelper.Setup(x => x.Exists(It.Is<string>(y => y == "abc_hang.dmp"))).Returns(true);
176176
_mockFileHelper.Setup(x => x.GetFullPath(It.Is<string>(y => y == "abc_hang.dmp"))).Returns("abc_hang.dmp");
177177
_mockProcessDumpUtility.Setup(x => x.StartHangBasedProcessDump(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<Action<string>>()));
178-
_mockProcessDumpUtility.Setup(x => x.GetDumpFiles(true, false)).Returns(new[] { dumpFile });
178+
_mockProcessDumpUtility.Setup(x => x.GetDumpFiles(true, It.IsAny<bool>())).Returns(new[] { dumpFile });
179179
_mockDataCollectionSink.Setup(x => x.SendFileAsync(It.IsAny<FileTransferInformation>())).Callback(() => hangBasedDumpcollected.Set());
180180

181181
_blameDataCollector.Initialize(
@@ -187,7 +187,7 @@ public void InitializeWithDumpForHangShouldCaptureADumpOnTimeout()
187187

188188
hangBasedDumpcollected.Wait(1000);
189189
_mockProcessDumpUtility.Verify(x => x.StartHangBasedProcessDump(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<Action<string>>()), Times.Once);
190-
_mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, false), Times.Once);
190+
_mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, It.IsAny<bool>()), Times.Once);
191191
_mockDataCollectionSink.Verify(x => x.SendFileAsync(It.Is<FileTransferInformation>(y => y.Path == dumpFile)), Times.Once);
192192
}
193193

@@ -210,7 +210,7 @@ public void InitializeWithDumpForHangShouldCaptureKillTestHostOnTimeoutEvenIfGet
210210
_mockFileHelper.Setup(x => x.Exists(It.Is<string>(y => y == "abc_hang.dmp"))).Returns(true);
211211
_mockFileHelper.Setup(x => x.GetFullPath(It.Is<string>(y => y == "abc_hang.dmp"))).Returns("abc_hang.dmp");
212212
_mockProcessDumpUtility.Setup(x => x.StartHangBasedProcessDump(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<Action<string>>()));
213-
_mockProcessDumpUtility.Setup(x => x.GetDumpFiles(true, false)).Callback(() => hangBasedDumpcollected.Set()).Throws(new Exception("Some exception"));
213+
_mockProcessDumpUtility.Setup(x => x.GetDumpFiles(true, It.IsAny<bool>())).Callback(() => hangBasedDumpcollected.Set()).Throws(new Exception("Some exception"));
214214

215215
_blameDataCollector.Initialize(
216216
GetDumpConfigurationElement(false, false, true, 0),
@@ -221,7 +221,7 @@ public void InitializeWithDumpForHangShouldCaptureKillTestHostOnTimeoutEvenIfGet
221221

222222
hangBasedDumpcollected.Wait(1000);
223223
_mockProcessDumpUtility.Verify(x => x.StartHangBasedProcessDump(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<Action<string>>()), Times.Once);
224-
_mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, false), Times.Once);
224+
_mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, It.IsAny<bool>()), Times.Once);
225225
}
226226

227227
/// <summary>
@@ -244,7 +244,7 @@ public void InitializeWithDumpForHangShouldCaptureKillTestHostOnTimeoutEvenIfAtt
244244
_mockFileHelper.Setup(x => x.Exists(It.Is<string>(y => y == "abc_hang.dmp"))).Returns(true);
245245
_mockFileHelper.Setup(x => x.GetFullPath(It.Is<string>(y => y == "abc_hang.dmp"))).Returns("abc_hang.dmp");
246246
_mockProcessDumpUtility.Setup(x => x.StartHangBasedProcessDump(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<Action<string>>()));
247-
_mockProcessDumpUtility.Setup(x => x.GetDumpFiles(true, false)).Returns(new[] { dumpFile });
247+
_mockProcessDumpUtility.Setup(x => x.GetDumpFiles(true, It.IsAny<bool>())).Returns(new[] { dumpFile });
248248
_mockDataCollectionSink.Setup(x => x.SendFileAsync(It.IsAny<FileTransferInformation>())).Callback(() => hangBasedDumpcollected.Set()).Throws(new Exception("Some other exception"));
249249

250250
_blameDataCollector.Initialize(
@@ -256,7 +256,7 @@ public void InitializeWithDumpForHangShouldCaptureKillTestHostOnTimeoutEvenIfAtt
256256

257257
hangBasedDumpcollected.Wait(1000);
258258
_mockProcessDumpUtility.Verify(x => x.StartHangBasedProcessDump(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<Action<string>>()), Times.Once);
259-
_mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, false), Times.Once);
259+
_mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, It.IsAny<bool>()), Times.Once);
260260
_mockDataCollectionSink.Verify(x => x.SendFileAsync(It.Is<FileTransferInformation>(y => y.Path == dumpFile)), Times.Once);
261261
}
262262

0 commit comments

Comments
 (0)