From 1504602fe868399d4f4b4c9156f4932bdbc6b939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20Ili=C4=87?= Date: Tue, 11 Aug 2026 17:24:52 +0200 Subject: [PATCH] use mutex for ipc ownership --- AiDocumentation/asseteditor-ipc.md | 7 + AssetEditor/App.xaml.cs | 8 +- Editors/Ipc/IpcEditor/AssetEditorIpcServer.cs | 131 +++++++++++++++--- 3 files changed, 123 insertions(+), 23 deletions(-) diff --git a/AiDocumentation/asseteditor-ipc.md b/AiDocumentation/asseteditor-ipc.md index 7bb2fd593..6a6596c6b 100644 --- a/AiDocumentation/asseteditor-ipc.md +++ b/AiDocumentation/asseteditor-ipc.md @@ -11,6 +11,13 @@ This document describes the current IPC endpoint implemented by `AssetEditor`. ## Pipe Path (Windows) - `\\.\pipe\TheAssetEditor.Ipc` +## Multiple AssetEditor Instances +- IPC starts automatically; the `Start_IPC` command-line argument is no longer required +- Every AssetEditor instance participates in ownership of the fixed pipe +- A per-session named mutex (`Local\TheAssetEditor.Ipc.Owner`) ensures only one instance hosts the pipe at a time +- Other instances wait without repeatedly trying to create the pipe +- When the owner exits, one waiting instance takes ownership and starts serving IPC requests + ## Request Format Send one JSON object followed by a newline. diff --git a/AssetEditor/App.xaml.cs b/AssetEditor/App.xaml.cs index ef5521ce3..fbf6a1a2e 100644 --- a/AssetEditor/App.xaml.cs +++ b/AssetEditor/App.xaml.cs @@ -89,11 +89,9 @@ protected override void OnStartup(StartupEventArgs e) ShowMainWindow(); - if (e.Args.Contains("Start_IPC")) - { - _ipcServer = _serviceProvider.GetRequiredService(); - _ipcServer.Start(); - } + _ipcServer = _serviceProvider.GetRequiredService(); + _ipcServer.Start(); + _ = CheckVersion(uiCommandFactory); } diff --git a/Editors/Ipc/IpcEditor/AssetEditorIpcServer.cs b/Editors/Ipc/IpcEditor/AssetEditorIpcServer.cs index 0f8e58998..857bc6a90 100644 --- a/Editors/Ipc/IpcEditor/AssetEditorIpcServer.cs +++ b/Editors/Ipc/IpcEditor/AssetEditorIpcServer.cs @@ -9,6 +9,7 @@ namespace Editors.Ipc public class AssetEditorIpcServer : IDisposable { public const string PipeName = "TheAssetEditor.Ipc"; + public const string OwnershipMutexName = @"Local\TheAssetEditor.Ipc.Owner"; private static readonly JsonSerializerOptions SerializerOptions = new() { @@ -21,9 +22,9 @@ public class AssetEditorIpcServer : IDisposable private readonly IServiceScopeFactory _scopeFactory; private readonly object _syncLock = new(); - private CancellationTokenSource _cancellationTokenSource; - private Task _serverTask; - private NamedPipeServerStream _activePipe; + private CancellationTokenSource? _cancellationTokenSource; + private Thread? _serverThread; + private NamedPipeServerStream? _activePipe; private bool _disposed; public AssetEditorIpcServer(IServiceScopeFactory scopeFactory) @@ -38,11 +39,81 @@ public void Start() if (_disposed) throw new ObjectDisposedException(nameof(AssetEditorIpcServer)); - if (_serverTask != null) + if (_serverThread != null) return; - _cancellationTokenSource = new CancellationTokenSource(); - _serverTask = Task.Run(() => RunServerLoopAsync(_cancellationTokenSource.Token)); + var cancellationTokenSource = new CancellationTokenSource(); + var serverThread = new Thread(() => RunOwnershipLoop(cancellationTokenSource.Token)) + { + IsBackground = true, + Name = "AssetEditor IPC ownership" + }; + + _cancellationTokenSource = cancellationTokenSource; + _serverThread = serverThread; + + try + { + serverThread.Start(); + } + catch + { + _cancellationTokenSource = null; + _serverThread = null; + cancellationTokenSource.Dispose(); + throw; + } + } + } + + private void RunOwnershipLoop(CancellationToken cancellationToken) + { + try + { + using var ownershipMutex = new Mutex(false, OwnershipMutexName); + _logger.Here().Information($"Waiting for IPC ownership on {OwnershipMutexName}"); + + var ownsMutex = false; + try + { + int waitResult; + try + { + waitResult = WaitHandle.WaitAny([ownershipMutex, cancellationToken.WaitHandle]); + } + catch (AbandonedMutexException ex) when (ex.MutexIndex == 0) + { + // The previous owner exited without releasing the mutex. Windows + // grants ownership to this thread, so it is safe to start the pipe. + waitResult = 0; + _logger.Here().Warning("Taking over IPC ownership from an exited Asset Editor instance"); + } + + if (waitResult != 0) + return; + + ownsMutex = true; + _logger.Here().Information("Acquired IPC ownership"); + + // A Windows mutex must be released by the thread that acquired it. + // Keep this thread alive while the async server loop runs elsewhere. + RunServerLoopAsync(cancellationToken).GetAwaiter().GetResult(); + } + finally + { + if (ownsMutex) + { + ownershipMutex.ReleaseMutex(); + _logger.Here().Information("Released IPC ownership"); + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + } + catch (Exception ex) + { + _logger.Here().Error(ex, "IPC ownership coordinator stopped unexpectedly"); } } @@ -52,10 +123,16 @@ private async Task RunServerLoopAsync(CancellationToken cancellationToken) while (cancellationToken.IsCancellationRequested == false) { - NamedPipeServerStream pipe = null; + NamedPipeServerStream? pipe = null; + var retryAfterFailure = false; try { - pipe = new NamedPipeServerStream(PipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + pipe = new NamedPipeServerStream( + PipeName, + PipeDirection.InOut, + 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); SetActivePipe(pipe); await pipe.WaitForConnectionAsync(cancellationToken); @@ -74,12 +151,27 @@ private async Task RunServerLoopAsync(CancellationToken cancellationToken) catch (Exception ex) { _logger.Here().Error(ex, "Unhandled exception in IPC server loop"); + retryAfterFailure = true; } finally { - ClearActivePipe(pipe); + if (pipe != null) + ClearActivePipe(pipe); + pipe?.Dispose(); } + + if (retryAfterFailure) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + break; + } + } } _logger.Here().Information("IPC named pipe server stopped"); @@ -93,7 +185,7 @@ private async Task ProcessRequestAsync(NamedPipeServerStream pipe, if (string.IsNullOrWhiteSpace(line)) return IpcResponse.Failure("Empty request"); - IpcRequest request; + IpcRequest? request; try { request = JsonSerializer.Deserialize(line, SerializerOptions); @@ -154,9 +246,9 @@ private void ClearActivePipe(NamedPipeServerStream pipe) public void Dispose() { - CancellationTokenSource cancellationTokenSource; - Task serverTask; - NamedPipeServerStream activePipe; + CancellationTokenSource? cancellationTokenSource; + Thread? serverThread; + NamedPipeServerStream? activePipe; lock (_syncLock) { @@ -165,11 +257,11 @@ public void Dispose() _disposed = true; cancellationTokenSource = _cancellationTokenSource; - serverTask = _serverTask; + serverThread = _serverThread; activePipe = _activePipe; _cancellationTokenSource = null; - _serverTask = null; + _serverThread = null; _activePipe = null; } @@ -189,18 +281,21 @@ public void Dispose() { } - if (serverTask != null) + var serverStopped = true; + if (serverThread != null) { try { - _ = serverTask.Wait(TimeSpan.FromSeconds(2)); + serverStopped = serverThread.Join(TimeSpan.FromSeconds(2)); } catch { + serverStopped = false; } } - cancellationTokenSource?.Dispose(); + if (serverStopped) + cancellationTokenSource?.Dispose(); } } }