using System.Windows.Threading; using CursorLang.Interop; namespace CursorLang.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. /// public sealed class SingleInstanceGate : IDisposable { private const string MutexName = "CursorLang.SingleInstance"; private const string ActivationEventName = "CursorLang.ActivationRequest"; private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher; private readonly string _mutexName; private readonly string _activationEventName; private Mutex? _mutex; private EventWaitHandle? _activationRequest; private RegisteredWaitHandle? _activationWait; private bool _isOwner; public SingleInstanceGate() : this(string.Empty) { } /// /// Adds a distinguishing part to the kernel object names. Needed by the tests: /// otherwise they would share the single-instance slot with the running /// application and get in its way. /// internal SingleInstanceGate(string nameSuffix) { _mutexName = MutexName + nameSuffix; _activationEventName = ActivationEventName + nameSuffix; } /// Another launch asks for the window to be shown. 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() { _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) { 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; } // The thread pool reports the request from wherever it happens to be, while the // window obeys only its own thread private void OnActivationSignalled(object? state, bool timedOut) => _dispatcher.BeginInvoke(() => ActivationRequested?.Invoke(this, EventArgs.Empty)); }