File: src\vstest\src\Microsoft.TestPlatform.Execution.Shared\DebuggerBreakpoint.cs
Web Access
Project: src\src\vstest\src\testhost\testhost.csproj (testhost)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if USE_EXTERN_ALIAS
extern alias Abstraction;
#endif

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

#if USE_EXTERN_ALIAS
using Abstraction::Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
#else
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
#endif
using Microsoft.VisualStudio.TestPlatform.Utilities;

namespace Microsoft.VisualStudio.TestPlatform.Execution;

[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0030:Do not used banned APIs", Justification = "StringUtils is not available for all TFMs of testhost")]
internal static class DebuggerBreakpoint
{
    internal static void AttachVisualStudioDebugger(string environmentVariable)
    {
        if (string.IsNullOrWhiteSpace(environmentVariable))
        {
            throw new ArgumentException($"'{nameof(environmentVariable)}' cannot be null or whitespace.", nameof(environmentVariable));
        }

        if (Debugger.IsAttached)
            return;

        var debugEnabled = Environment.GetEnvironmentVariable(environmentVariable);
        if (!string.IsNullOrEmpty(debugEnabled) && !debugEnabled.Equals("0", StringComparison.Ordinal))
        {
            int? vsPid = null;
            if (int.TryParse(debugEnabled, out int pid))
            {
                // The option is used to both enable and disable attaching (0 and 1)
                // and providing custom vs pid (any number higher than 1)
                vsPid = pid <= 1 ? null : (int?)pid;
            }

            if (vsPid == null)
            {
                ConsoleOutput.Instance.WriteLine("Attaching Visual Studio, either a parent or the one that was started first... To specify a VS instance to use, use the PID in the option, instead of 1.", OutputLevel.Information);
            }
            else
            {
                var processId =
#if NET
                    Environment.ProcessId;
#else
                    Process.GetCurrentProcess().Id;
#endif

                ConsoleOutput.Instance.WriteLine($"Attaching Visual Studio with PID {vsPid} to the process '{Process.GetCurrentProcess().ProcessName}({processId})'...", OutputLevel.Information);
            }

            AttachVs(Process.GetCurrentProcess(), vsPid);

            Break();
        }
    }

    private static bool AttachVs(Process process, int? vsPid)
    {
        // The way we attach VS is not compatible with .NET Core 2.1 and .NET Core 3.1, but works in .NET Framework and .NET.
        // We could call the library code directly here for .NET, and .NET Framework, but then we would also need to package it
        // together with testhost. So instead we always run the executable, and pass path to it using env variable.

        const string env = "VSTEST_DEBUG_ATTACHVS_PATH";
        var vsAttachPath = Environment.GetEnvironmentVariable(env) ?? FindAttachVs();

        // Always set it so we propagate it to child processes even if it was not previously set.
        Environment.SetEnvironmentVariable(env, vsAttachPath);

        if (vsAttachPath == null)
        {
            throw new InvalidOperationException($"Cannot find AttachVS.exe tool.");
        }

        if (!File.Exists(vsAttachPath))
        {
            throw new InvalidOperationException($"Cannot start tool, path {vsAttachPath} does not exist.");
        }
        var attachVsProcess = Process.Start(vsAttachPath, $"{process.Id} {vsPid}");
        attachVsProcess.WaitForExit();

        return attachVsProcess.ExitCode == 0;
    }

    private static string? FindAttachVs()
    {
        return FindOnPath("AttachVS.exe");
    }

    private static string? FindOnPath(string exeName)
    {
        // TODO: Skip when PATH is not defined.
        var paths = Environment.GetEnvironmentVariable("PATH")!.Split(';');
        foreach (var p in paths)
        {
            var path = Path.Combine(p, exeName);
            if (File.Exists(path))
            {
                return path;
            }
        }

        return null;
    }

    internal static void WaitForDebugger(string environmentVariable)
    {
        if (Debugger.IsAttached)
            return;

        if (string.IsNullOrWhiteSpace(environmentVariable))
        {
            throw new ArgumentException($"'{nameof(environmentVariable)}' cannot be null or whitespace.", nameof(environmentVariable));
        }

        var debugEnabled = Environment.GetEnvironmentVariable(environmentVariable);
        if (!string.IsNullOrEmpty(debugEnabled) && debugEnabled.Equals("1", StringComparison.Ordinal))
        {
            ConsoleOutput.Instance.WriteLine("Waiting for debugger attach...", OutputLevel.Information);

            var currentProcess = Process.GetCurrentProcess();
            ConsoleOutput.Instance.WriteLine(
                $"Process Id: {currentProcess.Id}, Name: {currentProcess.ProcessName}",
                OutputLevel.Information);

            while (!Debugger.IsAttached)
            {
                Task.Delay(1000).GetAwaiter().GetResult();
            }

            Break();
        }
    }

    internal static void WaitForNativeDebugger(string environmentVariable)
    {
        if (string.IsNullOrWhiteSpace(environmentVariable))
        {
            throw new ArgumentException($"'{nameof(environmentVariable)}' cannot be null or whitespace.", nameof(environmentVariable));
        }

        // Check if native debugging is enabled and OS is windows.
        var nativeDebugEnabled = Environment.GetEnvironmentVariable(environmentVariable);

        if (!string.IsNullOrEmpty(nativeDebugEnabled) && nativeDebugEnabled.Equals("1", StringComparison.Ordinal)
                                                      && new PlatformEnvironment().OperatingSystem.Equals(PlatformOperatingSystem.Windows))
        {
            while (!IsDebuggerPresent())
            {
                Task.Delay(1000).Wait();
            }

            BreakNative();
        }
    }

    private static void Break()
    {
        if (ShouldNotBreak())
        {
            return;
        }

        Debugger.Break();
    }

    private static bool ShouldNotBreak()
    {
        return Environment.GetEnvironmentVariable("VSTEST_DEBUG_NOBP")?.Equals("1") ?? false;
    }

    private static void BreakNative()
    {
        if (ShouldNotBreak())
        {
            return;
        }

        DebugBreak();
    }

    // Native APIs for enabling native debugging.
    [DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool IsDebuggerPresent();

    [DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)]
    internal static extern void DebugBreak();
}