Lumi/companion/src/Lumi.Companion.App/SingleInstanceCoordinator.cs
2026-07-22 11:39:05 +02:00

63 lines
2.1 KiB
C#

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