XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFE.SpaceEngineers.AgentBridge

【SpaceEngineer】AI调试插件

公开
关注 0 Fork 0 Star 0
UTF-8
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Newtonsoft.Json.Linq;
using XFE.SeAgent.Plugin.Protocol;

namespace XFE.SeAgent.Plugin.Transport
{
    /// <summary>Local, current-user JSON RPC. Only Pump invokes game-facing callbacks.</summary>
    public sealed class AgentServer : IDisposable
    {
        public const int MaximumConnections = 4;
        public const int MaximumQueuedRequests = 128;
        private const int ReadTimeoutMs = 10000;
        private const int WriteTimeoutMs = 5000;
        private readonly Func<string, JObject, JObject> execute;
        private readonly Action<string> log;
        private readonly object gate = new object();
        private readonly Queue<PendingRequest> requests = new Queue<PendingRequest>();
        private readonly Queue<string> logs = new Queue<string>();
        private readonly HashSet<NamedPipeServerStream> pipes = new HashSet<NamedPipeServerStream>();
        private readonly CancellationTokenSource shutdown = new CancellationTokenSource();
        private int disposed;
        private int pumpThread;
        private int pumping;
        private bool started;

        public string PipeName { get; }

        public AgentServer(Func<string, JObject, JObject> execute, Action<string> log)
        {
            this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
            this.log = log ?? throw new ArgumentNullException(nameof(log));
            PipeName = "XFE.SE.Agent." + Process.GetCurrentProcess().Id;
        }

        public void Start()
        {
            lock (gate)
            {
                ThrowIfDisposed();
                if (started) return;
                var initial = new List<NamedPipeServerStream>();
                try
                {
                    for (int i = 0; i < MaximumConnections; i++)
                    {
                        var pipe = CreatePipe();
                        initial.Add(pipe);
                        pipes.Add(pipe);
                    }
                    started = true;
                    foreach (var pipe in initial) _ = Task.Run(() => ListenAsync(pipe));
                    EnqueueLog("Agent pipe ready: " + PipeName);
                }
                catch
                {
                    foreach (var pipe in initial) { pipes.Remove(pipe); pipe.Dispose(); }
                    throw;
                }
            }
        }

