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<PersistentProtectionProcess?> 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<PersistentProtectionProcess> 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.UtcNow<until){await Task.Delay(80,token);}
}
}
public void Dispose()=>observer.Dispose(); // Never terminate protection on worker unwind.
}
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<PersistentProtectionProcess?> 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<PersistentProtectionProcess> 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.UtcNow<until){await Task.Delay(80,token);}
}
}
public void Dispose()=>observer.Dispose(); // Never terminate protection on worker unwind.
}