-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Attach to VS automatically #3197
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> |
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 | ||
nohwnd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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 | ||
nohwnd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
? 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++) | ||
nohwnd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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}"); | ||
nohwnd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would the above There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right. It's probably pointless now. |
||
{ | ||
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 | ||
nohwnd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: looks off
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure why, but lately pressing format in .csproj totally wrecks the file...