using System; using System.Diagnostics; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using XFE.SeAgent.Plugin.Protocol; namespace XFE.SeAgent.Plugin.Transport { internal sealed class PendingRequest { private readonly object gate = new object(); private readonly long deadline; private readonly Func connectionAlive; private readonly Func serverStopped; private readonly TaskCompletionSource completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private RequestState state; internal AgentRequest Request { get; } internal Task Completion => completion.Task; internal PendingRequest(AgentRequest request, Func connectionAlive, Func serverStopped) { Request = request; this.connectionAlive = connectionAlive; this.serverStopped = serverStopped; deadline = Stopwatch.GetTimestamp() + (long)Math.Ceiling(request.TimeoutMs * (double)Stopwatch.Frequency / 1000); } // This is the execution boundary. Cancellation before this claim prevents dispatch; // an already executing game operation cannot safely be interrupted by a pipe worker. internal bool TryStart() { lock (gate) { if (state != RequestState.Waiting) return false; if (serverStopped()) return CancelLocked("server_stopped", "Agent server has stopped."); if (Stopwatch.GetTimestamp() >= deadline) return CancelLocked("timeout", "Request expired before execution."); if (!connectionAlive()) return CancelLocked("disconnected", "Client disconnected before execution."); if (serverStopped()) return CancelLocked("server_stopped", "Agent server has stopped."); if (Stopwatch.GetTimestamp() >= deadline) return CancelLocked("timeout", "Request expired before execution."); state = RequestState.Executing; return true; } } internal void Complete(JObject response) { lock (gate) { if (state != RequestState.Executing) return; state = RequestState.Finished; completion.TrySetResult(response); } } internal void Cancel(string code, string message) { lock (gate) { if (state == RequestState.Finished || state == RequestState.Canceled) return; if (state == RequestState.Executing && code == "timeout") message = "Request timed out after execution began; the operation may have completed. Check state before retrying."; CancelLocked(code, message); } } private bool CancelLocked(string code, string message) { state = RequestState.Canceled; completion.TrySetResult(AgentProtocol.Error(Request.Id, code, message)); return false; } private enum RequestState { Waiting, Executing, Finished, Canceled } } }