Merge nucleic/warm-north-vole-pf37 into dev
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user