From a433203b7d365442a1ffc5fc395851b82d0370aa Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Fri, 3 May 2024 18:59:32 +0200 Subject: [PATCH 01/32] Add tests for existing command line parsing --- ILSpy.Tests/CommandLineArgumentsTests.cs | 48 ++++++++++++++++++++++++ ILSpy.Tests/ILSpy.Tests.csproj | 1 + 2 files changed, 49 insertions(+) create mode 100644 ILSpy.Tests/CommandLineArgumentsTests.cs diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs new file mode 100644 index 000000000..7acaaaa29 --- /dev/null +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -0,0 +1,48 @@ +using FluentAssertions; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests +{ + [TestFixture] + public class CommandLineArgumentsTests + { + [Test] + public void VerifyEmptyArgumentsArray() + { + var cmdLineArgs = new CommandLineArguments(new string[] { }); + + cmdLineArgs.AssembliesToLoad.Should().BeEmpty(); + cmdLineArgs.SingleInstance.Should().BeNull(); + cmdLineArgs.NavigateTo.Should().BeNull(); + cmdLineArgs.Search.Should().BeNull(); + cmdLineArgs.Language.Should().BeNull(); + cmdLineArgs.NoActivate.Should().BeFalse(); + cmdLineArgs.ConfigFile.Should().BeNull(); + } + + [Test] + public void VerifySeparateOption() + { + var cmdLineArgs = new CommandLineArguments(new string[] { "/separate" }); + cmdLineArgs.SingleInstance.Should().BeFalse(); + } + + [Test] + public void VerifySingleInstanceOption() + { + var cmdLineArgs = new CommandLineArguments(new string[] { "/singleInstance" }); + cmdLineArgs.SingleInstance.Should().BeTrue(); + } + + [Test] + public void VerifySeparateSingleInstanceOptionOrdering() + { + var cmdLineArgsCase1 = new CommandLineArguments(new string[] { "/singleInstance", "/separate" }); + cmdLineArgsCase1.SingleInstance.Should().BeFalse(); + + var cmdLineArgsCase2 = new CommandLineArguments(new string[] { "/separate", "/singleInstance" }); + cmdLineArgsCase2.SingleInstance.Should().BeTrue(); + } + } +} diff --git a/ILSpy.Tests/ILSpy.Tests.csproj b/ILSpy.Tests/ILSpy.Tests.csproj index 79e95586a..17337e7b2 100644 --- a/ILSpy.Tests/ILSpy.Tests.csproj +++ b/ILSpy.Tests/ILSpy.Tests.csproj @@ -39,6 +39,7 @@ + From e39403289d49d27407380d4a7781f0601ec5d504 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Fri, 3 May 2024 20:10:55 +0200 Subject: [PATCH 02/32] Switch CommandLineArguments to McMaster --- Directory.Packages.props | 1 + ILSpy.Tests/CommandLineArgumentsTests.cs | 64 ++++++++++++++++-- ILSpy/CommandLineArguments.cs | 82 +++++++++++++++++------- ILSpy/ILSpy.csproj | 1 + 4 files changed, 119 insertions(+), 29 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1a9168125..af214e6a6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index 7acaaaa29..3b60fd716 100644 --- a/ILSpy.Tests/CommandLineArgumentsTests.cs +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -24,25 +24,75 @@ namespace ICSharpCode.ILSpy.Tests [Test] public void VerifySeparateOption() { - var cmdLineArgs = new CommandLineArguments(new string[] { "/separate" }); + var cmdLineArgs = new CommandLineArguments(new string[] { "--instancing", "Multi" }); cmdLineArgs.SingleInstance.Should().BeFalse(); } [Test] public void VerifySingleInstanceOption() { - var cmdLineArgs = new CommandLineArguments(new string[] { "/singleInstance" }); + var cmdLineArgs = new CommandLineArguments(new string[] { "--instancing", "Single" }); cmdLineArgs.SingleInstance.Should().BeTrue(); } [Test] - public void VerifySeparateSingleInstanceOptionOrdering() + public void VerifyNavigateToOption() { - var cmdLineArgsCase1 = new CommandLineArguments(new string[] { "/singleInstance", "/separate" }); - cmdLineArgsCase1.SingleInstance.Should().BeFalse(); + const string navigateTo = "MyNamespace.MyClass"; + var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateto", navigateTo }); + cmdLineArgs.NavigateTo.Should().BeEquivalentTo(navigateTo); + } + + [Test] + public void VerifySearchOption() + { + const string searchWord = "TestContainers"; + var cmdLineArgs = new CommandLineArguments(new string[] { "--search", searchWord }); + cmdLineArgs.Search.Should().BeEquivalentTo(searchWord); + } + + [Test] + public void VerifyLanguageOption() + { + const string language = "csharp"; + var cmdLineArgs = new CommandLineArguments(new string[] { "--language", language }); + cmdLineArgs.Language.Should().BeEquivalentTo(language); + } - var cmdLineArgsCase2 = new CommandLineArguments(new string[] { "/separate", "/singleInstance" }); - cmdLineArgsCase2.SingleInstance.Should().BeTrue(); + [Test] + public void VerifyConfigOption() + { + const string configFile = "myilspyoptions.xml"; + var cmdLineArgs = new CommandLineArguments(new string[] { "--config", configFile }); + cmdLineArgs.ConfigFile.Should().BeEquivalentTo(configFile); + } + + [Test] + public void VerifyNoActivateOption() + { + var cmdLineArgs = new CommandLineArguments(new string[] { "--noactivate" }); + cmdLineArgs.NoActivate.Should().BeTrue(); + } + + [Test] + public void MultipleAssembliesAsArguments() + { + var cmdLineArgs = new CommandLineArguments(new string[] { "assembly1", "assembly2", "assembly3" }); + cmdLineArgs.AssembliesToLoad.Should().HaveCount(3); + } + + [Test] + public void PassAtFileArgumentsSpaceSeparated() + { + string filepath = System.IO.Path.GetTempFileName(); + + System.IO.File.WriteAllText(filepath, "assembly1 assembly2 assembly3 --instancing multi --noactivate"); + + var cmdLineArgs = new CommandLineArguments(new string[] { $"@{filepath}" }); + + cmdLineArgs.SingleInstance.Should().BeFalse(); + cmdLineArgs.NoActivate.Should().BeTrue(); + cmdLineArgs.AssembliesToLoad.Should().HaveCount(3); } } } diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index c6c1b5947..fa43dba9d 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -18,9 +18,20 @@ using System; using System.Collections.Generic; +using System.Linq; + +using McMaster.Extensions.CommandLineUtils; + +using TomsToolbox.Essentials; namespace ICSharpCode.ILSpy { + internal enum InstancingMode + { + Single, + Multi + } + sealed class CommandLineArguments { // see /doc/Command Line.txt for details @@ -34,32 +45,59 @@ namespace ICSharpCode.ILSpy public CommandLineArguments(IEnumerable arguments) { - foreach (string arg in arguments) + var app = new CommandLineApplication() { + ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated, + }; + + var oInstancing = app.Option("-i|--instancing ", "Single or multi instance", CommandOptionType.SingleValue); + var oNavigateTo = app.Option("-n|--navigateto ", "Navigates to the member specified by the given ID string", CommandOptionType.SingleValue); + var oSearch = app.Option("-s|--search ", "Search for", CommandOptionType.SingleValue); + var oLanguage = app.Option("-l|--language ", "Selects the specified language", CommandOptionType.SingleValue); + var oConfig = app.Option("-c|--config ", "Search for", CommandOptionType.SingleValue); + var oNoActivate = app.Option("--noactivate", "Search for", CommandOptionType.NoValue); + + // https://natemcmaster.github.io/CommandLineUtils/docs/arguments.html#variable-numbers-of-arguments + // To enable this, MultipleValues must be set to true, and the argument must be the last one specified. + var files = app.Argument("Assemblies", "Assemblies to load", multipleValues: true); + + app.Parse(arguments.ToArray()); + + if (oInstancing.Value != null) { - if (arg.Length == 0) - continue; - if (arg[0] == '/') + if (Enum.TryParse(oInstancing.Value(), true, out var mode)) { - if (arg.Equals("/singleInstance", StringComparison.OrdinalIgnoreCase)) - this.SingleInstance = true; - else if (arg.Equals("/separate", StringComparison.OrdinalIgnoreCase)) - this.SingleInstance = false; - else if (arg.StartsWith("/navigateTo:", StringComparison.OrdinalIgnoreCase)) - this.NavigateTo = arg.Substring("/navigateTo:".Length); - else if (arg.StartsWith("/search:", StringComparison.OrdinalIgnoreCase)) - this.Search = arg.Substring("/search:".Length); - else if (arg.StartsWith("/language:", StringComparison.OrdinalIgnoreCase)) - this.Language = arg.Substring("/language:".Length); - else if (arg.Equals("/noActivate", StringComparison.OrdinalIgnoreCase)) - this.NoActivate = true; - else if (arg.StartsWith("/config:", StringComparison.OrdinalIgnoreCase)) - this.ConfigFile = arg.Substring("/config:".Length); - } - else - { - this.AssembliesToLoad.Add(arg); + switch (mode) + { + case InstancingMode.Single: + SingleInstance = true; + break; + case InstancingMode.Multi: + SingleInstance = false; + break; + } } } + + if (oNavigateTo.Value != null) + NavigateTo = oNavigateTo.Value(); + + if (oSearch.Value != null) + Search = oSearch.Value(); + + if (oLanguage.Value != null) + Language = oLanguage.Value(); + + if (oConfig.Value != null) + ConfigFile = oConfig.Value(); + + if (oNoActivate.HasValue()) + NoActivate = true; + + foreach (var assembly in files.Values) + { + if (!string.IsNullOrWhiteSpace(assembly)) + AssembliesToLoad.Add(assembly); + } } } } diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 93feab3a1..9d2b317c2 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -45,6 +45,7 @@ + From 1d76d0a7046d9394e59ef8b7d58f55d3bc6d7ec2 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Fri, 3 May 2024 20:13:20 +0200 Subject: [PATCH 03/32] Fix namespace import --- ILSpy/CommandLineArguments.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index fa43dba9d..7c77775e3 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -16,14 +16,12 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using McMaster.Extensions.CommandLineUtils; + using System; using System.Collections.Generic; using System.Linq; -using McMaster.Extensions.CommandLineUtils; - -using TomsToolbox.Essentials; - namespace ICSharpCode.ILSpy { internal enum InstancingMode From 4b529259c9e7e21e9ff13645fa6adc1b1e23ca55 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 4 May 2024 08:08:53 +0200 Subject: [PATCH 04/32] Add more help text, type some options --- ILSpy/CommandLineArguments.cs | 51 +++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index 7c77775e3..da46ed883 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -47,17 +47,41 @@ namespace ICSharpCode.ILSpy ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated, }; - var oInstancing = app.Option("-i|--instancing ", "Single or multi instance", CommandOptionType.SingleValue); - var oNavigateTo = app.Option("-n|--navigateto ", "Navigates to the member specified by the given ID string", CommandOptionType.SingleValue); - var oSearch = app.Option("-s|--search ", "Search for", CommandOptionType.SingleValue); - var oLanguage = app.Option("-l|--language ", "Selects the specified language", CommandOptionType.SingleValue); - var oConfig = app.Option("-c|--config ", "Search for", CommandOptionType.SingleValue); - var oNoActivate = app.Option("--noactivate", "Search for", CommandOptionType.NoValue); + var oInstancing = app.Option("-i|--instancing ", + "Single or multi instance", + CommandOptionType.SingleValue); + + var oNavigateTo = app.Option("-n|--navigateto ", + "Navigates to the member specified by the given ID string.\r\nThe member is searched for only in the assemblies specified on the command line.\r\nExample: 'ILSpy ILSpy.exe --navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments'", + CommandOptionType.SingleValue); + oNavigateTo.DefaultValue = null; + + var oSearch = app.Option("-s|--search ", + "Search for t:TypeName, m:Member or c:Constant; use exact match (=term), 'should not contain' (-term) or 'must contain' (+term); use /reg(ular)?Ex(pressions)?/ or both - t:/Type(Name)?/...", + CommandOptionType.SingleValue); + oSearch.DefaultValue = null; + + var oLanguage = app.Option("-l|--language ", + "Selects the specified language.\r\nExample: 'ILSpy --language:C#' or 'ILSpy --language:IL'", + CommandOptionType.SingleValue); + oLanguage.DefaultValue = null; + + var oConfig = app.Option("-c|--config ", + "Provide a specific configuration file.\r\nExample: 'ILSpy --config:myconfig.xml'", + CommandOptionType.SingleValue); + oConfig.DefaultValue = null; + + var oNoActivate = app.Option("--noactivate", + "Do not activate the existing ILSpy instance. This option has no effect if a new ILSpy instance is being started.", + CommandOptionType.NoValue); // https://natemcmaster.github.io/CommandLineUtils/docs/arguments.html#variable-numbers-of-arguments // To enable this, MultipleValues must be set to true, and the argument must be the last one specified. var files = app.Argument("Assemblies", "Assemblies to load", multipleValues: true); + + // string helptext = app.GetHelpText(); + app.Parse(arguments.ToArray()); if (oInstancing.Value != null) @@ -76,17 +100,10 @@ namespace ICSharpCode.ILSpy } } - if (oNavigateTo.Value != null) - NavigateTo = oNavigateTo.Value(); - - if (oSearch.Value != null) - Search = oSearch.Value(); - - if (oLanguage.Value != null) - Language = oLanguage.Value(); - - if (oConfig.Value != null) - ConfigFile = oConfig.Value(); + NavigateTo = oNavigateTo.ParsedValue; + Search = oSearch.ParsedValue; + Language = oLanguage.ParsedValue; + ConfigFile = oConfig.ParsedValue; if (oNoActivate.HasValue()) NoActivate = true; From d0c443cae1f0569673e987a99a884b2eeac89e3f Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 4 May 2024 08:28:24 +0200 Subject: [PATCH 05/32] Switch to --newinstance --- ILSpy.Tests/CommandLineArgumentsTests.cs | 13 +---- ILSpy/CommandLineArguments.cs | 33 ++--------- doc/Command Line.txt | 73 ++++++++---------------- 3 files changed, 32 insertions(+), 87 deletions(-) diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index 3b60fd716..0d6626d9c 100644 --- a/ILSpy.Tests/CommandLineArgumentsTests.cs +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -22,19 +22,12 @@ namespace ICSharpCode.ILSpy.Tests } [Test] - public void VerifySeparateOption() + public void VerifyForceNewInstanceOption() { - var cmdLineArgs = new CommandLineArguments(new string[] { "--instancing", "Multi" }); + var cmdLineArgs = new CommandLineArguments(new string[] { "--newinstance" }); cmdLineArgs.SingleInstance.Should().BeFalse(); } - [Test] - public void VerifySingleInstanceOption() - { - var cmdLineArgs = new CommandLineArguments(new string[] { "--instancing", "Single" }); - cmdLineArgs.SingleInstance.Should().BeTrue(); - } - [Test] public void VerifyNavigateToOption() { @@ -86,7 +79,7 @@ namespace ICSharpCode.ILSpy.Tests { string filepath = System.IO.Path.GetTempFileName(); - System.IO.File.WriteAllText(filepath, "assembly1 assembly2 assembly3 --instancing multi --noactivate"); + System.IO.File.WriteAllText(filepath, "assembly1 assembly2 assembly3 --newinstance --noactivate"); var cmdLineArgs = new CommandLineArguments(new string[] { $"@{filepath}" }); diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index da46ed883..7822743bb 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -18,18 +18,11 @@ using McMaster.Extensions.CommandLineUtils; -using System; using System.Collections.Generic; using System.Linq; namespace ICSharpCode.ILSpy { - internal enum InstancingMode - { - Single, - Multi - } - sealed class CommandLineArguments { // see /doc/Command Line.txt for details @@ -44,12 +37,13 @@ namespace ICSharpCode.ILSpy public CommandLineArguments(IEnumerable arguments) { var app = new CommandLineApplication() { + // https://natemcmaster.github.io/CommandLineUtils/docs/response-file-parsing.html?tabs=using-attributes ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated, }; - var oInstancing = app.Option("-i|--instancing ", - "Single or multi instance", - CommandOptionType.SingleValue); + var oForceNewInstance = app.Option("--newinstance", + "Start a new instance of ILSpy even if the user configuration is set to single-instance", + CommandOptionType.NoValue); var oNavigateTo = app.Option("-n|--navigateto ", "Navigates to the member specified by the given ID string.\r\nThe member is searched for only in the assemblies specified on the command line.\r\nExample: 'ILSpy ILSpy.exe --navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments'", @@ -79,26 +73,11 @@ namespace ICSharpCode.ILSpy // To enable this, MultipleValues must be set to true, and the argument must be the last one specified. var files = app.Argument("Assemblies", "Assemblies to load", multipleValues: true); - // string helptext = app.GetHelpText(); - app.Parse(arguments.ToArray()); - if (oInstancing.Value != null) - { - if (Enum.TryParse(oInstancing.Value(), true, out var mode)) - { - switch (mode) - { - case InstancingMode.Single: - SingleInstance = true; - break; - case InstancingMode.Multi: - SingleInstance = false; - break; - } - } - } + if (oForceNewInstance.HasValue()) + SingleInstance = false; NavigateTo = oNavigateTo.ParsedValue; Search = oSearch.ParsedValue; diff --git a/doc/Command Line.txt b/doc/Command Line.txt index 1cc041d66..05b6521a5 100644 --- a/doc/Command Line.txt +++ b/doc/Command Line.txt @@ -1,54 +1,27 @@ ILSpy Command Line Arguments -Command line arguments can be either options or file names. -If an argument is a file name, the file will be opened as assembly and added to the current assembly list. +Usage: [options] + @ResponseFile.rsp -Available options: - /singleInstance If ILSpy is already running, activates the existing instance - and passes command line arguments to that instance. - This is the default value if /list is not used. - - /separate Start up a separate ILSpy instance even if it is already running. - - /noActivate Do not activate the existing ILSpy instance. This option has no effect - if a new ILSpy instance is being started. - - /list:listname Specifies the name of the assembly list that is loaded initially. - When this option is not specified, ILSpy loads the previously opened list. - Specify "/list" (without value) to open the default list. - - When this option is used, ILSpy will activate an existing instance - only if it uses the same list as specified. - - [Note: Assembly Lists are not yet implemented] - - /clearList Clears the assembly list before loading the specified assemblies. - [Note: Assembly Lists are not yet implemented] - - /navigateTo:tag Navigates to the member specified by the given ID string. - The member is searched for only in the assemblies specified on the command line. - Example: 'ILSpy ILSpy.exe /navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments' - - The syntax of ID strings is described in appendix A of the C# language specification. - - /language:name Selects the specified language. - Example: 'ILSpy /language:C#' or 'ILSpy /language:IL' +Arguments: + Assemblies Assemblies to load -WM_COPYDATA (SendMessage API): - ILSpy can be controlled by other programs that send a WM_COPYDATA message to its main window. - The message data must be an Unicode (UTF-16) string starting with "ILSpy:\r\n". - All lines except the first ("ILSpy:") in that string are handled as command-line arguments. - There must be exactly one argument per line. - - That is, by sending this message: - ILSpy: - C:\Assembly.dll - /navigateTo:T:Type - The target ILSpy instance will open C:\Assembly.dll and navigate to the specified type. - - ILSpy will return TRUE (1) if it handles the message, and FALSE (0) otherwise. - The /separate option will be ignored; WM_COPYDATA will never start up a new instance. - The /noActivate option has no effect, sending WM_COPYDATA will never activate the window. - Instead, the calling process should use SetForegroundWindow(). - If you use /list with WM_COPYDATA, you need to specify /singleInstance as well, otherwise - ILSpy will not handle the message if it has opened a different assembly list. +Options: + --newinstance Start a new instance of ILSpy even if the user configuration is set to single-instance + -n|--navigateto Navigates to the member specified by the given ID string. + The member is searched for only in the assemblies specified on the command line. + Example: 'ILSpy ILSpy.exe --navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments' + -s|--search Search for t:TypeName, m:Member or c:Constant; use exact match (=term), + 'should not contain' (-term) or 'must contain' (+term); use + /reg(ular)?Ex(pressions)?/ or both - t:/Type(Name)?/... + -l|--language Selects the specified language. + Example: 'ILSpy --language:C#' or 'ILSpy --language:IL' + -c|--config Provide a specific configuration file. + Example: 'ILSpy --config:myconfig.xml' + --noactivate Do not activate the existing ILSpy instance. + This option has no effect if a new ILSpy instance is being started. + +Note on @ResponseFile.rsp: + +* The response file should contain the arguments as if they were passed on the command line (space-separated). +* Use it when the list of assemblies is too long to fit on the command line. \ No newline at end of file From 60251044cd1745589199c2c1da684ec3e44420f1 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 4 May 2024 11:21:07 +0200 Subject: [PATCH 06/32] Adapt ILSpyInstance:Start to new command line options --- ILSpy.AddIn.Shared/ILSpyInstance.cs | 92 ++++---------------- ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj | 1 - ILSpy.AddIn/ILSpy.AddIn.csproj | 1 - publishlocaldev.ps1 | 7 ++ 4 files changed, 23 insertions(+), 78 deletions(-) create mode 100644 publishlocaldev.ps1 diff --git a/ILSpy.AddIn.Shared/ILSpyInstance.cs b/ILSpy.AddIn.Shared/ILSpyInstance.cs index dc90e39a2..c5ebb897e 100644 --- a/ILSpy.AddIn.Shared/ILSpyInstance.cs +++ b/ILSpy.AddIn.Shared/ILSpyInstance.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; namespace ICSharpCode.ILSpy.AddIn { @@ -47,85 +44,28 @@ namespace ICSharpCode.ILSpy.AddIn { var commandLineArguments = parameters?.AssemblyFileNames?.Concat(parameters.Arguments); string ilSpyExe = GetILSpyPath(); - var process = new Process() { - StartInfo = new ProcessStartInfo() { - FileName = ilSpyExe, - UseShellExecute = false, - Arguments = "/navigateTo:none" - } - }; - process.Start(); + + const string defaultOptions = "--navigateto:none"; + string argumentsToPass = defaultOptions; if ((commandLineArguments != null) && commandLineArguments.Any()) { - // Only need a message to started process if there are any parameters to pass - SendMessage(ilSpyExe, "ILSpy:\r\n" + string.Join(Environment.NewLine, commandLineArguments), true); - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "")] - void SendMessage(string ilSpyExe, string message, bool activate) - { - string expectedProcessName = Path.GetFileNameWithoutExtension(ilSpyExe); - // We wait asynchronously until target window can be found and try to find it multiple times - Task.Run(async () => { - bool success = false; - int remainingAttempts = 20; - do - { - NativeMethods.EnumWindows( - (hWnd, lParam) => { - string windowTitle = NativeMethods.GetWindowText(hWnd, 100); - if (windowTitle.StartsWith("ILSpy", StringComparison.Ordinal)) - { - string processName = NativeMethods.GetProcessNameFromWindow(hWnd); - Debug.WriteLine("Found {0:x4}: '{1}' in '{2}'", hWnd, windowTitle, processName); - if (string.Equals(processName, expectedProcessName, StringComparison.OrdinalIgnoreCase)) - { - IntPtr result = Send(hWnd, message); - Debug.WriteLine("WM_COPYDATA result: {0:x8}", result); - if (result == (IntPtr)1) - { - if (activate) - NativeMethods.SetForegroundWindow(hWnd); - success = true; - return false; // stop enumeration - } - } - } - return true; // continue enumeration - }, IntPtr.Zero); + string assemblyArguments = string.Join(" ", commandLineArguments); - // Wait some time before next attempt - await Task.Delay(500); - remainingAttempts--; - } while (!success && (remainingAttempts > 0)); - }); - } + string filepath = Path.GetTempFileName(); + File.WriteAllText(filepath, assemblyArguments); - unsafe static IntPtr Send(IntPtr hWnd, string message) - { - const uint SMTO_NORMAL = 0; + argumentsToPass = $"@\"{filepath}\""; + } - CopyDataStruct lParam; - lParam.Padding = IntPtr.Zero; - lParam.Size = message.Length * 2; - fixed (char* buffer = message) - { - lParam.Buffer = (IntPtr)buffer; - IntPtr result; - // SendMessage with 3s timeout (e.g. when the target process is stopped in the debugger) - if (NativeMethods.SendMessageTimeout( - hWnd, NativeMethods.WM_COPYDATA, IntPtr.Zero, ref lParam, - SMTO_NORMAL, 3000, out result) != IntPtr.Zero) - { - return result; - } - else - { - return IntPtr.Zero; + var process = new Process() { + StartInfo = new ProcessStartInfo() { + FileName = ilSpyExe, + UseShellExecute = false, + Arguments = argumentsToPass } - } + }; + process.Start(); } } } diff --git a/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj b/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj index e0d1db28c..83404ae2f 100644 --- a/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj +++ b/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj @@ -70,7 +70,6 @@ - diff --git a/ILSpy.AddIn/ILSpy.AddIn.csproj b/ILSpy.AddIn/ILSpy.AddIn.csproj index 5ad17dfb5..9b41766fd 100644 --- a/ILSpy.AddIn/ILSpy.AddIn.csproj +++ b/ILSpy.AddIn/ILSpy.AddIn.csproj @@ -76,7 +76,6 @@ - diff --git a/publishlocaldev.ps1 b/publishlocaldev.ps1 new file mode 100644 index 000000000..f236e56ea --- /dev/null +++ b/publishlocaldev.ps1 @@ -0,0 +1,7 @@ +# For local development of the VSIX package - build and publish the x64 build only + +$output_x64 = "./ILSpy/bin/Debug/net8.0-windows/win-x64/publish/fwdependent" + +dotnet publish ./ILSpy/ILSpy.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 +dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 +dotnet publish ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 \ No newline at end of file From d8c4c855f569bcf5d313a63e4c11d3b0004f0925 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 4 May 2024 12:10:22 +0200 Subject: [PATCH 07/32] Document behavior of option case sensitivity --- ILSpy.Tests/CommandLineArgumentsTests.cs | 20 +++++++++++++++++++- ILSpy/CommandLineArguments.cs | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index 0d6626d9c..76f9cc479 100644 --- a/ILSpy.Tests/CommandLineArgumentsTests.cs +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -1,4 +1,6 @@ -using FluentAssertions; +using System; + +using FluentAssertions; using NUnit.Framework; @@ -36,6 +38,22 @@ namespace ICSharpCode.ILSpy.Tests cmdLineArgs.NavigateTo.Should().BeEquivalentTo(navigateTo); } + [Test] + public void VerifyNavigateToOption_NoneTest_Matching_VSAddin() + { + var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateto:none" }); + cmdLineArgs.NavigateTo.Should().BeEquivalentTo("none"); + } + + [Test] + public void VerifyCaseSensitivityOfOptionsThrows() + { + Action act = () => new CommandLineArguments(new string[] { "--navigateTo:none" }); + + act.Should().Throw() + .WithMessage("Unrecognized option '--navigateTo:none'"); + } + [Test] public void VerifySearchOption() { diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index 7822743bb..30a887b79 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -39,6 +39,9 @@ namespace ICSharpCode.ILSpy var app = new CommandLineApplication() { // https://natemcmaster.github.io/CommandLineUtils/docs/response-file-parsing.html?tabs=using-attributes ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated, + + // Note: options are case-sensitive (!), and, default behavior would be UnrecognizedArgumentHandling.Throw on Parse() + // UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue }; var oForceNewInstance = app.Option("--newinstance", From 896bf761413129825dbc778a23eca74f6da7c175 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 12 May 2024 11:40:46 +0200 Subject: [PATCH 08/32] Fix --navigateto for code items --- ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs b/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs index 718304a33..8da05f8bb 100644 --- a/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs +++ b/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs @@ -140,7 +140,7 @@ namespace ICSharpCode.ILSpy.AddIn.Commands return; } - OpenAssembliesInILSpy(new ILSpyParameters(validRefs.Select(r => r.AssemblyFile), "/navigateTo:" + + OpenAssembliesInILSpy(new ILSpyParameters(validRefs.Select(r => r.AssemblyFile), "--navigateto:" + (symbol.OriginalDefinition ?? symbol).GetDocumentationCommentId())); } From 4f217e8f40553b03cb9230d2429f85cb6391e84e Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 12 May 2024 12:23:15 +0200 Subject: [PATCH 09/32] launchSettings.json had a /separate param included (newinstance now) --- ILSpy/Properties/launchSettings.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ILSpy/Properties/launchSettings.json b/ILSpy/Properties/launchSettings.json index 706f4dfc0..f986930a4 100644 --- a/ILSpy/Properties/launchSettings.json +++ b/ILSpy/Properties/launchSettings.json @@ -1,13 +1,13 @@ { - "profiles": { - "ILSpy": { - "commandName": "Executable", - "executablePath": ".\\ILSpy.exe", - "commandLineArgs": "/separate" - }, - "ILSpy single-instance": { - "commandName": "Executable", - "executablePath": ".\\ILSpy.exe" - } - } + "profiles": { + "ILSpy": { + "commandName": "Executable", + "executablePath": ".\\ILSpy.exe", + "commandLineArgs": "--newinstance" + }, + "ILSpy single-instance": { + "commandName": "Executable", + "executablePath": ".\\ILSpy.exe" + } + } } \ No newline at end of file From 1fc9f9291790ab8620d86f88c94f8f92834943e0 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 12 May 2024 13:12:53 +0200 Subject: [PATCH 10/32] Enable help option (going with https://blog.rsuter.com/write-application-can-act-console-application-wpf-gui-application/ to display help leads to a flickering console window) --- ILSpy.Tests/CommandLineArgumentsTests.cs | 7 +++++++ ILSpy/CommandLineArguments.cs | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index 76f9cc479..6efe430a4 100644 --- a/ILSpy.Tests/CommandLineArgumentsTests.cs +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -23,6 +23,13 @@ namespace ICSharpCode.ILSpy.Tests cmdLineArgs.ConfigFile.Should().BeNull(); } + [Test] + public void VerifyHelpOption() + { + var cmdLineArgs = new CommandLineArguments(new string[] { "--help" }); + cmdLineArgs.ArgumentsParser.IsShowingInformation.Should().BeTrue(); + } + [Test] public void VerifyForceNewInstanceOption() { diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index 30a887b79..64736138c 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -23,7 +23,7 @@ using System.Linq; namespace ICSharpCode.ILSpy { - sealed class CommandLineArguments + public sealed class CommandLineArguments { // see /doc/Command Line.txt for details public List AssembliesToLoad = new List(); @@ -34,6 +34,8 @@ namespace ICSharpCode.ILSpy public bool NoActivate; public string ConfigFile; + public CommandLineApplication ArgumentsParser { get; } + public CommandLineArguments(IEnumerable arguments) { var app = new CommandLineApplication() { @@ -44,6 +46,9 @@ namespace ICSharpCode.ILSpy // UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue }; + app.HelpOption(); + ArgumentsParser = app; + var oForceNewInstance = app.Option("--newinstance", "Start a new instance of ILSpy even if the user configuration is set to single-instance", CommandOptionType.NoValue); @@ -76,7 +81,6 @@ namespace ICSharpCode.ILSpy // To enable this, MultipleValues must be set to true, and the argument must be the last one specified. var files = app.Argument("Assemblies", "Assemblies to load", multipleValues: true); - // string helptext = app.GetHelpText(); app.Parse(arguments.ToArray()); if (oForceNewInstance.HasValue()) From 6762dd6f75a4e4251f76b88ec7c400af77026203 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 12 May 2024 13:19:32 +0200 Subject: [PATCH 11/32] Show help - via a dialog box and not a flickering command line --- ILSpy/App.xaml.cs | 5 +++++ ILSpy/CommandLineArguments.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 0e88f19ca..5947d08bc 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -87,6 +87,11 @@ namespace ICSharpCode.ILSpy Hyperlink.RequestNavigateEvent, new RequestNavigateEventHandler(Window_RequestNavigate)); ILSpyTraceListener.Install(); + + if (App.CommandLineArguments.ArgumentsParser.IsShowingInformation) + { + MessageBox.Show(App.CommandLineArguments.ArgumentsParser.GetHelpText(), "ILSpy Command Line Arguments"); + } } static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName) diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index 64736138c..541e8a768 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -54,7 +54,7 @@ namespace ICSharpCode.ILSpy CommandOptionType.NoValue); var oNavigateTo = app.Option("-n|--navigateto ", - "Navigates to the member specified by the given ID string.\r\nThe member is searched for only in the assemblies specified on the command line.\r\nExample: 'ILSpy ILSpy.exe --navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments'", + "Navigates to the member specified by the given ID string.\r\nThe member is searched for only in the assemblies specified on the command line.\r\nExample: 'ILSpy ILSpy.exe --navigateto:T:ICSharpCode.ILSpy.CommandLineArguments'", CommandOptionType.SingleValue); oNavigateTo.DefaultValue = null; From d2221783ba211be78072701876a1ed490248230c Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 12 May 2024 14:11:08 +0200 Subject: [PATCH 12/32] Keep track of temp files and try to clean in QueryClose (if VS does not shut down cleanly obviously files will be left over) --- ILSpy.AddIn.Shared/ILSpyAddInPackage.cs | 18 ++++++++++++++++++ ILSpy.AddIn.Shared/ILSpyInstance.cs | 8 ++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs b/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs index 195940b68..14f3f9227 100644 --- a/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs +++ b/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs @@ -97,6 +97,24 @@ namespace ICSharpCode.ILSpy.AddIn OpenReferenceCommand.Register(this); OpenCodeItemCommand.Register(this); } + + protected override int QueryClose(out bool canClose) + { + var tempFiles = ILSpyInstance.TempFiles; + while (tempFiles.TryPop(out var filename)) + { + try + { + System.IO.File.Delete(filename); + } + catch (Exception) + { + } + } + + return base.QueryClose(out canClose); + } + #endregion public void ShowMessage(string format, params object[] items) diff --git a/ILSpy.AddIn.Shared/ILSpyInstance.cs b/ILSpy.AddIn.Shared/ILSpyInstance.cs index c5ebb897e..f43f9552c 100644 --- a/ILSpy.AddIn.Shared/ILSpyInstance.cs +++ b/ILSpy.AddIn.Shared/ILSpyInstance.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -20,8 +21,9 @@ namespace ICSharpCode.ILSpy.AddIn class ILSpyInstance { - readonly ILSpyParameters parameters; + internal static readonly ConcurrentStack TempFiles = new ConcurrentStack(); + readonly ILSpyParameters parameters; public ILSpyInstance(ILSpyParameters parameters = null) { this.parameters = parameters; @@ -55,6 +57,8 @@ namespace ICSharpCode.ILSpy.AddIn string filepath = Path.GetTempFileName(); File.WriteAllText(filepath, assemblyArguments); + TempFiles.Push(filepath); + argumentsToPass = $"@\"{filepath}\""; } From 049b86712826560a0edf1cbb30dfc688256ee3f9 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 12 May 2024 14:48:42 +0200 Subject: [PATCH 13/32] Special-case @file in FullyQualityPath to avoid situations like System.IO.IOException: The filename, directory name, or volume label syntax is incorrect. : 'D:\GitWorkspace\ILSpy\ILSpy.AddIn\bin\Debug\net472\@C:\Users\christophw\AppData\Local\Temp\tmp1E90.tmp' --- ILSpy/ILSpy.csproj | 1 - ILSpy/SingleInstanceHandling.cs | 6 +++++- publishlocaldev.ps1 | 10 ++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 9d2b317c2..0ecb0caad 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -66,7 +66,6 @@ - diff --git a/ILSpy/SingleInstanceHandling.cs b/ILSpy/SingleInstanceHandling.cs index 5a59f1ee2..5736dffc7 100644 --- a/ILSpy/SingleInstanceHandling.cs +++ b/ILSpy/SingleInstanceHandling.cs @@ -64,10 +64,14 @@ namespace ICSharpCode.ILSpy { // Fully qualify the paths before passing them to another process, // because that process might use a different current directory. - if (string.IsNullOrEmpty(argument) || argument[0] == '/') + if (string.IsNullOrEmpty(argument) || argument[0] == '-') return argument; try { + if (argument.StartsWith("@")) + { + return "@" + FullyQualifyPath(argument.Substring(1)); + } return Path.Combine(Environment.CurrentDirectory, argument); } catch (ArgumentException) diff --git a/publishlocaldev.ps1 b/publishlocaldev.ps1 index f236e56ea..a8ebda02f 100644 --- a/publishlocaldev.ps1 +++ b/publishlocaldev.ps1 @@ -1,7 +1,13 @@ -# For local development of the VSIX package - build and publish the x64 build only +# For local development of the VSIX package - build and publish (VS2022 also needs arm64) $output_x64 = "./ILSpy/bin/Debug/net8.0-windows/win-x64/publish/fwdependent" dotnet publish ./ILSpy/ILSpy.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 -dotnet publish ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 \ No newline at end of file +dotnet publish ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 + +$output_arm64 = "./ILSpy/bin/Debug/net8.0-windows/win-arm64/publish/fwdependent" + +dotnet publish ./ILSpy/ILSpy.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64 +dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64 +dotnet publish ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64 \ No newline at end of file From 84b78b399f03fc67ccb86872d34e2ca5607428e8 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Mon, 13 May 2024 07:04:21 +0200 Subject: [PATCH 14/32] Switch to per-line arguments --- ILSpy.AddIn.Shared/ILSpyInstance.cs | 2 +- ILSpy.Tests/CommandLineArgumentsTests.cs | 12 ++++++++++-- ILSpy/CommandLineArguments.cs | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ILSpy.AddIn.Shared/ILSpyInstance.cs b/ILSpy.AddIn.Shared/ILSpyInstance.cs index f43f9552c..42059aa77 100644 --- a/ILSpy.AddIn.Shared/ILSpyInstance.cs +++ b/ILSpy.AddIn.Shared/ILSpyInstance.cs @@ -52,7 +52,7 @@ namespace ICSharpCode.ILSpy.AddIn if ((commandLineArguments != null) && commandLineArguments.Any()) { - string assemblyArguments = string.Join(" ", commandLineArguments); + string assemblyArguments = string.Join("\r\n", commandLineArguments); string filepath = Path.GetTempFileName(); File.WriteAllText(filepath, assemblyArguments); diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index 6efe430a4..659281df9 100644 --- a/ILSpy.Tests/CommandLineArgumentsTests.cs +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -100,14 +100,22 @@ namespace ICSharpCode.ILSpy.Tests } [Test] - public void PassAtFileArgumentsSpaceSeparated() + public void PassAtFileArguments() { string filepath = System.IO.Path.GetTempFileName(); - System.IO.File.WriteAllText(filepath, "assembly1 assembly2 assembly3 --newinstance --noactivate"); + System.IO.File.WriteAllText(filepath, "assembly1\r\nassembly2\r\nassembly3\r\n--newinstance\r\n--noactivate"); var cmdLineArgs = new CommandLineArguments(new string[] { $"@{filepath}" }); + try + { + System.IO.File.Delete(filepath); + } + catch (Exception) + { + } + cmdLineArgs.SingleInstance.Should().BeFalse(); cmdLineArgs.NoActivate.Should().BeTrue(); cmdLineArgs.AssembliesToLoad.Should().HaveCount(3); diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index 541e8a768..d48141480 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -40,7 +40,7 @@ namespace ICSharpCode.ILSpy { var app = new CommandLineApplication() { // https://natemcmaster.github.io/CommandLineUtils/docs/response-file-parsing.html?tabs=using-attributes - ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated, + ResponseFileHandling = ResponseFileHandling.ParseArgsAsLineSeparated, // Note: options are case-sensitive (!), and, default behavior would be UnrecognizedArgumentHandling.Throw on Parse() // UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue From 8ad24cd470f5cd38172310134ac486d887277d02 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Mon, 13 May 2024 16:40:54 +0200 Subject: [PATCH 15/32] Set UnrecognizedArgumentHandling to CollectAndContinue --- ILSpy.Tests/CommandLineArgumentsTests.cs | 7 +++---- ILSpy/App.xaml.cs | 6 ++++++ ILSpy/CommandLineArguments.cs | 2 +- ILSpy/Properties/launchSettings.json | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index 659281df9..026c86b81 100644 --- a/ILSpy.Tests/CommandLineArgumentsTests.cs +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -53,12 +53,11 @@ namespace ICSharpCode.ILSpy.Tests } [Test] - public void VerifyCaseSensitivityOfOptionsThrows() + public void VerifyCaseSensitivityOfOptionsDoesntThrow() { - Action act = () => new CommandLineArguments(new string[] { "--navigateTo:none" }); + var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateTo:none" }); - act.Should().Throw() - .WithMessage("Unrecognized option '--navigateTo:none'"); + cmdLineArgs.ArgumentsParser.RemainingArguments.Should().HaveCount(1); } [Test] diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 5947d08bc..5779727c4 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -92,6 +92,12 @@ namespace ICSharpCode.ILSpy { MessageBox.Show(App.CommandLineArguments.ArgumentsParser.GetHelpText(), "ILSpy Command Line Arguments"); } + + if (App.CommandLineArguments.ArgumentsParser.RemainingArguments.Any()) + { + string unknownArguments = string.Join(", ", App.CommandLineArguments.ArgumentsParser.RemainingArguments); + MessageBox.Show(unknownArguments, "ILSpy Unknown Command Line Arguments Passed"); + } } static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName) diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index d48141480..df1114e38 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -43,7 +43,7 @@ namespace ICSharpCode.ILSpy ResponseFileHandling = ResponseFileHandling.ParseArgsAsLineSeparated, // Note: options are case-sensitive (!), and, default behavior would be UnrecognizedArgumentHandling.Throw on Parse() - // UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue + UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue }; app.HelpOption(); diff --git a/ILSpy/Properties/launchSettings.json b/ILSpy/Properties/launchSettings.json index f986930a4..79731519a 100644 --- a/ILSpy/Properties/launchSettings.json +++ b/ILSpy/Properties/launchSettings.json @@ -2,12 +2,12 @@ "profiles": { "ILSpy": { "commandName": "Executable", - "executablePath": ".\\ILSpy.exe", + "executablePath": "./ilspy.exe", "commandLineArgs": "--newinstance" }, "ILSpy single-instance": { "commandName": "Executable", - "executablePath": ".\\ILSpy.exe" + "executablePath": "./ilspy.exe" } } } \ No newline at end of file From e4e2dd3bff8f2e569cac6a80e23b51d410a267b2 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Tue, 14 May 2024 07:46:25 +0200 Subject: [PATCH 16/32] Fix docs for space for line syntax in response file --- doc/Command Line.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Command Line.txt b/doc/Command Line.txt index 05b6521a5..62a3a7551 100644 --- a/doc/Command Line.txt +++ b/doc/Command Line.txt @@ -23,5 +23,5 @@ Options: Note on @ResponseFile.rsp: -* The response file should contain the arguments as if they were passed on the command line (space-separated). +* The response file should contain the arguments, one argument per line (not space-separated!). * Use it when the list of assemblies is too long to fit on the command line. \ No newline at end of file From e64795aad8f17c1896f6cb77faef81fe7703f480 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 26 May 2024 11:56:09 +0200 Subject: [PATCH 17/32] Prepare new AppEnv namespace that will house all application environment code (configuration and runtime) --- ILSpy/App.xaml.cs | 1 + ILSpy/{ => AppEnv}/CommandLineArguments.cs | 2 +- ILSpy/{ => AppEnv}/CommandLineTools.cs | 2 +- ILSpy/Commands/ScopeSearchToAssembly.cs | 1 + ILSpy/Commands/ScopeSearchToNamespace.cs | 1 + ILSpy/MainWindow.xaml.cs | 1 + ILSpy/Options/MiscSettingsViewModel.cs | 1 + ILSpy/Search/SearchPane.cs | 1 + 8 files changed, 8 insertions(+), 2 deletions(-) rename ILSpy/{ => AppEnv}/CommandLineArguments.cs (99%) rename ILSpy/{ => AppEnv}/CommandLineTools.cs (99%) diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 5779727c4..26bad4cfe 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -30,6 +30,7 @@ using System.Windows.Documents; using System.Windows.Navigation; using System.Windows.Threading; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Options; using ICSharpCode.ILSpyX.Analyzers; using ICSharpCode.ILSpyX.Settings; diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/AppEnv/CommandLineArguments.cs similarity index 99% rename from ILSpy/CommandLineArguments.cs rename to ILSpy/AppEnv/CommandLineArguments.cs index df1114e38..116d8b188 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/AppEnv/CommandLineArguments.cs @@ -21,7 +21,7 @@ using McMaster.Extensions.CommandLineUtils; using System.Collections.Generic; using System.Linq; -namespace ICSharpCode.ILSpy +namespace ICSharpCode.ILSpy.AppEnv { public sealed class CommandLineArguments { diff --git a/ILSpy/CommandLineTools.cs b/ILSpy/AppEnv/CommandLineTools.cs similarity index 99% rename from ILSpy/CommandLineTools.cs rename to ILSpy/AppEnv/CommandLineTools.cs index 898981996..4ff581ba6 100644 --- a/ILSpy/CommandLineTools.cs +++ b/ILSpy/AppEnv/CommandLineTools.cs @@ -20,7 +20,7 @@ using System; using System.Collections.Generic; using System.Text; -namespace ICSharpCode.ILSpy +namespace ICSharpCode.ILSpy.AppEnv { public class CommandLineTools { diff --git a/ILSpy/Commands/ScopeSearchToAssembly.cs b/ILSpy/Commands/ScopeSearchToAssembly.cs index 861e7c8f5..1ec6ef73b 100644 --- a/ILSpy/Commands/ScopeSearchToAssembly.cs +++ b/ILSpy/Commands/ScopeSearchToAssembly.cs @@ -20,6 +20,7 @@ using System; using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.TreeNodes; diff --git a/ILSpy/Commands/ScopeSearchToNamespace.cs b/ILSpy/Commands/ScopeSearchToNamespace.cs index 2c5fead92..11a411ed6 100644 --- a/ILSpy/Commands/ScopeSearchToNamespace.cs +++ b/ILSpy/Commands/ScopeSearchToNamespace.cs @@ -18,6 +18,7 @@ using System; using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.TreeNodes; diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 999abdca3..474a86dd2 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -44,6 +44,7 @@ using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem.Implementation; using ICSharpCode.ILSpy.Analyzers; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Commands; using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.Options; diff --git a/ILSpy/Options/MiscSettingsViewModel.cs b/ILSpy/Options/MiscSettingsViewModel.cs index 6e9562e1b..bb29b91e3 100644 --- a/ILSpy/Options/MiscSettingsViewModel.cs +++ b/ILSpy/Options/MiscSettingsViewModel.cs @@ -24,6 +24,7 @@ using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Input; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Commands; using ICSharpCode.ILSpyX.Settings; diff --git a/ILSpy/Search/SearchPane.cs b/ILSpy/Search/SearchPane.cs index 87a65c6fa..55a29370c 100644 --- a/ILSpy/Search/SearchPane.cs +++ b/ILSpy/Search/SearchPane.cs @@ -32,6 +32,7 @@ using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpyX; From 82fece3d167d0209776da6cd8b697dd6d476c283 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 26 May 2024 12:23:06 +0200 Subject: [PATCH 18/32] Add unmodified Medo SingleInstance https://github.com/medo64/Medo/blob/main/src/Medo/Application/SingleInstance.cs --- ILSpy.Tests/CommandLineArgumentsTests.cs | 2 + ILSpy/App.xaml.cs | 16 +- ILSpy/AppEnv/CommandLineTools.cs | 21 ++ ILSpy/AppEnv/SingleInstance.cs | 290 +++++++++++++++++++++++ ILSpy/MainWindow.xaml.cs | 40 +--- ILSpy/SingleInstanceHandling.cs | 158 ------------ 6 files changed, 331 insertions(+), 196 deletions(-) create mode 100644 ILSpy/AppEnv/SingleInstance.cs delete mode 100644 ILSpy/SingleInstanceHandling.cs diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index 026c86b81..addf682d4 100644 --- a/ILSpy.Tests/CommandLineArgumentsTests.cs +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -2,6 +2,8 @@ using FluentAssertions; +using ICSharpCode.ILSpy.AppEnv; + using NUnit.Framework; namespace ICSharpCode.ILSpy.Tests diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 26bad4cfe..86dbc410c 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -35,6 +35,8 @@ using ICSharpCode.ILSpy.Options; using ICSharpCode.ILSpyX.Analyzers; using ICSharpCode.ILSpyX.Settings; +using Medo.Application; + using Microsoft.VisualStudio.Composition; using TomsToolbox.Wpf.Styles; @@ -69,7 +71,8 @@ namespace ICSharpCode.ILSpy && !MiscSettingsPanel.CurrentMiscSettings.AllowMultipleInstances; if (forceSingleInstance) { - SingleInstanceHandling.ForceSingleInstance(cmdArgs); + SingleInstance.Attach(); // will auto-exit for second instance + SingleInstance.NewInstanceDetected += SingleInstance_NewInstanceDetected; } InitializeComponent(); @@ -101,6 +104,17 @@ namespace ICSharpCode.ILSpy } } + private static void SingleInstance_NewInstanceDetected(object? sender, NewInstanceEventArgs e) + { + var mainWindow = ICSharpCode.ILSpy.MainWindow.Instance; + + var args = new CommandLineArguments(e.Args); + if (mainWindow.HandleCommandLineArguments(args)) + { + mainWindow.HandleCommandLineArgumentsAfterShowList(args); + } + } + static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName) { var rootPath = Path.GetDirectoryName(typeof(App).Assembly.Location); diff --git a/ILSpy/AppEnv/CommandLineTools.cs b/ILSpy/AppEnv/CommandLineTools.cs index 4ff581ba6..7379db12e 100644 --- a/ILSpy/AppEnv/CommandLineTools.cs +++ b/ILSpy/AppEnv/CommandLineTools.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; namespace ICSharpCode.ILSpy.AppEnv @@ -110,6 +111,26 @@ namespace ICSharpCode.ILSpy.AppEnv b.Append('"'); } } + + public static string FullyQualifyPath(string argument) + { + // Fully qualify the paths before passing them to another process, + // because that process might use a different current directory. + if (string.IsNullOrEmpty(argument) || argument[0] == '-') + return argument; + try + { + if (argument.StartsWith("@")) + { + return "@" + FullyQualifyPath(argument.Substring(1)); + } + return Path.Combine(Environment.CurrentDirectory, argument); + } + catch (ArgumentException) + { + return argument; + } + } } // Source: https://github.com/dotnet/runtime/blob/bc9fc5a774d96f95abe0ea5c90fac48b38ed2e67/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs#L574-L606 diff --git a/ILSpy/AppEnv/SingleInstance.cs b/ILSpy/AppEnv/SingleInstance.cs new file mode 100644 index 000000000..0983eeab4 --- /dev/null +++ b/ILSpy/AppEnv/SingleInstance.cs @@ -0,0 +1,290 @@ +// Source: https://github.com/medo64/Medo/blob/main/src/Medo/Application/SingleInstance.cs + +/* Josip Medved * www.medo64.com * MIT License */ + +//2022-12-01: Compatible with .NET 6 and 7 +//2012-11-24: Suppressing bogus CA5122 warning (http://connect.microsoft.com/VisualStudio/feedback/details/729254/bogus-ca5122-warning-about-p-invoke-declarations-should-not-be-safe-critical) +//2010-10-07: Added IsOtherInstanceRunning method +//2008-11-14: Reworked code to use SafeHandle +//2008-04-11: Cleaned code to match FxCop 1.36 beta 2 (SpecifyMarshalingForPInvokeStringArguments, NestedTypesShouldNotBeVisible) +//2008-04-10: NewInstanceEventArgs is not nested class anymore +//2008-01-26: AutoExit parameter changed to NoAutoExit +//2008-01-08: Main method is now called Attach +//2008-01-06: System.Environment.Exit returns E_ABORT (0x80004004) +//2008-01-03: Added Resources +//2007-12-29: New version + +namespace Medo.Application; + +using System; +using System.Diagnostics; +using System.IO.Pipes; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; + +/// +/// Handles detection and communication of programs multiple instances. +/// This class is thread safe. +/// +public static class SingleInstance +{ + + private static Mutex? _mtxFirstInstance; + private static Thread? _thread; + private static readonly object _syncRoot = new(); + + + /// + /// Returns true if this application is not already started. + /// Another instance is contacted via named pipe. + /// + /// API call failed. + public static bool Attach() + { + return Attach(false); + } + + /// + /// Returns true if this application is not already started. + /// Another instance is contacted via named pipe. + /// + /// If true, application will exit after informing another instance. + /// API call failed. + public static bool Attach(bool noAutoExit) + { + lock (_syncRoot) + { + var isFirstInstance = false; + try + { + _mtxFirstInstance = new Mutex(initiallyOwned: true, @"Global\" + MutexName, out isFirstInstance); + if (isFirstInstance == false) + { //we need to contact previous instance + var contentObject = new SingleInstanceArguments() { + CommandLine = Environment.CommandLine, + CommandLineArgs = Environment.GetCommandLineArgs(), + }; + var contentBytes = JsonSerializer.SerializeToUtf8Bytes(contentObject); + using var clientPipe = new NamedPipeClientStream(".", + MutexName, + PipeDirection.Out, + PipeOptions.CurrentUserOnly | PipeOptions.WriteThrough); + clientPipe.Connect(); + clientPipe.Write(contentBytes, 0, contentBytes.Length); + } + else + { //there is no application already running. + _thread = new Thread(Run) { + Name = typeof(SingleInstance).FullName, + IsBackground = true + }; + _thread.Start(); + } + } + catch (Exception ex) + { + Trace.TraceWarning(ex.Message + " {Medo.Application.SingleInstance}"); + } + + if ((isFirstInstance == false) && (noAutoExit == false)) + { + Trace.TraceInformation("Exit due to another instance running." + " [" + nameof(SingleInstance) + "]"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Environment.Exit(unchecked((int)0x80004004)); // E_ABORT(0x80004004) + } + else + { + Environment.Exit(114); // EALREADY(114) + } + } + + return isFirstInstance; + } + } + + private static string? _mutexName; + private static string MutexName { + get { + lock (_syncRoot) + { + if (_mutexName == null) + { + var assembly = Assembly.GetEntryAssembly(); + + var sbMutextName = new StringBuilder(); + var assName = assembly?.GetName().Name; + if (assName != null) + { + sbMutextName.Append(assName, 0, Math.Min(assName.Length, 31)); + sbMutextName.Append('.'); + } + + var sbHash = new StringBuilder(); + sbHash.AppendLine(Environment.MachineName); + sbHash.AppendLine(Environment.UserName); + if (assembly != null) + { + sbHash.AppendLine(assembly.FullName); + sbHash.AppendLine(assembly.Location); + } + else + { + var args = Environment.GetCommandLineArgs(); + if (args.Length > 0) + { sbHash.AppendLine(args[0]); } + } + foreach (var b in SHA256.HashData(Encoding.UTF8.GetBytes(sbHash.ToString()))) + { + if (sbMutextName.Length == 63) + { sbMutextName.AppendFormat("{0:X1}", b >> 4); } // just take the first nubble + if (sbMutextName.Length == 64) + { break; } + sbMutextName.AppendFormat("{0:X2}", b); + } + _mutexName = sbMutextName.ToString(); + } + return _mutexName; + } + } + } + + /// + /// Gets whether there is another instance running. + /// It temporary creates mutex. + /// + public static bool IsOtherInstanceRunning { + get { + lock (_syncRoot) + { + if (_mtxFirstInstance != null) + { + return false; //no other instance is running + } + else + { + var tempInstance = new Mutex(true, MutexName, out var isFirstInstance); + tempInstance.Close(); + return (isFirstInstance == false); + } + } + } + } + + /// + /// Occurs in first instance when new instance is detected. + /// + public static event EventHandler? NewInstanceDetected; + + + /// + /// Thread function. + /// + private static void Run() + { + using var serverPipe = new NamedPipeServerStream(MutexName, + PipeDirection.In, + maxNumberOfServerInstances: 1, + PipeTransmissionMode.Byte, + PipeOptions.CurrentUserOnly | PipeOptions.WriteThrough); + while (_mtxFirstInstance != null) + { + try + { + if (!serverPipe.IsConnected) + { serverPipe.WaitForConnection(); } + var contentObject = JsonSerializer.Deserialize(serverPipe); + serverPipe.Disconnect(); + if (contentObject != null) + { + NewInstanceDetected?.Invoke(null, + new NewInstanceEventArgs( + contentObject.CommandLine, + contentObject.CommandLineArgs)); + } + } + catch (Exception ex) + { + Trace.TraceWarning(ex.Message + " [" + nameof(SingleInstance) + "]"); + Thread.Sleep(100); + } + } + } + + + [Serializable] + private sealed record SingleInstanceArguments + { // just a storage +#if NET7_0_OR_GREATER + [JsonInclude] + public required string CommandLine; + + [JsonInclude] + public required string[] CommandLineArgs; +#else + public SingleInstanceArguments() { + CommandLine = string.Empty; + CommandLineArgs = Array.Empty(); + } + + [JsonInclude] + public string CommandLine; + + [JsonInclude] + public string[] CommandLineArgs; +#endif + } + +} + + +/// +/// Arguments for newly detected application instance. +/// +public sealed class NewInstanceEventArgs : EventArgs +{ + /// + /// Creates new instance. + /// + /// Command line. + /// String array containing the command line arguments in the same format as Environment.GetCommandLineArgs. + internal NewInstanceEventArgs(string commandLine, string[] commandLineArgs) + { + CommandLine = commandLine; + _commandLineArgs = new string[commandLineArgs.Length]; + Array.Copy(commandLineArgs, _commandLineArgs, _commandLineArgs.Length); + } + + /// + /// Gets the command line. + /// + public string CommandLine { get; } + + private readonly string[] _commandLineArgs; + /// + /// Returns a string array containing the command line arguments. + /// + public string[] GetCommandLineArgs() + { + var argCopy = new string[_commandLineArgs.Length]; + Array.Copy(_commandLineArgs, argCopy, argCopy.Length); + return argCopy; + } + + /// + /// Gets a string array containing the command line arguments without the name of exectuable. + /// + public string[] Args { + get { + var argCopy = new string[_commandLineArgs.Length - 1]; + Array.Copy(_commandLineArgs, 1, argCopy, 0, argCopy.Length); + return argCopy; + } + } + +} diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 474a86dd2..5b01ec67a 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -600,12 +600,7 @@ namespace ICSharpCode.ILSpy { base.OnSourceInitialized(e); PresentationSource source = PresentationSource.FromVisual(this); - HwndSource hwndSource = source as HwndSource; - if (hwndSource != null) - { - hwndSource.AddHook(WndProc); - } - SingleInstanceHandling.ReleaseSingleInstanceMutex(); + // Validate and Set Window Bounds Rect bounds = Rect.Transform(sessionSettings.WindowBounds, source.CompositionTarget.TransformToDevice); var boundsRect = new System.Drawing.Rectangle((int)bounds.Left, (int)bounds.Top, (int)bounds.Width, (int)bounds.Height); @@ -624,35 +619,6 @@ namespace ICSharpCode.ILSpy this.WindowState = sessionSettings.WindowState; } - unsafe IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) - { - if (msg == NativeMethods.WM_COPYDATA) - { - CopyDataStruct* copyData = (CopyDataStruct*)lParam; - string data = new string((char*)copyData->Buffer, 0, copyData->Size / sizeof(char)); - if (data.StartsWith("ILSpy:\r\n", StringComparison.Ordinal)) - { - data = data.Substring(8); - List lines = new List(); - using (StringReader r = new StringReader(data)) - { - string line; - while ((line = r.ReadLine()) != null) - lines.Add(line); - } - var args = new CommandLineArguments(lines); - if (HandleCommandLineArguments(args)) - { - if (!args.NoActivate && WindowState == WindowState.Minimized) - WindowState = WindowState.Normal; - HandleCommandLineArgumentsAfterShowList(args); - handled = true; - return (IntPtr)1; - } - } - } - return IntPtr.Zero; - } #endregion protected override void OnKeyDown(KeyEventArgs e) @@ -686,7 +652,7 @@ namespace ICSharpCode.ILSpy List commandLineLoadedAssemblies = new List(); - bool HandleCommandLineArguments(CommandLineArguments args) + internal bool HandleCommandLineArguments(CommandLineArguments args) { LoadAssemblies(args.AssembliesToLoad, commandLineLoadedAssemblies, focusNode: false); if (args.Language != null) @@ -698,7 +664,7 @@ namespace ICSharpCode.ILSpy /// Called on startup or when passed arguments via WndProc from a second instance. /// In the format case, spySettings is non-null; in the latter it is null. /// - void HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ILSpySettings spySettings = null) + internal void HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ILSpySettings spySettings = null) { var relevantAssemblies = commandLineLoadedAssemblies.ToList(); commandLineLoadedAssemblies.Clear(); // clear references once we don't need them anymore diff --git a/ILSpy/SingleInstanceHandling.cs b/ILSpy/SingleInstanceHandling.cs deleted file mode 100644 index 5736dffc7..000000000 --- a/ILSpy/SingleInstanceHandling.cs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2022 AlphaSierraPapa for the SharpDevelop Team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this -// software and associated documentation files (the "Software"), to deal in the Software -// without restriction, including without limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons -// to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE -// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; - -namespace ICSharpCode.ILSpy -{ - internal static class SingleInstanceHandling - { - internal static Mutex SingleInstanceMutex; - - internal static void ForceSingleInstance(IEnumerable cmdArgs) - { - bool isFirst; - try - { - SingleInstanceMutex = new Mutex(initiallyOwned: true, @"Local\ILSpyInstance", out isFirst); - } - catch (WaitHandleCannotBeOpenedException) - { - isFirst = true; - } - if (!isFirst) - { - try - { - SingleInstanceMutex.WaitOne(10000); - } - catch (AbandonedMutexException) - { - // continue, there is no concurrent start happening. - } - } - cmdArgs = cmdArgs.Select(FullyQualifyPath); - string message = string.Join(Environment.NewLine, cmdArgs); - if (SendToPreviousInstance("ILSpy:\r\n" + message, !App.CommandLineArguments.NoActivate)) - { - ReleaseSingleInstanceMutex(); - Environment.Exit(0); - } - } - - internal static string FullyQualifyPath(string argument) - { - // Fully qualify the paths before passing them to another process, - // because that process might use a different current directory. - if (string.IsNullOrEmpty(argument) || argument[0] == '-') - return argument; - try - { - if (argument.StartsWith("@")) - { - return "@" + FullyQualifyPath(argument.Substring(1)); - } - return Path.Combine(Environment.CurrentDirectory, argument); - } - catch (ArgumentException) - { - return argument; - } - } - - internal static void ReleaseSingleInstanceMutex() - { - var mutex = SingleInstanceMutex; - SingleInstanceMutex = null; - if (mutex == null) - { - return; - } - using (mutex) - { - mutex.ReleaseMutex(); - } - } - - #region Pass Command Line Arguments to previous instance - internal static bool SendToPreviousInstance(string message, bool activate) - { - string ownProcessName; - using (var ownProcess = Process.GetCurrentProcess()) - { - ownProcessName = ownProcess.ProcessName; - } - - bool success = false; - NativeMethods.EnumWindows( - (hWnd, lParam) => { - string windowTitle = NativeMethods.GetWindowText(hWnd, 100); - if (windowTitle.StartsWith("ILSpy", StringComparison.Ordinal)) - { - string processName = NativeMethods.GetProcessNameFromWindow(hWnd); - Debug.WriteLine("Found {0:x4}: '{1}' in '{2}'", hWnd, windowTitle, processName); - if (string.Equals(processName, ownProcessName, StringComparison.OrdinalIgnoreCase)) - { - IntPtr result = Send(hWnd, message); - Debug.WriteLine("WM_COPYDATA result: {0:x8}", result); - if (result == (IntPtr)1) - { - if (activate) - NativeMethods.SetForegroundWindow(hWnd); - success = true; - return false; // stop enumeration - } - } - } - return true; // continue enumeration - }, IntPtr.Zero); - return success; - } - - unsafe static IntPtr Send(IntPtr hWnd, string message) - { - const uint SMTO_NORMAL = 0; - - CopyDataStruct lParam; - lParam.Padding = IntPtr.Zero; - lParam.Size = message.Length * 2; - fixed (char* buffer = message) - { - lParam.Buffer = (IntPtr)buffer; - IntPtr result; - // SendMessage with 3s timeout (e.g. when the target process is stopped in the debugger) - if (NativeMethods.SendMessageTimeout( - hWnd, NativeMethods.WM_COPYDATA, IntPtr.Zero, ref lParam, - SMTO_NORMAL, 3000, out result) != IntPtr.Zero) - { - return result; - } - else - { - return IntPtr.Zero; - } - } - } - #endregion - } -} From 5fa25a606c0dbb6579c8de3c27fe56fe78d14e2d Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 26 May 2024 12:29:32 +0200 Subject: [PATCH 19/32] Add relative path logic to new SingleInstance code --- ILSpy/AppEnv/SingleInstance.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ILSpy/AppEnv/SingleInstance.cs b/ILSpy/AppEnv/SingleInstance.cs index 0983eeab4..55cb6a60b 100644 --- a/ILSpy/AppEnv/SingleInstance.cs +++ b/ILSpy/AppEnv/SingleInstance.cs @@ -19,6 +19,7 @@ namespace Medo.Application; using System; using System.Diagnostics; using System.IO.Pipes; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -27,6 +28,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; +using ICSharpCode.ILSpy.AppEnv; + /// /// Handles detection and communication of programs multiple instances. /// This class is thread safe. @@ -49,6 +52,13 @@ public static class SingleInstance return Attach(false); } + private static string[] GetILSpyCommandLineArgs() + { + var cmdArgs = Environment.GetCommandLineArgs().Skip(1); + cmdArgs = cmdArgs.Select(CommandLineTools.FullyQualifyPath); + return cmdArgs.ToArray(); + } + /// /// Returns true if this application is not already started. /// Another instance is contacted via named pipe. @@ -67,7 +77,7 @@ public static class SingleInstance { //we need to contact previous instance var contentObject = new SingleInstanceArguments() { CommandLine = Environment.CommandLine, - CommandLineArgs = Environment.GetCommandLineArgs(), + CommandLineArgs = GetILSpyCommandLineArgs(), }; var contentBytes = JsonSerializer.SerializeToUtf8Bytes(contentObject); using var clientPipe = new NamedPipeClientStream(".", From 2f93d41abbcb4e6fdabe8deae6e3a48601eba84f Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 26 May 2024 12:29:48 +0200 Subject: [PATCH 20/32] Remove obsolete native imports --- ILSpy/NativeMethods.cs | 79 ------------------------------------------ 1 file changed, 79 deletions(-) diff --git a/ILSpy/NativeMethods.cs b/ILSpy/NativeMethods.cs index d24aeba6d..8ba9b02fa 100644 --- a/ILSpy/NativeMethods.cs +++ b/ILSpy/NativeMethods.cs @@ -17,73 +17,12 @@ // DEALINGS IN THE SOFTWARE. using System; -using System.ComponentModel; -using System.Diagnostics; using System.Runtime.InteropServices; -using System.Text; namespace ICSharpCode.ILSpy { static class NativeMethods { - public const uint WM_COPYDATA = 0x4a; - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - internal static extern unsafe int GetWindowThreadProcessId(IntPtr hWnd, int* lpdwProcessId); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - static extern int GetWindowText(IntPtr hWnd, [Out] StringBuilder title, int size); - - public static string GetWindowText(IntPtr hWnd, int maxLength) - { - StringBuilder b = new StringBuilder(maxLength + 1); - if (GetWindowText(hWnd, b, b.Capacity) != 0) - return b.ToString(); - else - return string.Empty; - } - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - internal static extern IntPtr SendMessageTimeout( - IntPtr hWnd, uint msg, IntPtr wParam, ref CopyDataStruct lParam, - uint flags, uint timeout, out IntPtr result); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool SetForegroundWindow(IntPtr hWnd); - - public unsafe static string GetProcessNameFromWindow(IntPtr hWnd) - { - int processId; - GetWindowThreadProcessId(hWnd, &processId); - try - { - using (var p = Process.GetProcessById(processId)) - { - return p.ProcessName; - } - } - catch (ArgumentException ex) - { - Debug.WriteLine(ex.Message); - return null; - } - catch (InvalidOperationException ex) - { - Debug.WriteLine(ex.Message); - return null; - } - catch (Win32Exception ex) - { - Debug.WriteLine(ex.Message); - return null; - } - } - [DllImport("dwmapi.dll", PreserveSig = true)] public static extern int DwmSetWindowAttribute(IntPtr hwnd, DwmWindowAttribute attr, ref int attrValue, int attrSize); @@ -95,24 +34,6 @@ namespace ICSharpCode.ILSpy } } - [return: MarshalAs(UnmanagedType.Bool)] - delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - - [StructLayout(LayoutKind.Sequential)] - struct CopyDataStruct - { - public IntPtr Padding; - public int Size; - public IntPtr Buffer; - - public CopyDataStruct(IntPtr padding, int size, IntPtr buffer) - { - this.Padding = padding; - this.Size = size; - this.Buffer = buffer; - } - } - public enum DwmWindowAttribute : uint { NCRenderingEnabled = 1, From 7946f720242ae693f04bd0257e47eebc6300bba3 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 26 May 2024 13:05:47 +0200 Subject: [PATCH 21/32] Centralize single instancing logic back in MainWindow, fix Skip(1) error due to .Args property implementation --- ILSpy/App.xaml.cs | 8 +------- ILSpy/AppEnv/SingleInstance.cs | 7 ++++--- ILSpy/MainWindow.xaml.cs | 16 ++++++++++++++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 86dbc410c..2387c487a 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -106,13 +106,7 @@ namespace ICSharpCode.ILSpy private static void SingleInstance_NewInstanceDetected(object? sender, NewInstanceEventArgs e) { - var mainWindow = ICSharpCode.ILSpy.MainWindow.Instance; - - var args = new CommandLineArguments(e.Args); - if (mainWindow.HandleCommandLineArguments(args)) - { - mainWindow.HandleCommandLineArgumentsAfterShowList(args); - } + ICSharpCode.ILSpy.MainWindow.Instance.HandleSingleInstanceCommandLineArguments(e.Args); } static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName) diff --git a/ILSpy/AppEnv/SingleInstance.cs b/ILSpy/AppEnv/SingleInstance.cs index 55cb6a60b..eb8e39966 100644 --- a/ILSpy/AppEnv/SingleInstance.cs +++ b/ILSpy/AppEnv/SingleInstance.cs @@ -54,9 +54,10 @@ public static class SingleInstance private static string[] GetILSpyCommandLineArgs() { - var cmdArgs = Environment.GetCommandLineArgs().Skip(1); - cmdArgs = cmdArgs.Select(CommandLineTools.FullyQualifyPath); - return cmdArgs.ToArray(); + // Note: NO Skip(1) here because .Args property on SingleInstanceArguments does this for us + return Environment.GetCommandLineArgs().AsEnumerable() + .Select(CommandLineTools.FullyQualifyPath) + .ToArray(); } /// diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 5b01ec67a..757cdbe45 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -652,7 +652,19 @@ namespace ICSharpCode.ILSpy List commandLineLoadedAssemblies = new List(); - internal bool HandleCommandLineArguments(CommandLineArguments args) + internal async Task HandleSingleInstanceCommandLineArguments(string[] args) + { + var cmdArgs = new CommandLineArguments(args); + + await Dispatcher.InvokeAsync(() => { + if (HandleCommandLineArguments(cmdArgs)) + { + HandleCommandLineArgumentsAfterShowList(cmdArgs); + } + }); + } + + bool HandleCommandLineArguments(CommandLineArguments args) { LoadAssemblies(args.AssembliesToLoad, commandLineLoadedAssemblies, focusNode: false); if (args.Language != null) @@ -664,7 +676,7 @@ namespace ICSharpCode.ILSpy /// Called on startup or when passed arguments via WndProc from a second instance. /// In the format case, spySettings is non-null; in the latter it is null. /// - internal void HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ILSpySettings spySettings = null) + void HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ILSpySettings spySettings = null) { var relevantAssemblies = commandLineLoadedAssemblies.ToList(); commandLineLoadedAssemblies.Clear(); // clear references once we don't need them anymore From 8fa41a9cb019eb11a31c39120f80708864828dd5 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 26 May 2024 13:18:32 +0200 Subject: [PATCH 22/32] Remove obsolete code --- ILSpy/AppEnv/SingleInstance.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ILSpy/AppEnv/SingleInstance.cs b/ILSpy/AppEnv/SingleInstance.cs index eb8e39966..22d5ce6e0 100644 --- a/ILSpy/AppEnv/SingleInstance.cs +++ b/ILSpy/AppEnv/SingleInstance.cs @@ -231,24 +231,11 @@ public static class SingleInstance [Serializable] private sealed record SingleInstanceArguments { // just a storage -#if NET7_0_OR_GREATER [JsonInclude] public required string CommandLine; [JsonInclude] public required string[] CommandLineArgs; -#else - public SingleInstanceArguments() { - CommandLine = string.Empty; - CommandLineArgs = Array.Empty(); - } - - [JsonInclude] - public string CommandLine; - - [JsonInclude] - public string[] CommandLineArgs; -#endif } } From 94654e6d4952531155804c8722c30f63ebe48d0b Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 26 May 2024 13:37:10 +0200 Subject: [PATCH 23/32] Add back WindowState handling from command line arguments --- ILSpy/MainWindow.xaml.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index 757cdbe45..eed3732db 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -659,6 +659,9 @@ namespace ICSharpCode.ILSpy await Dispatcher.InvokeAsync(() => { if (HandleCommandLineArguments(cmdArgs)) { + if (!cmdArgs.NoActivate && WindowState == WindowState.Minimized) + WindowState = WindowState.Normal; + HandleCommandLineArgumentsAfterShowList(cmdArgs); } }); From bc8decc74847b05fd5e44e59a3fb7fc4b44f6613 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 1 Jun 2024 15:42:56 +0200 Subject: [PATCH 24/32] Switch from DllImport to LibraryImport source generator --- ILSpy/NativeMethods.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ILSpy/NativeMethods.cs b/ILSpy/NativeMethods.cs index 8ba9b02fa..ba3e03b67 100644 --- a/ILSpy/NativeMethods.cs +++ b/ILSpy/NativeMethods.cs @@ -21,16 +21,19 @@ using System.Runtime.InteropServices; namespace ICSharpCode.ILSpy { - static class NativeMethods + // Uses https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation + internal static partial class NativeMethods { - [DllImport("dwmapi.dll", PreserveSig = true)] - public static extern int DwmSetWindowAttribute(IntPtr hwnd, DwmWindowAttribute attr, ref int attrValue, int attrSize); + const int S_OK = 0; + + [LibraryImport("dwmapi.dll", EntryPoint = "DwmSetWindowAttribute")] + internal static partial int DwmSetWindowAttribute(IntPtr hwnd, DwmWindowAttribute attr, ref int attrValue, int attrSize); public static bool UseImmersiveDarkMode(IntPtr hWnd, bool enable) { int darkMode = enable ? 1 : 0; - int hr = DwmSetWindowAttribute(hWnd, DwmWindowAttribute.UseImmersiveDarkMode, ref darkMode, sizeof(int)); - return hr >= 0; + int hResult = DwmSetWindowAttribute(hWnd, DwmWindowAttribute.UseImmersiveDarkMode, ref darkMode, sizeof(int)); + return hResult > S_OK; } } From baea9c940afad2bf22c9708bcacb01762c78fbfb Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 1 Jun 2024 15:53:09 +0200 Subject: [PATCH 25/32] CommandLineArguments ctor to static Create method --- ILSpy.Tests/CommandLineArgumentsTests.cs | 24 ++--- ILSpy/App.xaml.cs | 2 +- ILSpy/AppEnv/CommandLineArguments.cs | 115 +++++++++++++---------- ILSpy/MainWindow.xaml.cs | 2 +- 4 files changed, 79 insertions(+), 64 deletions(-) diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index addf682d4..20704250c 100644 --- a/ILSpy.Tests/CommandLineArgumentsTests.cs +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -14,7 +14,7 @@ namespace ICSharpCode.ILSpy.Tests [Test] public void VerifyEmptyArgumentsArray() { - var cmdLineArgs = new CommandLineArguments(new string[] { }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { }); cmdLineArgs.AssembliesToLoad.Should().BeEmpty(); cmdLineArgs.SingleInstance.Should().BeNull(); @@ -28,14 +28,14 @@ namespace ICSharpCode.ILSpy.Tests [Test] public void VerifyHelpOption() { - var cmdLineArgs = new CommandLineArguments(new string[] { "--help" }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--help" }); cmdLineArgs.ArgumentsParser.IsShowingInformation.Should().BeTrue(); } [Test] public void VerifyForceNewInstanceOption() { - var cmdLineArgs = new CommandLineArguments(new string[] { "--newinstance" }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--newinstance" }); cmdLineArgs.SingleInstance.Should().BeFalse(); } @@ -43,21 +43,21 @@ namespace ICSharpCode.ILSpy.Tests public void VerifyNavigateToOption() { const string navigateTo = "MyNamespace.MyClass"; - var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateto", navigateTo }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--navigateto", navigateTo }); cmdLineArgs.NavigateTo.Should().BeEquivalentTo(navigateTo); } [Test] public void VerifyNavigateToOption_NoneTest_Matching_VSAddin() { - var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateto:none" }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--navigateto:none" }); cmdLineArgs.NavigateTo.Should().BeEquivalentTo("none"); } [Test] public void VerifyCaseSensitivityOfOptionsDoesntThrow() { - var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateTo:none" }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--navigateTo:none" }); cmdLineArgs.ArgumentsParser.RemainingArguments.Should().HaveCount(1); } @@ -66,7 +66,7 @@ namespace ICSharpCode.ILSpy.Tests public void VerifySearchOption() { const string searchWord = "TestContainers"; - var cmdLineArgs = new CommandLineArguments(new string[] { "--search", searchWord }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--search", searchWord }); cmdLineArgs.Search.Should().BeEquivalentTo(searchWord); } @@ -74,7 +74,7 @@ namespace ICSharpCode.ILSpy.Tests public void VerifyLanguageOption() { const string language = "csharp"; - var cmdLineArgs = new CommandLineArguments(new string[] { "--language", language }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--language", language }); cmdLineArgs.Language.Should().BeEquivalentTo(language); } @@ -82,21 +82,21 @@ namespace ICSharpCode.ILSpy.Tests public void VerifyConfigOption() { const string configFile = "myilspyoptions.xml"; - var cmdLineArgs = new CommandLineArguments(new string[] { "--config", configFile }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--config", configFile }); cmdLineArgs.ConfigFile.Should().BeEquivalentTo(configFile); } [Test] public void VerifyNoActivateOption() { - var cmdLineArgs = new CommandLineArguments(new string[] { "--noactivate" }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "--noactivate" }); cmdLineArgs.NoActivate.Should().BeTrue(); } [Test] public void MultipleAssembliesAsArguments() { - var cmdLineArgs = new CommandLineArguments(new string[] { "assembly1", "assembly2", "assembly3" }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { "assembly1", "assembly2", "assembly3" }); cmdLineArgs.AssembliesToLoad.Should().HaveCount(3); } @@ -107,7 +107,7 @@ namespace ICSharpCode.ILSpy.Tests System.IO.File.WriteAllText(filepath, "assembly1\r\nassembly2\r\nassembly3\r\n--newinstance\r\n--noactivate"); - var cmdLineArgs = new CommandLineArguments(new string[] { $"@{filepath}" }); + var cmdLineArgs = CommandLineArguments.Create(new string[] { $"@{filepath}" }); try { diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 2387c487a..0ac78e0f7 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -65,7 +65,7 @@ namespace ICSharpCode.ILSpy ILSpySettings.SettingsFilePathProvider = new ILSpySettingsFilePathProvider(); var cmdArgs = Environment.GetCommandLineArgs().Skip(1); - App.CommandLineArguments = new CommandLineArguments(cmdArgs); + App.CommandLineArguments = CommandLineArguments.Create(cmdArgs); bool forceSingleInstance = (App.CommandLineArguments.SingleInstance ?? true) && !MiscSettingsPanel.CurrentMiscSettings.AllowMultipleInstances; diff --git a/ILSpy/AppEnv/CommandLineArguments.cs b/ILSpy/AppEnv/CommandLineArguments.cs index 116d8b188..14e72f452 100644 --- a/ILSpy/AppEnv/CommandLineArguments.cs +++ b/ILSpy/AppEnv/CommandLineArguments.cs @@ -18,6 +18,7 @@ using McMaster.Extensions.CommandLineUtils; +using System; using System.Collections.Generic; using System.Linq; @@ -36,7 +37,12 @@ namespace ICSharpCode.ILSpy.AppEnv public CommandLineApplication ArgumentsParser { get; } - public CommandLineArguments(IEnumerable arguments) + private CommandLineArguments(CommandLineApplication app) + { + ArgumentsParser = app; + } + + public static CommandLineArguments Create(IEnumerable arguments) { var app = new CommandLineApplication() { // https://natemcmaster.github.io/CommandLineUtils/docs/response-file-parsing.html?tabs=using-attributes @@ -47,58 +53,67 @@ namespace ICSharpCode.ILSpy.AppEnv }; app.HelpOption(); - ArgumentsParser = app; - - var oForceNewInstance = app.Option("--newinstance", - "Start a new instance of ILSpy even if the user configuration is set to single-instance", - CommandOptionType.NoValue); - - var oNavigateTo = app.Option("-n|--navigateto ", - "Navigates to the member specified by the given ID string.\r\nThe member is searched for only in the assemblies specified on the command line.\r\nExample: 'ILSpy ILSpy.exe --navigateto:T:ICSharpCode.ILSpy.CommandLineArguments'", - CommandOptionType.SingleValue); - oNavigateTo.DefaultValue = null; - - var oSearch = app.Option("-s|--search ", - "Search for t:TypeName, m:Member or c:Constant; use exact match (=term), 'should not contain' (-term) or 'must contain' (+term); use /reg(ular)?Ex(pressions)?/ or both - t:/Type(Name)?/...", - CommandOptionType.SingleValue); - oSearch.DefaultValue = null; - - var oLanguage = app.Option("-l|--language ", - "Selects the specified language.\r\nExample: 'ILSpy --language:C#' or 'ILSpy --language:IL'", - CommandOptionType.SingleValue); - oLanguage.DefaultValue = null; - - var oConfig = app.Option("-c|--config ", - "Provide a specific configuration file.\r\nExample: 'ILSpy --config:myconfig.xml'", - CommandOptionType.SingleValue); - oConfig.DefaultValue = null; + var instance = new CommandLineArguments(app); - var oNoActivate = app.Option("--noactivate", - "Do not activate the existing ILSpy instance. This option has no effect if a new ILSpy instance is being started.", - CommandOptionType.NoValue); - - // https://natemcmaster.github.io/CommandLineUtils/docs/arguments.html#variable-numbers-of-arguments - // To enable this, MultipleValues must be set to true, and the argument must be the last one specified. - var files = app.Argument("Assemblies", "Assemblies to load", multipleValues: true); - - app.Parse(arguments.ToArray()); - - if (oForceNewInstance.HasValue()) - SingleInstance = false; - - NavigateTo = oNavigateTo.ParsedValue; - Search = oSearch.ParsedValue; - Language = oLanguage.ParsedValue; - ConfigFile = oConfig.ParsedValue; - - if (oNoActivate.HasValue()) - NoActivate = true; - - foreach (var assembly in files.Values) + try + { + var oForceNewInstance = app.Option("--newinstance", + "Start a new instance of ILSpy even if the user configuration is set to single-instance", + CommandOptionType.NoValue); + + var oNavigateTo = app.Option("-n|--navigateto ", + "Navigates to the member specified by the given ID string.\r\nThe member is searched for only in the assemblies specified on the command line.\r\nExample: 'ILSpy ILSpy.exe --navigateto T:ICSharpCode.ILSpy.CommandLineArguments'", + CommandOptionType.SingleValue); + oNavigateTo.DefaultValue = null; + + var oSearch = app.Option("-s|--search ", + "Search for t:TypeName, m:Member or c:Constant; use exact match (=term), 'should not contain' (-term) or 'must contain' (+term); use /reg(ular)?Ex(pressions)?/ or both - t:/Type(Name)?/...", + CommandOptionType.SingleValue); + oSearch.DefaultValue = null; + + var oLanguage = app.Option("-l|--language ", + "Selects the specified language.\r\nExample: 'ILSpy --language:C#' or 'ILSpy --language IL'", + CommandOptionType.SingleValue); + oLanguage.DefaultValue = null; + + var oConfig = app.Option("-c|--config ", + "Provide a specific configuration file.\r\nExample: 'ILSpy --config myconfig.xml'", + CommandOptionType.SingleValue); + oConfig.DefaultValue = null; + + var oNoActivate = app.Option("--noactivate", + "Do not activate the existing ILSpy instance. This option has no effect if a new ILSpy instance is being started.", + CommandOptionType.NoValue); + + // https://natemcmaster.github.io/CommandLineUtils/docs/arguments.html#variable-numbers-of-arguments + // To enable this, MultipleValues must be set to true, and the argument must be the last one specified. + var files = app.Argument("Assemblies", "Assemblies to load", multipleValues: true); + + app.Parse(arguments.ToArray()); + + if (oForceNewInstance.HasValue()) + instance.SingleInstance = false; + + instance.NavigateTo = oNavigateTo.ParsedValue; + instance.Search = oSearch.ParsedValue; + instance.Language = oLanguage.ParsedValue; + instance.ConfigFile = oConfig.ParsedValue; + + if (oNoActivate.HasValue()) + instance.NoActivate = true; + + foreach (var assembly in files.Values) + { + if (!string.IsNullOrWhiteSpace(assembly)) + instance.AssembliesToLoad.Add(assembly); + } + } + catch (Exception ex) { - if (!string.IsNullOrWhiteSpace(assembly)) - AssembliesToLoad.Add(assembly); + // Intentionally ignore exceptions if any, this is only added to always have an exception-free startup } + + return instance; } } } diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index eed3732db..10525cbb8 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -654,7 +654,7 @@ namespace ICSharpCode.ILSpy internal async Task HandleSingleInstanceCommandLineArguments(string[] args) { - var cmdArgs = new CommandLineArguments(args); + var cmdArgs = CommandLineArguments.Create(args); await Dispatcher.InvokeAsync(() => { if (HandleCommandLineArguments(cmdArgs)) From 13418aa00a557477dcdce6b75b15eb5a98411b9f Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 2 Jun 2024 07:11:03 +0200 Subject: [PATCH 26/32] Fix warnings for single instance handling --- ILSpy/App.xaml.cs | 4 +++- ILSpy/AppEnv/CommandLineArguments.cs | 2 +- ILSpy/AppEnv/SingleInstance.cs | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 0ac78e0f7..26dc4d1a8 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -104,9 +104,11 @@ namespace ICSharpCode.ILSpy } } - private static void SingleInstance_NewInstanceDetected(object? sender, NewInstanceEventArgs e) + private static void SingleInstance_NewInstanceDetected(object sender, NewInstanceEventArgs e) { +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed ICSharpCode.ILSpy.MainWindow.Instance.HandleSingleInstanceCommandLineArguments(e.Args); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName) diff --git a/ILSpy/AppEnv/CommandLineArguments.cs b/ILSpy/AppEnv/CommandLineArguments.cs index 14e72f452..dea96c13f 100644 --- a/ILSpy/AppEnv/CommandLineArguments.cs +++ b/ILSpy/AppEnv/CommandLineArguments.cs @@ -108,7 +108,7 @@ namespace ICSharpCode.ILSpy.AppEnv instance.AssembliesToLoad.Add(assembly); } } - catch (Exception ex) + catch (Exception) { // Intentionally ignore exceptions if any, this is only added to always have an exception-free startup } diff --git a/ILSpy/AppEnv/SingleInstance.cs b/ILSpy/AppEnv/SingleInstance.cs index 22d5ce6e0..e264c6b62 100644 --- a/ILSpy/AppEnv/SingleInstance.cs +++ b/ILSpy/AppEnv/SingleInstance.cs @@ -14,6 +14,8 @@ //2008-01-03: Added Resources //2007-12-29: New version +#nullable enable + namespace Medo.Application; using System; From f996283233cc51574ec581d5ab59371017d57efa Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 2 Jun 2024 07:15:05 +0200 Subject: [PATCH 27/32] Update NuGet packages --- Directory.Packages.props | 6 +++--- .../Properties/DecompilerVersionInfo.template.cs | 2 +- ILSpy.Installer/ILSpy.Installer.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index af214e6a6..15f165921 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,11 +22,11 @@ - + - + @@ -34,7 +34,7 @@ - + diff --git a/ICSharpCode.Decompiler/Properties/DecompilerVersionInfo.template.cs b/ICSharpCode.Decompiler/Properties/DecompilerVersionInfo.template.cs index 5472ee6d0..ee12ad9b8 100644 --- a/ICSharpCode.Decompiler/Properties/DecompilerVersionInfo.template.cs +++ b/ICSharpCode.Decompiler/Properties/DecompilerVersionInfo.template.cs @@ -4,7 +4,7 @@ public const string Minor = "0"; public const string Build = "0"; public const string Revision = "$INSERTREVISION$"; - public const string VersionName = "preview1"; + public const string VersionName = "preview2"; public const string FullVersion = Major + "." + Minor + "." + Build + ".$INSERTREVISION$$INSERTBRANCHPOSTFIX$$INSERTVERSIONNAMEPOSTFIX$"; public const string FullVersionWithShortCommitHash = FullVersion + "-$INSERTSHORTCOMMITHASH$"; diff --git a/ILSpy.Installer/ILSpy.Installer.csproj b/ILSpy.Installer/ILSpy.Installer.csproj index d5cab6a35..6edfaab49 100644 --- a/ILSpy.Installer/ILSpy.Installer.csproj +++ b/ILSpy.Installer/ILSpy.Installer.csproj @@ -15,7 +15,7 @@ - + From 399ba1c0100b1d252360bf4a6872a2ab19141813 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 2 Jun 2024 08:49:19 +0200 Subject: [PATCH 28/32] Enable certain commands only on Windows (#3217) --- ILSpy/AppEnv/AppEnvironment.cs | 9 +++++++++ ILSpy/Commands/OpenFromGacCommand.cs | 7 +++++++ ILSpy/Options/MiscSettingsPanel.xaml | 2 +- ILSpy/Options/MiscSettingsViewModel.cs | 7 +++++-- 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 ILSpy/AppEnv/AppEnvironment.cs diff --git a/ILSpy/AppEnv/AppEnvironment.cs b/ILSpy/AppEnv/AppEnvironment.cs new file mode 100644 index 000000000..15d779f4e --- /dev/null +++ b/ILSpy/AppEnv/AppEnvironment.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace ICSharpCode.ILSpy.AppEnv +{ + public static class AppEnvironment + { + public static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } +} diff --git a/ILSpy/Commands/OpenFromGacCommand.cs b/ILSpy/Commands/OpenFromGacCommand.cs index 8f72d3b6a..a077ceb9d 100644 --- a/ILSpy/Commands/OpenFromGacCommand.cs +++ b/ILSpy/Commands/OpenFromGacCommand.cs @@ -16,12 +16,19 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Properties; + namespace ICSharpCode.ILSpy { [ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.OpenFrom_GAC), MenuIcon = "Images/AssemblyListGAC", MenuCategory = nameof(Resources.Open), MenuOrder = 1)] sealed class OpenFromGacCommand : SimpleCommand { + public override bool CanExecute(object parameter) + { + return AppEnvironment.IsWindows; + } + public override void Execute(object parameter) { OpenFromGacDialog dlg = new OpenFromGacDialog(); diff --git a/ILSpy/Options/MiscSettingsPanel.xaml b/ILSpy/Options/MiscSettingsPanel.xaml index 09a9c5485..502f65fcc 100644 --- a/ILSpy/Options/MiscSettingsPanel.xaml +++ b/ILSpy/Options/MiscSettingsPanel.xaml @@ -11,7 +11,7 @@ -