Skip to content

Commit

Permalink
add postmortem blame mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Rossignoli committed Oct 7, 2022
1 parent c9ecf5c commit 87e9cc8
Show file tree
Hide file tree
Showing 46 changed files with 1,313 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

<ItemGroup>
<FileToCopy Include="$(SourcePath)vstest.console\bin\$(Configuration)\$(NetFrameworkMinimum)\**\*.*" SubFolder="" />
<FileToCopy Include="$(SourcePath)Microsoft.TestPlatform.TestHostProvider\bin\$(Configuration)\$(NetFrameworkMinimum)\**\*.*" SubFolder="vstest.console\Extensions\" />
<FileToCopy Include="$(SourcePath)Microsoft.TestPlatform.TestHostProvider\bin\$(Configuration)\$(NetFrameworkMinimum)\**\*.*" SubFolder="Extensions\" />

<!-- copy net462, net47, net471, net472 and net48 testhosts" -->
<FileToCopy Include="$(SourcePath)testhost.x86\bin\$(Configuration)\$(NetFrameworkMinimum)\win7-x86\**\*.*" SubFolder="TestHostNetFramework\" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@
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;
Expand All @@ -37,11 +42,13 @@ public class BlameCollector : DataCollector, ITestExecutionEnvironmentSpecifier
private Dictionary<Guid, BlameTestObject>? _testObjectDictionary;
private readonly IBlameReaderWriter _blameReaderWriter;
private readonly IFileHelper _fileHelper;
private readonly IProcessHelper _processHelper;
private XmlElement? _configurationElement;
private int _testStartCount;
private int _testEndCount;
private bool _collectProcessDumpOnCrash;
private bool _collectProcessDumpOnHang;
private bool _monitorPostmortemDumpFolder;
private bool _collectDumpAlways;
private string? _attachmentGuid;

Expand All @@ -53,17 +60,19 @@ public class BlameCollector : DataCollector, ITestExecutionEnvironmentSpecifier
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())
: this(new XmlReaderWriter(), new ProcessDumpUtility(), null, new FileHelper(), new ProcessHelper())
{
}

Expand All @@ -86,12 +95,14 @@ internal BlameCollector(
IBlameReaderWriter blameReaderWriter,
IProcessDumpUtility processDumpUtility,
IInactivityTimer? inactivityTimer,
IFileHelper fileHelper)
IFileHelper fileHelper,
IProcessHelper processHelper)
{
_blameReaderWriter = blameReaderWriter;
_processDumpUtility = processDumpUtility;
_inactivityTimer = inactivityTimer;
_fileHelper = fileHelper;
_processHelper = processHelper;
}

