using System.IO.Pipes; using System.Security.Cryptography; using System.Text; namespace Lumi.Companion.App; internal sealed class SingleInstanceCoordinator : IDisposable { private readonly Mutex _mutex; private readonly CancellationTokenSource _lifetime = new(); private readonly string _pipeName; private Task? _listener; public SingleInstanceCoordinator() { var userKey = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"{Environment.UserDomainName}\\{Environment.UserName}"))).Substring(0, 16); _pipeName = $"Lumi.Companion.Activate.v1.{userKey}"; _mutex = new Mutex(true, $"Local\\Lumi.Companion.v1.{userKey}", out var created); IsPrimary = created; if (created) _listener = Task.Run(() => ListenAsync(_lifetime.Token)); } public bool IsPrimary { get; } public event Action? ActivationRequested; public void SignalPrimary() { try { using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.Out, PipeOptions.Asynchronous); pipe.Connect(1000); pipe.WriteByte(1); pipe.Flush(); } catch { } } private async Task ListenAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { try { await using var pipe = new NamedPipeServerStream(_pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly, 1, 1); await pipe.WaitForConnectionAsync(cancellationToken); if (pipe.ReadByte() == 1) ActivationRequested?.Invoke(); } catch (OperationCanceledException) { break; } catch when (!cancellationToken.IsCancellationRequested) { await Task.Delay(200, cancellationToken); } } } public void Dispose() { _lifetime.Cancel(); try { _listener?.Wait(500); } catch { } if (IsPrimary) try { _mutex.ReleaseMutex(); } catch { } _mutex.Dispose(); _lifetime.Dispose(); } }