65.9K
CodeProject 正在变化。 阅读更多。
Home

VSLauncherX - Visual Studio更好的最近列表(及更多)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2023年9月27日

CPOL

10分钟阅读

viewsIcon

4179

Visual Studio 更好的最近列表、解决方案和项目管理,不取代启动窗口

  • 源代码和安装程序可在 Github 上获取

引言

作为一名企业开发人员,人们经常需要同时运行多个 Visual Studio 实例,并使用不同的解决方案和配置。

手动执行此操作通常是一个繁琐且耗时的过程。启动您需要的 VS 版本,可能“以管理员身份”,在最近列表中找到解决方案或项目,等等。

此外,最近 Visual Studio 的“启动窗口”也不是最有用的,除了它在管理其最近使用列表方面的固有局限性(记住的项目数量、搜索列表的速度、缺乏分组)。

这让我萌生了创建一个程序的想法,它可以帮助我管理我的解决方案(在工作中,我有很多解决方案,有些非常具体),并加快启动任何解决方案的工作过程。

虽然市场上有很多 Visual Studio 扩展可以以这样或那样的方式弥补这种情况,但它们都有一个限制,即在执行任何操作之前都必须先启动 Visual Studio。

注意:我将使用 SoP 作为“解决方案和/或项目”的缩写。

主要特点

  • 创建可一次性使用的 SoP 组
  • 定义项目特定设置,例如“以管理员身份运行”,在 Visual Studio 之前和/或之后执行另一个应用程序或命令
  • 为每个 SoP 使用特定的 VS 配置文件
  • 将文件夹中的所有 SoP 作为一组导入
  • 在指定的显示器上启动每个 SoP
  • 快速搜索和执行
  • 自动将列表与 Visual Studio 的最近列表同步
  • “最近文件”列表无限制,可以添加几乎无限量的 SoP
  • 分组,带子组
  • 可从任务栏上下文菜单直接调用的收藏夹
  • 显示解决方案/项目的 git 状态(干净/已修改)
  • 显示应用程序的管理员权限和提升状态
  • 自动启动并始终以管理员身份运行

正在运行的功能

  • 从文件夹导入
  • 从任何 Visual Studio 版本和配置文件导入
  • 支持 VS 2017、2019 和 2022
  • 启动包含子组的整个 SoP 组,每个组都在其定义的 VS 版本、工作文件夹和 VS 配置文件中
  • 在每个组或 SoP 之前和/或之后执行其他应用程序
  • 在特定显示器上启动每个 SoP(在不同设置上未100%验证)
  • 快速搜索和执行
  • 从 VS 导入时检测不存在的 SoP
  • 以管理员身份运行的用户提权
  • 在 SoP 或组之前和/或之后运行其他命令,可选择等待完成再执行下一步操作

计划中的功能

  • 自动与 VS 同步
  • 支持 Visual Studio Code
  • 任务栏集成,实现更快启动(正在进行中)
  • 支持深色模式
  • 支持非 Windows 环境

因此,如果有人愿意提供帮助,请前往 Github 贡献 - https://github.com/Hefaistos68/VSLauncherX

背景

该程序带来了一些小挑战,包括检测已安装的 Visual Studio 版本和实例,处理权限(下文详述),以特定权限和在给定显示器上启动应用程序,自动启动和提权。

使用应用程序

首先,要么使用带有 VS 符号的工具栏按钮从 VS 导入整个最近列表,要么直接使用向上箭头文件夹工具栏按钮从文件夹导入。您也可以直接将解决方案或项目从资源管理器拖入其中。

列表中至少有一个 SoP 后,您可以通过按 Enter 键或双击所选项目来启动它。当您双击文件夹/组时,将启动整个文件夹,每个项目都在其自己的 VS 实例中,如项目设置中所定义。

