using CursorLang.Core.Interop; namespace CursorLang.Core.Services; /// /// Lets only one instance of the application run: a second launch does not bring up /// a second window but shows the window of the one already running. /// /// /// The kernel object names are left without the Global prefix, that is, they live in /// the session namespace. A single instance for the whole machine would make for an /// odd picture with fast user switching: the second user would be left without the /// application, and showing them the window of the first one is impossible anyway — /// windows belong to a session. /// /// Two processes use this now, and each guards its own slot: the agent so that one /// background process runs, the settings window so that a second "Settings" from the /// tray raises the window already open instead of a second one. Hence the name part. /// public sealed class SingleInstanceGate : IDisposable { /// The agent's slot — one background process per session. public const string AgentName = ".Agent"; /// The settings window's slot — one window per session. public const string SettingsName = ".Settings"; private const string MutexName = "CursorLang.SingleInstance"; private const string ActivationEventName = "CursorLang.ActivationRequest"; private readonly string _mutexName; private readonly string _activationEventName; private Mutex? _mutex; private EventWaitHandle? _activationRequest; private RegisteredWaitHandle? _activationWait; private bool _isOwner; /// /// Takes a named slot. The name tells the agent's slot from the settings window's, /// and the tests use one of their own: otherwise they would share a slot with the /// running application and get in its way. /// public SingleInstanceGate(string nameSuffix) { _mutexName = MutexName + nameSuffix; _activationEventName = ActivationEventName + nameSuffix; } /// /// Another launch asks for the window to be shown. /// /// /// Raised on a thread pool thread, wherever the wait happened to be answered. The /// two hosts get back to their own thread differently — one through the dispatcher, /// one by posting to its window — so neither is assumed here. /// public event EventHandler? ActivationRequested; /// /// Takes the single-instance slot. When the application is already running, asks /// it to show itself and returns false — the caller is left to exit. /// public bool TryAcquire() => TryAcquire(showRunningInstance: true); /// /// The same, with a say in what is to happen to the application already running. /// /// /// Whether the running application is to be brought up. A launch by Windows /// itself passes false: it was not asked for a window, and the /// application already in the tray is answer enough. /// public bool TryAcquire(bool showRunningInstance) { _mutex = new Mutex(initiallyOwned: false, _mutexName); try { _isOwner = _mutex.WaitOne(TimeSpan.Zero, exitContext: false); } catch (AbandonedMutexException) { // The previous instance crashed and did not release the mutex. // It has no owner now, which means the slot is free _isOwner = true; } // The event is opened by both instances: the first one to wait for a request, // the second one to make it. Which of them creates the object depends on who // came first and does not affect the work _activationRequest = new EventWaitHandle(false, EventResetMode.AutoReset, _activationEventName); if (!_isOwner) { if (showRunningInstance) { ForegroundPermissionNative.GrantToAnyProcess(); _activationRequest.Set(); } return false; } // The wait is handed over to the thread pool: there is no reason to hold a // thread of our own for it, and the request may never come _activationWait = ThreadPool.RegisterWaitForSingleObject( _activationRequest, OnActivationSignalled, state: null, Timeout.Infinite, executeOnlyOnce: false); return true; } public void Dispose() { _activationWait?.Unregister(null); _activationWait = null; _activationRequest?.Dispose(); _activationRequest = null; // The mutex is released by the same thread that took it: both happen // on the user interface thread if (_isOwner) { _mutex?.ReleaseMutex(); _isOwner = false; } _mutex?.Dispose(); _mutex = null; } private void OnActivationSignalled(object? state, bool timedOut) => ActivationRequested?.Invoke(this, EventArgs.Empty); }