Reimplement cross-platform single-instance handling#3889
Reimplement cross-platform single-instance handling#3889siegfriedpammer wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Restores ILSpy’s single-instance behavior in the Avalonia port with a cross-platform implementation that deduplicates launches per user/machine and forwards command-line arguments to the already-running instance (including activation control), while adding an --instanceid filter for build-affinity reuse.
Changes:
- Add cross-platform single-instance coordination using a named
MutexplusNamedPipeargument forwarding. - Wire the gate into startup (pre-Avalonia) and handle forwarded arguments in the running UI instance (optionally activating the window).
- Add
--instanceidparsing + tests, and update the VS2022 add-in to pass an instance id.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ILSpy/Program.cs | Adds an early single-instance gate before Avalonia lifetime starts. |
| ILSpy/AppEnv/UiContext.cs | Adds a helper to activate/restore the main window for forwarded launches. |
| ILSpy/AppEnv/SingleInstance.cs | Implements mutex + named-pipe single-instance coordination and argument forwarding. |
| ILSpy/AppEnv/CommandLineArguments.cs | Adds --instanceid command-line parsing support. |
| ILSpy/App.axaml.cs | Hooks the forwarded-argument event and routes it to AssemblyTreeModel on the UI thread. |
| ILSpy.Tests/Commands/SingleInstanceTests.cs | Adds unit tests for instance naming, reuse logic, and path qualification. |
| ILSpy.AddIn.VS2022/ILSpyInstance.cs | Passes --instanceid so the VS add-in targets its bundled ILSpy build. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public static event Action<string[]> NewInstanceDetected { | ||
| add { | ||
| lock (gate) | ||
| { | ||
| newInstanceHandler += value; | ||
| if (bufferedArgs is { } pending) | ||
| { | ||
| bufferedArgs = null; | ||
| value(pending); | ||
| } | ||
| } | ||
| } | ||
| remove { | ||
| lock (gate) | ||
| { | ||
| newInstanceHandler -= value; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Good catch, addressed in 43887d4. The buffered-args replay is now captured inside the lock and invoked only after the lock is released, so a subscriber's handler never runs (and can never re-enter SingleInstance) while gate is held.
Reply written by Claude Code on Siegfried's behalf.
| switch (ForwardToRunningInstance(name, args.InstanceId)) | ||
| { | ||
| case ForwardResult.Accepted: | ||
| mutex.Dispose(); | ||
| return false; | ||
| case ForwardResult.Rejected: | ||
| // A live instance owns the namespace but declined (build-affinity mismatch): | ||
| // start a standalone window without hijacking its mutex/pipe. | ||
| mutex.Dispose(); | ||
| return true; | ||
| default: | ||
| // The previous owner is gone (e.g. it crashed): take over the namespace so | ||
| // subsequent launches dedup against this instance. | ||
| BecomeServer(mutex, name, args.InstanceId); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Addressed in 43887d4. Ownership is now taken with Mutex.WaitOne(0) instead of being inferred from createdNew: a live owner holds the mutex, so a second launch always forwards rather than becoming a server; a crashed owner leaves it abandoned, so exactly one next waiter acquires it (AbandonedMutexException) and takes over. Hand-off failure (transient error or unreachable listener) no longer falls through to BecomeServer — that path now starts a normal window without acquiring ownership — so a startup race or transient pipe failure can never leave two processes both serving.
Reply written by Claude Code on Siegfried's behalf.
The Avalonia port dropped ILSpy's single-instance feature, leaving three inert surfaces behind: the --newinstance / --noactivate switches, and the "Allow multiple instances" option were parsed and persisted but never read, and every launch started a new process. The former WPF implementation also encoded the executable location in the mutex name, so two ILSpy builds at different paths never shared an instance. That broke the Windows "Open with ILSpy" shell command: it launches a fixed executable and would not reuse a running instance started from elsewhere. Reimplement it with portable primitives (named Mutex + named pipes, no P/Invoke): the first launch for a user takes the mutex and listens; a later launch forwards its arguments over the pipe and exits. The namespace is derived from machine + user only -- never the location -- so any launcher reuses the running instance. --instanceid is a runtime reuse filter matched against the running instance's actual executable identity (via Environment.ProcessPath, single-file-bundle safe), not part of the mutex name, so it never re-introduces the location partitioning it replaces. The VS add-in passes its bundled exe path as --instanceid to prefer its own build while still sharing with a plain launch of that same executable. Assisted-by: Claude:claude-opus-4-8:Claude Code
91f4e15 to
43887d4
Compare
Summary
The Avalonia port dropped ILSpy's single-instance feature. Three surfaces were left inert:
--newinstance/--noactivateand the Allow multiple instances option were parsed and persisted but never read, and every launch started a new process. This restores the behavior and reimplements it cross-platform (Linux/macOS/Windows).The bug being fixed
The former WPF implementation encoded the executable location in the mutex name, so two ILSpy builds at different paths never shared an instance. That broke the Windows "Open with ILSpy" shell command: it launches a fixed executable and would not reuse a running instance started from elsewhere (e.g. a debug build).
Approach
Portable primitives only (named
Mutex+System.IO.Pipes, no P/Invoke, no vendored code):--noactivate).--instanceid <NAME>is a runtime reuse filter, not part of the namespace: it is matched against the running instance's real executable identity (Environment.ProcessPath, single-file-bundle safe). Baking an id into the mutex name would re-introduce the same location partitioning being removed, so it is deliberately kept out. The VS add-in passes its bundled exe path as--instanceid, so "Open in ILSpy" prefers its own build while still sharing a window with a plain launch of that same executable.--newinstancenow actually disable single-instance.Single-instance coordination is wrapped so any failure falls back to a normal launch; a dead/crashed previous owner is taken over so subsequent launches keep deduplicating.
Verification
GetInstanceName,ShouldReuse,FullyQualifyPath,--instanceid/--newinstanceparsing); fullILSpy.Testssuite green, no regressions.--instanceidmatching the running executable reused it even when that instance had no launch id; a mismatching id and--newinstanceeach opened their own window; the forwarded assembly loaded, navigated, and the window activated; enabling Allow multiple instances made a second launch open its own window.Notes
ILSpy.AddIn.VS2022change is net472/Windows-only (builds underILSpy.VSExtensions.slnx) and was not exercised on the Linux dev box.--instanceid) launches each open their own window rather than deduplicating among themselves; the common paths (no id, or same executable) all reuse correctly.🤖 Generated with Claude Code