设置对话框允许您为该项目(以及所有包含的项目)选择特定的 VS 版本,选择一个 “实例”,添加额外的 命令,定义此 VS 出现的特定显示器,提升状态以及是否显示 Visual Studio 的启动画面。

此外,您可以添加执行前和执行后设置,这允许您在启动项目之前或之后运行其他命令或应用程序,例如 git.exe 或您可能需要的任何其他内容。

当您单击问号(或 VS 版本的版本文本)时,对话框将直接带您到特定项目的帮助页面。

正如预期的那样,您可以使用拖放来排序项目,Alt+Enter 打开项目设置,Del 删除项目等。请记住,文件夹/组项目的设置将在其中所有项目之前应用。这意味着当您将文件夹设置为以管理员身份执行时,所有包含的 SoP 也将以管理员身份运行。您可以为每个 SoP 设置要打开它的(已安装的)Visual Studio 版本。每个项目都会显示推荐版本,因为它在解决方案文件中定义。还有一些上下文菜单,您可以右键单击项目来调用它们。当您“收藏”一个项目时,它会添加到任务栏应用程序图标中的任务上下文菜单中(这仍在进行中,预计会出现错误)。

关注点

以管理员身份运行

我们来谈谈“以管理员身份运行”——这是一个在任何与编程或 Windows 管理相关的网站上都备受讨论的话题。然而,对于它的实际工作方式仍然存在很大的误解。即使是微软有时也会出错(请参阅 Visual Studio 反馈网站上的“Visual Studio 不识别管理员状态”)。

首先,你为什么会需要以管理员身份运行 Visual Studio 呢?嗯,为了调试需要提升权限的服务,比如系统服务,任何打开某些被策略阻止的 TCP 端口或管道的应用程序。可能还有其他一些情况下你会需要它。

对于大多数用户来说,“以管理员身份运行”几乎没有用处,要么他们总是管理员(开发人员喜欢这样),要么他们除了在极少数情况下安装新东西之外不需要它。当您进入企业环境时,情况会迅速升级。在那里,域用户通常是 BUILTIN\Administrators 组的成员,但该组设置为“拒绝”,这意味着用户无法从该组获得任何权限。当您现在以管理员身份运行时,UAC 对话框弹出,要求确认或凭据(取决于其设置方式和您当前的提升级别),然后启用管理员组,并且新的进程令牌获得该组的权限(好的,这里简化了)。

那么,程序如何确定它们是否具有管理员身份呢?通常,通过检查进程令牌中组中是否存在 BUILTIN\Administrators 组 SID。简单易行,就像这样

public static bool IsAdmin(Process process) 
{
   WindowsIdentity processIdentity = GetProcessIdentity(process); 
   return new WindowsPrincipal(processIdentity).IsInRole(WindowsBuiltInRole.Administrator); 
}

不幸的是,这并不完全正确,适用于许多情况,但并非所有情况。例如,在我的工作环境中,除了域管理员,没有人是管理员组的成员。相反,他们使用另一个组来执行相同的任务,为了功能性,一切都按预期工作,它只是破坏了所有依赖于管理员 SID 存在的程序。

确定提升的正确方法(这才是我们最终要讨论的)是检查令牌完整性值或令牌提升。我发现完整性值在所有情况下都更可靠。有关详细信息,请查看 ProcessHelper SecurityHelper 类。

VSLauncherX 同时执行这两项操作——检测用户是否是 BUILTIN\Administrators 组的成员,以及进程是否具有提升的权限。

这两个事实都显示在应用程序标题栏中——“ADMIN”和/或“Elevated”将添加到标题中,因此您将始终知道您处于什么状态。

Visual Studio 及其注意事项

有很多方法可以找出安装了哪些 VS 版本以及安装在哪里,从搜索注册表、使用安装程序 API、摸索目录,所有这些方法都在一定程度上有效,并且难度级别各不相同。幸运的是,微软有远见,将 Visual Studio 的 WMI 注册包括在内。您可以在此处阅读相关信息

