140 lines
5.1 KiB
C#
140 lines
5.1 KiB
C#
using CursorLang.Core.Interop;
|
|
|
|
namespace CursorLang.Core.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.
|
|
///
|
|
/// 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.
|
|
/// </remarks>
|
|
public sealed class SingleInstanceGate : IDisposable
|
|
{
|
|
/// <summary>The agent's slot — one background process per session.</summary>
|
|
public const string AgentName = ".Agent";
|
|
|
|
/// <summary>The settings window's slot — one window per session.</summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public SingleInstanceGate(string nameSuffix)
|
|
{
|
|
_mutexName = MutexName + nameSuffix;
|
|
_activationEventName = ActivationEventName + nameSuffix;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Another launch asks for the window to be shown.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
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() => TryAcquire(showRunningInstance: true);
|
|
|
|
/// <summary>
|
|
/// The same, with a say in what is to happen to the application already running.
|
|
/// </summary>
|
|
/// <param name="showRunningInstance">
|
|
/// Whether the running application is to be brought up. A launch by Windows
|
|
/// itself passes <c>false</c>: it was not asked for a window, and the
|
|
/// application already in the tray is answer enough.
|
|
/// </param>
|
|
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);
|
|
}
|