-
Notifications
You must be signed in to change notification settings - Fork 325
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
11 changed files
with
537 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.