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)); }