Files

102 lines
3.8 KiB
C#
Raw Permalink Normal View History

2026-08-04 02:46:53 -07:00
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<HostdRendezvous> 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<HostdRendezvous?> 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;
}
}
}