带参数的 Visual Studio 加载器





5.00/5 (6投票s)
带参数的 Visual Studio 加载器
引言
您是否曾有过从一段代码启动 Visual Studio 的需求,而您需要调试的代码(即您首次打开 Visual Studio 实例的原因)需要命令行参数。而目前的情况是,devenv.exe 不允许您将参数传递给您的调试进程。这就是我工作中的麻烦的开始。
我们需要一种解决方案来加载 Visual Studio,但需要提供参数。否则,我们就无法轻松调试进程,除非我们也从外部运行调试来查找当前参数,然后手动将其放入另一个解决方案中。
解决方案路径
为了找到一种可以接收参数的方法,有几种选择是可能的:
- 将它们放入注册表中并读出,这意味着每个调试项目都需要自己的设置,并且总是需要为每个项目进行额外的工作。
- 将其放入project.user文件中,该文件实际上提供了一种将参数传递给项目的方法。这种方法的一个问题是,如果解决方案中有多个可能的启动对象,几乎不可能知道使用哪一个,除非您解开suo文件的神秘面纱。
- 找到一种方法来以任何方式告诉调试项目一个数字。
AppDomainManager vshost 样式
当我在调试中寻找是否能找到任何漏洞时,我们注意到通过反射,如果通过 vshost 加载,应用程序域管理器将拥有它所控制的 Visual Studio 的 ID。
这让我想尝试看看是否能在 devenv
本身的启动命令行中放置任何内容。事实证明,在 /run solution/project 之后,参数被忽略了。至少据我所能判断。这给了我需要的空间来向调试进程伪造一些内容。
WindowBridge
由于几个原因:
- 不缩短命令行允许的字符数
- 以免添加的参数部分被 Visual Studio 捕获
我决定使用一个中间件。我创建了 WindowBridge
,一个独立的应用程序,它只有一个目的:传递参数,然后释放它们。WindowBridge
工作在一个不显示的窗口中,该窗口接受参数并为其提供一个 ID 号。
三驾马车
为了让窗口桥接更容易使用,我想要一个帮助项目来放置参数行并接收其 ID,以及一个帮助项目来确定它需要从窗口桥接加载参数行,并将其提供得尽可能接近正常的命令行。
放置参数行的帮助项目被赋予了次要目的:帮助轻松启动 Visual Studio 环境。通过解决方案文件中的版本号,它决定使用哪个版本的 Visual Studio(对于 Visual Studio 2017 和 2019,这是一个有根据的猜测,仅仅因为我当时没有安装这两者)。这个原理使用信息来加载它所针对的 Visual Studio,或者如果它不存在,则检测是否有指定的最低 Visual Studio 版本,然后从新到旧搜索是否存在任何兼容版本并启动它。此外,启动例程还可以选择等待 Visual Studio 结束,在这种情况下,它将使用 /runexit
启动 devenv
。结果是,当调试进程停止时,Visual Studio 环境将关闭,启动进程就知道它已经结束了。
工作原理
在本节中,我将重点介绍代码部分并解释它们为什么是这样的。
项目 WindowBridge
using System;
using System.Windows.Forms;
namespace WindowBridge
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (WindowBridgeFinder.GetWindowBridge() == IntPtr.Zero)//isolate our version
{
using (Bridge OurBridge = new Bridge())
{
Application.Run();
}
}
}
}
}
public class Bridge : NativeWindow, IDisposable
{
...
private List<uint> UniqueId = new List<uint>();
private List<KeyValuePair<uint, StringThingy>> Message =
new List<KeyValuePair<uint, StringThingy>>();
}
我将在下面解释的 WindowBridgeFinder
可以防止我们运行多个实例。通过在应用程序运行的 using
块之外运行 Bridge,当窗口接收到 WM_DESTROY
窗口消息时,将发生 dispose
。
通过将 Bridge 制作成 NativeWindow
,您可以轻松地创建一个监听窗口,而不会像 WindowForm
那样出现过载。UniqueId
列表跟踪已发出的 ID。Message
列表跟踪哪些参数属于哪个 uniqueid
。
如果您查看 Bridge
的窗口过程,您会发现在消息总线上没有使用指针。字符是一个接一个地通信的,这是为了避免进程间内存的问题。由于窗口消息的顺序不保证,字符是按位置通信的。
WindowBridgefinder 例程,是所有 3 个项目的一部分
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
using static WindowBridge.WindowMessages;
namespace WindowBridge
{
/// <summary>
/// Finds the WindowBridge
/// </summary>
internal static class WindowBridgeFinder
{
private delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool EnumThreadWindows
(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParm);
[DllImport("user32.dll", EntryPoint = "GetClassName", CharSet = CharSet.Auto)]
private static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, EntryPoint = "SendMessage")]
private static extern IntPtr SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
private const int WM_USER = 0x400;
/// <summary>
/// retrieves all window(top level) handles of a process
/// </summary>
/// <param name="ToRetreiveItFor">The process to get this information for</param>
/// <returns>The found window handles</returns>
private static List<IntPtr> GetProcessWindowHandles(Process ToRetreiveItFor)
{
List<IntPtr> handles = new List<IntPtr>();
foreach (ProcessThread thread in ToRetreiveItFor.Threads)
{
EnumThreadWindows(thread.Id, (hWnd, lParam) =>
{
handles.Add(hWnd);
return true;
}, IntPtr.Zero);
}
return handles;
}
/// <summary>
/// Retrieves the class name of a window
/// </summary>
/// <param name="Handle">The hWnd to retrieve the class name for</param>
/// <returns>The retrieved classname</returns>
private static string GetClassName(IntPtr Handle)
{
string Result = null;
int WantedSize = 1000;
StringBuilder touse = new StringBuilder("", WantedSize + 5);
int Returned = GetClassName(Handle, touse, WantedSize + 2);
if (Returned > 0)
{
Result = touse.ToString();
}
return Result;
}
/// <summary>
/// Retrieve the hWnd of the WindowBridge
/// </summary>
/// <returns>The hWnd on succes, IntPtr.Zero otherwise</returns>
public static IntPtr GetWindowBridge()
{
IntPtr Result = IntPtr.Zero;
Process[] Listing = Process.GetProcesses();
foreach (Process Current in Listing)
{
if (Current.ProcessName.StartsWith("WindowBridge"))
{
List<IntPtr> Handles = GetProcessWindowHandles(Current);
foreach (IntPtr ToCheck in Handles)
{
string TheName = GetClassName(ToCheck);
bool StartsWith = false;
if (!ReferenceEquals(TheName, null))
{
StartsWith = TheName.StartsWith("WindowsForms");
}
if (StartsWith && TheName.Contains(WindowBridgeClassPart))
{
Result = ToCheck;
break;
}
}
}
if (Result != IntPtr.Zero)
{
break;
}
}
return Result;
}
}
}
GetWindowBridge
函数有点棘手。我们需要找到 WindowBridge
的进程,然后找到它的监听窗口。我们首先检索所有进程。在进程中,我们寻找以 WindowBridge
开头的进程,然后检索该进程的顶级窗口句柄,并检查其中是否有符合我们预期的窗口(类名以 WindowsForms
开头并包含单词 message,因为我们基于 message
类)。如果所有要求都已找到,我们就停止搜索过程,并且知道 WindowBridge
正在运行,以及使用哪个窗口句柄与之通信。
Visual Studio 加载器
public static bool RunDevEnv(string SolutionFile, string Arguments, bool WaitForExit){}
Visual Studio 加载器包含一个公开函数 RunDevEnv
,其参数 Arguments
和 WaitForExit
通过重载设为可选。
solutionfile
参数包含您要加载的解决方案。
arguments 参数包含您要发送的命令行。请注意,没有参数也是一个参数。在调试进程中的用法中,参数可以在项目属性中设置。这部分原因也是因为会通信空命令行。
WaitForExit
参数设置是否等待要调试的进程的结束。
返回值告诉 Visual Studio 环境的执行是否成功。即使 WindowBridge
未加载,也会尝试执行。在这种情况下,将无法在调试进程端检索命令行。
public class Loader
{
...
/// <summary>
/// Loads all info about visual studio's present on this platform
/// </summary>
static Loader()
{
...
StudioVersionInfo = new Dictionary<int, StudioVersionDescriptor>()
{
{ 8, new StudioVersionDescriptor("Version_9.00", "",
"# Visual Studio 2005")}, //<-- just a gamble this 1 could be off!
{ 9, new StudioVersionDescriptor("Version_10.00", "",
"# Visual Studio 2008")},
{ 10, new StudioVersionDescriptor("Version_11.00", "",
"# Visual Studio 2010")},
{ 11, new StudioVersionDescriptor("Version_12.00",
"StudioVersion_11", "# Visual Studio 12")},
{ 12, new StudioVersionDescriptor("Version_12.00",
"StudioVersion_12", "# Visual Studio 13")},
{ 14, new StudioVersionDescriptor("Version_12.00",
"StudioVersion_14", "# Visual Studio 15")},
{ 15, new StudioVersionDescriptor("Version_12.00",
"StudioVersion_15", "# Visual Studio 17")},
{ 16, new StudioVersionDescriptor("Version_12.00",
"StudioVersion_16", "# Visual Studio 19")},
{ 17, new StudioVersionDescriptor("Version_12.00",
"StudioVersion_17", "# Visual Studio 21")}
};
KnownRegistryLocations = new string[][]
{
new string[]
{
@"CLSID\{FE10D39B-A7F1-412c-83BA-D00788532ABB}\LocalServer32",
@"Wow6432Node\CLSID\{1B2EEDD6-C203-4d04-BD59-78906E3E8AAB}\LocalServer32",
@"Wow6432Node\CLSID\{BA018599-1DB3-44f9-83B4-461454C84BF8}\LocalServer32",
} ,
new string[]
{
@"VisualStudio.accessor.9.0\shell\Open\Command",
@"CLSID\{1BD51F8C-8CFC-4708-A88D-5690DE4D5C16}\LocalServer32",
@"Wow6432Node\CLSID\{1A5AC6AE-7B95-478C-B422-0E994FD727D6}\LocalServer32",
@"Wow6432Node\CLSID\{8B10A141-87EE-4A0F-823F-D79F5FF7B10A}\LocalServer32",
} ,
new string[]
{
@"VisualStudio.accessor.10.0\shell\Open\Command",
@"VisualStudio.sln.10.0\shell\Open\command",
@"Wow6432Node\CLSID\{656D8328-93F5-41a7-A48C-B42858161F25}\LocalServer32",
@"Wow6432Node\CLSID\{68681A5C-C22A-421d-B68B-5BA9D01F35C5}\LocalServer32",
@"Wow6432Node\CLSID\{6F5BF5E0-D729-46dd-891C-167FE3851574}\LocalServer32",
} ,
new string[]
{
@"VisualStudio.accessor.11.0\shell\Open\Command",
@"VisualStudio.sln.11.0\shell\Open\command",
@"Wow6432Node\CLSID\{059618E6-4639-4D1A-A248-1384E368D5C3}\LocalServer32",
@"Wow6432Node\CLSID\{7751A556-096C-44B5-B60D-4CC78885F0E5}\LocalServer32",
@"Wow6432Node\CLSID\{EB1425FE-3641-47AB-9484-32B62FC8B0B0}\LocalServer32",
} ,
new string[]
{
@"VisualStudio.accessor.12.0\shell\Open\Command",
@"VisualStudio.sln.12.0\shell\Open\command",
@"Wow6432Node\CLSID\{02CD4067-3D8F-4F9E-957F-F273804560C5}\LocalServer32",
@"Wow6432Node\CLSID\{3C0D7ACB-790B-4437-8DD2-815CA17C474D}\LocalServer32",
@"Wow6432Node\CLSID\{48AE9D34-2FE7-48A7-9D8A-A65534E3C20C}\LocalServer32",
} ,
new string[]
{
@"VisualStudio.accessor.14.0\shell\Open\Command",
@"VisualStudio.sln.14.0\shell\Open\command",
@"Wow6432Node\CLSID\{31F45B04-7198-45ED-A13F-F224A4A1686A}\LocalServer32",
@"Wow6432Node\CLSID\{A2FA2136-EB44-4D10-A1D3-6FE1D63A7C05}\LocalServer32",
@"Wow6432Node\CLSID\{CACE29C3-10A7-4B66-A8CA-82C1ECEC1FA3}\LocalServer32",
} ,
new string[]
{
@"VisualStudio.accessor.X.0\shell\Open\Command",
@"VisualStudio.sln.X.0\shell\Open\command",
}
};
#endregion InitMemory
for (int Counter = 0; Counter <= Studio20XX; Counter++)
{
bool Alter = (Counter == Studio20XX);
int Loop = Alter ? 3 : 1;
for (int ExtraLoop = 0; ExtraLoop < Loop; ExtraLoop++)//dirty trick
//for finding 'guesses'
{
string[] Use = KnownRegistryLocations[Counter];
if (Alter)
{
string[] ToCopy = KnownRegistryLocations[Counter];
Use = new string[ToCopy.Length];
for (int SubCounter = 0; SubCounter < ToCopy.Length; SubCounter++)
{
Use[SubCounter] = ToCopy[SubCounter];
}
for (int ReplaceCounter = 0; ReplaceCounter < Use.Length; ReplaceCounter++)
{
Use[ReplaceCounter] =
Use[ReplaceCounter].Replace(".X.", "." + (15 + ExtraLoop).ToString() + ".");
}
}
Discover(Use, (string ToCheck) =>
{
bool Found = false;
FileVersionInfo Info =
FileVersionInfo.GetVersionInfo(ToCheck); //We want the user
//to get bothered with no access dialog if an error occur so don't try catch it.
if (!ReferenceEquals(Info, null))
{
StudioVersionDescriptor BelongsTo;
if (StudioVersionInfo.TryGetValue(Info.FileMajorPart, out BelongsTo))
{
BelongsTo.Location = ToCheck;
FoundDevelopers.Add(Info.FileMajorPart, BelongsTo);
Found = true;
}
else
{
Debug.WriteLine(ToCheck + "
contains an unknown version if Visual Studio,
this program might be out of date.");
}
}
return Found;
});
}
}
if (!ReferenceEquals(FoundDevelopers, null) && (FoundDevelopers.Count > 0))
{
Dictionary<int, StudioVersionDescriptor>.ValueCollection Values =
FoundDevelopers.Values;
StudioVersionDescriptor[] Temp = new StudioVersionDescriptor[Values.Count];
Values.CopyTo(Temp, 0);
LastKnownStudioVersion = Temp[Temp.Length - 1];
}
}
...
}
...
}
在 Loader
类的静态构造函数中,准备了很多内容来选择正确的 Visual Studio 版本。StudioVersionInfo
包含已知的版本 ID 以及已知的解决方案文件标签(2017 年和 2019 年是猜测的)。KnownRegistryLocations
包含其名称所暗示的内容。我们知道 Visual Studio 使用的注册表键。通过这些信息,我们有可能确定几个版本的当前安装位置,同样 2017 年和 2019 年是猜测的。我只使用local_machine/classroot键,这样我们就不会受到其他用户安装但该用户尚未使用的未存在数据的困扰。
LastKnownStudioVersion
将包含我们找到的最高 Visual Studio 版本。如果出于某种原因无法确定要使用的版本,将使用此版本。
internal static class ProcessSln
{
public static bool Process(string solutionFile, out string Version,
out string SubVersion, out string MinimumVisualStudioVersion, out string[] Projects)
{...}
}
ProcessSln
类包含读取 solutionfile
内容并从中检索所需信息的实际逻辑。它将检索 version
、subversion
和 MinimumVisualStudioVersion
。它还返回找到的项目,但在此时此项目中,这不再被使用。(我实际上是为我们 Trac 上的一个buildnumber更新例程编写了这个类)。
当提供项目文件而不是解决方案文件时,Loader.cs 将尝试查找项目所属的解决方案文件,以便通过这种方式确定所需的 Visual Studio。它将搜索项目文件的目录及其上一级目录。
CommandLineLoader
commandline
加载器负责确定使用哪个 commandline
,并以单行方式提供给用户。为了确定使用哪个 commandline
,它首先必须确定我们是否在 Visual Studio 调试之下。
/// <summary>
/// Helps in retrieving the command line from normal route or Window Bridge route
/// </summary>
public static class CommandLine
{
...
private static readonly BindingFlags GetAll;
private static readonly int DevEnvId;
static CommandLine()
{
GetAll = BindingFlags.CreateInstance |
BindingFlags.FlattenHierarchy |
BindingFlags.GetField |
BindingFlags.Instance |
BindingFlags.InvokeMethod |
BindingFlags.NonPublic |
BindingFlags.Public |
BindingFlags.SetField |
BindingFlags.GetProperty |
BindingFlags.SetProperty |
BindingFlags.Static;
DevEnvId = GetDevenv();
}
/// <summary>
/// Determines if we've a visual studio whom has loaded us
/// </summary>
/// <returns>It's processid if any, 0 otherwise</returns>
private static int GetDevenv()
{
int DevenvId = 0;
AppDomain Current = AppDomain.CurrentDomain;
AppDomainManager Manager = null;
FieldInfo m_hpListenerField = null;
object m_hpListener = null;
Process Process = null;
FieldInfo m_procVSField = null;
if (!ReferenceEquals(Current, null))
{
Manager = Current.DomainManager;
}
if (!ReferenceEquals(Manager, null))
{
m_hpListenerField = (Manager.GetType().GetField("m_hpListener", GetAll));
}
if (!ReferenceEquals(m_hpListenerField, null))
{
m_hpListener = m_hpListenerField.GetValue(Manager);
}
if (!ReferenceEquals(m_hpListener, null))
{
m_procVSField = (m_hpListener.GetType()).GetField("m_procVS", GetAll);
}
if (!ReferenceEquals(m_procVSField, null))
{
Process = m_procVSField.GetValue(m_hpListener) as Process;
}
if (!ReferenceEquals(Process, null))
{
DevenvId = Process.Id;
}
return DevenvId;
}
...
}
GetDevEnv
是确定 Visual Studio 是否存在的函数。它通过尝试从特殊的应用程序域管理器检索其进程 ID 来做到这一点,该管理器在通过 vshost 加载时存在。我已经在 2005、2008、2010、2012、2013、2015 版本下测试了此路线。仅在 2008 版本下,我发现此路线不起作用,但可能我需要先保存并编译该项目。只有当所有步骤都成功完成并且我们获得了负责调试我们的进程的 Visual Studio 环境的进程 ID 时,我们才会说我们找到了它。
/// <summary>
/// Retrieves the arguments with aid from the visual studio commandline
/// to get the id needed to do so
/// </summary>
/// <param name="DevEnvProcessId">Id to the process of our visual studio instance.</param>
/// <param name="WithExecutable">
/// Do we need to prefix the commandline with or without executable
/// on the line (gui or console)</param>
/// <param name="HadId">Did we find any arguments</param>
/// <returns>The found command line to use</returns>
private static string GetDevEnvArguments
(int DevEnvProcessId, bool WithExecutable, ref bool HadId)
{
string Result = string.Empty;
ManagementObjectSearcher commandlineSearcher =
new ManagementObjectSearcher
(
"SELECT CommandLine FROM Win32_Process WHERE ProcessId = " +
DevEnvProcessId.ToString()
);
String CommandLine = "";
bool Added = false;
foreach (ManagementObject commandlineObject in commandlineSearcher.Get())
{
if (!Added)
{
Added = true;
}
else
{
CommandLine += " ";
}
CommandLine += commandlineObject["CommandLine"] as string;
}
if (!string.IsNullOrWhiteSpace(CommandLine))
{
int pos = CommandLine.LastIndexOf(" ");
if (pos >= 0)
{
string Number = CommandLine.Substring(pos).TrimEnd();
int ArgumentsId;
if (int.TryParse(Number, out ArgumentsId))
{
HadId = true;
string DevArguments = Bridge.GetArguments(ArgumentsId);
if (WithExecutable)
{
string EnvCmd = Environment.CommandLine;
int Pos = EnvCmd.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
if (Pos >= 0)
{
DevArguments = EnvCmd.Substring(0, Pos + 4) + " " + DevArguments;
}
}
Result = DevArguments;
}
}
}
return Result;
}
有了找到的进程 ID,就会检索用于调试进程的参数行。
首先通过 WMI,检索 Visual Studio 实例的 commandline
。当这成功后,查找行上的最后一个空格。如果我们伪造了一个数字,那就是它的位置。我们检索行的最后一部分并尝试将其转换为整数。如果成功,我们就向 WindowBridge
请求属于找到的标识符的 commandline
。如果标识符有效,我们就知道我们拥有用于调试进程的 commandline
。根据检索是用于 main(string[]), StartUpEventArgs
还是 main()
,我们需要在前面添加可执行文件名或不添加。WithExecutable
帮助我们做到这一点。当所有内容都连接在一起后,我们最终得到了要使用的 commandline
。
public static class CommandLine
{
...
/// <summary>
/// Retrieves the command line from the window bridge if started from out dev environment.
/// If not started through window bridge it will return the supplied input arguments on current
/// </summary>
/// <param name="Current">The real args array from main</param>
/// <returns>The found arguments array to use</returns>
public static string[] GetCommandLine(string[] Current)
/// <summary>
/// Retrieves the command line from the window bridge if started from out dev environment.
/// If not started through window bridge
/// it will return the normal System.Environment.CommandLine
/// </summary>
/// <returns>The found command line</returns>
public static string GetCommandLine()
/// <summary>
/// Retrieves the command line from the window bridge if started from out dev environment.
/// If not started through window bridge
/// it will return the normal System.Environment.CommandLine
/// </summary>
/// <returns>The found command line in a startupevent argument</returns>
public static void GetStartupEventArguments(ref StartupEventArgs ToUpdate)
...
}
public CommandLine
类有三个不同的函数,每个函数用于不同的场景。
string[] GetCommandLine(string[] Current)
函数用于 main(string[] args)
之后的直接使用。
args = WindowBridge.CommandLine.GetCommandLine(Args);
string GetCommandLine()
函数用作 System.Environment.Commandline
函数的替代品,将缓存放置的 commandline
。
void GetStartupEventArguments(ref StartupEventArgs ToUpdate)
函数用于 PresentationFramework
中 Application.OnStartup
的重载。
...
protected override void OnStartup(StartupEventArgs e)
{
WindowBridge.CommandLine.GetStartupEventArguments(ref e);
}
...
StartupEventArgs _args
变量通过反射进行更新。一旦参数包含可以解决丢失工作可能性的内容,该类本身就会停止检索新信息。
总结
总而言之,我们有一个 Visual Studio 加载器,它允许我们轻松地为指定的解决方案(或项目)文件启动调试进程。通过运行 WindowBridge
并在调试进程的代码中使用 CommandLineLoader
,我们可以提供命令行参数来运行调试进程。对调试进程源代码的更改量被最小化为单行。
只要启动项目已保存在解决方案中,这种方法就可以用于解决方案中的任何可能的 startupobject
,在初始准备之后,代码将永远不会改变,其用户文件也不会改变以传递参数。
1 个加载器,1 座桥,1 个数据检索器。
关注点
通过创建这个加载器,我更新了我对 windowproc
和窗口类的旧知识,我再次玩了玩 WMI,并更仔细地观察了 commandline
参数在我们能够将其用作 Args[]
之前是如何被分解成片段的。我更新了关于解决方案文件包含什么但尤其不包含什么的知识,例如要启动的项目。
历史
- V1.0:初始发布