File: ProcessCodeMethods.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.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;

namespace Microsoft.TestPlatform.Extensions.BlameDataCollector;

/// <summary>
/// Helper functions for process info.
/// </summary>
internal static class ProcessCodeMethods
{
    private const int InvalidProcessId = -1;

    public static void Suspend(this Process process)
    {
        if (process.HasExited)
        {
            EqtTrace.Verbose($"ProcessCodeMethods.Suspend: Process {process.Id} - {process.ProcessName} already exited, skipping.");
            return;
        }

        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            // There is no supported or documented API to suspend a process. If there was you should call it here.
            // SuspendWindows(process);
        }
        else
        {
            // TODO: do not suspend on Mac and Linux, this prevents the process from being dumped when we use the net client dumper, checking if we can post a different signal
            // SuspendLinuxMacOs(process);
        }
    }

    public static List<ProcessTreeNode> GetProcessTree(this Process process)
    {
        var childProcesses = Process.GetProcesses()
            .Where(p => IsChildCandidate(p, process))
            .ToList();

        var acc = new List<ProcessTreeNode>();
        foreach (var c in childProcesses)
        {
            try
            {
                var parentId = GetParentPid(c);

                // c.ParentId = parentId;
                acc.Add(new ProcessTreeNode { ParentId = parentId, Process = c });
            }
            catch
            {
                // many things can go wrong with this
                // just ignore errors
            }
        }

        var level = 1;
        var limit = 10;
        ResolveChildren(process, acc, level, limit);

        return new List<ProcessTreeNode> { new() { Process = process, Level = 0 } }.Concat(acc.Where(a => a.Level > 0)).ToList();
    }

    /// <summary>
    /// Returns the parent id of a process or -1 if it fails.
    /// </summary>
    /// <param name="process">The process to find parent of.</param>
    /// <returns>The pid of the parent process.</returns>
    internal static int GetParentPid(Process process)
    {
        return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
            ? GetParentPidWindows(process)
            : RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
                ? GetParentPidLinux(process)
                : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
                    GetParentPidMacOs(process)
                    : throw new PlatformNotSupportedException();
    }

    internal static int GetParentPidWindows(Process process)
    {
        try
        {
            var handle = process.Handle;
            var res = NtQueryInformationProcess(handle, 0, out var pbi, Marshal.SizeOf<ProcessBasicInformation>(), out int size);

            var p = res != 0 ? InvalidProcessId : pbi.InheritedFromUniqueProcessId.ToInt32();

            return p;
        }
        catch (Exception ex)
        {
            EqtTrace.Verbose($"ProcessCodeMethods.GetParentPidWindows: Error getting parent of process {process.Id} - {process.ProcessName}, {ex}.");
            return InvalidProcessId;
        }
    }

    /// <summary>Read the /proc file system for information about the parent.</summary>
    /// <param name="process">The process to get the parent process from.</param>
    /// <returns>The process id.</returns>
    internal static int GetParentPidLinux(Process process)
    {
        var pid = process.Id;

        // read /proc/<pid>/stat
        // 4th column will contain the ppid, 92 in the example below
        // ex: 93 (bash) S 92 93 2 4294967295 ...
        var path = $"/proc/{pid}/stat";
        try
        {
            var stat = File.ReadAllText(path);
            var parts = stat.Split(' ');

            return parts.Length < 5 ? InvalidProcessId : int.Parse(parts[3], CultureInfo.CurrentCulture);
        }
        catch (Exception ex)
        {
            EqtTrace.Verbose($"ProcessCodeMethods.GetParentPidLinux: Error getting parent of process {process.Id} - {process.ProcessName}, {ex}.");
            return InvalidProcessId;
        }
    }

    internal static int GetParentPidMacOs(Process process)
    {
        try
        {
            var output = new StringBuilder();
            var err = new StringBuilder();
            Process ps = new();
            ps.StartInfo.FileName = "ps";
            ps.StartInfo.Arguments = $"-o ppid= {process.Id}";
            ps.StartInfo.UseShellExecute = false;
            ps.StartInfo.RedirectStandardOutput = true;
            ps.OutputDataReceived += (_, e) => output.Append(e.Data);
            ps.ErrorDataReceived += (_, e) => err.Append(e.Data);
            ps.Start();
            ps.BeginOutputReadLine();
            ps.WaitForExit(5_000);

            var o = output.ToString();
            var parent = int.TryParse(o.Trim(), out var ppid) ? ppid : InvalidProcessId;

            if (err.ToString() is string error && !error.IsNullOrWhiteSpace())
            {
                EqtTrace.Verbose($"ProcessCodeMethods.GetParentPidMacOs: Error getting parent of process {process.Id} - {process.ProcessName}, {error}.");
            }

            return parent;
        }
        catch (Exception ex)
        {
            EqtTrace.Verbose($"ProcessCodeMethods.GetParentPidMacOs: Error getting parent of process {process.Id} - {process.ProcessName}, {ex}.");
            return InvalidProcessId;
        }
    }

    private static void ResolveChildren(Process parent, List<ProcessTreeNode> acc, int level, int limit)
    {
        if (limit < 0)
        {
            // hit recursion limit, just returning
            return;
        }

        // only take children that are newer than the parent, because process ids (PIDs) get recycled
        var children = acc.Where(p => p.ParentId == parent.Id && p.Process?.StartTime > parent.StartTime).ToList();

        foreach (var child in children)
        {
            child.Level = level;
            ResolveChildren(child.Process!, acc, level + 1, limit);
        }
    }

    private static bool IsChildCandidate(Process child, Process parent)
    {
        // this is extremely slow under debugger, but fast without it
        try
        {
            return child.StartTime > parent.StartTime && child.Id != parent.Id;
        }
        catch
        {
            /* access denied or process has exits most likely */
            return false;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct ProcessBasicInformation
    {
        public readonly IntPtr ExitStatus;
        public readonly IntPtr PebBaseAddress;
        public readonly IntPtr AffinityMask;
        public readonly IntPtr BasePriority;
        public readonly IntPtr UniqueProcessId;
        public IntPtr InheritedFromUniqueProcessId;
    }

    [DllImport("ntdll.dll", SetLastError = true)]
    private static extern int NtQueryInformationProcess(
        IntPtr processHandle,
        int processInformationClass,
        out ProcessBasicInformation processInformation,
        int processInformationLength,
        out int returnLength);

    // This call is undocumented api, and should not be used.
    //[DllImport("ntdll.dll", SetLastError = true)]
    //private static extern IntPtr NtSuspendProcess(IntPtr processHandle);
}