Skip to content

Reimplement cross-platform single-instance handling#3889

Open
siegfriedpammer wants to merge 1 commit into
masterfrom
reimplement-single-instance
Open

Reimplement cross-platform single-instance handling#3889
siegfriedpammer wants to merge 1 commit into
masterfrom
reimplement-single-instance

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

Summary

The Avalonia port dropped ILSpy's single-instance feature. Three surfaces were left inert: --newinstance / --noactivate and 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):

  • The first launch for a user takes the mutex and listens on a named pipe; a later launch forwards its (path-qualified) arguments over the pipe and exits before Avalonia starts, so no second window flashes. The running window opens the assemblies, navigates, and comes to the foreground (unless --noactivate).
  • The mutex/pipe namespace is derived from machine + user only — never the executable location — so any launcher reuses the running instance. This is the actual fix for "Open with ILSpy".
  • New --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.
  • The Allow multiple instances option and --newinstance now 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

  • New unit tests for the pure logic (GetInstanceName, ShouldReuse, FullyQualifyPath, --instanceid / --newinstance parsing); full ILSpy.Tests suite green, no regressions.
  • Two real processes on Linux: a second launch forwarded and exited in ~0.2s while the first survived; --instanceid matching the running executable reused it even when that instance had no launch id; a mismatching id and --newinstance each 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

  • The ILSpy.AddIn.VS2022 change is net472/Windows-only (builds under ILSpy.VSExtensions.slnx) and was not exercised on the Linux dev box.
  • Accepted edge case: while an unrelated ILSpy holds the mutex, repeated build-affinity (--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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Mutex plus NamedPipe argument forwarding.
  • Wire the gate into startup (pre-Avalonia) and handle forwarded arguments in the running UI instance (optionally activating the window).
  • Add --instanceid parsing + 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.

Comment on lines +61 to +79
public static event Action<string[]> NewInstanceDetected {
add {
lock (gate)
{
newInstanceHandler += value;
if (bufferedArgs is { } pending)
{
bufferedArgs = null;
value(pending);
}
}
}
remove {
lock (gate)
{
newInstanceHandler -= value;
}
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread ILSpy/AppEnv/SingleInstance.cs Outdated
Comment on lines +184 to +199
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;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@siegfriedpammer
siegfriedpammer force-pushed the reimplement-single-instance branch from 91f4e15 to 43887d4 Compare July 19, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants