From a3c78672c65d0e11eb661b1d75244a0786ffa185 Mon Sep 17 00:00:00 2001 From: Adrian Alonso Date: Mon, 27 Apr 2020 19:42:51 -0300 Subject: [PATCH] Added KillCommand --- VisualStudio.Tests/CommandFactoryTests.cs | 6 ++- VisualStudio/AllOption.cs | 23 +++++++++++ VisualStudio/Chooser.cs | 45 ++++++++++++++++++++ VisualStudio/CommandFactory.cs | 1 + VisualStudio/ConfigCommand.cs | 2 +- VisualStudio/KillCommand.cs | 46 +++++++++++++++++++++ VisualStudio/KillCommandDescriptor.cs | 21 ++++++++++ VisualStudio/LogCommand.cs | 4 +- VisualStudio/LogCommandDescriptor.cs | 10 ++--- VisualStudio/ModifyCommand.cs | 2 +- VisualStudio/ProcessExtensions.cs | 19 ++++++++- VisualStudio/Properties/launchSettings.json | 4 +- VisualStudio/UpdateCommand.cs | 2 +- VisualStudio/VisualStudio.csproj | 1 + VisualStudio/VisualStudioInstanceChooser.cs | 32 -------------- VisualStudio/VisualStudioOptions.cs | 18 +++++++- 16 files changed, 187 insertions(+), 49 deletions(-) create mode 100644 VisualStudio/AllOption.cs create mode 100644 VisualStudio/Chooser.cs create mode 100644 VisualStudio/KillCommand.cs create mode 100644 VisualStudio/KillCommandDescriptor.cs delete mode 100644 VisualStudio/VisualStudioInstanceChooser.cs diff --git a/VisualStudio.Tests/CommandFactoryTests.cs b/VisualStudio.Tests/CommandFactoryTests.cs index ee9c795..005bc6f 100644 --- a/VisualStudio.Tests/CommandFactoryTests.cs +++ b/VisualStudio.Tests/CommandFactoryTests.cs @@ -35,11 +35,15 @@ public void when_creating_command_with_help_argument_then_throws_show_usage() [InlineData("where", typeof(WhereCommand))] [InlineData("modify", typeof(ModifyCommand))] [InlineData("update", typeof(UpdateCommand))] - + [InlineData("config", typeof(ConfigCommand))] + [InlineData("log", typeof(LogCommand))] + [InlineData("kill", typeof(KillCommand))] public void when_creating_builtin_command_then_then_command_is_created(string commandName, Type expectedCommandType) { var commandFactory = new CommandFactory(); + Assert.True(commandFactory.IsCommandRegistered(commandName)); + var command = commandFactory.CreateCommand(commandName, Enumerable.Empty()); Assert.NotNull(command); diff --git a/VisualStudio/AllOption.cs b/VisualStudio/AllOption.cs new file mode 100644 index 0000000..3a20ebb --- /dev/null +++ b/VisualStudio/AllOption.cs @@ -0,0 +1,23 @@ +using System; +using Mono.Options; + +namespace VisualStudio +{ + public class AllOption : OptionSet + { + public AllOption(string verb) + { + Add("all", $"{verb} all instances.", e => All = e != null); + } + + protected override bool Parse(string argument, OptionContext c) + { + if ("all".Equals(argument, StringComparison.OrdinalIgnoreCase)) + argument = "--all"; + + return base.Parse(argument, c); + } + + public bool All { get; private set; } + } +} diff --git a/VisualStudio/Chooser.cs b/VisualStudio/Chooser.cs new file mode 100644 index 0000000..2d91388 --- /dev/null +++ b/VisualStudio/Chooser.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using vswhere; +using System.Diagnostics; + +namespace VisualStudio +{ + class Chooser + { + readonly string title; + + public Chooser(string verb = "run") => title = $"Multiple instances found. Select the one to {verb}:"; + + public T Choose(IEnumerable instances, TextWriter output) + { + var instancesAsList = instances.ToList(); + + if (instancesAsList.Count > 1) + { + output.WriteLine(title); + for (int i = 0; i < instancesAsList.Count; i++) + output.WriteLine($"{i + 1}: {GetItemDescription(instancesAsList[i])}"); + + if (int.TryParse(Console.ReadLine(), out var index) && index > 0 && index <= instancesAsList.Count) + return instancesAsList[index - 1]; + + return default; + } + + return instances.FirstOrDefault(); + } + + string GetItemDescription(object value) + { + if (value is VisualStudioInstance visualStudioInstance) + return $"{ visualStudioInstance.DisplayName} - Version { visualStudioInstance.Catalog.ProductDisplayVersion}"; + else if (value is Process process) + return $"{process.MainWindowTitle} ({process.Id})"; + + return value.ToString(); + } + } +} diff --git a/VisualStudio/CommandFactory.cs b/VisualStudio/CommandFactory.cs index d1acc6c..f42fe6e 100644 --- a/VisualStudio/CommandFactory.cs +++ b/VisualStudio/CommandFactory.cs @@ -21,6 +21,7 @@ public CommandFactory() RegisterCommand("modify", x => new ModifyCommand(x, whereService, installerService)); RegisterCommand("config", x => new ConfigCommand(x, whereService)); RegisterCommand("log", x => new LogCommand(x, whereService)); + RegisterCommand("kill", x => new KillCommand(x, whereService)); } public IEnumerable RegisteredCommands => factories.Keys; diff --git a/VisualStudio/ConfigCommand.cs b/VisualStudio/ConfigCommand.cs index d4c7756..4d3d6dd 100644 --- a/VisualStudio/ConfigCommand.cs +++ b/VisualStudio/ConfigCommand.cs @@ -17,7 +17,7 @@ public ConfigCommand(ConfigCommandDescriptor descriptor, WhereService whereServi public override async Task ExecuteAsync(TextWriter output) { var instances = await whereService.GetAllInstancesAsync(Descriptor.Sku, Descriptor.Channel); - var instance = new VisualStudioInstanceChooser().Choose(instances, output); + var instance = new Chooser().Choose(instances, output); if (instance != null) { diff --git a/VisualStudio/KillCommand.cs b/VisualStudio/KillCommand.cs new file mode 100644 index 0000000..c241ad7 --- /dev/null +++ b/VisualStudio/KillCommand.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using vswhere; + +namespace VisualStudio +{ + class KillCommand : Command + { + readonly WhereService whereService; + + public KillCommand(KillCommandDescriptor descriptor, WhereService whereService) : base(descriptor) => + this.whereService = whereService; + + public override async Task ExecuteAsync(TextWriter output) + { + var devenvProcesses = Process.GetProcessesByName("devenv").ToList(); + var targetProcesses = + (from instance in await whereService.GetAllInstancesAsync(Descriptor.Sku, Descriptor.Channel) + from devenvProcess in devenvProcesses + where Match(devenvProcess, instance) + select devenvProcess).Distinct().ToList(); + + if (!Descriptor.KillAll) + { + var process = new Chooser("kill").Choose(targetProcesses, output); + + targetProcesses.Clear(); + if (process != null) + targetProcesses.Add(process); + } + + foreach (var process in targetProcesses) + { + output.WriteLine($"Killing {process.MainWindowTitle} ({process.Id})..."); + process.Kill(); + } + } + + bool Match(Process devenvProcess, VisualStudioInstance instance) => + devenvProcess.MainModule.FileName.StartsWith(instance.InstallationPath, StringComparison.OrdinalIgnoreCase) && + (!Descriptor.IsExperimental || devenvProcess.GetCommandLine().Contains("/rootSuffix Exp", StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/VisualStudio/KillCommandDescriptor.cs b/VisualStudio/KillCommandDescriptor.cs new file mode 100644 index 0000000..4864c5d --- /dev/null +++ b/VisualStudio/KillCommandDescriptor.cs @@ -0,0 +1,21 @@ +using System; +using Mono.Options; + +namespace VisualStudio +{ + class KillCommandDescriptor : CommandDescriptor + { + readonly VisualStudioOptions options = new VisualStudioOptions(channelVerb: "Kill", showNickname: false, showExp: true); + readonly AllOption allOption = new AllOption("Kill"); + + public KillCommandDescriptor() => OptionSet = new CompositeOptionSet(options, allOption); + + public Channel? Channel => options.Channel; + + public Sku? Sku => options.Sku; + + public bool IsExperimental => options.IsExperimental; + + public bool KillAll => allOption.All; + } +} diff --git a/VisualStudio/LogCommand.cs b/VisualStudio/LogCommand.cs index 958b416..3370280 100644 --- a/VisualStudio/LogCommand.cs +++ b/VisualStudio/LogCommand.cs @@ -17,12 +17,12 @@ public LogCommand(LogCommandDescriptor descriptor, WhereService whereService) : public override async Task ExecuteAsync(TextWriter output) { var instances = await whereService.GetAllInstancesAsync(Descriptor.Sku, Descriptor.Channel); - var instance = new VisualStudioInstanceChooser().Choose(instances, output); + var instance = new Chooser().Choose(instances, output); if (instance != null) { var instanceDir = instance.InstallationVersion.Major + ".0_" + instance.InstanceId; - if (Descriptor.Experimental) + if (Descriptor.IsExperimental) instanceDir += "Exp"; var path = Path.Combine( diff --git a/VisualStudio/LogCommandDescriptor.cs b/VisualStudio/LogCommandDescriptor.cs index ba2eae6..7b83df6 100644 --- a/VisualStudio/LogCommandDescriptor.cs +++ b/VisualStudio/LogCommandDescriptor.cs @@ -5,18 +5,14 @@ namespace VisualStudio { class LogCommandDescriptor : CommandDescriptor { - readonly VisualStudioOptions options = new VisualStudioOptions(channelVerb: "Open", showNickname: false); - bool exp; + readonly VisualStudioOptions options = new VisualStudioOptions(channelVerb: "Open", showNickname: false, showExp: true); - public LogCommandDescriptor() => OptionSet = new CompositeOptionSet(options, new OptionSet - { - { "exp", "Use experimental instance instead of regular.", e => exp = e != null }, - }); + public LogCommandDescriptor() => OptionSet = new CompositeOptionSet(options); public Channel? Channel => options.Channel; public Sku? Sku => options.Sku; - public bool Experimental => exp; + public bool IsExperimental => options.IsExperimental; } } diff --git a/VisualStudio/ModifyCommand.cs b/VisualStudio/ModifyCommand.cs index 9139c9d..6f65100 100644 --- a/VisualStudio/ModifyCommand.cs +++ b/VisualStudio/ModifyCommand.cs @@ -22,7 +22,7 @@ public override async Task ExecuteAsync(TextWriter output) { var instances = await whereService.GetAllInstancesAsync(Descriptor.Sku, Descriptor.Channel); - var instance = new VisualStudioInstanceChooser().Choose(instances, output); + var instance = new Chooser().Choose(instances, output); if (instance != null) { diff --git a/VisualStudio/ProcessExtensions.cs b/VisualStudio/ProcessExtensions.cs index 4c89138..8f6eb1b 100644 --- a/VisualStudio/ProcessExtensions.cs +++ b/VisualStudio/ProcessExtensions.cs @@ -1,5 +1,8 @@ -using System.Diagnostics; +using System; +using System.Linq; +using System.Diagnostics; using System.IO; +using System.Management; namespace VisualStudio { @@ -19,5 +22,19 @@ public static void Log(this ProcessStartInfo info, TextWriter output) } static string Quote(string value) => value.Contains(' ') ? "\"" + value + "\"" : value; + + public static string GetCommandLine(this Process process) + { + try + { + using (var searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id)) + using (var objects = searcher.Get()) + return objects.OfType().FirstOrDefault()?["CommandLine"]?.ToString(); + } + catch + { + return string.Empty; + } + } } } diff --git a/VisualStudio/Properties/launchSettings.json b/VisualStudio/Properties/launchSettings.json index 6797052..e647a9b 100644 --- a/VisualStudio/Properties/launchSettings.json +++ b/VisualStudio/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "VisualStudio": { "commandName": "Project", - "commandLineArgs": "config -pre -exp" + "commandLineArgs": "kill all exp" } } -} +} \ No newline at end of file diff --git a/VisualStudio/UpdateCommand.cs b/VisualStudio/UpdateCommand.cs index 9fa6f3c..7d976e9 100644 --- a/VisualStudio/UpdateCommand.cs +++ b/VisualStudio/UpdateCommand.cs @@ -21,7 +21,7 @@ public override async Task ExecuteAsync(TextWriter output) { var instances = await whereService.GetAllInstancesAsync(Descriptor.Sku, Descriptor.Channel); - var instance = new VisualStudioInstanceChooser().Choose(instances, output); + var instance = new Chooser().Choose(instances, output); if (instance != null) { diff --git a/VisualStudio/VisualStudio.csproj b/VisualStudio/VisualStudio.csproj index f53935c..15a5cbb 100644 --- a/VisualStudio/VisualStudio.csproj +++ b/VisualStudio/VisualStudio.csproj @@ -21,6 +21,7 @@ + diff --git a/VisualStudio/VisualStudioInstanceChooser.cs b/VisualStudio/VisualStudioInstanceChooser.cs deleted file mode 100644 index 4723195..0000000 --- a/VisualStudio/VisualStudioInstanceChooser.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using System.IO; -using vswhere; - -namespace VisualStudio -{ - class VisualStudioInstanceChooser - { - public VisualStudioInstance Choose(IEnumerable instances, TextWriter output) - { - var instancesAsList = instances.ToList(); - - if (instancesAsList.Count > 1) - { - output.WriteLine("Multiple instances found. Select the one to run:"); - for (int i = 0; i < instancesAsList.Count; i++) - { - output.WriteLine($"{i + 1}: {instancesAsList[i].DisplayName} - Version {instancesAsList[i].Catalog.ProductDisplayVersion}"); - } - - if (int.TryParse(Console.ReadLine(), out var index) && index > 0 && index <= instancesAsList.Count) - return instancesAsList[index - 1]; - - return null; - } - - return instances.FirstOrDefault(); - } - } -} diff --git a/VisualStudio/VisualStudioOptions.cs b/VisualStudio/VisualStudioOptions.cs index 8e07bfb..fb85818 100644 --- a/VisualStudio/VisualStudioOptions.cs +++ b/VisualStudio/VisualStudioOptions.cs @@ -12,8 +12,14 @@ public class VisualStudioOptions : OptionSet { string[] channelShortcuts = new[] { "pre", "preview", "int", "internal", "master" }; string[] skuShortcuts = new[] { "e", "ent", "enterprise", "p", "pro", "professional", "c", "com", "community" }; + string[] experimentalShortcuts = new[] { "exp", "experimental" }; - public VisualStudioOptions(string channelVerb = "Install", bool showChannel = true, bool showSku = true, bool showNickname = true) + public VisualStudioOptions( + string channelVerb = "Install", + bool showChannel = true, + bool showSku = true, + bool showNickname = true, + bool showExp = false) { if (showChannel) { @@ -34,6 +40,11 @@ public VisualStudioOptions(string channelVerb = "Install", bool showChannel = tr // Nickname Add("nick|nickname:", "Optional nickname to use", n => Nickname = n); } + + if (showExp) + { + Add("exp|experimental", $"{channelVerb} experimental instance instead of regular.", e => IsExperimental = e != null); + } } Sku ParseSku(string sku) @@ -56,6 +67,9 @@ protected override bool Parse(string argument, OptionContext c) if (skuShortcuts.Contains(argument.ToLowerInvariant())) argument = "--sku=" + argument; + if (experimentalShortcuts.Contains(argument.ToLowerInvariant())) + argument = "--" + argument; + return base.Parse(argument, c); } @@ -64,5 +78,7 @@ protected override bool Parse(string argument, OptionContext c) public Sku? Sku { get; private set; } public string Nickname { get; private set; } + + public bool IsExperimental { get; private set; } } }