Skip to content
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

Merged
merged 5 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: looks off

Copy link
Member Author

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...

<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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would the above try block throw anything ? Looks like we're only doing a string comparison in there and some tracing and we made sure to check process is not null first.

Copy link
Member Author

Choose a reason for hiding this comment

The 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);
}
}
Loading