diff --git a/Source/Hurl.BrowserSelector/App.xaml.cs b/Source/Hurl.BrowserSelector/App.xaml.cs index daeb5c9..b4d1572 100644 --- a/Source/Hurl.BrowserSelector/App.xaml.cs +++ b/Source/Hurl.BrowserSelector/App.xaml.cs @@ -8,10 +8,6 @@ using Microsoft.Extensions.Hosting; using System; using System.Diagnostics; -using System.IO; -using System.IO.Pipes; -using System.Security.AccessControl; -using System.Security.Principal; using System.Text.Json; using System.Threading; using System.Windows; @@ -26,13 +22,10 @@ public partial class App : Application private MainWindow? _mainWindow; private const string MUTEX_NAME = "Hurl_Mutex_3721"; - private const string EVENT_NAME = "Hurl_Event_3721"; private Mutex? _singleInstanceMutex; - private EventWaitHandle? _singleInstanceWaitHandle; + private NamedPipeUrlReceiver? _pipeReceiver; - private readonly CancellationTokenSource _cancelTokenSource = new(); - private Thread? _pipeServerListenThread; public static IHost? AppHost { get; private set; } public App() @@ -77,41 +70,19 @@ private void Dispatcher_UnhandledException(object sender, System.Windows.Threadi protected override void OnStartup(StartupEventArgs e) { _singleInstanceMutex = new Mutex(true, MUTEX_NAME, out var isOwned); - _singleInstanceWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, EVENT_NAME); if (!isOwned) { - _singleInstanceWaitHandle.Set(); + // Another instance is running - it will receive the URL via pipe from the Launcher Shutdown(); return; } - new Thread(() => - { - while (_singleInstanceWaitHandle.WaitOne()) - { - Current.Dispatcher.BeginInvoke(() => - { - if (Current.MainWindow is { } window) - { - _mainWindow?.ShowWindow(); - } - else - { - Shutdown(); - } - }); - } - }) - { - IsBackground = true - }.Start(); - - _pipeServerListenThread = new Thread(PipeServer); - _pipeServerListenThread.Start(); + // Start the pipe receiver (always ready for connections with overlapping listeners) + _pipeReceiver = new NamedPipeUrlReceiver(OnInstanceInvoked); + _pipeReceiver.Start(); var cliArgs = CliArgs.GatherInfo(e.Args, false); - //OpenedUri.Value = cliArgs.Url; AppHost?.Services.GetRequiredService().Set(cliArgs.Url); _mainWindow = new(); @@ -120,16 +91,17 @@ protected override void OnStartup(StartupEventArgs e) protected override void OnExit(ExitEventArgs e) { - _cancelTokenSource.Cancel(); - _pipeServerListenThread?.Join(); + if (_pipeReceiver != null) + { + _pipeReceiver.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } _singleInstanceMutex?.Close(); - _singleInstanceWaitHandle?.Close(); base.OnExit(e); } - public void OnInstanceInvoked(string[] args) + private void OnInstanceInvoked(string[] args) { Current.Dispatcher.InvokeAsync(() => { @@ -139,47 +111,10 @@ public void OnInstanceInvoked(string[] args) if (!IsTimedSet) { Debug.WriteLine($"Hurl Browser Selector: Instance Invoked with URL: {cliArgs.Url}"); - AppHost.Services.GetRequiredService().Set(cliArgs.Url); + AppHost?.Services.GetRequiredService().Set(cliArgs.Url); _mainWindow?.Init(cliArgs); } }); } - - public void PipeServer() - { - PipeSecurity pipeSecurity = new(); - pipeSecurity.AddAccessRule(new PipeAccessRule( - new SecurityIdentifier(WellKnownSidType.WorldSid, null), - PipeAccessRights.ReadWrite, - AccessControlType.Allow)); - - while (!_cancelTokenSource.Token.IsCancellationRequested) - { - try - { - using var _pipeserver = NamedPipeServerStreamAcl.Create( - "HurlNamedPipe", - PipeDirection.InOut, 1, - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 0, 0, - pipeSecurity); - _pipeserver.WaitForConnectionAsync(_cancelTokenSource.Token).Wait(); - - using StreamReader sr = new(_pipeserver); - string args = sr.ReadToEnd(); - string[] argsArray = JsonSerializer.Deserialize(args) ?? []; - OnInstanceInvoked(argsArray); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception e) - { - Debug.WriteLine($"Error in PipeServer: {e.Message}"); - } - } - } } } diff --git a/Source/Hurl.BrowserSelector/Services/NamedPipeUrlReceiver.cs b/Source/Hurl.BrowserSelector/Services/NamedPipeUrlReceiver.cs new file mode 100644 index 0000000..29061cf --- /dev/null +++ b/Source/Hurl.BrowserSelector/Services/NamedPipeUrlReceiver.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Hurl.BrowserSelector.Services; + +/// +/// A robust Named Pipe server that maintains overlapping server instances +/// to eliminate race conditions. Always has at least one pipe ready to accept connections. +/// +internal sealed class NamedPipeUrlReceiver : IAsyncDisposable +{ + private const string PipeName = "HurlNamedPipe"; + private const int MaxServerInstances = 4; + private const int TargetListenerCount = 2; + + private readonly CancellationTokenSource _cts = new(); + private readonly Action _onUrlReceived; + private readonly PipeSecurity _pipeSecurity; + + private int _activeListeners = 0; + + public NamedPipeUrlReceiver(Action onUrlReceived) + { + _onUrlReceived = onUrlReceived ?? throw new ArgumentNullException(nameof(onUrlReceived)); + + _pipeSecurity = new PipeSecurity(); + _pipeSecurity.AddAccessRule(new PipeAccessRule( + new SecurityIdentifier(WellKnownSidType.WorldSid, null), + PipeAccessRights.ReadWrite, + AccessControlType.Allow)); + } + + public void Start() + { + for (int i = 0; i < TargetListenerCount; i++) + { + _ = StartListenerAsync(); + } + } + + private async Task StartListenerAsync() + { + Interlocked.Increment(ref _activeListeners); + + try + { + while (!_cts.Token.IsCancellationRequested) + { + NamedPipeServerStream? pipeServer = null; + + try + { + pipeServer = NamedPipeServerStreamAcl.Create( + PipeName, + PipeDirection.InOut, + MaxServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | PipeOptions.WriteThrough, + inBufferSize: 4096, + outBufferSize: 4096, + _pipeSecurity); + + try + { + await pipeServer.WaitForConnectionAsync(_cts.Token).ConfigureAwait(false); + } + catch (IOException ex) when (ex.HResult == -2147024664) // ERROR_PIPE_CONNECTED + { + // Client connected before WaitForConnectionAsync was called - connection is already established + } + + // Spawn replacement listener IMMEDIATELY before processing + EnsureMinimumListeners(); + + await ProcessConnectionAsync(pipeServer).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + Debug.WriteLine($"Pipe listener error: {ex.Message}"); + try + { + await Task.Delay(100, _cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + } + finally + { + pipeServer?.Dispose(); + } + } + } + finally + { + Interlocked.Decrement(ref _activeListeners); + } + } + + private void EnsureMinimumListeners() + { + int current = Volatile.Read(ref _activeListeners); + // Spawn a replacement if we're at or below target, ensuring always have listeners ready + if (current <= TargetListenerCount && !_cts.Token.IsCancellationRequested) + { + _ = StartListenerAsync(); + } + } + + private async Task ProcessConnectionAsync(NamedPipeServerStream pipeServer) + { + try + { + using var reader = new StreamReader(pipeServer, leaveOpen: true); + + using var readCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + readCts.CancelAfter(TimeSpan.FromSeconds(5)); + + string data = await reader.ReadToEndAsync(readCts.Token).ConfigureAwait(false); + + if (!string.IsNullOrWhiteSpace(data)) + { + string[]? args = JsonSerializer.Deserialize(data); + if (args != null && args.Length > 0) + { + _onUrlReceived(args); + } + } + } + catch (OperationCanceledException) + { + // Timeout or shutdown + } + catch (JsonException ex) + { + Debug.WriteLine($"Failed to parse pipe data: {ex.Message}"); + } + catch (Exception ex) + { + Debug.WriteLine($"Error processing pipe connection: {ex.Message}"); + } + } + + public async ValueTask DisposeAsync() + { + _cts.Cancel(); + + int maxWaitIterations = 50; + while (Volatile.Read(ref _activeListeners) > 0 && maxWaitIterations-- > 0) + { + await Task.Delay(100).ConfigureAwait(false); + } + + _cts.Dispose(); + } +}