using System.Windows.Threading;
using CursorLang.Interop;
namespace CursorLang.Services;
///
/// Пускает работать только один экземпляр приложения: повторный запуск не
/// поднимает второе окно, а показывает окно уже работающего.
///
///
/// Имена объектов ядра оставлены без префикса Global, то есть живут в пространстве
/// имён сеанса. Один экземпляр на всю машину дал бы при быстром переключении
/// пользователей странную картину: второй пользователь остался бы без приложения,
/// а показать ему окно первого всё равно нельзя — окна принадлежат сеансу.
///
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;
/// Другой запуск просит показать окно.
public event EventHandler? ActivationRequested;
///
/// Занимает место единственного экземпляра. Если приложение уже работает,
/// просит его показаться и возвращает false — вызвавшему остаётся выйти.
///
public bool TryAcquire()
{
_mutex = new Mutex(initiallyOwned: false, MutexName);
try
{
_isOwner = _mutex.WaitOne(TimeSpan.Zero, exitContext: false);
}
catch (AbandonedMutexException)
{
// Предыдущий экземпляр завершился аварийно и мьютекс не отпустил.
// Владельца у него теперь нет, а значит место свободно
_isOwner = true;
}
// Событие открывают оба экземпляра: первый — чтобы ждать просьбы,
// второй — чтобы её подать. Кто из них создаст объект, зависит от того,
// кто оказался первым, и на работу не влияет
_activationRequest = new EventWaitHandle(false, EventResetMode.AutoReset, ActivationEventName);
if (!_isOwner)
{
ForegroundPermissionNative.GrantToAnyProcess();
_activationRequest.Set();
return false;
}
// Ожидание отдано пулу потоков: держать ради него свой поток не за чем,
// а просьба может не прийти никогда
_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;
// Мьютекс отпускает тот же поток, что его занял: и то и другое
// происходит на потоке пользовательского интерфейса
if (_isOwner)
{
_mutex?.ReleaseMutex();
_isOwner = false;
}
_mutex?.Dispose();
_mutex = null;
}
// Пул потоков сообщает о просьбе где придётся, а окно слушается только
// своего потока
private void OnActivationSignalled(object? state, bool timedOut) =>
_dispatcher.BeginInvoke(() => ActivationRequested?.Invoke(this, EventArgs.Empty));
}