Browse Source

Merge pull request #3212 from icsharpcode/feature/singleinstance2024

New Single Instance Handling code
pull/3217/head
Siegfried Pammer 12 months ago committed by GitHub
parent
commit
3304d6bcb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 26
      ILSpy.Tests/CommandLineArgumentsTests.cs
  2. 13
      ILSpy/App.xaml.cs
  3. 119
      ILSpy/AppEnv/CommandLineArguments.cs
  4. 23
      ILSpy/AppEnv/CommandLineTools.cs
  5. 288
      ILSpy/AppEnv/SingleInstance.cs
  6. 104
      ILSpy/CommandLineArguments.cs
  7. 1
      ILSpy/Commands/ScopeSearchToAssembly.cs
  8. 1
      ILSpy/Commands/ScopeSearchToNamespace.cs
  9. 52
      ILSpy/MainWindow.xaml.cs
  10. 90
      ILSpy/NativeMethods.cs
  11. 1
      ILSpy/Options/MiscSettingsViewModel.cs
  12. 1
      ILSpy/Search/SearchPane.cs
  13. 158
      ILSpy/SingleInstanceHandling.cs

26
ILSpy.Tests/CommandLineArgumentsTests.cs

@ -2,6 +2,8 @@
using FluentAssertions; using FluentAssertions;
using ICSharpCode.ILSpy.AppEnv;
using NUnit.Framework; using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests namespace ICSharpCode.ILSpy.Tests
@ -12,7 +14,7 @@ namespace ICSharpCode.ILSpy.Tests
[Test] [Test]
public void VerifyEmptyArgumentsArray() public void VerifyEmptyArgumentsArray()
{ {
var cmdLineArgs = new CommandLineArguments(new string[] { }); var cmdLineArgs = CommandLineArguments.Create(new string[] { });
cmdLineArgs.AssembliesToLoad.Should().BeEmpty(); cmdLineArgs.AssembliesToLoad.Should().BeEmpty();
cmdLineArgs.SingleInstance.Should().BeNull(); cmdLineArgs.SingleInstance.Should().BeNull();
@ -26,14 +28,14 @@ namespace ICSharpCode.ILSpy.Tests
[Test] [Test]
public void VerifyHelpOption() public void VerifyHelpOption()
{ {
var cmdLineArgs = new CommandLineArguments(new string[] { "--help" }); var cmdLineArgs = CommandLineArguments.Create(new string[] { "--help" });
cmdLineArgs.ArgumentsParser.IsShowingInformation.Should().BeTrue(); cmdLineArgs.ArgumentsParser.IsShowingInformation.Should().BeTrue();
} }
[Test] [Test]
public void VerifyForceNewInstanceOption() public void VerifyForceNewInstanceOption()
{ {
var cmdLineArgs = new CommandLineArguments(new string[] { "--newinstance" }); var cmdLineArgs = CommandLineArguments.Create(new string[] { "--newinstance" });
cmdLineArgs.SingleInstance.Should().BeFalse(); cmdLineArgs.SingleInstance.Should().BeFalse();
} }
@ -41,21 +43,21 @@ namespace ICSharpCode.ILSpy.Tests
public void VerifyNavigateToOption() public void VerifyNavigateToOption()
{ {
const string navigateTo = "MyNamespace.MyClass"; 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); cmdLineArgs.NavigateTo.Should().BeEquivalentTo(navigateTo);
} }
[Test] [Test]
public void VerifyNavigateToOption_NoneTest_Matching_VSAddin() 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"); cmdLineArgs.NavigateTo.Should().BeEquivalentTo("none");
} }
[Test] [Test]
public void VerifyCaseSensitivityOfOptionsDoesntThrow() 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); cmdLineArgs.ArgumentsParser.RemainingArguments.Should().HaveCount(1);
} }
@ -64,7 +66,7 @@ namespace ICSharpCode.ILSpy.Tests
public void VerifySearchOption() public void VerifySearchOption()
{ {
const string searchWord = "TestContainers"; 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); cmdLineArgs.Search.Should().BeEquivalentTo(searchWord);
} }
@ -72,7 +74,7 @@ namespace ICSharpCode.ILSpy.Tests
public void VerifyLanguageOption() public void VerifyLanguageOption()
{ {
const string language = "csharp"; 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); cmdLineArgs.Language.Should().BeEquivalentTo(language);
} }
@ -80,21 +82,21 @@ namespace ICSharpCode.ILSpy.Tests
public void VerifyConfigOption() public void VerifyConfigOption()
{ {
const string configFile = "myilspyoptions.xml"; 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); cmdLineArgs.ConfigFile.Should().BeEquivalentTo(configFile);
} }
[Test] [Test]
public void VerifyNoActivateOption() public void VerifyNoActivateOption()
{ {
var cmdLineArgs = new CommandLineArguments(new string[] { "--noactivate" }); var cmdLineArgs = CommandLineArguments.Create(new string[] { "--noactivate" });
cmdLineArgs.NoActivate.Should().BeTrue(); cmdLineArgs.NoActivate.Should().BeTrue();
} }
[Test] [Test]
public void MultipleAssembliesAsArguments() 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); 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"); 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 try
{ {

13
ILSpy/App.xaml.cs

@ -30,10 +30,13 @@ using System.Windows.Documents;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Threading; using System.Windows.Threading;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Options; using ICSharpCode.ILSpy.Options;
using ICSharpCode.ILSpyX.Analyzers; using ICSharpCode.ILSpyX.Analyzers;
using ICSharpCode.ILSpyX.Settings; using ICSharpCode.ILSpyX.Settings;
using Medo.Application;
using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.Composition;
using TomsToolbox.Wpf.Styles; using TomsToolbox.Wpf.Styles;
@ -62,13 +65,14 @@ namespace ICSharpCode.ILSpy
ILSpySettings.SettingsFilePathProvider = new ILSpySettingsFilePathProvider(); ILSpySettings.SettingsFilePathProvider = new ILSpySettingsFilePathProvider();
var cmdArgs = Environment.GetCommandLineArgs().Skip(1); var cmdArgs = Environment.GetCommandLineArgs().Skip(1);
App.CommandLineArguments = new CommandLineArguments(cmdArgs); App.CommandLineArguments = CommandLineArguments.Create(cmdArgs);
bool forceSingleInstance = (App.CommandLineArguments.SingleInstance ?? true) bool forceSingleInstance = (App.CommandLineArguments.SingleInstance ?? true)
&& !MiscSettingsPanel.CurrentMiscSettings.AllowMultipleInstances; && !MiscSettingsPanel.CurrentMiscSettings.AllowMultipleInstances;
if (forceSingleInstance) if (forceSingleInstance)
{ {
SingleInstanceHandling.ForceSingleInstance(cmdArgs); SingleInstance.Attach(); // will auto-exit for second instance
SingleInstance.NewInstanceDetected += SingleInstance_NewInstanceDetected;
} }
InitializeComponent(); 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) static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName)
{ {
var rootPath = Path.GetDirectoryName(typeof(App).Assembly.Location); var rootPath = Path.GetDirectoryName(typeof(App).Assembly.Location);

119
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<string> AssembliesToLoad = new List<string>();
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<string> 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<string>("-n|--navigateto <TYPENAME>",
"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<string>("-s|--search <SEARCHTERM>",
"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<string>("-l|--language <LANGUAGEIDENTIFIER>",
"Selects the specified language.\r\nExample: 'ILSpy --language:C#' or 'ILSpy --language IL'",
CommandOptionType.SingleValue);
oLanguage.DefaultValue = null;
var oConfig = app.Option<string>("-c|--config <CONFIGFILENAME>",
"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;
}
}
}

