Skip to content

Commit 83de0ad

Browse files
authored
Attach to VS automatically (#3197)
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.
1 parent d36b7d8 commit 83de0ad

File tree

11 files changed

+537
-3
lines changed

11 files changed

+537
-3
lines changed

TestPlatform.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DumpMinitool", "src\DataCol
168168
EndProject
169169
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DumpMinitool.x86", "src\DataCollectors\DumpMinitool.x86\DumpMinitool.x86.csproj", "{2C88C923-3D7A-4492-9241-7A489750CAB7}"
170170
EndProject
171+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AttachVS", "src\AttachVS\AttachVS.csproj", "{8238A052-D626-49EB-A011-51DC6D0DBA30}"
172+
EndProject
171173
Global
172174
GlobalSection(SharedMSBuildProjectFiles) = preSolution
173175
src\Microsoft.TestPlatform.Execution.Shared\Microsoft.TestPlatform.Execution.Shared.projitems*{10b6ade1-f808-4612-801d-4452f5b52242}*SharedItemsImports = 5
@@ -821,6 +823,18 @@ Global
821823
{2C88C923-3D7A-4492-9241-7A489750CAB7}.Release|x64.Build.0 = Release|Any CPU
822824
{2C88C923-3D7A-4492-9241-7A489750CAB7}.Release|x86.ActiveCfg = Release|Any CPU
823825
{2C88C923-3D7A-4492-9241-7A489750CAB7}.Release|x86.Build.0 = Release|Any CPU
826+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
827+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|Any CPU.Build.0 = Debug|Any CPU
828+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|x64.ActiveCfg = Debug|Any CPU
829+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|x64.Build.0 = Debug|Any CPU
830+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|x86.ActiveCfg = Debug|Any CPU
831+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Debug|x86.Build.0 = Debug|Any CPU
832+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|Any CPU.ActiveCfg = Release|Any CPU
833+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|Any CPU.Build.0 = Release|Any CPU
834+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x64.ActiveCfg = Release|Any CPU
835+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x64.Build.0 = Release|Any CPU
836+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x86.ActiveCfg = Release|Any CPU
837+
{8238A052-D626-49EB-A011-51DC6D0DBA30}.Release|x86.Build.0 = Release|Any CPU
824838
EndGlobalSection
825839
GlobalSection(SolutionProperties) = preSolution
826840
HideSolutionNode = FALSE
@@ -892,6 +906,7 @@ Global
892906
{7F26EDA3-C8C4-4B7F-A9B6-D278C2F40A13} = {ED0C35EB-7F31-4841-A24F-8EB708FFA959}
893907
{33A20B85-7024-4112-B1E7-00CD0E4A9F96} = {B705537C-B82C-4A30-AFA5-6244D9A7DAEB}
894908
{2C88C923-3D7A-4492-9241-7A489750CAB7} = {B705537C-B82C-4A30-AFA5-6244D9A7DAEB}
909+
{8238A052-D626-49EB-A011-51DC6D0DBA30} = {ED0C35EB-7F31-4841-A24F-8EB708FFA959}
895910
EndGlobalSection
896911
GlobalSection(ExtensibilityGlobals) = postSolution
897912
SolutionGuid = {0541B30C-FF51-4E28-B172-83F5F3934BCD}

scripts/build.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ $dependencies = Get-Content -Raw -Encoding UTF8 $dependenciesPath
113113
$updatedDependencies = $dependencies -replace "<NETTestSdkVersion>.*?</NETTestSdkVersion>", "<NETTestSdkVersion>$TPB_Version</NETTestSdkVersion>"
114114
$updatedDependencies | Set-Content -Encoding UTF8 $dependenciesPath -NoNewline
115115

116+
$attachVsPath = "$env:TP_ROOT_DIR\src\AttachVS\bin\Debug\net472"
117+
118+
if ($env:PATH -notlike "*$attachVsPath") {
119+
Write-Log "Adding AttachVS to PATH"
120+
$env:PATH = "$attachVsPath;$env:PATH"
121+
}
122+
116123
function Invoke-Build
117124
{
118125
$timer = Start-Timer

scripts/build/TestPlatform.Dependencies.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
33
<PropertyGroup>
44
<VSSdkBuildToolsVersion>15.8.3247</VSSdkBuildToolsVersion>

src/AttachVS/AttachVS.csproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TestPlatformRoot Condition="$(TestPlatformRoot) == ''">..\..\</TestPlatformRoot>
4+
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
5+
</PropertyGroup>
6+
<Import Project="$(TestPlatformRoot)scripts/build/TestPlatform.Settings.targets" />
7+
8+
<PropertyGroup>
9+
<OutputType>Exe</OutputType>
10+
<TargetFrameworks>net472</TargetFrameworks>
11+
<LangVersion>preview</LangVersion>
12+
<AssemblyName>AttachVS</AssemblyName>
13+
</PropertyGroup>
14+
15+
<Import Project="$(TestPlatformRoot)scripts\build\TestPlatform.targets" />
16+
</Project>

src/AttachVS/AttachVs.cs

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using System.Runtime.InteropServices.ComTypes;
9+
using System.Threading;
10+
11+
namespace Nohwnd.AttachVS
12+
{
13+
internal class DebuggerUtility
14+
{
15+
internal static bool AttachVSToProcess(int? pid, int? vsPid)
16+
{
17+
try
18+
{
19+
Trace($"Starting with pid '{pid}', and vsPid '{vsPid}'");
20+
if (pid == null)
21+
{
22+
Trace($"FAIL: Pid is null.");
23+
return false;
24+
}
25+
var process = Process.GetProcessById(pid.Value);
26+
Trace($"Using pid: {pid} to get parent VS.");
27+
var vs = GetVsFromPid(vsPid != null
28+
? Process.GetProcessById(vsPid.Value)
29+
: Process.GetProcessById(process.Id));
30+
31+
if (vs != null)
32+
{
33+
Trace($"Parent VS is {vs.ProcessName} ({vs.Id}).");
34+
AttachTo(process, vs);
35+
}
36+
else
37+
{
38+
Trace($"Parent VS not found, finding the first VS that started.");
39+
var processes = Process.GetProcesses().Where(p => p.ProcessName == "devenv").Select(p =>
40+
{
41+
try
42+
{
43+
return new { Process = p, StartTime = p.StartTime, HasExited = p.HasExited };
44+
}
45+
catch
46+
{
47+
return null;
48+
}
49+
}).Where(p => p != null && !p.HasExited).OrderBy(p => p.StartTime).ToList();
50+
51+
var firstVs = processes.FirstOrDefault();
52+
Trace($"Found VS {firstVs.Process.Id}");
53+
AttachTo(process, firstVs.Process);
54+
}
55+
return true;
56+
}
57+
catch (Exception ex)
58+
{
59+
Trace($"ERROR: {ex}, {ex.StackTrace}");
60+
return false;
61+
}
62+
}
63+
64+
private static void AttachTo(Process process, Process vs)
65+
{
66+
var attached = AttachVs(vs, process.Id);
67+
if (attached)
68+
{
69+
// 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.
70+
Trace($"SUCCESS: Attached process: {process.ProcessName} ({process.Id})");
71+
}
72+
else
73+
{
74+
Trace($"FAIL: Could not attach process: {process.ProcessName} ({process.Id})");
75+
}
76+
}
77+
78+
private static bool AttachVs(Process vs, int pid)
79+
{
80+
IBindCtx bindCtx = null;
81+
IRunningObjectTable runninObjectTable = null;
82+
IEnumMoniker enumMoniker = null;
83+
try
84+
{
85+
var r = CreateBindCtx(0, out bindCtx);
86+
Marshal.ThrowExceptionForHR(r);
87+
if (bindCtx == null)
88+
{
89+
Trace($"BindCtx is null. Cannot attach VS.");
90+
return false;
91+
}
92+
bindCtx.GetRunningObjectTable(out runninObjectTable);
93+
if (runninObjectTable == null)
94+
{
95+
Trace($"RunningObjectTable is null. Cannot attach VS.");
96+
return false;
97+
}
98+
99+
runninObjectTable.EnumRunning(out enumMoniker);
100+
if (enumMoniker == null)
101+
{
102+
Trace($"EnumMoniker is null. Cannot attach VS.");
103+
return false;
104+
}
105+
106+
var dteSuffix = ":" + vs.Id;
107+
108+
var moniker = new IMoniker[1];
109+
while (enumMoniker.Next(1, moniker, IntPtr.Zero) == 0 && moniker[0] != null)
110+
{
111+
string dn;
112+
113+
moniker[0].GetDisplayName(bindCtx, null, out dn);
114+
115+
if (dn.StartsWith("!VisualStudio.DTE.") && dn.EndsWith(dteSuffix))
116+
{
117+
object dte, dbg, lps;
118+
runninObjectTable.GetObject(moniker[0], out dte);
119+
120+
for (var i = 0; i < 10; i++)
121+
{
122+
try
123+
{
124+
dbg = dte.GetType().InvokeMember("Debugger", BindingFlags.GetProperty, null, dte, null);
125+
lps = dbg.GetType().InvokeMember("LocalProcesses", BindingFlags.GetProperty, null, dbg, null);
126+
var lpn = (System.Collections.IEnumerator)lps.GetType().InvokeMember("GetEnumerator", BindingFlags.InvokeMethod, null, lps, null);
127+
128+
while (lpn.MoveNext())
129+
{
130+
var pn = Convert.ToInt32(lpn.Current.GetType().InvokeMember("ProcessID", BindingFlags.GetProperty, null, lpn.Current, null));
131+
132+
if (pn == pid)
133+
{
134+
lpn.Current.GetType().InvokeMember("Attach", BindingFlags.InvokeMethod, null, lpn.Current, null);
135+
return true;
136+
}
137+
}
138+
}
139+
catch (COMException ex)
140+
{
141+
Trace($"ComException: Tetrying in 250ms.\n{ex}");
142+
Thread.Sleep(250);
143+
}
144+
}
145+
Marshal.ReleaseComObject(moniker[0]);
146+
147+
break;
148+
}
149+
150+
Marshal.ReleaseComObject(moniker[0]);
151+
}
152+
return false;
153+
}
154+
finally
155+
{
156+
if (enumMoniker != null)
157+
{
158+
try
159+
{
160+
Marshal.ReleaseComObject(enumMoniker);
161+
}
162+
catch { }
163+
}
164+
if (runninObjectTable != null)
165+
{
166+
try
167+
{
168+
Marshal.ReleaseComObject(runninObjectTable);
169+
}
170+
catch { }
171+
}
172+
if (bindCtx != null)
173+
{
174+
try
175+
{
176+
Marshal.ReleaseComObject(bindCtx);
177+
}
178+
catch { }
179+
}
180+
}
181+
}
182+
183+
private static Process GetVsFromPid(Process process)
184+
{
185+
var parent = process;
186+
while (!IsVsOrNull(parent))
187+
{
188+
parent = GetParentProcess(parent);
189+
}
190+
191+
return parent;
192+
}
193+
194+
private static bool IsVsOrNull(Process process)
195+
{
196+
if (process == null)
197+
{
198+
Trace("Parent process is null..");
199+
return true;
200+
}
201+
202+
try
203+
{
204+
var isVs = process.ProcessName.Equals("devenv", StringComparison.InvariantCultureIgnoreCase);
205+
if (isVs)
206+
{
207+
Trace($"Process {process.ProcessName} ({process.Id}) is VS.");
208+
}
209+
else
210+
{
211+
Trace($"Process {process.ProcessName} ({process.Id}) is not VS.");
212+
}
213+
214+
return isVs;
215+
}
216+
catch
217+
{
218+
return true;
219+
}
220+
}
221+
222+
private static bool IsCorrectParent(Process currentProcess, Process parent)
223+
{
224+
try
225+
{
226+
// Parent needs to start before the child, otherwise it might be a different process
227+
// that is just reusing the same PID.
228+
if (parent.StartTime <= currentProcess.StartTime)
229+
{
230+
return true;
231+
}
232+
else
233+
{
234+
Trace($"Process {parent.ProcessName} ({parent.Id}) is not a valid parent because it started after the current process.");
235+
return false;
236+
}
237+
238+
}
239+
catch
240+
{
241+
// Access denied or process exited while we were holding the Process object.
242+
return false;
243+
}
244+
}
245+
246+
private static Process GetParentProcess(Process process)
247+
{
248+
var id = -1;
249+
try
250+
{
251+
var handle = process.Handle;
252+
var res = NtQueryInformationProcess(handle, 0, out var pbi, Marshal.SizeOf<PROCESS_BASIC_INFORMATION>(), out int size);
253+
254+
var p = res != 0 ? -1 : pbi.InheritedFromUniqueProcessId.ToInt32();
255+
256+
id = p;
257+
}
258+
catch
259+
{
260+
id = -1;
261+
}
262+
263+
Process parent = null;
264+
if (id != -1)
265+
{
266+
try
267+
{
268+
parent = Process.GetProcessById(id);
269+
}
270+
catch
271+
{
272+
// throws when parent no longer runs
273+
}
274+
}
275+
276+
return IsCorrectParent(process, parent) ? parent : null;
277+
}
278+
279+
private static void Trace(string message, [CallerMemberName] string methodName = null)
280+
{
281+
System.Diagnostics.Trace.WriteLine($"{methodName}: {message}");
282+
}
283+
284+
[StructLayout(LayoutKind.Sequential)]
285+
private struct PROCESS_BASIC_INFORMATION
286+
{
287+
public IntPtr ExitStatus;
288+
public IntPtr PebBaseAddress;
289+
public IntPtr AffinityMask;
290+
public IntPtr BasePriority;
291+
public IntPtr UniqueProcessId;
292+
public IntPtr InheritedFromUniqueProcessId;
293+
}
294+
295+
[DllImport("ntdll.dll", SetLastError = true)]
296+
private static extern int NtQueryInformationProcess(
297+
IntPtr processHandle,
298+
int processInformationClass,
299+
out PROCESS_BASIC_INFORMATION processInformation,
300+
int processInformationLength,
301+
out int returnLength);
302+
303+
[DllImport("Kernel32")]
304+
private static extern uint GetTickCount();
305+
306+
[DllImport("ole32.dll")]
307+
private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);
308+
}
309+
}

0 commit comments

Comments
 (0)