Skip to content

Commit

Permalink
Attach to VS automatically (#3197)
Browse files Browse the repository at this point in the history
Add a tool to attach sub-processes to an instance of Visual Studio, so we can run vstest.console via wrapper and get it attached, or similarly run testhost, and get it automatically attached to VS.
  • Loading branch information
nohwnd authored Nov 29, 2021
1 parent d36b7d8 commit 83de0ad
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 3 deletions.
15 changes: 15 additions & 0 deletions TestPlatform.sln
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DumpMinitool", "src\DataCol
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DumpMinitool.x86", "src\DataCollectors\DumpMinitool.x86\DumpMinitool.x86.csproj", "{2C88C923-3D7A-4492-9241-7A489750CAB7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AttachVS", "src\AttachVS\AttachVS.csproj", "{8238A052-D626-49EB-A011-51DC6D0DBA30}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Microsoft.TestPlatform.Execution.Shared\Microsoft.TestPlatform.Execution.Shared.projitems*{10b6ade1-f808-4612-801d-4452f5b52242}*SharedItemsImports = 5
Expand Down Expand Up @@ -821,6 +823,18 @@ Global
{2C88C923-3D7A-4492-9241-7A489750CAB7}.Release|x64.Build.0 = Release|Any CPU
{2C88C923-3D7A-4492-9241-7A489750CAB7}.Release|x86.ActiveCfg = Release|Any CPU
{2C88C923-3D7A-4492-9241-7A489750CAB7}.Release|x86.Build.0 = Release|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|x64.ActiveCfg = Debug|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|x64.Build.0 = Debug|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|x86.ActiveCfg = Debug|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|x86.Build.0 = Debug|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|Any CPU.Build.0 = Release|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x64.ActiveCfg = Release|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x64.Build.0 = Release|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x86.ActiveCfg = Release|Any CPU
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -892,6 +906,7 @@ Global
{7F26EDA3-C8C4-4B7F-A9B6-D278C2F40A13} = {ED0C35EB-7F31-4841-A24F-8EB708FFA959}
{33A20B85-7024-4112-B1E7-00CD0E4A9F96} = {B705537C-B82C-4A30-AFA5-6244D9A7DAEB}
{2C88C923-3D7A-4492-9241-7A489750CAB7} = {B705537C-B82C-4A30-AFA5-6244D9A7DAEB}
{8238A052-D626-49EB-A011-51DC6D0DBA30} = {ED0C35EB-7F31-4841-A24F-8EB708FFA959}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0541B30C-FF51-4E28-B172-83F5F3934BCD}
Expand Down
7 changes: 7 additions & 0 deletions scripts/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ $dependencies = Get-Content -Raw -Encoding UTF8 $dependenciesPath
$updatedDependencies = $dependencies -replace "<NETTestSdkVersion>.*?</NETTestSdkVersion>", "<NETTestSdkVersion>$TPB_Version</NETTestSdkVersion>"
$updatedDependencies | Set-Content -Encoding UTF8 $dependenciesPath -NoNewline

$attachVsPath = "$env:TP_ROOT_DIR\src\AttachVS\bin\Debug\net472"

if ($env:PATH -notlike "*$attachVsPath") {
Write-Log "Adding AttachVS to PATH"
$env:PATH = "$attachVsPath;$env:PATH"
}

function Invoke-Build
{
$timer = Start-Timer
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/TestPlatform.Dependencies.props
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VSSdkBuildToolsVersion>15.8.3247</VSSdkBuildToolsVersion>
Expand Down
16 changes: 16 additions & 0 deletions src/AttachVS/AttachVS.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TestPlatformRoot Condition="$(TestPlatformRoot) == ''">..\..\</TestPlatformRoot>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<Import Project="$(TestPlatformRoot)scripts/build/TestPlatform.Settings.targets" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net472</TargetFrameworks>
<LangVersion>preview</LangVersion>
<AssemblyName>AttachVS</AssemblyName>
</PropertyGroup>

<Import Project="$(TestPlatformRoot)scripts\build\TestPlatform.targets" />
</Project>
309 changes: 309 additions & 0 deletions src/AttachVS/AttachVs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Threading;

namespace Nohwnd.AttachVS
{
internal class DebuggerUtility
{
internal static bool AttachVSToProcess(int? pid, int? vsPid)
{
try
{
Trace($"Starting with pid '{pid}', and vsPid '{vsPid}'");
if (pid == null)
{
Trace($"FAIL: Pid is null.");
return false;
}
var process = Process.GetProcessById(pid.Value);
Trace($"Using pid: {pid} to get parent VS.");
var vs = GetVsFromPid(vsPid != null
? Process.GetProcessById(vsPid.Value)
: Process.GetProcessById(process.Id));

if (vs != null)
{
Trace($"Parent VS is {vs.ProcessName} ({vs.Id}).");
AttachTo(process, vs);
}
else
{
Trace($"Parent VS not found, finding the first VS that started.");
var processes = Process.GetProcesses().Where(p => p.ProcessName == "devenv").Select(p =>
{
try
{
return new { Process = p, StartTime = p.StartTime, HasExited = p.HasExited };
}
catch
{
return null;
}
}).Where(p => p != null && !p.HasExited).OrderBy(p => p.StartTime).ToList();

var firstVs = processes.FirstOrDefault();
Trace($"Found VS {firstVs.Process.Id}");
AttachTo(process, firstVs.Process);
}
return true;
}
catch (Exception ex)
{
Trace($"ERROR: {ex}, {ex.StackTrace}");
return false;
}
}

private static void AttachTo(Process process, Process vs)
{
var attached = AttachVs(vs, process.Id);
if (attached)
{
// You won't see this in DebugView++ because at this point VS is already attached and all the output goes into Debug window in VS.
Trace($"SUCCESS: Attached process: {process.ProcessName} ({process.Id})");
}
else
{
Trace($"FAIL: Could not attach process: {process.ProcessName} ({process.Id})");
}
}

private static bool AttachVs(Process vs, int pid)
{
IBindCtx bindCtx = null;
IRunningObjectTable runninObjectTable = null;
IEnumMoniker enumMoniker = null;
try
{
var r = CreateBindCtx(0, out bindCtx);
Marshal.ThrowExceptionForHR(r);
if (bindCtx == null)
{
Trace($"BindCtx is null. Cannot attach VS.");
return false;
}
bindCtx.GetRunningObjectTable(out runninObjectTable);
if (runninObjectTable == null)
{
Trace($"RunningObjectTable is null. Cannot attach VS.");
return false;
}

runninObjectTable.EnumRunning(out enumMoniker);
if (enumMoniker == null)
{
Trace($"EnumMoniker is null. Cannot attach VS.");
return false;
}

var dteSuffix = ":" + vs.Id;

var moniker = new IMoniker[1];
while (enumMoniker.Next(1, moniker, IntPtr.Zero) == 0 && moniker[0] != null)
{
string dn;

moniker[0].GetDisplayName(bindCtx, null, out dn);

if (dn.StartsWith("!VisualStudio.DTE.") && dn.EndsWith(dteSuffix))
{
object dte, dbg, lps;
runninObjectTable.GetObject(moniker[0], out dte);

for (var i = 0; i < 10; i++)
{
try
{
dbg = dte.GetType().InvokeMember("Debugger", BindingFlags.GetProperty, null, dte, null);
lps = dbg.GetType().InvokeMember("LocalProcesses", BindingFlags.GetProperty, null, dbg, null);
var lpn = (System.Collections.IEnumerator)lps.GetType().InvokeMember("GetEnumerator", BindingFlags.InvokeMethod, null, lps, null);

while (lpn.MoveNext())
{
var pn = Convert.ToInt32(lpn.Current.GetType().InvokeMember("ProcessID", BindingFlags.GetProperty, null, lpn.Current, null));

if (pn == pid)
{
lpn.Current.GetType().InvokeMember("Attach", BindingFlags.InvokeMethod, null, lpn.Current, null);
return true;
}
}
}
catch (COMException ex)
{
Trace($"ComException: Tetrying in 250ms.\n{ex}");
Thread.Sleep(250);
}
}
Marshal.ReleaseComObject(moniker[0]);

break;
}

Marshal.ReleaseComObject(moniker[0]);
}
return false;
}
finally
{
if (enumMoniker != null)
{
try
{
Marshal.ReleaseComObject(enumMoniker);
}
catch { }
}
if (runninObjectTable != null)
{
try
{
Marshal.ReleaseComObject(runninObjectTable);
}
catch { }
}
if (bindCtx != null)
{
try
{
Marshal.ReleaseComObject(bindCtx);
}
catch { }
}
}
}

private static Process GetVsFromPid(Process process)
{
var parent = process;
while (!IsVsOrNull(parent))
{
parent = GetParentProcess(parent);
}

return parent;
}

private static bool IsVsOrNull(Process process)
{
if (process == null)
{
Trace("Parent process is null..");
return true;
}

try
{
var isVs = process.ProcessName.Equals("devenv", StringComparison.InvariantCultureIgnoreCase);
if (isVs)
{
Trace($"Process {process.ProcessName} ({process.Id}) is VS.");
}
else
{
Trace($"Process {process.ProcessName} ({process.Id}) is not VS.");
}

return isVs;
}
catch
{
return true;
}
}

private static bool IsCorrectParent(Process currentProcess, Process parent)
{
try
{
// Parent needs to start before the child, otherwise it might be a different process
// that is just reusing the same PID.
if (parent.StartTime <= currentProcess.StartTime)
{
return true;
}
else
{
Trace($"Process {parent.ProcessName} ({parent.Id}) is not a valid parent because it started after the current process.");
return false;
}

}
catch
{
// Access denied or process exited while we were holding the Process object.
return false;
}
}

private static Process GetParentProcess(Process process)
{
var id = -1;
try
{
var handle = process.Handle;
var res = NtQueryInformationProcess(handle, 0, out var pbi, Marshal.SizeOf<PROCESS_BASIC_INFORMATION>(), out int size);

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

id = p;
}
catch
{
id = -1;
}

Process parent = null;
if (id != -1)
{
try
{
parent = Process.GetProcessById(id);
}
catch
{
// throws when parent no longer runs
}
}

return IsCorrectParent(process, parent) ? parent : null;
}

private static void Trace(string message, [CallerMemberName] string methodName = null)
{
System.Diagnostics.Trace.WriteLine($"{methodName}: {message}");
}

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

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

[DllImport("Kernel32")]
private static extern uint GetTickCount();

[DllImport("ole32.dll")]
private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);
}
}
Loading

0 comments on commit 83de0ad

Please sign in to comment.