|
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Xml;
using Microsoft.VisualStudio.TestPlatform.Execution;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;
namespace Microsoft.TestPlatform.Extensions.BlameDataCollector;
/// <summary>
/// The blame collector.
/// </summary>
[DataCollectorFriendlyName("Blame")]
[DataCollectorTypeUri("datacollector://Microsoft/TestPlatform/Extensions/Blame/v1")]
public class BlameCollector : DataCollector, ITestExecutionEnvironmentSpecifier
{
private const int DefaultInactivityTimeInMinutes = 60;
private DataCollectionSink? _dataCollectionSink;
private DataCollectionEnvironmentContext? _context;
private DataCollectionEvents? _events;
private DataCollectionLogger? _logger;
private readonly IProcessDumpUtility _processDumpUtility;
private ConcurrentQueue<Guid>? _testSequence;
private ConcurrentDictionary<Guid, BlameTestObject>? _testObjectDictionary;
private readonly IBlameReaderWriter _blameReaderWriter;
private readonly IFileHelper _fileHelper;
private readonly IProcessHelper _processHelper;
private XmlElement? _configurationElement;
private int _testEndCount;
private bool _collectProcessDumpOnCrash;
private bool _collectProcessDumpOnHang;
private bool _monitorPostmortemDumpFolder;
private bool _collectDumpAlways;
private string? _attachmentGuid;
private CrashDumpType _crashDumpType;
private HangDumpType? _hangDumpType;
private bool _inactivityTimerAlreadyFired;
private IInactivityTimer? _inactivityTimer;
private TimeSpan _inactivityTimespan = TimeSpan.FromMinutes(DefaultInactivityTimeInMinutes);
private int _testHostProcessId;
private string? _testHostProcessName;
private string? _targetFramework;
private readonly List<KeyValuePair<string, string>> _environmentVariables = new();
private bool _uploadDumpFiles;
private string? _tempDirectory;
private string? _monitorPostmortemDumpFolderPath;
/// <summary>
/// Initializes a new instance of the <see cref="BlameCollector"/> class.
/// Using XmlReaderWriter by default
/// </summary>
public BlameCollector()
: this(new XmlReaderWriter(), new ProcessDumpUtility(), null, new FileHelper(), new ProcessHelper())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BlameCollector"/> class.
/// </summary>
/// <param name="blameReaderWriter">
/// BlameReaderWriter instance.
/// </param>
/// <param name="processDumpUtility">
/// IProcessDumpUtility instance.
/// </param>
/// <param name="inactivityTimer">
/// InactivityTimer instance.
/// </param>
/// <param name="fileHelper">
/// Filehelper instance.
/// </param>
/// <param name="processHelper">Process helper instance.</param>
internal BlameCollector(
IBlameReaderWriter blameReaderWriter,
IProcessDumpUtility processDumpUtility,
IInactivityTimer? inactivityTimer,
IFileHelper fileHelper,
IProcessHelper processHelper)
{
_blameReaderWriter = blameReaderWriter;
_processDumpUtility = processDumpUtility;
_inactivityTimer = inactivityTimer;
_fileHelper = fileHelper;
_processHelper = processHelper;
}
/// <summary>
/// Gets environment variables that should be set in the test execution environment
/// </summary>
/// <returns>Environment variables that should be set in the test execution environment</returns>
public IEnumerable<KeyValuePair<string, string>> GetTestExecutionEnvironmentVariables()
{
return _environmentVariables;
}
/// <summary>
/// Initializes parameters for the new instance of the class <see cref="BlameDataCollector"/>
/// </summary>
/// <param name="configurationElement">The Xml Element to save to</param>
/// <param name="events">Data collection events to which methods subscribe</param>
/// <param name="dataSink">A data collection sink for data transfer</param>
/// <param name="logger">Data Collection Logger to send messages to the client </param>
/// <param name="environmentContext">Context of data collector environment</param>
[MemberNotNull(nameof(_events), nameof(_dataCollectionSink), nameof(_testSequence), nameof(_testObjectDictionary), nameof(_logger))]
public override void Initialize(
XmlElement? configurationElement,
DataCollectionEvents events,
DataCollectionSink dataSink,
DataCollectionLogger logger,
DataCollectionEnvironmentContext? environmentContext)
{
DebuggerBreakpoint.WaitForDebugger(WellKnownDebugEnvironmentVariables.VSTEST_BLAMEDATACOLLECTOR_DEBUG);
_events = events;
_dataCollectionSink = dataSink;
_context = environmentContext;
_configurationElement = configurationElement;
_testSequence = new ConcurrentQueue<Guid>();
_testObjectDictionary = new ConcurrentDictionary<Guid, BlameTestObject>();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// Subscribing to events
_events.TestHostLaunched += TestHostLaunchedHandler;
_events.SessionEnd += SessionEndedHandler;
_events.TestCaseStart += EventsTestCaseStart;
_events.TestCaseEnd += EventsTestCaseEnd;
if (_configurationElement != null)
{
if (_configurationElement[Constants.DumpModeKey] is XmlElement collectDumpNode)
{
_collectProcessDumpOnCrash = true;
ValidateAndAddCrashProcessDumpParameters(collectDumpNode);
// enabling dumps on MacOS needs to be done explicitly https://github.com/dotnet/runtime/pull/40105
_environmentVariables.Add(new KeyValuePair<string, string>("COMPlus_DbgEnableElfDumpOnMacOS", "1"));
_environmentVariables.Add(new KeyValuePair<string, string>("COMPlus_DbgEnableMiniDump", "1"));
// https://github.com/dotnet/coreclr/blob/master/Documentation/botr/xplat-minidump-generation.md
// 2 MiniDumpWithPrivateReadWriteMemory
// 4 MiniDumpWithFullMemory
_environmentVariables.Add(new KeyValuePair<string, string>("COMPlus_DbgMiniDumpType", _crashDumpType == CrashDumpType.Full ? "4" : "2"));
var dumpDirectory = GetDumpDirectory();
var dumpPath = Path.Combine(dumpDirectory, $"%e_%p_%t_crashdump.dmp");
_environmentVariables.Add(new KeyValuePair<string, string>("COMPlus_DbgMiniDumpName", dumpPath));
}
else
{
_collectProcessDumpOnCrash = false;
}
if (_configurationElement[Constants.CollectDumpOnTestSessionHang] is XmlElement collectHangBasedDumpNode)
{
_collectProcessDumpOnHang = true;
// enabling dumps on MacOS needs to be done explicitly https://github.com/dotnet/runtime/pull/40105
_environmentVariables.Add(new KeyValuePair<string, string>("COMPlus_DbgEnableElfDumpOnMacOS", "1"));
ValidateAndAddHangProcessDumpParameters(collectHangBasedDumpNode!);
}
else
{
_collectProcessDumpOnHang = false;
}
_monitorPostmortemDumpFolder = _configurationElement[Constants.MonitorPostmortemDebugger] is XmlElement monitorPostmortemNode &&
ValidateMonitorPostmortemDebuggerParameters(monitorPostmortemNode);
EqtTrace.Info($"[MonitorPostmortemDump]Monitor enabled: '{_monitorPostmortemDumpFolder}'");
var tfm = _configurationElement[Constants.TargetFramework]?.InnerText;
if (!tfm.IsNullOrWhiteSpace())
{
_targetFramework = tfm;
}
}
_attachmentGuid = Guid.NewGuid().ToString("N");
if (_collectProcessDumpOnHang)
{
_inactivityTimer ??= new InactivityTimer(CollectDumpAndAbortTesthost);
ResetInactivityTimer();
}
}
/// <summary>
/// Disposes of the timer when called to prevent further calls.
/// Kills the other instance of proc dump if launched for collecting crash dumps.
/// Starts and waits for a new proc dump process to collect a single dump and then
/// kills the testhost process.
/// </summary>
private void CollectDumpAndAbortTesthost()
{
TPDebug.Assert(_logger != null && _context != null && _dataCollectionSink != null, "Initialize must be called before calling this method");
_inactivityTimerAlreadyFired = true;
string value;
string unit;
if (_inactivityTimespan.TotalSeconds <= 90)
{
value = ((int)_inactivityTimespan.TotalSeconds).ToString(CultureInfo.InvariantCulture);
unit = Resources.Resources.Seconds;
}
else
{
value = Math.Round(_inactivityTimespan.TotalMinutes, 2).ToString(CultureInfo.InvariantCulture);
unit = Resources.Resources.Minutes;
}
var message = string.Format(CultureInfo.CurrentCulture, Resources.Resources.InactivityTimeout, value, unit);
EqtTrace.Warning(message);
_logger.LogWarning(_context.SessionDataCollectionContext, message);
try
{
EqtTrace.Verbose("Calling dispose on Inactivity timer.");
_inactivityTimer?.Dispose();
}
catch
{
EqtTrace.Verbose("Inactivity timer is already disposed.");
}
if (_collectProcessDumpOnCrash)
{
// Detach the dumper from the testhost process to prevent crashing testhost process. When the dumper is procdump.exe
// it must be detached before we try to dump the process, and simply killing it would take down the testhost process.
//
// Detaching also prevents creating an extra dump at the exit of the testhost process.
_processDumpUtility.DetachFromTargetProcess(_testHostProcessId);
}
// Skip creating the dump if the option is set to none, and just kill the process.
if ((_hangDumpType ?? HangDumpType.Full) != HangDumpType.None)
{
try
{
Action<string> logWarning = m => _logger.LogWarning(_context.SessionDataCollectionContext, m);
var dumpDirectory = GetDumpDirectory();
_processDumpUtility.StartHangBasedProcessDump(_testHostProcessId, dumpDirectory, _hangDumpType == HangDumpType.Full, _targetFramework!, logWarning);
}
catch (Exception ex)
{
_logger.LogError(_context.SessionDataCollectionContext, $"Blame: Creating hang dump failed with error.", ex);
}
if (_uploadDumpFiles)
{
try
{
var dumpFiles = _processDumpUtility.GetDumpFiles(true,
true /* Get all dumps that there are, if crashdump was created before we hangdumped, get it. It probably has interesting info. */);
foreach (var dumpFile in dumpFiles)
{
try
{
if (!dumpFile.IsNullOrEmpty())
{
var fileTransferInformation = new FileTransferInformation(_context.SessionDataCollectionContext, dumpFile, true, _fileHelper);
_dataCollectionSink.SendFileAsync(fileTransferInformation);
}
}
catch (Exception ex)
{
// Eat up any exception here and log it but proceed with killing the test host process.
EqtTrace.Error(ex);
}
if (!dumpFiles.Any())
{
EqtTrace.Error("BlameCollector.CollectDumpAndAbortTesthost: blame:CollectDumpOnHang was enabled but dump file was not generated.");
}
}
}
catch (Exception ex)
{
_logger.LogError(_context.SessionDataCollectionContext, $"Blame: Collecting hang dump failed with error.", ex);
}
}
else
{
EqtTrace.Info("BlameCollector.CollectDumpAndAbortTesthost: Custom path to dump directory was provided via VSTEST_DUMP_PATH. Skipping attachment upload, the caller is responsible for collecting and uploading the dumps themselves.");
}
}
try
{
var p = Process.GetProcessById(_testHostProcessId);
try
{
if (!p.HasExited)
{
p.Kill();
}
}
catch (InvalidOperationException)
{
// Process may have already exited — safe to ignore.
}
}
catch (Exception ex)
{
EqtTrace.Error(ex);
}
}
private bool ValidateMonitorPostmortemDebuggerParameters(XmlElement collectDumpNode)
{
TPDebug.Assert(_logger != null && _context != null, "Initialize must be called before calling this method");
if (StringUtils.IsNullOrEmpty(_monitorPostmortemDumpFolderPath = collectDumpNode.GetAttribute("DumpDirectoryPath")))
{
_logger.LogWarning(_context.SessionDataCollectionContext, Resources.Resources.MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter);
return false;
}
if (!_fileHelper.DirectoryExists(_monitorPostmortemDumpFolderPath))
{
_logger.LogWarning(_context.SessionDataCollectionContext, Resources.Resources.MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter);
return false;
}
return true;
}
private void ValidateAndAddCrashProcessDumpParameters(XmlElement collectDumpNode)
{
TPDebug.Assert(_logger != null && _context != null, "Initialize must be called before calling this method");
foreach (XmlAttribute blameAttribute in collectDumpNode.Attributes)
{
switch (blameAttribute)
{
case XmlAttribute attribute when string.Equals(attribute.Name, Constants.CollectDumpAlwaysKey, StringComparison.OrdinalIgnoreCase):
if ((!string.Equals(attribute.Value, Constants.TrueConfigurationValue, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(attribute.Value, Constants.FalseConfigurationValue, StringComparison.OrdinalIgnoreCase))
|| !bool.TryParse(attribute.Value, out _collectDumpAlways))
{
_logger.LogWarning(_context.SessionDataCollectionContext, FormatBlameParameterValueIncorrectMessage(attribute, [Constants.TrueConfigurationValue, Constants.FalseConfigurationValue]));
}
break;
case XmlAttribute attribute when string.Equals(attribute.Name, Constants.DumpTypeKey, StringComparison.OrdinalIgnoreCase):
if (Enum.TryParse(attribute.Value, ignoreCase: true, out CrashDumpType value) && Enum.IsDefined(typeof(CrashDumpType), value))
{
_crashDumpType = value;
}
else
{
_logger.LogWarning(_context.SessionDataCollectionContext, FormatBlameParameterValueIncorrectMessage(attribute, Enum.GetNames(typeof(CrashDumpType))));
}
break;
default:
_logger.LogWarning(_context.SessionDataCollectionContext, string.Format(CultureInfo.CurrentCulture, Resources.Resources.BlameParameterKeyIncorrect, blameAttribute.Name));
break;
}
}
}
internal static string FormatBlameParameterValueIncorrectMessage(XmlAttribute attribute, params string[] validValues)
{
return string.Format(CultureInfo.CurrentCulture, Resources.Resources.BlameParameterValueIncorrect, attribute.Name, attribute.Value, string.Join(", ", validValues));
}
private void ValidateAndAddHangProcessDumpParameters(XmlElement collectDumpNode)
{
TPDebug.Assert(_logger != null && _context != null, "Initialize must be called before calling this method");
foreach (XmlAttribute blameAttribute in collectDumpNode.Attributes)
{
switch (blameAttribute)
{
case XmlAttribute attribute when string.Equals(attribute.Name, Constants.TestTimeout, StringComparison.OrdinalIgnoreCase):
if (!attribute.Value.IsNullOrWhiteSpace() && TimeSpanParser.TryParse(attribute.Value, out var timeout))
{
_inactivityTimespan = timeout;
}
else
{
_logger.LogWarning(_context.SessionDataCollectionContext, string.Format(CultureInfo.CurrentCulture, Resources.Resources.UnexpectedValueForInactivityTimespanValue, attribute.Value));
}
break;
// allow HangDumpType attribute to be used on the hang dump this is the prefered way
case XmlAttribute attribute when string.Equals(attribute.Name, Constants.HangDumpTypeKey, StringComparison.OrdinalIgnoreCase):
if (Enum.TryParse(attribute.Value, ignoreCase: true, out HangDumpType value) && Enum.IsDefined(typeof(HangDumpType), value))
{
_hangDumpType = value;
}
else
{
_logger.LogWarning(_context.SessionDataCollectionContext, FormatBlameParameterValueIncorrectMessage(attribute, Enum.GetNames(typeof(HangDumpType))));
}
break;
// allow DumpType attribute to be used on the hang dump for backwards compatibility
case XmlAttribute attribute when string.Equals(attribute.Name, Constants.DumpTypeKey, StringComparison.OrdinalIgnoreCase):
// DumpType and HangDumpType are both valid ways to define the dump type. In case we get HangDumpType and DumpType in the command we want HangDumpType to win.
if (Enum.TryParse(attribute.Value, ignoreCase: true, out HangDumpType value2) && Enum.IsDefined(typeof(HangDumpType), value2))
{
_hangDumpType = value2;
}
else
{
// This error is using CrashDumpType on purpose, because the option we are providing is actually supposed to take only CrashDumpType values. We are parsing it into
// HangDumpType to have easier time converting.
_logger.LogWarning(_context.SessionDataCollectionContext, FormatBlameParameterValueIncorrectMessage(attribute, Enum.GetNames(typeof(CrashDumpType))));
}
break;
default:
_logger.LogWarning(_context.SessionDataCollectionContext, string.Format(CultureInfo.CurrentCulture, Resources.Resources.BlameParameterKeyIncorrect, blameAttribute.Name));
break;
}
}
}
/// <summary>
/// Called when Test Case Start event is invoked
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">TestCaseStartEventArgs</param>
private void EventsTestCaseStart(object? sender, TestCaseStartEventArgs e)
{
TPDebug.Assert(_testSequence != null && _testObjectDictionary != null, "Initialize must be called before calling this method");
ResetInactivityTimer();
EqtTrace.Info("BlameCollector.EventsTestCaseStart: Test Case Start");
TPDebug.Assert(e.TestElement is not null, "e.TestElement is null");
var blameTestObject = new BlameTestObject(e.TestElement);
// Add guid to ordered collection of test sequence to maintain the order.
_testSequence.Enqueue(blameTestObject.Id);
// Add the test object to the dictionary.
_testObjectDictionary.TryAdd(blameTestObject.Id, blameTestObject);
}
/// <summary>
/// Called when Test Case End event is invoked
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">TestCaseEndEventArgs</param>
private void EventsTestCaseEnd(object? sender, TestCaseEndEventArgs e)
{
TPDebug.Assert(_testObjectDictionary != null, "Initialize must be called before calling this method");
ResetInactivityTimer();
EqtTrace.Info("BlameCollector.EventsTestCaseEnd: Test Case End");
Interlocked.Increment(ref _testEndCount);
// Update the test object in the dictionary as the test has completed.
TPDebug.Assert(e.TestElement is not null, "e.TestElement is null");
_testObjectDictionary[e.TestElement.Id].IsCompleted = true;
}
/// <summary>
/// Called when Session End event is invoked
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="args">SessionEndEventArgs</param>
private void SessionEndedHandler(object? sender, SessionEndEventArgs args)
{
TPDebug.Assert(_testSequence != null && _testObjectDictionary != null && _context != null && _dataCollectionSink != null && _logger != null, "Initialize must be called before calling this method");
ResetInactivityTimer();
EqtTrace.Info("Blame Collector: Session End");
try
{
// If the last test crashes, it will not invoke a test case end and therefore
// In case of crash testStartCount will be greater than testEndCount and we need to write the sequence
// And send the attachment. This won't indicate failure if there are 0 tests in the assembly, or when it fails in setup.
var processCrashedWhenRunningTests = _testSequence.Count > _testEndCount;
if (processCrashedWhenRunningTests)
{
var filepath = Path.Combine(GetTempDirectory(), Constants.AttachmentFileName + "_" + _attachmentGuid);
List<Guid> testSequenceCopy;
Dictionary<Guid, BlameTestObject> testObjectDictionaryCopy;
testSequenceCopy = [.. _testSequence];
testObjectDictionaryCopy = new Dictionary<Guid, BlameTestObject>(_testObjectDictionary);
filepath = _blameReaderWriter.WriteTestSequence(testSequenceCopy, testObjectDictionaryCopy, filepath);
var fti = new FileTransferInformation(_context.SessionDataCollectionContext, filepath, true);
_dataCollectionSink.SendFileAsync(fti);
}
else
{
if (_collectProcessDumpOnHang)
{
_logger.LogWarning(_context.SessionDataCollectionContext, Resources.Resources.NotGeneratingSequenceFile);
}
}
if (_uploadDumpFiles)
{
try
{
var dumpFiles = _processDumpUtility.GetDumpFiles(warnOnNoDumpFiles: _collectDumpAlways, processCrashedWhenRunningTests);
foreach (var dumpFile in dumpFiles)
{
if (!dumpFile.IsNullOrEmpty())
{
try
{
var fileTransferInformation = new FileTransferInformation(_context.SessionDataCollectionContext, dumpFile, true);
_dataCollectionSink.SendFileAsync(fileTransferInformation);
}
catch (FileNotFoundException ex)
{
EqtTrace.Warning(ex.ToString());
_logger.LogWarning(args.Context, ex.ToString());
}
}
}
}
catch (FileNotFoundException ex)
{
EqtTrace.Warning(ex.ToString());
_logger.LogWarning(args.Context, ex.ToString());
}
}
else
{
EqtTrace.Info("BlameCollector.CollectDumpAndAbortTesthost: Custom path to dump directory was provided via VSTEST_DUMP_PATH. Skipping attachment upload, the caller is responsible for collecting and uploading the dumps themselves.");
}
if (_monitorPostmortemDumpFolder)
{
if (!_fileHelper.DirectoryExists(_monitorPostmortemDumpFolderPath))
{
_logger.LogWarning(_context.SessionDataCollectionContext, Resources.Resources.MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter);
}
else
{
// We do ToArray() because we're moving files and we cannot move file and enumerate at the same time
foreach (var dumpFileNameFullPath in _fileHelper.GetFiles(_monitorPostmortemDumpFolderPath, "*.dmp", SearchOption.TopDirectoryOnly).ToArray())
{
EqtTrace.Info($"[MonitorPostmortemDump]'{dumpFileNameFullPath}' dump file found during postmortem monitoring");
// Ensure exclusive access to the dump file, it can happen if we run more test module in parallel.
// We cannot ensure that we'll move only "our" dump because procdump -i produce a name that doesn't have the pid in it(because PID is reusable).
// The name of the file starts with the process name, that's the only filtering we can do.
// So there's one possible benign race condition when another test is dumping an host and we take lock on the name but the dump is not finished.
// In that case we'll fail for file locking but it's fine. The correct or subsequent "SessionEndedHandler" will move that one.
using SHA256 hashedLockName = SHA256.Create();
// LPCSTR An LPCSTR is a 32-bit pointer to a constant null-terminated string of 8-bit Windows (ANSI) characters.
var toGuid = new byte[16];
Array.Copy(hashedLockName.ComputeHash(Encoding.UTF8.GetBytes(dumpFileNameFullPath)), toGuid, 16);
Guid id = new(toGuid);
string muxerName = @$"Global\{id}";
using Mutex lockFile = new(true, muxerName, out bool createdNew);
EqtTrace.Info($"[MonitorPostmortemDump]Acquired global muxer '{muxerName}' for {dumpFileNameFullPath}");
if (createdNew)
{
string dumpFileName = Path.GetFileNameWithoutExtension(dumpFileNameFullPath);
TPDebug.Assert(_testHostProcessName != null, $"TestHostLaunchedHandler must run before this method and set the _testHostProcessName");
// Expected format testhost.exe_221004_123127.dmp processName.exe_yyMMdd_HHmmss.dmp
if (dumpFileName.StartsWith(_testHostProcessName, StringComparison.OrdinalIgnoreCase))
{
EqtTrace.Info($"[MonitorPostmortemDump]Valid pattern start with '{_testHostProcessName}' found for {dumpFileNameFullPath}");
try
{
var fileTranferInformation = new FileTransferInformation(_context.SessionDataCollectionContext, dumpFileNameFullPath, true);
EqtTrace.Info($"[MonitorPostmortemDump]Transferring {dumpFileNameFullPath}");
_dataCollectionSink.SendFileAsync(fileTranferInformation);
}
catch (IOException ex)
{
// In case of race condition explained in the comment above we simply log a warning.
EqtTrace.Warning(ex.ToString());
_logger.LogWarning(args.Context, ex.ToString());
}
}
}
}
}
}
}
finally
{
// Attempt to terminate the proc dump process if proc dump was enabled
if (_collectProcessDumpOnCrash)
{
_processDumpUtility.DetachFromTargetProcess(_testHostProcessId);
}
DeregisterEvents();
}
}
/// <summary>
/// Called when Test Host Initialized is invoked
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="args">TestHostLaunchedEventArgs</param>
private void TestHostLaunchedHandler(object? sender, TestHostLaunchedEventArgs args)
{
ResetInactivityTimer();
_testHostProcessId = args.TestHostProcessId;
_testHostProcessName = _processHelper.GetProcessName(args.TestHostProcessId);
if (!_collectProcessDumpOnCrash)
{
return;
}
TPDebug.Assert(_logger != null && _context != null, "Initialize must be called before calling this method");
try
{
var dumpDirectory = GetDumpDirectory();
Action<string> logWarning = m => _logger.LogWarning(_context.SessionDataCollectionContext, m);
_processDumpUtility.StartTriggerBasedProcessDump(args.TestHostProcessId, dumpDirectory, _crashDumpType == CrashDumpType.Full, _targetFramework!, _collectDumpAlways, logWarning);
}
catch (TestPlatformException e)
{
EqtTrace.Warning("BlameCollector.TestHostLaunchedHandler: Could not start process dump. {0}", e);
_logger.LogWarning(args.Context, string.Format(CultureInfo.CurrentCulture, Resources.Resources.ProcDumpCouldNotStart, e.Message));
}
catch (Exception e)
{
EqtTrace.Warning("BlameCollector.TestHostLaunchedHandler: Could not start process dump. {0}", e);
_logger.LogWarning(args.Context, string.Format(CultureInfo.CurrentCulture, Resources.Resources.ProcDumpCouldNotStart, e.ToString()));
}
}
/// <summary>
/// Resets the inactivity timer
/// </summary>
private void ResetInactivityTimer()
{
if (!_collectProcessDumpOnHang || _inactivityTimerAlreadyFired)
{
return;
}
EqtTrace.Verbose("Reset the inactivity timer since an event was received.");
try
{
_inactivityTimer?.ResetTimer(_inactivityTimespan);
}
catch (Exception e)
{
EqtTrace.Warning($"Failed to reset the inactivity timer with error {e}");
}
}
/// <summary>
/// Method to de-register handlers and cleanup
/// </summary>
private void DeregisterEvents()
{
TPDebug.Assert(_events != null, "Initialize must be called before calling this method");
_events.SessionEnd -= SessionEndedHandler;
_events.TestCaseStart -= EventsTestCaseStart;
_events.TestCaseEnd -= EventsTestCaseEnd;
}
private string GetTempDirectory()
{
if (_tempDirectory.IsNullOrWhiteSpace())
{
// DUMP_TEMP_PATH will be used as temporary storage location
// for the dumps, this won't affect the dump uploads. Just the place where
// we store them before moving them to the final folder.
// AGENT_TEMPDIRECTORY is AzureDevops variable, which is set to path
// that is cleaned up after every job. This is preferable to use over
// just the normal temp.
var temp = Environment.GetEnvironmentVariable("VSTEST_DUMP_TEMP_PATH") ?? Environment.GetEnvironmentVariable("AGENT_TEMPDIRECTORY") ?? Path.GetTempPath();
_tempDirectory = Path.Combine(temp, Guid.NewGuid().ToString());
Directory.CreateDirectory(_tempDirectory);
return _tempDirectory;
}
return _tempDirectory;
}
private string GetDumpDirectory()
{
TPDebug.Assert(_logger != null && _context != null, "Initialize must be called before calling this method");
// Using a custom dump path for scenarios where we want to upload the
// dump files ourselves, such as when running in Helix.
// This will save into the directory specified via VSTEST_DUMP_PATH, and
// skip uploading dumps via attachments.
var dumpDirectoryOverride = Environment.GetEnvironmentVariable("VSTEST_DUMP_PATH");
var dumpDirectoryOverrideHasValue = !dumpDirectoryOverride.IsNullOrWhiteSpace();
_uploadDumpFiles = !dumpDirectoryOverrideHasValue;
var dumpDirectory = dumpDirectoryOverrideHasValue ? dumpDirectoryOverride! : GetTempDirectory();
Directory.CreateDirectory(dumpDirectory);
var dumpPath = Path.Combine(Path.GetFullPath(dumpDirectory));
if (!_uploadDumpFiles)
{
_logger.LogWarning(_context.SessionDataCollectionContext, $"VSTEST_DUMP_PATH is specified. Dump files will be saved in: {dumpPath}, and won't be added to attachments.");
}
return dumpPath;
}
}
|