diff --git a/src/UniGetUI.Avalonia/Infrastructure/SingleInstanceRedirector.cs b/src/UniGetUI.Avalonia/Infrastructure/SingleInstanceRedirector.cs index a7f753be5c..fd79927b0f 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/SingleInstanceRedirector.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/SingleInstanceRedirector.cs @@ -34,6 +34,11 @@ private static string BuildPipeName() private static Thread? _listener; + // The mutex proves a first instance exists, so keep probing this long before giving up. + private static readonly TimeSpan ForwardBudget = TimeSpan.FromSeconds(5); + // Bound each read so a client that connects but never sends EOF can't wedge the loop. + private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(5); + /// /// Start a background listener thread. When a second instance forwards its args, /// is invoked on the Avalonia UI thread. @@ -44,28 +49,33 @@ public static void StartListener(Action onArgsReceived) { while (true) { + NamedPipeServerStream? pipe = null; try { - using var pipe = new NamedPipeServerStream( + pipe = new NamedPipeServerStream( PipeName, PipeDirection.In, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte); + maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous); pipe.WaitForConnection(); - using var reader = new StreamReader(pipe, Encoding.UTF8); - string payload = reader.ReadToEnd(); - - // Args are newline-delimited. - var args = payload.Split('\n', StringSplitOptions.RemoveEmptyEntries); - Dispatcher.UIThread.Post(() => onArgsReceived(args)); + // Read off-thread and immediately re-arm: a single slow/stuck client can + // never block the next accept nor "busy out" the pipe into timeouts. + HandleConnection(pipe, onArgsReceived); + pipe = null; // ownership handed to the read task } catch (Exception ex) when (ex is not ThreadAbortException) { // Keep the listener alive through errors (the pipe breaks on disconnect, etc.) Logger.Warn($"SingleInstanceRedirector listener error: {ex.Message}"); } + finally + { + // Dispose the stream if the handoff never happened, so a broken accept can't leak handles. + pipe?.Dispose(); + } } }) { @@ -76,36 +86,71 @@ public static void StartListener(Action onArgsReceived) _listener.Start(); } + private static void HandleConnection(NamedPipeServerStream pipe, Action onArgsReceived) + { + _ = Task.Run(async () => + { + try + { + using (pipe) + using (var reader = new StreamReader(pipe, Encoding.UTF8)) + using (var cts = new CancellationTokenSource(ReadTimeout)) + { + string payload = await reader.ReadToEndAsync(cts.Token); + // Args are newline-delimited. + var args = payload.Split('\n', StringSplitOptions.RemoveEmptyEntries); + Dispatcher.UIThread.Post(() => onArgsReceived(args)); + } + } + catch (Exception ex) + { + Logger.Warn($"SingleInstanceRedirector read error: {ex.Message}"); + } + }); + } + /// /// Try to forward to the already-running first instance. /// /// true if the message was delivered successfully. public static bool TryForwardToFirstInstance(string[] args) + => ForwardToFirstInstanceAsync(args).GetAwaiter().GetResult(); + + private static async Task ForwardToFirstInstanceAsync(string[] args) { - if (args.Length == 0) - { - // Nothing to forward — still show the window by sending an empty payload. - } + // One token bounds connect, write and flush across retries, so a peer that connects + // then stops reading can never block past the budget and skip the fallback. + using var cts = new CancellationTokenSource(ForwardBudget); + byte[] payload = Encoding.UTF8.GetBytes(string.Join('\n', args)); // newline-delimited args + Exception? last = null; - try - { - using var pipe = new NamedPipeClientStream( - serverName: ".", - pipeName: PipeName, - direction: PipeDirection.Out); - - pipe.Connect(500); - - using var writer = new StreamWriter(pipe, Encoding.UTF8); - // Newline-delimited args payload. - writer.Write(string.Join('\n', args)); - writer.Flush(); - return true; - } - catch (Exception ex) + while (true) { - Logger.Warn($"Could not forward args to first instance: {ex.Message}"); - return false; + try + { + using var pipe = new NamedPipeClientStream( + serverName: ".", + pipeName: PipeName, + direction: PipeDirection.Out, + options: PipeOptions.Asynchronous); + + await pipe.ConnectAsync(cts.Token).ConfigureAwait(false); + await pipe.WriteAsync(payload, cts.Token).ConfigureAwait(false); + await pipe.FlushAsync(cts.Token).ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + last = ex; + if (cts.IsCancellationRequested) + break; + + try { await Task.Delay(100, cts.Token).ConfigureAwait(false); } + catch (OperationCanceledException) { break; } + } } + + Logger.Warn($"Could not forward args to first instance: {last?.Message}"); + return false; } }