child/HostBridge.cs
|
#nullable enable using System; using System.IO; using System.Security.Cryptography; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace DeskPilot.Child { /// <summary>Host-owned message framing and independent per-process lease renewal.</summary> public sealed class HostBridge : IDisposable { private readonly MessageChannel _channel; private readonly Timer _renewal; private readonly CancellationTokenSource _cancel = new CancellationTokenSource(); private int _disposed; /// <summary>Initializes a unique authenticated connection over already owned process streams.</summary> public HostBridge(Stream input, Stream output, int leaseSeconds, int maximumBytes) { if (leaseSeconds < 1 || leaseSeconds > 30) { throw new ArgumentOutOfRangeException(nameof(leaseSeconds)); } byte[] key = RandomNumberGenerator.GetBytes(32); _channel = new MessageChannel(input, output, key, true, maximumBytes); try { output.Write(key); output.Flush(); } finally { CryptographicOperations.ZeroMemory(key); } _renewal = new Timer(_ => { try { if (Volatile.Read(ref _disposed) == 0) { _channel.Send("{\"type\":\"renew\"}"); } } catch { _cancel.Cancel(); } }, null, 0, Math.Max(100, leaseSeconds * 1000 / 3)); try { using var startup = new CancellationTokenSource(30000); using JsonDocument response = JsonDocument.Parse(_channel.ReceiveAsync(startup.Token).GetAwaiter().GetResult()); if (response.RootElement.GetProperty("type").GetString() != "ready") { throw new InvalidDataException("The owned process did not acknowledge startup."); } } catch { Dispose(); throw; } } /// <summary>Sends the trusted launch configuration once; the peer rejects repeats.</summary> public void Configure(string configuration) { using JsonDocument document = JsonDocument.Parse(configuration); _channel.Send(JsonSerializer.Serialize(new { type = "configure", configuration = document.RootElement })); } /// <summary>Replies only to one authenticated peer request id.</summary> public void Reply(string id, string payload) { if (!System.Text.RegularExpressions.Regex.IsMatch(id, "^[a-f0-9]{32}$")) { throw new InvalidDataException("Invalid host reply identity."); } using JsonDocument document = JsonDocument.Parse(payload); _channel.Send(JsonSerializer.Serialize(new { type = "reply", id, payload = document.RootElement })); } /// <summary>Receives one bounded message with caller and connection cancellation.</summary> public async Task<string> ReceiveAsync(CancellationToken cancellationToken) { using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancel.Token); return await _channel.ReceiveAsync(linked.Token).ConfigureAwait(false); } /// <summary>Withdraws authority renewal without relying on the peer Runspace.</summary> public void WithdrawLease() => _renewal.Dispose(); /// <summary>Closes the connection after owned-process termination.</summary> public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } _renewal.Dispose(); _cancel.Cancel(); _channel.Dispose(); } } } |