using System.Diagnostics; using System.Net.Sockets; using NucleicProtocol.Interop; namespace NucleicApp.Services; public sealed record HostdLaunchOptions( string ExecutablePath, string RendezvousPath, string? WorkingDirectory = null, TimeSpan? StartupTimeout = null); /// Starts the channel-matched full-trust host and waits for a live owner-only rendezvous. public sealed class HostdLauncher { private readonly HostdLaunchOptions options; public HostdLauncher(HostdLaunchOptions options) => this.options = options; public async Task EnsureRunningAsync( CancellationToken cancellationToken = default) { if (await TryHealthyRendezvousAsync(cancellationToken).ConfigureAwait(false) is { } healthy) return healthy; Process? launched = null; if (!HostMutexExists()) { if (!File.Exists(options.ExecutablePath)) throw new FileNotFoundException("nucleic-hostd was not found", options.ExecutablePath); launched = Process.Start(new ProcessStartInfo { FileName = options.ExecutablePath, WorkingDirectory = options.WorkingDirectory ?? Path.GetDirectoryName(options.ExecutablePath) ?? Environment.CurrentDirectory, UseShellExecute = false, CreateNoWindow = true, }) ?? throw new InvalidOperationException("Windows refused to start nucleic-hostd"); } var timeout = options.StartupTimeout ?? TimeSpan.FromSeconds(30); using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); deadline.CancelAfter(timeout); while (true) { deadline.Token.ThrowIfCancellationRequested(); if (launched is { HasExited: true }) throw new InvalidOperationException($"nucleic-hostd exited during startup ({launched.ExitCode})"); if (await TryHealthyRendezvousAsync(deadline.Token).ConfigureAwait(false) is { } ready) return ready; await Task.Delay(100, deadline.Token).ConfigureAwait(false); } } private async Task TryHealthyRendezvousAsync(CancellationToken cancellationToken) { HostdRendezvous record; try { record = await RendezvousFile.ReadAsync(options.RendezvousPath, cancellationToken); } catch (Exception error) when (error is IOException or UnauthorizedAccessException or System.Text.Json.JsonException) { return null; } if (!ProcessIsAlive(record.ProcessId)) return null; try { using var client = new TcpClient(); using var probe = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); probe.CancelAfter(TimeSpan.FromSeconds(1)); await client.ConnectAsync(record.Host, record.Port, probe.Token).ConfigureAwait(false); return record; } catch (Exception error) when (error is SocketException or OperationCanceledException) { return null; } } private static bool ProcessIsAlive(int processId) { try { return !Process.GetProcessById(processId).HasExited; } catch (ArgumentException) { return false; } } private static bool HostMutexExists() { if (!OperatingSystem.IsWindows()) return false; try { if (!Mutex.TryOpenExisting("Local\\nucleic-hostd" + BuildChannel.Suffix, out var mutex)) return false; mutex.Dispose(); return true; } catch (UnauthorizedAccessException) { // Existence without access is still existence; hostd's own guard remains definitive. return true; } } }