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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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.
}