Refuse to start a second instance unless asked to - #8
Merged
Merged
Conversation
A second MCPHub is not a harmless duplicate window. It binds the same fixed proxy port, starts every auto-start service on its own fixed port, and writes to the same shared servers folder, so the two fight over all three and the loser's failures read as unrelated server faults. SingleInstanceGuard takes an exclusive OS lock on mcphub.lock in the per-user data directory and holds it for the life of the process: an exclusive open on Windows, flock(LOCK_EX) on Unix, which keeps the Linux build honest. The kernel releases it however the process ends, so there is no stale PID to second-guess and a leftover lock file never blocks a restart. The pid written into the file is diagnostic only; nothing reads it back to make a decision. Program.Main claims it before anything else, because by the time Avalonia is up the ports and services are already being taken. A refused start writes the reason to standard error and exits 2. Pass --allow-multiple-instances to start another instance deliberately. The guard lives in Core rather than App so it can be tested: acquire, refuse-while-held, release-and-reacquire, idempotent dispose, stale file, missing directory, and that near misses like --allow-multiple do not bypass it.
Wixely
added a commit
that referenced
this pull request
Sep 6, 2026
Refuse to start a second instance unless asked to
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A second MCPHub is not a harmless duplicate window. It binds the same fixed proxy port, starts every auto-start service on its own fixed port, and writes to the same shared servers folder. The two instances fight over all three, and the loser's failures read as unrelated server faults.
How
SingleInstanceGuardtakes an exclusive OS lock onmcphub.lockin the per-user data directory and holds it for the life of the process -- an exclusive open on Windows,flock(LOCK_EX)on Unix, which keeps thelinux-x64build honest.The lock is the mechanism, not the file's contents. The kernel releases it however the process ends -- kill, crash, power cut -- so there is no stale PID to second-guess and a leftover
mcphub.locknever wedges a restart. The pid written into the file is diagnostic only; nothing reads it back to make a decision.A file lock was chosen over a named
Mutexbecause releases build for bothwin-x64andlinux-x64, and .NET's named mutexes behave differently on Unix.Program.Mainclaims it before anything else, since by the time Avalonia is up the ports and services are already being taken.Mainnow returnsint; a refused start writes the reason to standard error and exits2.The override
--allow-multiple-instancesskips the guard entirely, for deliberately running a new build against an old one. Nothing else changes: both instances still compete for the port and the servers folder, so the second needs different settings first. Documented in Troubleshooting.Tests
14 new tests, 140 passing overall. The guard lives in
MCPHub.Corerather thanMCPHub.Appspecifically so the test project can reach it.Covered: acquire, refuse-while-held, release-and-reacquire, idempotent dispose, missing directory created, a stale lock file not blocking startup, and that near misses (
--allow-multiple,allow-multiple-instances,--allow-multiple-instances=true) do not bypass the guard.Also verified live on Windows: a stand-in process held the lock exclusively, then the real built
MCPHub.exewas launched. Exit code2, correct message on stderr, no window, no proxy, no services started.Not verified end to end
The
--allow-multiple-instancespath was not exercised live. Doing so would start a genuine second hub binding port 5800 and spawning all nine services against the already-running instance -- the exact collision this change exists to prevent. It is covered by unit tests and is a three-line branch inMain.Possible follow-up
With Close to tray on, the realistic case is double-clicking MCPHub while it sits in the tray, which now produces nothing visible -- stderr does not reach a double-click. Focusing the existing window instead of exiting quietly would be nicer, but needs IPC between instances, so it is left out of this change.