Christoph Wille 1 year ago
parent
commit
82fece3d16
  1. 2
      ILSpy.Tests/CommandLineArgumentsTests.cs
  2. 16
      ILSpy/App.xaml.cs
  3. 21
      ILSpy/AppEnv/CommandLineTools.cs
  4. 290
      ILSpy/AppEnv/SingleInstance.cs
  5. 40
      ILSpy/MainWindow.xaml.cs
  6. 158
      ILSpy/SingleInstanceHandling.cs

2
ILSpy.Tests/CommandLineArgumentsTests.cs

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
using FluentAssertions;
using ICSharpCode.ILSpy.AppEnv;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests

16
ILSpy/App.xaml.cs

@ -35,6 +35,8 @@ using ICSharpCode.ILSpy.Options; @@ -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 @@ -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 @@ -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);

21
ILSpy/AppEnv/CommandLineTools.cs

@ -18,6 +18,7 @@ @@ -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 @@ -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

290
ILSpy/AppEnv/SingleInstance.cs

@ -0,0 +1,290 @@ @@ -0,0 +1,290 @@
// 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.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
/// <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);
}
/// <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 = 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;
}
}
}
/// <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
#if NET7_0_OR_GREATER
[JsonInclude]
public required string CommandLine;
[JsonInclude]
public required string[] CommandLineArgs;
#else
public SingleInstanceArguments() {
CommandLine = string.Empty;
CommandLineArgs = Array.Empty<string>();
}
[JsonInclude]
public string CommandLine;
[JsonInclude]
public string[] CommandLineArgs;
#endif
}
}
/// <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;
}
}
}

40
ILSpy/MainWindow.xaml.cs

@ -600,12 +600,7 @@ namespace ICSharpCode.ILSpy @@ -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 @@ -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<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
protected override void OnKeyDown(KeyEventArgs e)
@ -686,7 +652,7 @@ namespace ICSharpCode.ILSpy @@ -686,7 +652,7 @@ namespace ICSharpCode.ILSpy
List<LoadedAssembly> commandLineLoadedAssemblies = new List<LoadedAssembly>();
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 @@ -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.
/// </summary>
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

158
ILSpy/SingleInstanceHandling.cs

@ -1,158 +0,0 @@ @@ -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