所以这一切都归结为一个简单的 WMI 查询,我们就可以得到所有已安装的 VS 版本及其位置的列表。

ManagementObjectSearcher searcher = new ManagementObjectSearcher
{
	Query = new SelectQuery("MSFT_VSInstance ", "", 
            new[] { "Name", "Version", "ProductLocation", "IdentifyingNumber" })
};
ManagementObjectCollection collection = searcher.Get();
ManagementObjectCollection.ManagementObjectEnumerator em = collection.GetEnumerator();
 
while (em.MoveNext())
{
	ManagementBaseObject baseObj = em.Current;
	if (baseObj.Properties["Version"].Value != null)
	{
		try
		{
			string? name = baseObj.Properties["Name"].Value.ToString();
			string? version = baseObj.Properties["Version"].Value.ToString();
			string? location = baseObj.Properties["ProductLocation"].Value.ToString();
			string? identifier = 
                    baseObj.Properties["IdentifyingNumber"].Value.ToString();
 
			if (name != null && version != null && location != null && identifier != null)
			{
				list.Add(new VisualStudioInstance(name, version, location, 
                identifier, VisualStudioInstanceManager.YearFromVersion(version[..2])));
			}
		}
		catch (Exception ex)
		{
			Debug.WriteLine(ex.ToString());
		}
	}
}

由于我们只对版本、位置和实例号感兴趣,因此这些是此查询中请求的所有属性。

更有趣的是 VS 如何存储其最近列表。它是一个 JSON 字符串,包含在一个存储在本地应用程序数据文件夹中的 XML 文件中(我不知道是谁想出这个主意的)。

从那里开始是一个简单的路径,读取列表,检查目标是否存在,并将其添加到要导入的列表中。还解析 .SLN 和项目文件,以获取创建该文件的 Visual Studio 版本号,并将其设置为推荐版本。如果该版本当前未安装,用户可以选择从项目设置对话框中的链接下载它,或选择一个已安装的版本,或只是让它使用默认版本启动(始终是安装的最高版本)。

自动启动和始终管理员权限

另一个有趣的问题是如何通过设置让应用程序自动启动并始终以管理员身份运行,而不是修改应用程序快捷方式。

我选择通过 TaskScheduler 实现这一点,在设置为自动启动时使用登录触发器。任务允许在选择始终以管理员身份运行时提升应用程序。为此,应用程序使用 ProcessStartInfo 中的“runas”动词重新启动自身,以便弹出 UAC 对话框并授予访问权限,然后添加任务。

所有实现此功能的功能都在 AutoRun 类中。SetupLauncherTask() 方法完成在 TaskScheduler 中创建文件夹、创建带(或不带)触发器的任务的所有工作。

