| File: src\vstest\src\Microsoft.TestPlatform.Execution.Shared\DebuggerBreakpoint.cs | Web Access |
| Project: 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(); }