23
ILSpy/CommandLineTools.cs → ILSpy/AppEnv/CommandLineTools.cs

@ -18,9 +18,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Text; using System.Text;
namespace ICSharpCode.ILSpy namespace ICSharpCode.ILSpy.AppEnv
{ {
public class CommandLineTools public class CommandLineTools
{ {
@ -110,6 +111,26 @@ namespace ICSharpCode.ILSpy
b.Append('"'); 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 // Source: https://github.com/dotnet/runtime/blob/bc9fc5a774d96f95abe0ea5c90fac48b38ed2e67/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs#L574-L606

288
ILSpy/AppEnv/SingleInstance.cs

@ -0,0 +1,288 @@
// Source: https://github.com/medo64/Medo/blob/main/src/Medo/Application/SingleInstance.cs
/* Josip Medved <jmedved@jmedved.com> * 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;
/// <summary>
/// Handles detection and communication of programs multiple instances.
/// This class is thread safe.
/// </summary>
public static class SingleInstance
{
private static Mutex? _mtxFirstInstance;
private static Thread? _thread;
private static readonly object _syncRoot = new();
/// <summary>
/// Returns true if this application is not already started.
/// Another instance is contacted via named pipe.
/// </summary>
/// <exception cref="InvalidOperationException">API call failed.</exception>
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();
}
/// <summary>
/// Returns true if this application is not already started.
/// Another instance is contacted via named pipe.
/// </summary>
/// <param name="noAutoExit">If true, application will exit after informing another instance.</param>
/// <exception cref="InvalidOperationException">API call failed.</exception>
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;
}
}
}
/// <summary>
/// Gets whether there is another instance running.
/// It temporary creates mutex.
/// </summary>
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);
}
}
}
}
/// <summary>
/// Occurs in first instance when new instance is detected.
/// </summary>
public static event EventHandler<NewInstanceEventArgs>? NewInstanceDetected;
/// <summary>
/// Thread function.
/// </summary>
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<SingleInstanceArguments>(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;
}
}
/// <summary>
/// Arguments for newly detected application instance.
/// </summary>
public sealed class NewInstanceEventArgs : EventArgs
{
/// <summary>
/// Creates new instance.
/// </summary>
/// <param name="commandLine">Command line.</param>
/// <param name="commandLineArgs">String array containing the command line arguments in the same format as Environment.GetCommandLineArgs.</param>
internal NewInstanceEventArgs(string commandLine, string[] commandLineArgs)
{
CommandLine = commandLine;
_commandLineArgs = new string[commandLineArgs.Length];
Array.Copy(commandLineArgs, _commandLineArgs, _commandLineArgs.Length);
}
/// <summary>
/// Gets the command line.
/// </summary>
public string CommandLine { get; }
private readonly string[] _commandLineArgs;
/// <summary>
/// Returns a string array containing the command line arguments.
/// </summary>
public string[] GetCommandLineArgs()
{
var argCopy = new string[_commandLineArgs.Length];
Array.Copy(_commandLineArgs, argCopy, argCopy.Length);
return argCopy;
}
/// <summary>
/// Gets a string array containing the command line arguments without the name of exectuable.
/// </summary>
public string[] Args {
get {
var argCopy = new string[_commandLineArgs.Length - 1];
Array.Copy(_commandLineArgs, 1, argCopy, 0, argCopy.Length);
return argCopy;
}
}
}

104
ILSpy/CommandLineArguments.cs

@ -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<string> AssembliesToLoad = new List<string>();
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<string> 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<string>("-n|--navigateto <TYPENAME>",
"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<string>("-s|--search <SEARCHTERM>",
"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<string>("-l|--language <LANGUAGEIDENTIFIER>",
"Selects the specified language.\r\nExample: 'ILSpy --language:C#' or 'ILSpy --language:IL'",
CommandOptionType.SingleValue);
oLanguage.DefaultValue = null;
var oConfig = app.Option<string>("-c|--config <CONFIGFILENAME>",
"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);
}
}
}
}

1
ILSpy/Commands/ScopeSearchToAssembly.cs

@ -20,6 +20,7 @@
using System; using System;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpy.TreeNodes;

1
ILSpy/Commands/ScopeSearchToNamespace.cs

@ -18,6 +18,7 @@
using System; using System;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpy.TreeNodes;

52
ILSpy/MainWindow.xaml.cs

@ -44,6 +44,7 @@ using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation; using ICSharpCode.Decompiler.TypeSystem.Implementation;
using ICSharpCode.ILSpy.Analyzers; using ICSharpCode.ILSpy.Analyzers;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Commands; using ICSharpCode.ILSpy.Commands;
using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.Docking;
using ICSharpCode.ILSpy.Options; using ICSharpCode.ILSpy.Options;
@ -599,12 +600,7 @@ namespace ICSharpCode.ILSpy
{ {
base.OnSourceInitialized(e); base.OnSourceInitialized(e);
PresentationSource source = PresentationSource.FromVisual(this); PresentationSource source = PresentationSource.FromVisual(this);
HwndSource hwndSource = source as HwndSource;
if (hwndSource != null)
{
hwndSource.AddHook(WndProc);
}
SingleInstanceHandling.ReleaseSingleInstanceMutex();
// Validate and Set Window Bounds // Validate and Set Window Bounds
Rect bounds = Rect.Transform(sessionSettings.WindowBounds, source.CompositionTarget.TransformToDevice); 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); 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; 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<string> lines = new List<string>();
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 #endregion
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
@ -685,6 +652,21 @@ namespace ICSharpCode.ILSpy
List<LoadedAssembly> commandLineLoadedAssemblies = new List<LoadedAssembly>(); List<LoadedAssembly> commandLineLoadedAssemblies = new List<LoadedAssembly>();
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) bool HandleCommandLineArguments(CommandLineArguments args)
{ {
LoadAssemblies(args.AssembliesToLoad, commandLineLoadedAssemblies, focusNode: false); LoadAssemblies(args.AssembliesToLoad, commandLineLoadedAssemblies, focusNode: false);

90
ILSpy/NativeMethods.cs

@ -17,99 +17,23 @@
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System; using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
namespace ICSharpCode.ILSpy 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)] [LibraryImport("dwmapi.dll", EntryPoint = "DwmSetWindowAttribute")]
[return: MarshalAs(UnmanagedType.Bool)] internal static partial int DwmSetWindowAttribute(IntPtr hwnd, DwmWindowAttribute attr, ref int attrValue, int attrSize);
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);
public static bool UseImmersiveDarkMode(IntPtr hWnd, bool enable) public static bool UseImmersiveDarkMode(IntPtr hWnd, bool enable)
{ {
int darkMode = enable ? 1 : 0; int darkMode = enable ? 1 : 0;
int hr = DwmSetWindowAttribute(hWnd, DwmWindowAttribute.UseImmersiveDarkMode, ref darkMode, sizeof(int)); int hResult = DwmSetWindowAttribute(hWnd, DwmWindowAttribute.UseImmersiveDarkMode, ref darkMode, sizeof(int));
return hr >= 0; return hResult > S_OK;
}
}
[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;
} }
} }

1
ILSpy/Options/MiscSettingsViewModel.cs

@ -24,6 +24,7 @@ using System.Runtime.CompilerServices;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Commands; using ICSharpCode.ILSpy.Commands;
using ICSharpCode.ILSpyX.Settings; using ICSharpCode.ILSpyX.Settings;

1
ILSpy/Search/SearchPane.cs

@ -32,6 +32,7 @@ using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.Docking;
using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.ViewModels;
using ICSharpCode.ILSpyX; using ICSharpCode.ILSpyX;

158
ILSpy/SingleInstanceHandling.cs

@ -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<string> 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
}
}
Loading…
Cancel
Save