child/EngineBridge.cs

#nullable enable
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace DeskPilot.Child
{
    /// <summary>Per-process Engine RPC with a control reader independent of its Runspace.</summary>
    public sealed class EngineBridge : IDisposable
    {
        private readonly MessageChannel _channel;
        private readonly Timer _lease;
        private readonly int _leaseMilliseconds;
        private readonly Stopwatch _clock = Stopwatch.StartNew();
        private readonly object _sync = new object();
        private readonly TaskCompletionSource<string> _configuration = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
        private TaskCompletionSource<string>? _pending;
        private string? _pendingId;
        private long _renewed;
        private int _disposed;

        /// <summary>One bridge owned by this fresh Engine process.</summary>
        public static EngineBridge? Current { get; private set; }

        private EngineBridge(MessageChannel channel, int leaseSeconds)
        {
            _channel = channel;
            _leaseMilliseconds = checked(leaseSeconds * 1000);
            _lease = new Timer(_ =>
            {
                if (Volatile.Read(ref _disposed) == 0 && _clock.ElapsedMilliseconds - Interlocked.Read(ref _renewed) > _leaseMilliseconds)
                { Terminate(); }
            }, null, 100, 100);
            _ = Task.Run(ReadControlAsync);
        }

        /// <summary>Reads a per-run key only from owned stdin, then starts control supervision.</summary>
        public static EngineBridge Start(int leaseSeconds, int maximumBytes)
        {
            if (leaseSeconds < 1 || leaseSeconds > 30) { throw new ArgumentOutOfRangeException(nameof(leaseSeconds)); }
            using var startup = new Timer(_ => Terminate(), null, leaseSeconds * 1000, Timeout.Infinite);
            Stream input = Console.OpenStandardInput();
            Stream output = Console.OpenStandardOutput();
            byte[] key = new byte[32];
            input.ReadExactly(key);
            var channel = new MessageChannel(input, output, key, false, maximumBytes);
            CryptographicOperations.ZeroMemory(key);
            if (Current != null) { throw new InvalidOperationException("An Engine bridge already exists."); }
            Current = new EngineBridge(channel, leaseSeconds);
            channel.Send("{\"type\":\"ready\"}");
            return Current;
        }

        /// <summary>Host-owned immutable launch configuration.</summary>
        public string Configuration => _configuration.Task.GetAwaiter().GetResult();

        /// <summary>Requests a provider call or confined Tool operation and awaits its correlated reply.</summary>
        public string Invoke(string kind, string payload)
        {
            if (kind != "provider" && kind != "tool") { throw new InvalidDataException("Unsupported Engine request kind."); }
            using JsonDocument document = JsonDocument.Parse(payload);
            TaskCompletionSource<string> response;
            string id = Guid.NewGuid().ToString("N");
            lock (_sync)
            {
                if (_pending != null || Volatile.Read(ref _disposed) != 0) { throw new InvalidOperationException("Engine request unavailable."); }
                response = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
                _pending = response;
                _pendingId = id;
            }
            _channel.Send(JsonSerializer.Serialize(new { type = kind, id, payload = document.RootElement }));
            return response.Task.GetAwaiter().GetResult();
        }

        /// <summary>Returns a bounded final result as data, never a control instruction.</summary>
        public void Complete(string result)
        {
            using JsonDocument document = JsonDocument.Parse(result);
            _channel.Send(JsonSerializer.Serialize(new { type = "complete", result = document.RootElement }));
        }

        private async Task ReadControlAsync()
        {
            try
            {
                while (Volatile.Read(ref _disposed) == 0)
                {
                    using JsonDocument document = JsonDocument.Parse(await _channel.ReceiveAsync(CancellationToken.None).ConfigureAwait(false));
                    JsonElement root = document.RootElement;
                    switch (root.GetProperty("type").GetString())
                    {
                        case "renew":
                            Interlocked.Exchange(ref _renewed, _clock.ElapsedMilliseconds);
                            break;
                        case "configure":
                            if (!_configuration.TrySetResult(root.GetProperty("configuration").GetRawText()))
                            { throw new InvalidDataException("Duplicate configuration."); }
                            break;
                        case "reply":
                            lock (_sync)
                            {
                                if (_pending == null || root.GetProperty("id").GetString() != _pendingId)
                                { throw new InvalidDataException("Stale or mismatched reply."); }
                                TaskCompletionSource<string> pending = _pending;
                                _pending = null;
                                _pendingId = null;
                                pending.SetResult(root.GetProperty("payload").GetRawText());
                            }
                            break;
                        case "stop":
                            Terminate();
                            break;
                        default:
                            throw new InvalidDataException("Unsupported host control.");
                    }
                }
            }
            catch
            {
                if (Volatile.Read(ref _disposed) == 0) { Terminate(); }
            }
        }

        private static void Terminate()
        {
            if (OperatingSystem.IsWindows()) { ExitProcess(125); }
            else if (OperatingSystem.IsLinux()) { ExitUnix(125); }
            else { Environment.FailFast("Unsupported child runtime platform."); }
        }

        [DllImport("kernel32.dll")]
        private static extern void ExitProcess(uint exitCode);

        [DllImport("libc", EntryPoint = "_exit")]
        private static extern void ExitUnix(int exitCode);

        /// <summary>Closes the channel and withdraws supervision only after the process has completed.</summary>
        public void Dispose()
        {
            Interlocked.Exchange(ref _disposed, 1);
            _lease.Dispose();
            _channel.Dispose();
        }
    }
}