diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs index 026c86b81..20704250c 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 @@ -12,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(); @@ -26,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(); } @@ -41,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); } @@ -64,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); } @@ -72,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); } @@ -80,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); } @@ -105,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 5779727c4..0ac78e0f7 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -30,10 +30,13 @@ 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; +using Medo.Application; + using Microsoft.VisualStudio.Composition; using TomsToolbox.Wpf.Styles; @@ -62,13 +65,14 @@ 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; if (forceSingleInstance) { - SingleInstanceHandling.ForceSingleInstance(cmdArgs); + SingleInstance.Attach(); // will auto-exit for second instance + SingleInstance.NewInstanceDetected += SingleInstance_NewInstanceDetected; } InitializeComponent(); @@ -100,6 +104,11 @@ namespace ICSharpCode.ILSpy } } + private static void SingleInstance_NewInstanceDetected(object? sender, NewInstanceEventArgs e) + { + ICSharpCode.ILSpy.MainWindow.Instance.HandleSingleInstanceCommandLineArguments(e.Args); + } + static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName) { var rootPath = Path.GetDirectoryName(typeof(App).Assembly.Location); diff --git a/ILSpy/AppEnv/CommandLineArguments.cs b/ILSpy/AppEnv/CommandLineArguments.cs new file mode 100644 index 000000000..14e72f452 --- /dev/null +++ b/ILSpy/AppEnv/CommandLineArguments.cs @@ -0,0 +1,119 @@ +// Copyright (c) 2011 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 McMaster.Extensions.CommandLineUtils; + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ICSharpCode.ILSpy.AppEnv +{ + public sealed class CommandLineArguments + { + // see /doc/Command Line.txt for details + public List AssembliesToLoad = new List(); + public bool? SingleInstance; + public string NavigateTo; + public string Search; + public string Language; + public bool NoActivate; + public string ConfigFile; + + public CommandLineApplication ArgumentsParser { get; } + + 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 + ResponseFileHandling = ResponseFileHandling.ParseArgsAsLineSeparated, + + // Note: options are case-sensitive (!), and, default behavior would be UnrecognizedArgumentHandling.Throw on Parse() + UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue + }; + + app.HelpOption(); + var instance = new CommandLineArguments(app); + + 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) + { + // Intentionally ignore exceptions if any, this is only added to always have an exception-free startup + } + + return instance; + } + } +} diff --git a/ILSpy/CommandLineTools.cs b/ILSpy/AppEnv/CommandLineTools.cs similarity index 92% rename from ILSpy/CommandLineTools.cs rename to ILSpy/AppEnv/CommandLineTools.cs index 898981996..7379db12e 100644 --- a/ILSpy/CommandLineTools.cs +++ b/ILSpy/AppEnv/CommandLineTools.cs @@ -18,9 +18,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; -namespace ICSharpCode.ILSpy +namespace ICSharpCode.ILSpy.AppEnv { public class CommandLineTools { @@ -110,6 +111,26 @@ namespace ICSharpCode.ILSpy 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..22d5ce6e0 --- /dev/null +++ b/ILSpy/AppEnv/SingleInstance.cs @@ -0,0 +1,288 @@ +// 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.Linq; +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; + +using ICSharpCode.ILSpy.AppEnv; + +/// +/// 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); + } + + private static string[] GetILSpyCommandLineArgs() + { + // Note: NO Skip(1) here because .Args property on SingleInstanceArguments does this for us + return Environment.GetCommandLineArgs().AsEnumerable() + .Select(CommandLineTools.FullyQualifyPath) + .ToArray(); + } + + /// + /// 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 = GetILSpyCommandLineArgs(), + }; + 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 + [JsonInclude] + public required string CommandLine; + + [JsonInclude] + public required string[] CommandLineArgs; + } + +} + + +/// +/// 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/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs deleted file mode 100644 index df1114e38..000000000 --- a/ILSpy/CommandLineArguments.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2011 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 McMaster.Extensions.CommandLineUtils; - -using System.Collections.Generic; -using System.Linq; - -namespace ICSharpCode.ILSpy -{ - public sealed class CommandLineArguments - { - // see /doc/Command Line.txt for details - public List AssembliesToLoad = new List(); - public bool? SingleInstance; - public string NavigateTo; - public string Search; - public string Language; - public bool NoActivate; - public string ConfigFile; - - public CommandLineApplication ArgumentsParser { get; } - - public CommandLineArguments(IEnumerable arguments) - { - var app = new CommandLineApplication() { - // https://natemcmaster.github.io/CommandLineUtils/docs/response-file-parsing.html?tabs=using-attributes - ResponseFileHandling = ResponseFileHandling.ParseArgsAsLineSeparated, - - // Note: options are case-sensitive (!), and, default behavior would be UnrecognizedArgumentHandling.Throw on Parse() - 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); - - 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()) - 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) - { - if (!string.IsNullOrWhiteSpace(assembly)) - AssembliesToLoad.Add(assembly); - } - } - } -} 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..10525cbb8 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; @@ -599,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); @@ -623,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) @@ -685,6 +652,21 @@ namespace ICSharpCode.ILSpy List commandLineLoadedAssemblies = new List(); + internal async Task HandleSingleInstanceCommandLineArguments(string[] args) + { + var cmdArgs = CommandLineArguments.Create(args); + + await Dispatcher.InvokeAsync(() => { + if (HandleCommandLineArguments(cmdArgs)) + { + if (!cmdArgs.NoActivate && WindowState == WindowState.Minimized) + WindowState = WindowState.Normal; + + HandleCommandLineArgumentsAfterShowList(cmdArgs); + } + }); + } + bool HandleCommandLineArguments(CommandLineArguments args) { LoadAssemblies(args.AssembliesToLoad, commandLineLoadedAssemblies, focusNode: false); diff --git a/ILSpy/NativeMethods.cs b/ILSpy/NativeMethods.cs index d24aeba6d..ba3e03b67 100644 --- a/ILSpy/NativeMethods.cs +++ b/ILSpy/NativeMethods.cs @@ -17,99 +17,23 @@ // 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 + // Uses https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation + internal static partial class NativeMethods { - public const uint WM_COPYDATA = 0x4a; + const int S_OK = 0; - [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); + [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; - } - } - - [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; + int hResult = DwmSetWindowAttribute(hWnd, DwmWindowAttribute.UseImmersiveDarkMode, ref darkMode, sizeof(int)); + return hResult > S_OK; } } 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; 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 - } -}