Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AiDocumentation/asseteditor-ipc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 3 additions & 5 deletions AssetEditor/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,9 @@ protected override void OnStartup(StartupEventArgs e)

ShowMainWindow();

if (e.Args.Contains("Start_IPC"))
{
_ipcServer = _serviceProvider.GetRequiredService<AssetEditorIpcServer>();
_ipcServer.Start();
}
_ipcServer = _serviceProvider.GetRequiredService<AssetEditorIpcServer>();
_ipcServer.Start();

_ = CheckVersion(uiCommandFactory);
}

Expand Down
131 changes: 113 additions & 18 deletions Editors/Ipc/IpcEditor/AssetEditorIpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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)
Expand All @@ -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");
}
}

Expand All @@ -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);
Expand All @@ -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");
Expand All @@ -93,7 +185,7 @@ private async Task<IpcResponse> ProcessRequestAsync(NamedPipeServerStream pipe,
if (string.IsNullOrWhiteSpace(line))
return IpcResponse.Failure("Empty request");

IpcRequest request;
IpcRequest? request;
try
{
request = JsonSerializer.Deserialize<IpcRequest>(line, SerializerOptions);
Expand Down Expand Up @@ -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)
{
Expand All @@ -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;
}

Expand All @@ -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();
}
}
}
Loading