Skip to content
Merged
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
107 changes: 76 additions & 31 deletions src/UniGetUI.Avalonia/Infrastructure/SingleInstanceRedirector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <summary>
/// Start a background listener thread. When a second instance forwards its args,
/// <paramref name="onArgsReceived"/> is invoked on the Avalonia UI thread.
Expand All @@ -44,28 +49,33 @@ public static void StartListener(Action<string[]> 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();
}
}
})
{
Expand All @@ -76,36 +86,71 @@ public static void StartListener(Action<string[]> onArgsReceived)
_listener.Start();
}

private static void HandleConnection(NamedPipeServerStream pipe, Action<string[]> 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}");
}
});
}

/// <summary>
/// Try to forward <paramref name="args"/> to the already-running first instance.
/// </summary>
/// <returns><c>true</c> if the message was delivered successfully.</returns>
public static bool TryForwardToFirstInstance(string[] args)
=> ForwardToFirstInstanceAsync(args).GetAwaiter().GetResult();

private static async Task<bool> 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;
}
}
Loading