File: WindowsHangDumper.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.Extensions.BlameDataCollector\Microsoft.TestPlatform.Extensions.BlameDataCollector.csproj (Microsoft.TestPlatform.Extensions.BlameDataCollector)
// 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;

namespace Microsoft.TestPlatform.Extensions.BlameDataCollector;

internal class WindowsHangDumper : IHangDumper
{
    private readonly Action<string> _logWarning;
    private readonly IProcessHelper _processHelper;

    public WindowsHangDumper(IProcessHelper processHelper, Action<string>? logWarning)
    {
        _logWarning = logWarning ?? (_ => { });
        _processHelper = processHelper;
    }

    private static Action<object?, string?> OutputReceivedCallback => (process, data) =>
        // useful for visibility when debugging this tool
        // Console.ForegroundColor = ConsoleColor.Cyan;
        // Console.WriteLine(data);
        // Console.ForegroundColor = ConsoleColor.White;
        // Log all standard output message of procdump in diag files.
        // Otherwise they end up coming on console in pipleine.
        EqtTrace.Info($"ProcDumpDumper.OutputReceivedCallback: Output received from procdump process: {data ?? "<null>"}");

    public void Dump(int processId, string outputDirectory, DumpTypeOption type)
    {
        var process = Process.GetProcessById(processId);
        var processTree = process.GetProcessTree().Where(p => p.Process?.ProcessName is not null and not "conhost" and not "WerFault" and not "createdump").ToList();

        if (processTree.Count > 1)
        {
            var tree = processTree.OrderBy(t => t.Level);
            EqtTrace.Verbose("WindowsHangDumper.Dump: Dumping this process tree (from bottom):");
            foreach (var p in tree)
            {
                EqtTrace.Verbose($"WindowsHangDumper.Dump: {new string(' ', p.Level)}{(p.Level != 0 ? " +" : " >-")} {p.Process!.Id} - {p.Process.ProcessName}");
            }

            // logging warning separately to avoid interleving the messages in the log which make this tree unreadable
            _logWarning(Resources.Resources.DumpingTree);
            foreach (var p in tree)
            {
                _logWarning($"{new string(' ', p.Level)}{(p.Level != 0 ? "+-" : ">")} {p.Process!.Id} - {p.Process.ProcessName}");
            }
        }
        else
        {
            EqtTrace.Verbose($"NetClientHangDumper.Dump: Dumping {process.Id} - {process.ProcessName}.");
            var message = string.Format(CultureInfo.CurrentCulture, Resources.Resources.Dumping, process.Id, process.ProcessName);
            _logWarning(message);
        }

        var bottomUpTree = processTree.OrderByDescending(t => t.Level).Select(t => t.Process);
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            foreach (var p in bottomUpTree)
            {
                TPDebug.Assert(p != null);
                try
                {
                    p.Suspend();
                }
                catch (Exception ex)
                {
                    EqtTrace.Error($"WindowsHangDumper.Dump: Error suspending process {p.Id} - {p.ProcessName}: {ex}.");
                }
            }
        }

        foreach (var p in bottomUpTree)
        {
            TPDebug.Assert(p != null);

            try
            {
                var outputFile = Path.Combine(outputDirectory, $"{p.ProcessName}_{p.Id}_{DateTime.Now:yyyyMMddTHHmmss}_hangdump.dmp");
                CollectDump(_processHelper, p, outputFile, type);
            }
            catch (Exception ex)
            {
                EqtTrace.Error($"WindowsHangDumper.Dump: Error dumping process {p.Id} - {p.ProcessName}: {ex}.");
            }

            try
            {
                EqtTrace.Verbose($"WindowsHangDumper.Dump: Killing process {p.Id} - {p.ProcessName}.");
                p.Kill();
            }
            catch (Exception ex)
            {
                EqtTrace.Error($"WindowsHangDumper.Dump: Error killing process {p.Id} - {p.ProcessName}: {ex}.");
            }
        }
    }

    internal static void CollectDump(IProcessHelper processHelper, Process process, string outputFile, DumpTypeOption type)
    {
        if (process.HasExited)
        {
            EqtTrace.Verbose($"WindowsHangDumper.CollectDump: {process.Id} - {process.ProcessName} already exited, skipping.");
            return;
        }

        EqtTrace.Verbose($"WindowsHangDumper.CollectDump: Selected dump type {type}. Dumping {process.Id} - {process.ProcessName} in {outputFile}. ");

        var currentProcessArchitecture = processHelper.GetCurrentProcessArchitecture();
        var targetProcessArchitecture = processHelper.GetProcessArchitecture(process.Id);

        if (currentProcessArchitecture == targetProcessArchitecture)
        {
            EqtTrace.Verbose($"WindowsHangDumper.CollectDump: Both processes are {currentProcessArchitecture}, using PInvoke dumper directly.");
            MiniDumpWriteDump.CollectDumpUsingMiniDumpWriteDump(process, outputFile, FromDumpType(type));
        }
        else
        {
            var dumpMinitoolName = targetProcessArchitecture switch
            {
                PlatformArchitecture.X86 => "DumpMinitool.x86.exe",
                PlatformArchitecture.X64 => "DumpMinitool.exe",
                PlatformArchitecture.ARM64 => "DumpMinitool.arm64.exe",
                _ => string.Empty
            };

            if (dumpMinitoolName == string.Empty)
            {
                EqtTrace.Verbose($"WindowsHangDumper.CollectDump: The target process architecture is {targetProcessArchitecture}, we don't have a DumpMinitool for that, falling back to using PInvoke directly.");
                MiniDumpWriteDump.CollectDumpUsingMiniDumpWriteDump(process, outputFile, FromDumpType(type));
            }

            var args = $"--file \"{outputFile}\" --processId {process.Id} --dumpType {type}";
            var dumpMinitoolPath = Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location)!, "dump", dumpMinitoolName);
            EqtTrace.Verbose($"WindowsHangDumper.CollectDump: The target process architecture is {targetProcessArchitecture}, dumping it via {dumpMinitoolName}.");

            if (!File.Exists(dumpMinitoolPath))
            {
                throw new FileNotFoundException("Could not find DumpMinitool", dumpMinitoolPath);
            }

            EqtTrace.Info($"ProcDumpDumper.CollectDump: Running DumpMinitool: '{dumpMinitoolPath} {args}'.");
            var dumpMiniTool = (Process)new ProcessHelper().LaunchProcess(
                dumpMinitoolPath,
                args,
                Path.GetDirectoryName(outputFile),
                null,
                null,
                null,
                OutputReceivedCallback);
            dumpMiniTool.WaitForExit();
            EqtTrace.Info($"ProcDumpDumper.CollectDump: {dumpMinitoolName} exited with exitcode: '{dumpMiniTool.ExitCode}'.");
        }

        EqtTrace.Verbose($"WindowsHangDumper.CollectDump: Finished dumping {process.Id} - {process.ProcessName} in {outputFile}. ");
    }

    private static MiniDumpTypeOption FromDumpType(DumpTypeOption type)
    {
        return type switch
        {
            DumpTypeOption.Full => MiniDumpTypeOption.Full,
            DumpTypeOption.WithHeap => MiniDumpTypeOption.WithHeap,
            DumpTypeOption.Mini => MiniDumpTypeOption.Mini,
            _ => throw new NotSupportedException($"Dump type {type} is not supported."),
        };
    }
}