using System.Diagnostics; using System.Text.Json; using XFEEDToolBox.Core.Navigation; // The observer owns its own files and lifetime. Disposing a navigation/trade // handle never stops it. Only the explicit user stop file ends this service. internal sealed class PersistentProtectionProcess:IDisposable { private static readonly AsyncLocal current=new(); internal static PersistentProtectionProcess? Current{get=>current.Value;set=>current.Value=value;} private readonly Process observer; private readonly int gamePid; private readonly long gameStarted,observerStarted; private readonly string report; private PersistentProtectionProcess(int pid,Process process,long expectedGameStart,string reportPath) {gamePid=pid;observer=process;gameStarted=expectedGameStart;observerStarted=process.StartTime.ToUniversalTime().Ticks;report=reportPath;} internal int ObserverPid=>observer.Id; internal bool IsAlive{get{try{return !observer.HasExited&&observer.StartTime.ToUniversalTime().Ticks==observerStarted;}catch{return false;}}} internal void RequireReady(int pid) { if(pid!=gamePid||!IsAlive)throw new InvalidOperationException("Independent ship protection has stopped; all automatic input is barred."); using var game=Process.GetProcessById(gamePid); if(game.HasExited||game.StartTime.ToUniversalTime().Ticks!=gameStarted)throw new InvalidOperationException("Protected game identity changed."); try { using var stream=new FileStream(report+".ready.json",FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete); using var document=JsonDocument.Parse(stream);var r=document.RootElement; if(r.GetProperty("gamePid").GetInt32()!=gamePid||r.GetProperty("gameStartedUtcTicks").GetInt64()!=gameStarted ||r.GetProperty("observerPid").GetInt32()!=ObserverPid||r.GetProperty("observerStartedUtcTicks").GetInt64()!=observerStarted||r.GetProperty("shipId").GetInt64()!=5) throw new InvalidOperationException("Independent protection witness belongs to another process."); var at=DateTimeOffset.FromUnixTimeMilliseconds(r.GetProperty("healthAtUnixMilliseconds").GetInt64());var now=DateTimeOffset.UtcNow; if(!r.GetProperty("ready").GetBoolean()||at>now.AddMilliseconds(50)||now-at>TimeSpan.FromMilliseconds(500)) throw new FlightTelemetryUnavailableException("Waiting for independently protected fresh ship health."); } catch(Exception error)when(error is IOException or JsonException) {throw new FlightTelemetryUnavailableException("Waiting for the independent protection witness.");} } internal void StopForUser()=>File.WriteAllText(report+".stop","Explicit user stop."); internal static async Task StartOrReuseAsync(int pid,string workRoot,CancellationToken token,bool allowStart=true) { workRoot=Path.GetFullPath(workRoot);using var game=Process.GetProcessById(pid);var gameStart=game.StartTime.ToUniversalTime().Ticks; var registry=Path.Combine(workRoot,"active-ship-protection.json");PersistentProtectionProcess? result=null; if(File.Exists(registry)) { using var saved=JsonDocument.Parse(await File.ReadAllTextAsync(registry,token));var r=saved.RootElement; try { var p=Process.GetProcessById(r.GetProperty("observerPid").GetInt32()); if(!p.HasExited&&p.StartTime.ToUniversalTime().Ticks==r.GetProperty("observerStartedUtcTicks").GetInt64()) { if(r.GetProperty("gamePid").GetInt32()!=pid||r.GetProperty("gameStartedUtcTicks").GetInt64()!=gameStart) {p.Dispose();throw new InvalidOperationException("Another game already has an active protection service.");} result=new(pid,p,gameStart,r.GetProperty("report").GetString()!); } else p.Dispose(); } catch(ArgumentException){} } if(result is null) { if(!allowStart)throw new InvalidOperationException("The expected independent protection service is not registered; diagnostic will not create a second observer."); var report=Path.Combine(workRoot,"production-protection-"+Guid.NewGuid().ToString("N")+".json"); var start=new ProcessStartInfo(Environment.ProcessPath!){UseShellExecute=false,CreateNoWindow=true,WindowStyle=ProcessWindowStyle.Hidden}; foreach(var argument in new[]{"observe-health-service",pid.ToString(),report})start.ArgumentList.Add(argument); var observer=Process.Start(start)??throw new IOException("Independent protection could not start.");result=new(pid,observer,gameStart,report); } var until=DateTimeOffset.UtcNow.AddSeconds(12); while(true) { token.ThrowIfCancellationRequested(); try{result.RequireReady(pid);return result;} catch(FlightTelemetryUnavailableException)when(DateTimeOffset.UtcNowobserver.Dispose(); // Never terminate protection on worker unwind. }