/// <summary>
Expand Down Expand Up @@ -172,6 +183,10 @@ public override void Initialize(
_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())
{
Expand Down Expand Up @@ -310,6 +325,24 @@ private void CollectDumpAndAbortTesthost()
}
}

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");
Expand Down Expand Up @@ -466,7 +499,7 @@ private void EventsTestCaseEnd(object? sender, TestCaseEndEventArgs e)
/// <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");
TPDebug.Assert(_testHostProcessName != null && _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");
Expand Down Expand Up @@ -525,6 +558,55 @@ private void SessionEndedHandler(object? sender, SessionEndEventArgs args)
{
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 MD5 md5LockName = MD5.Create();
// BitConverter converts into something like EC-1B-B6-22-81-00-41-C8-31-1D-B6-61-27-6A-65-8A valid muxer name
// LPCSTR An LPCSTR is a 32-bit pointer to a constant null-terminated string of 8-bit Windows (ANSI) characters.
string muxerName = @$"Global\{BitConverter.ToString(md5LockName.ComputeHash(Encoding.UTF8.GetBytes(dumpFileNameFullPath)))}";
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);

// 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 explained in the comment above we simply log a warning.
EqtTrace.Warning(ex.ToString());
_logger.LogWarning(args.Context, ex.ToString());
}
}
}
}
}
}
}
finally
{
Expand All @@ -547,6 +629,7 @@ private void TestHostLaunchedHandler(object? sender, TestHostLaunchedEventArgs a
{
ResetInactivityTimer();
_testHostProcessId = args.TestHostProcessId;
_testHostProcessName = _processHelper.GetProcessName(args.TestHostProcessId);

if (!_collectProcessDumpOnCrash)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ internal static class Constants
/// </summary>
public const string CollectDumpOnTestSessionHang = "CollectDumpOnTestSessionHang";

/// <summary>
/// Configuration key name for monitoring a folder for postmortem dumps
/// </summary>
public const string MonitorPostmortemDebugger = "MonitorPostmortemDebugger";

/// <summary>
/// Configuration key name for specifying what the expected execution time for the longest running test is.
/// If no events come from the test host for this period a dump will be collected and the test host process will
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,7 @@ This test may, or may not be the source of the crash.</value>
<data name="UnexpectedValueForInactivityTimespanValue" xml:space="preserve">
<value>Unexpected value '{0}' provided as timeout. Please provide a value in this format: 1.5h / 90m / 5400s / 5400000ms / 5400000</value>
</data>
</root>
<data name="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter" xml:space="preserve">
<value>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Tento test může a nemusí být příčinou chybového ukončení.</target>
<target state="translated">{0} nelze najít. Zkontrolujte, zda je spustitelný soubor k dispozici v cestě PATH, případně nastavte proměnnou prostředí PROCDUMP_PATH na adresář, který obsahuje spustitelný soubor {0}.</target>
<note>0 is procDumpPath, a name of procdump executable such as procdump64.exe, 0 is used twice on purpose. PROCDUMP_PATH and PATH are literal and should not be localized.</note>
</trans-unit>
<trans-unit id="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter">
<source>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</source>
<target state="new">Invalid 'DumpDirectoryPath' for postmortem debugger monitor</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Dieser Test kann, muss aber nicht unbedingt die Absturzursache sein.</target>
<target state="translated">{0} wurde nicht gefunden. Stellen Sie sicher, dass die ausführbare Datei unter PATH verfügbar ist. Legen Sie alternativ die Umgebungsvariable PROCDUMP_PATH auf ein Verzeichnis fest, das {0} ausführbare Datei enthält.</target>
<note>0 is procDumpPath, a name of procdump executable such as procdump64.exe, 0 is used twice on purpose. PROCDUMP_PATH and PATH are literal and should not be localized.</note>
</trans-unit>
<trans-unit id="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter">
<source>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</source>
<target state="new">Invalid 'DumpDirectoryPath' for postmortem debugger monitor</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Esta prueba puede ser el origen del bloqueo o no serlo.</target>
<target state="translated">no se encontró {0}. Cerciórese de que el ejecutable está disponible en PATH o establezca variable de entorno PROCDUMP_PATH en un directorio que contenga {0} ejecutable</target>
<note>0 is procDumpPath, a name of procdump executable such as procdump64.exe, 0 is used twice on purpose. PROCDUMP_PATH and PATH are literal and should not be localized.</note>
</trans-unit>
<trans-unit id="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter">
<source>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</source>
<target state="new">Invalid 'DumpDirectoryPath' for postmortem debugger monitor</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Ce test est éventuellement à l'origine du plantage.</target>
<target state="translated">{0} est introuvable, vérifiez que l’exécutable est disponible sur PATH. Vous pouvez également définir PROCDUMP_PATH variable d’environnement sur un répertoire qui contient l’exécutable {0}</target>
<note>0 is procDumpPath, a name of procdump executable such as procdump64.exe, 0 is used twice on purpose. PROCDUMP_PATH and PATH are literal and should not be localized.</note>
</trans-unit>
<trans-unit id="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter">
<source>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</source>
<target state="new">Invalid 'DumpDirectoryPath' for postmortem debugger monitor</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Il test potrebbe essere l'origine dell'arresto anomalo.</target>
<target state="translated">{0} non è stato trovato, verificare che il file eseguibile sia disponibile in PATH. In alternativa, impostare la variabile di ambiente PROCDUMP_PATH su una directory che contiene {0} eseguibile</target>
<note>0 is procDumpPath, a name of procdump executable such as procdump64.exe, 0 is used twice on purpose. PROCDUMP_PATH and PATH are literal and should not be localized.</note>
</trans-unit>
<trans-unit id="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter">
<source>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</source>
<target state="new">Invalid 'DumpDirectoryPath' for postmortem debugger monitor</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ This test may, or may not be the source of the crash.</source>
<target state="translated">{0} が見つかりませんでした。PATH で実行可能ファイルを使用できることを確認してください。または、PROCDUMP_PATH 環境変数を、実行可能ファイル {0} が含まれるディレクトリに設定してください</target>
<note>0 is procDumpPath, a name of procdump executable such as procdump64.exe, 0 is used twice on purpose. PROCDUMP_PATH and PATH are literal and should not be localized.</note>
</trans-unit>
<trans-unit id="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter">
<source>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</source>
<target state="new">Invalid 'DumpDirectoryPath' for postmortem debugger monitor</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ This test may, or may not be the source of the crash.</source>
<target state="translated">{0}을(를) 찾을 수 없습니다. 실행 파일을 PATH에서 사용할 수 있는지 확인합니다. 또는 PROCDUMP_PATH 환경 변수를 {0} 실행 파일이 포함된 디렉터리로 설정하세요.</target>
<note>0 is procDumpPath, a name of procdump executable such as procdump64.exe, 0 is used twice on purpose. PROCDUMP_PATH and PATH are literal and should not be localized.</note>
</trans-unit>
<trans-unit id="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter">
<source>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</source>
<target state="new">Invalid 'DumpDirectoryPath' for postmortem debugger monitor</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Ten test może, ale nie musi być źródłem awarii.</target>
<target state="translated">Nie można odnaleźć pliku wykonywalnego {0}. Upewnij się, że plik wykonywalny jest dostępny w ścieżce PATH. Możesz też ustawić zmienną środowiskową PROCDUMP_PATH na katalog zawierający plik wykonywalny {0}.</target>
<note>0 is procDumpPath, a name of procdump executable such as procdump64.exe, 0 is used twice on purpose. PROCDUMP_PATH and PATH are literal and should not be localized.</note>
</trans-unit>
<trans-unit id="MonitorPostmortemDebuggerInvalidDumpDirectoryPathParameter">
<source>Invalid 'DumpDirectoryPath' for postmortem debugger monitor</source>
<target state="new">Invalid 'DumpDirectoryPath' for postmortem debugger monitor</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Loading

0 comments on commit 87e9cc8

Please sign in to comment.