        private NamedPipeServerStream CreatePipe()
        {
            using (var identity = WindowsIdentity.GetCurrent())
            {
                var user = identity.User ?? throw new InvalidOperationException("Current Windows user SID is unavailable.");
                var security = new PipeSecurity();
                security.SetAccessRuleProtection(true, false);
                security.SetOwner(user);
                security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.NetworkSid, null), PipeAccessRights.FullControl, AccessControlType.Deny));
                security.AddAccessRule(new PipeAccessRule(user, PipeAccessRights.FullControl, AccessControlType.Allow));
#if NETFRAMEWORK
                return new NamedPipeServerStream(PipeName, PipeDirection.InOut, MaximumConnections,
                    PipeTransmissionMode.Byte, PipeOptions.Asynchronous, 4096, 4096, security);
#else
                return NamedPipeServerStreamAcl.Create(PipeName, PipeDirection.InOut, MaximumConnections,
                    PipeTransmissionMode.Byte, PipeOptions.Asynchronous, 4096, 4096, security);
#endif
            }
        }

        private async Task ListenAsync(NamedPipeServerStream first)
        {
            NamedPipeServerStream pipe = first;
            while (Volatile.Read(ref disposed) == 0)
            {
                try
                {
                    await pipe.WaitForConnectionAsync(shutdown.Token).ConfigureAwait(false);
                    await HandleConnectionAsync(pipe).ConfigureAwait(false);
                }
                catch (OperationCanceledException) when (Volatile.Read(ref disposed) != 0) { }
                catch (ObjectDisposedException) when (Volatile.Read(ref disposed) != 0) { }
                catch (Exception error)
                {
                    if (Volatile.Read(ref disposed) == 0) EnqueueLog("Agent connection ended: " + error.GetType().Name);
                }
                finally
                {
                    lock (gate) pipes.Remove(pipe);
                    pipe.Dispose();
                }
                lock (gate)
                {
                    if (disposed != 0) return;
                    try { pipe = CreatePipe(); pipes.Add(pipe); }
                    catch (Exception error) { EnqueueLog("Agent pipe listener stopped: " + error.GetType().Name); return; }
                }
            }
        }

        private async Task HandleConnectionAsync(NamedPipeServerStream pipe)
        {
            AgentRequest request;
            try
            {
                string line;
                using (var readTimeout = CancellationTokenSource.CreateLinkedTokenSource(shutdown.Token))
                {
                    readTimeout.CancelAfter(ReadTimeoutMs);
                    using (readTimeout.Token.Register(() => ClosePipe(pipe)))
                        line = await ReadLineAsync(pipe, readTimeout.Token).ConfigureAwait(false);
                }
                request = AgentProtocol.ParseRequest(line);
            }
            catch (AgentProtocolException error)
            {
                await WriteResponseAsync(pipe, AgentProtocol.Error(error.RequestId, error.Code, error.Message)).ConfigureAwait(false);
                return;
            }
            catch (System.Text.DecoderFallbackException)
            {
                await WriteResponseAsync(pipe, AgentProtocol.Error(null, "parse_error", "Request must be valid UTF-8.")).ConfigureAwait(false);
                return;
            }

            var pending = new PendingRequest(request, () => IsClientPresent(pipe), () => Volatile.Read(ref disposed) != 0);
            lock (gate)
            {
                if (disposed != 0) pending.Cancel("server_stopped", "Agent server has stopped.");
                else if (requests.Count >= MaximumQueuedRequests) pending.Cancel("server_busy", "Request queue is full. Retry later.");
                else requests.Enqueue(pending);
            }

            // One connection carries exactly one request. A second read detects remote close
            // while the game thread is busy, without a worker touching any game objects.
            Task disconnected = ObserveDisconnectAsync(pipe, pending);
            using (var wait = CancellationTokenSource.CreateLinkedTokenSource(shutdown.Token))
            {
                Task timeout = Task.Delay(request.TimeoutMs, wait.Token);
                Task finished = await Task.WhenAny(pending.Completion, disconnected, timeout).ConfigureAwait(false);
                if (finished == timeout)
                    pending.Cancel(Volatile.Read(ref disposed) == 0 ? "timeout" : "server_stopped", "Request expired before execution.");
                JObject response = await pending.Completion.ConfigureAwait(false);
                wait.Cancel();
                await WriteResponseAsync(pipe, response).ConfigureAwait(false);
            }
        }

        private static async Task<string> ReadLineAsync(NamedPipeServerStream pipe, CancellationToken token)
        {
            var buffer = new byte[4096];
            using (var line = new MemoryStream())
            {
                while (true)
                {
                    int count = await pipe.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
                    if (count == 0) throw new EndOfStreamException("Client closed before sending a complete request line.");
                    int newline = Array.IndexOf(buffer, (byte)'\n', 0, count);
                    int length = newline < 0 ? count : newline;
                    if (line.Length + length > AgentProtocol.MaxRequestBytes)
                        throw new AgentProtocolException("request_too_large", "Request exceeds the 1 MiB limit.");
                    line.Write(buffer, 0, length);
                    if (newline < 0) continue;
                    if (newline != count - 1)
                        throw new AgentProtocolException("invalid_request", "Only one request line is allowed per connection.");
                    byte[] data = line.ToArray();
                    int textLength = data.Length > 0 && data[data.Length - 1] == '\r' ? data.Length - 1 : data.Length;
                    return AgentProtocol.Utf8.GetString(data, 0, textLength);
                }
            }
        }

        private static async Task ObserveDisconnectAsync(NamedPipeServerStream pipe, PendingRequest pending)
        {
            try
            {
                var extra = new byte[1];
                int count = await pipe.ReadAsync(extra, 0, 1).ConfigureAwait(false);
                pending.Cancel(count == 0 ? "disconnected" : "invalid_request",
                    count == 0 ? "Client disconnected before completion." : "Only one request line is allowed per connection.");
            }
            catch (Exception error) when (error is IOException || error is ObjectDisposedException || error is InvalidOperationException || error is OperationCanceledException)
            {
                pending.Cancel("disconnected", "Client disconnected before completion.");
            }
        }

        private async Task WriteResponseAsync(NamedPipeServerStream pipe, JObject response)
        {
            byte[] bytes;
            try { bytes = AgentProtocol.SerializeResponse(response); }
            catch (AgentRpcException error) { bytes = AgentProtocol.SerializeResponse(AgentProtocol.Error(response["id"], error.Code, error.Message)); }
            using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(shutdown.Token))
            {
                timeout.CancelAfter(WriteTimeoutMs);
                using (timeout.Token.Register(() => ClosePipe(pipe)))
                {
                    await pipe.WriteAsync(bytes, 0, bytes.Length, timeout.Token).ConfigureAwait(false);
                    await pipe.FlushAsync(timeout.Token).ConfigureAwait(false);
                }
            }
        }

        public void Pump(int maxRequests = 4)
        {
            if (maxRequests < 0) throw new ArgumentOutOfRangeException(nameof(maxRequests));
            if (Volatile.Read(ref disposed) != 0) return;
            int thread = Thread.CurrentThread.ManagedThreadId;
            int owner = Interlocked.CompareExchange(ref pumpThread, thread, 0);
            if (owner != 0 && owner != thread) throw new InvalidOperationException("AgentServer.Pump must remain on the game Update thread.");
            if (Interlocked.CompareExchange(ref pumping, 1, 0) != 0) return;
            try
            {
                for (int i = 0; i < 8; i++)
                {
                    string entry;
                    lock (gate) { if (logs.Count == 0) break; entry = logs.Dequeue(); }
                    try { log(entry); } catch { /* Logging must not stop the game update loop. */ }
                }
                int executed = 0;
                for (int examined = 0; examined < MaximumQueuedRequests && executed < Math.Min(maxRequests, MaximumQueuedRequests); examined++)
                {
                    PendingRequest pending;
                    lock (gate) { if (requests.Count == 0 || disposed != 0) break; pending = requests.Dequeue(); }
                    if (!pending.TryStart()) continue;
                    executed++;
                    try { pending.Complete(AgentProtocol.Success(pending.Request.Id, execute(pending.Request.Method, pending.Request.Parameters))); }
                    catch (AgentRpcException error) { pending.Complete(AgentProtocol.Error(pending.Request.Id, error.Code, error.Message)); }
                    catch (Exception error) { pending.Complete(AgentProtocol.Error(pending.Request.Id, "execution_error", error.Message)); }
                }
            }
            finally { Volatile.Write(ref pumping, 0); }
        }

        private void EnqueueLog(string message)
        {
            lock (gate)
            {
                if (logs.Count >= MaximumQueuedRequests) logs.Dequeue();
                logs.Enqueue(message);
            }
        }

        private static bool IsClientPresent(NamedPipeServerStream pipe)
        {
            try
            {
                uint available;
                return pipe.IsConnected && PeekNamedPipe(pipe.SafePipeHandle, IntPtr.Zero, 0, IntPtr.Zero, out available, IntPtr.Zero);
            }
            catch (Exception error) when (error is IOException || error is ObjectDisposedException || error is InvalidOperationException) { return false; }
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool PeekNamedPipe(SafePipeHandle pipe, IntPtr buffer, uint bufferSize, IntPtr bytesRead, out uint bytesAvailable, IntPtr bytesLeftThisMessage);

        private static void ClosePipe(NamedPipeServerStream pipe)
        {
            try { pipe.Dispose(); } catch (IOException) { }
        }

        private void ThrowIfDisposed()
        {
            if (Volatile.Read(ref disposed) != 0) throw new ObjectDisposedException(nameof(AgentServer));
        }

        public void Dispose()
        {
            if (Interlocked.Exchange(ref disposed, 1) != 0) return;
            NamedPipeServerStream[] open;
            lock (gate)
            {
                foreach (var pending in requests) pending.Cancel("server_stopped", "Agent server has stopped.");
                requests.Clear();
                open = new NamedPipeServerStream[pipes.Count];
                pipes.CopyTo(open);
            }
            shutdown.Cancel();
            foreach (var pipe in open) ClosePipe(pipe);
            // The cancellation source stays alive until outstanding async pipe operations finish.
        }
    }
}