using (TaskService ts = new TaskService())
{
	TaskFolder folder;
    // check if the folder exists, if not create a new one
	var bFolderExists = ts.RootFolder.SubFolders.Any(sf => sf.Name == FolderName);
 
	if (!bFolderExists)
	{
		folder = ts.RootFolder.CreateFolder(FolderName);
	}
	else
	{
		folder = ts.RootFolder.SubFolders.First(sf => sf.Name == FolderName);
	}
 
	var user = System.Security.Principal.WindowsIdentity.GetCurrent();
 
    // setup the application to start, and add a new task called "Autostart for <user>"
	string location = Assembly.GetExecutingAssembly().Location.Replace(".dll", ".exe");
	var execAction = new ExecAction(location, "autostart" ,
                     workingDirectory: Path.GetDirectoryName(location));
	var taskName = TaskName + GetUserName(user);
 
	// create the task if it doesn't exist
	if (folder.AllTasks.Any(t => t.Name == taskName))
	{
		folder.DeleteTask(taskName);
	}
 
	// Create a new task definition and assign properties
	TaskDefinition td = ts.NewTask();
	td.RegistrationInfo.Author = user.Name;
	td.RegistrationInfo.Description = "Visual Studio Launcher";
 
    // if the app should run-as-admin or elevated, 
    // then give it highest available privileges
	td.Principal.RunLevel = bElevated ? TaskRunLevel.Highest : TaskRunLevel.LUA;
	td.Principal.LogonType = TaskLogonType.InteractiveToken;
 
    // some more less interesting start options to fine tune
	td.Settings.StartWhenAvailable = true;
	td.Settings.AllowDemandStart = true;
	td.Settings.IdleSettings.StopOnIdleEnd = false;
	td.Settings.DisallowStartIfOnBatteries = false;
	td.Settings.StopIfGoingOnBatteries = false;
	td.Settings.ExecutionTimeLimit = TimeSpan.Zero;
	td.Settings.AllowHardTerminate = true;
	td.Settings.MultipleInstances = TaskInstancesPolicy.Parallel;
 
    // so here is the autostart setup, which causes the app to start 
    // after the user logs on
    // this is archived through an LogonTrigger
	if (asAutostart)
	{
		// Create a trigger that will fire the task when the user logs on
		var logonTrigger = new LogonTrigger
		{
			UserId = user.User.ToString(),
			Delay = TimeSpan.FromSeconds(10)	// give task manager time to start too
		};
 
		td.Triggers.Add(logonTrigger);
	}
 
	td.Actions.Add(execAction);
 
	// Register the task in the root folder
	folder.RegisterTaskDefinition(taskName, td);
}

该死的 Process.Start...

有一个问题让我几乎要秃头了:Process.Start 失败并显示一个意义不大的错误消息
Process 对象必须将 UseShellExecute 属性设置为 false 才能使用环境变量。

这是什么鬼?我没有设置任何环境变量来启动进程。那我为什么会收到这个错误消息?在网上快速(然后更彻底地)搜索了一下,发现了一些结果,许多人都在问同样的问题。然后我查看了 .NET 代码,方法上的原始注释给了我一个提示

//
// Summary:
//     When the System.Diagnostics.ProcessStartInfo.UseShellExecute property is false,
//     gets or sets the working directory for the process to be started. 
//     When System.Diagnostics.ProcessStartInfo.UseShellExecute
//     is true, gets or sets the directory that contains the process to be started.
//
// Returns:
//     When System.Diagnostics.ProcessStartInfo.UseShellExecute is true, 
//     the fully qualified name of the directory that contains 
//     the process to be started. 
//     When the System.Diagnostics.ProcessStartInfo.UseShellExecute
//     property is false, the working directory for the process to be started. 
//     The default is an empty string ("").

哦,对了,所以工作目录也和它有关。

一旦我停止设置工作目录并使用将 UseShellExecute 属性设置为 true,一切都恢复正常。所以建议很简单:当你想要使用 shell 执行你的应用程序时,不要触碰 WorkingDirectory 属性(不,甚至不要读取它),你只有在你想要使用“run”以外的任何动词时才需要它。当你通过 shell 启动应用程序时,工作目录会自动设置为应用程序目录。

所有其他

所有这些都是相当直接的 WinForm 代码,没什么特别需要提及的。我本可以用 WPF 来做,但我不喜欢它,也不了解它。所以就用 WinForms 吧。

第三方库

该应用程序还使用了一个略微修改的 ObjectListView 版本(https://objectlistview.sourceforge.net/cs/index.html),我添加了在树视图中渲染多行和图像的功能,也许我有时间将其推送回原始仓库。

对于 GIT 支持,它使用 LibGit2Sharp,目前仅用于获取仓库状态并通过红色或绿色 GIT 图标在 UI 中显示。

任务计划程序由 https://github.com/dahall/taskscheduler 库支持

对于某些 Windows API,使用 https://github.com/Wagnerp/Windows-API-CodePack-NET 库。

历史

历史记录不多,代码刚发布到 Github 没多久,我一有时间就会改进它。

© . All rights reserved.