added a ban on running a second instance of the application.
This commit is contained in:
@@ -13,11 +13,23 @@ namespace CursorLang;
|
|||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
private ServiceProvider? _services;
|
private ServiceProvider? _services;
|
||||||
|
private SingleInstanceGate? _instanceGate;
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
var gate = new SingleInstanceGate();
|
||||||
|
if (!gate.TryAcquire())
|
||||||
|
{
|
||||||
|
gate.Dispose();
|
||||||
|
Shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_instanceGate = gate;
|
||||||
|
_instanceGate.ActivationRequested += OnActivationRequested;
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
ConfigureServices(services);
|
ConfigureServices(services);
|
||||||
_services = services.BuildServiceProvider();
|
_services = services.BuildServiceProvider();
|
||||||
@@ -34,9 +46,31 @@ public partial class App : Application
|
|||||||
protected override void OnExit(ExitEventArgs e)
|
protected override void OnExit(ExitEventArgs e)
|
||||||
{
|
{
|
||||||
_services?.Dispose();
|
_services?.Dispose();
|
||||||
|
|
||||||
|
if (_instanceGate is not null)
|
||||||
|
{
|
||||||
|
_instanceGate.ActivationRequested -= OnActivationRequested;
|
||||||
|
_instanceGate.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
base.OnExit(e);
|
base.OnExit(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnActivationRequested(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (MainWindow is not { IsVisible: true })
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MainWindow.WindowState == WindowState.Minimized)
|
||||||
|
{
|
||||||
|
MainWindow.WindowState = WindowState.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindow.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
private static void ConfigureServices(IServiceCollection services)
|
private static void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton(new KeyboardLayoutOptions());
|
services.AddSingleton(new KeyboardLayoutOptions());
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Право выводить окно на передний план.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ForegroundPermissionNative
|
||||||
|
{
|
||||||
|
/// <summary>ASFW_ANY — право получает любой процесс.</summary>
|
||||||
|
private const uint AnyProcess = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Уступает своё право вывести окно на передний план другим процессам.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Поменять окно переднего плана Windows разрешает не всякому: нужно быть
|
||||||
|
/// процессом, с которым пользователь работал последним. Давно запущенный
|
||||||
|
/// экземпляр приложения к таким не относится, и его окно всплывёт только
|
||||||
|
/// если правом поделится процесс, который пользователь запустил только что.
|
||||||
|
/// </remarks>
|
||||||
|
internal static void GrantToAnyProcess() => AllowSetForegroundWindow(AnyProcess);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool AllowSetForegroundWindow(uint dwProcessId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using System.Windows.Threading;
|
||||||
|
using CursorLang.Interop;
|
||||||
|
|
||||||
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пускает работать только один экземпляр приложения: повторный запуск не
|
||||||
|
/// поднимает второе окно, а показывает окно уже работающего.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Имена объектов ядра оставлены без префикса Global, то есть живут в пространстве
|
||||||
|
/// имён сеанса. Один экземпляр на всю машину дал бы при быстром переключении
|
||||||
|
/// пользователей странную картину: второй пользователь остался бы без приложения,
|
||||||
|
/// а показать ему окно первого всё равно нельзя — окна принадлежат сеансу.
|
||||||
|
/// </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>
|
||||||
|
/// Занимает место единственного экземпляра. Если приложение уже работает,
|
||||||
|
/// просит его показаться и возвращает <c>false</c> — вызвавшему остаётся выйти.
|
||||||
|
/// </summary>
|
||||||
|
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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user