Files
cursor-lang/CursorLang/Services/SingleInstanceGate.cs
T
2026-08-09 20:02:23 +05:00

100 lines
3.5 KiB
C#

using System.Windows.Threading;
using CursorLang.Interop;
namespace CursorLang.Services;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public sealed class SingleInstanceGate : IDisposable
{
private const string MutexName = "CursorLang.SingleInstance";
private const string ActivationEventName = "CursorLang.ActivationRequest";
private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher;
private Mutex? _mutex;
private EventWaitHandle? _activationRequest;
private RegisteredWaitHandle? _activationWait;
private bool _isOwner;
/// <summary>Другой запуск просит показать окно.</summary>
public event EventHandler? ActivationRequested;
/// <summary>
/// Takes the single-instance slot. When the application is already running, asks
/// it to show itself and returns <c>false</c> — the caller is left to exit.
/// </summary>
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));
}