namespace WxAgent.Core; /// Serializes commands locally and across processes; the OS releases the file handle after a crash. public sealed class InterprocessCommandGate : IDisposable { private readonly SemaphoreSlim _local = new(1, 1); private readonly string _path; private FileStream? _lease; public InterprocessCommandGate(string path) { _path = Path.GetFullPath(path); Directory.CreateDirectory(Path.GetDirectoryName(_path)!); } public async Task WaitAsync(CancellationToken cancellationToken) { await _local.WaitAsync(cancellationToken).ConfigureAwait(false); try { while (true) { cancellationToken.ThrowIfCancellationRequested(); try { _lease = new FileStream(_path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); return; } catch (IOException exception) when ((exception.HResult & 0xffff) is 11 or 32 or 33) { // EAGAIN on Unix; sharing/lock violation on Windows. Other I/O errors must surface. await Task.Delay(50, cancellationToken).ConfigureAwait(false); } } } catch { _local.Release(); throw; } } public void Release() { try { _lease?.Dispose(); } finally { _lease = null; _local.Release(); } } public void Dispose() { _lease?.Dispose(); _local.Dispose(); } }