using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
internal static class SessionRecoveryAcceptance
{
private const string Service="SessionRecovery0514k";
private sealed record Attempt(string RequestId,string Action,ulong MenuSequence,DateTimeOffset At,string Outcome,JsonElement? Receipt=null);
internal static async Task ProbeBridgeAsync(int pid,string report)
{
if(File.Exists(report))throw new IOException("Use a fresh read-only bridge report.");
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(2));
var value=await new NavigationPipeClient(pid,Service).CallAsync(new{op="docking.reset.probe"},timeout.Token);
await File.WriteAllTextAsync(report,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions));
Console.WriteLine(JsonSerializer.Serialize(new{pid,service=Service,bridgePassed=value.GetProperty("bridgePassed").GetBoolean(),stage=value.GetProperty("stage").GetString(),report},ToolboxSettings.JsonOptions));
if(!value.GetProperty("bridgePassed").GetBoolean())throw new IOException("Read-only original navigation bridge failed; no input was sent.");
}
internal static async Task InspectAsync(int pid,string service,string report,bool deactivate)
{
if(service is not("SessionRecovery0514" or "SessionRecovery0514c" or "SessionRecovery0514d" or "SessionRecovery0514e" or "SessionRecovery0514f" or "SessionRecovery0514g" or "SessionRecovery0514h" or "SessionRecovery0514i" or "SessionRecovery0514j" or Service))throw new ArgumentException("Expected a known diagnostic extension.");
if(File.Exists(report))throw new IOException("Use a fresh extension report.");
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var result=await new NavigationPipeClient(pid,service).CallAsync(new{op=deactivate?"deactivate":"read"},timeout.Token);
await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,pid,service,deactivate,result},ToolboxSettings.JsonOptions));
Console.WriteLine(result);
}
internal static async Task RunAsync(int pid,string package,string report,bool leave,string? protectionWorkRoot=null)
{
package=Path.GetFullPath(package);report=Path.GetFullPath(report);
if(File.Exists(report))throw new IOException("Use a fresh recovery report.");
Directory.CreateDirectory(Path.GetDirectoryName(report)!);
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(90));var token=timeout.Token;
using var process=Process.GetProcessById(pid);var started=process.StartTime.ToUniversalTime().Ticks;
var nav=new NavigationPipeClient(pid);var pipe=new NavigationPipeClient(pid,Service);
JsonElement? initial=null,current=null,status=null;var masks=Array.Empty<uint>();var resetMasks=Array.Empty<uint>();var telemetryMasks=Array.Empty<uint>();var startMasks=Array.Empty<uint>();var contextMasks=Array.Empty<uint>();
var attempts=new List<Attempt>();var loaded=false;var reached=false;Exception? failure=null;
async Task Save(string phase)
{
var temp=report+".tmp";
await File.WriteAllTextAsync(temp,JsonSerializer.Serialize(new{pid,started,at=DateTimeOffset.UtcNow,phase,loaded,fixtureMasks=masks,resetFixtureMasks=resetMasks,resetTelemetryFixtureMasks=telemetryMasks,dockingStartFixtureMasks=startMasks,flightContextFixtureMasks=contextMasks,
initial,current,status,attempts,actionSent=attempts.Count>0,actualMainMenu=reached,passed=leave?reached:(bool?)null,
error=failure?.ToString(),clientTerminated=false,gameAxesWritten=false,osInputSent=false,authFieldsWritten=false},ToolboxSettings.JsonOptions));
File.Move(temp,report,true);
}
async Task<JsonElement> Read()
{
process.Refresh();if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=started)throw new IOException("Game identity changed.");
current=await pipe.CallAsync(new{op="read"},token);return current.Value;
}
async Task<JsonElement> WaitReady()
{
var until=DateTimeOffset.UtcNow.AddSeconds(12);
do
{
var value=await Read();
if(value.GetProperty("menuReady").GetBoolean())return value;
await Task.Delay(25,token);
}while(DateTimeOffset.UtcNow<until);
throw new IOException("Original recovery menu is not ready: "+current?.GetProperty("menuReason").GetString());
}
try
{
var hello=await nav.CallAsync(new{op="describe"},token);
if(hello.GetProperty("gameSha256").GetString()!=NativeRelogChannel.SupportedGameSha256||hello.GetProperty("processId").GetInt32()!=pid)
throw new InvalidDataException("Expected the existing pinned game instance.");
// AOT fixture libraries remain loaded until this tool process exits.
var fixture=NativeLibrary.Load(Path.Combine(package,"XFEEDInteraction.dll"));
var selfTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFESessionRecoverySelfTest"));
var start=Marshal.GetDelegateForFunctionPointer<Start>(NativeLibrary.GetExport(fixture,"XFEInteractionStart"));
masks=Enumerable.Range(0,8).Select(_=>selfTest()).ToArray();
if(masks.Any(value=>value!=255)||start(0)!=0||start(nuint.MaxValue)!=0)throw new InvalidDataException("Purpose-built recovery guard/unsupported fixture checks failed.");
var resetTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEDockingResetSelfTest"));
resetMasks=Enumerable.Range(0,8).Select(_=>resetTest()).ToArray();
var telemetryTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEDockingResetTelemetrySelfTest"));
telemetryMasks=Enumerable.Range(0,8).Select(_=>telemetryTest()).ToArray();
if(resetMasks.Any(value=>value!=255)||telemetryMasks.Any(value=>value!=255))throw new InvalidDataException("Original reset guards or actual optional-interface regression failed their isolated self-tests.");
var dockingStartTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEDockingStartSelfTest"));
startMasks=Enumerable.Range(0,8).Select(_=>dockingStartTest()).ToArray();
if(startMasks.Any(value=>value!=255))throw new InvalidDataException("Original docking start counterexample guards failed their isolated self-tests.");
var contextTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEFlightContextSelfTest"));
contextMasks=Enumerable.Range(0,8).Select(_=>contextTest()).ToArray();
if(contextMasks.Any(value=>value!=511))throw new InvalidDataException("Original lowest-priority Flight context counterexample guards failed their isolated self-tests.");
var trialTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEFlightThrottleTrialSelfTest"));
if(Enumerable.Range(0,8).Any(_=>trialTest()!=255))throw new InvalidDataException("Original persistent throttle trial counterexamples failed their isolated self-tests.");
await RuntimeInteractionModule.LoadAsync(pid,NativeRelogChannel.SupportedGameSha256,hello.GetProperty("owningThread").GetInt32(),package,token);loaded=true;
var observedUntil=DateTimeOffset.UtcNow.AddSeconds(3);
do{var sample=await Read();if(sample.GetProperty("menuReady").GetBoolean())break;await Task.Delay(25,token);}while(DateTimeOffset.UtcNow<observedUntil);
initial=current;await Save("observed");
if(leave)
{
using var exclusivity=new FileStream(Path.Combine(Path.GetDirectoryName(report)!,"docking-fixture-wait.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();var player=journal.State;
if(!player.InGame||player.CurrentShipId!=5||player.CargoCount!=0||NavigationSafety.UnhealthyHull(player)||NavigationSafety.HasExternalThreat(player))
throw new InvalidDataException("Healthy empty diagnostic ship required before leaving through its real menu.");
using var guardian=await PersistentProtectionProcess.StartOrReuseAsync(pid,protectionWorkRoot??Path.GetDirectoryName(report)!,token,allowStart:false);
PersistentProtectionProcess.Current=guardian;
try
{
await using var protection=await FlightTestProtectionScope.ArmAsync(pid,exitForFailedHandoff:false);
// Exactly two permitted original menu steps. Persist every
// intent before dispatch; reconcile unknown acknowledgements
// by ID without submitting the operation again.
for(var step=0;step<2&&!reached;step++)
{
var menu=await WaitReady();
var actions=menu.GetProperty("actions").EnumerateArray().Where(row=>row.GetProperty("enabled").GetBoolean()&&row.GetProperty("recoveryAction").GetBoolean()).ToArray();
if(actions.Length!=1)throw new InvalidDataException("One exact enabled original recovery item required.");
var action=actions[0].GetProperty("name").GetString()!;
var id=Guid.NewGuid().ToString("N");var sequence=menu.GetProperty("menuSequence").GetUInt64();
guardian.RequireReady(pid);
attempts.Add(new(id,action,sequence,DateTimeOffset.UtcNow,"intent-persisted"));await Save("before-dispatch");
try
{
current=await pipe.CallAsync(new{op="session.recovery.leave",requestId=id,action,menuSequence=sequence,ttlMilliseconds=250},token);
attempts[^1]=attempts[^1] with{Outcome="acknowledged",Receipt=current};
}
catch(Exception error)
{
attempts[^1]=attempts[^1] with{Outcome="acknowledgement-unknown: "+error.Message};await Save("reconciling");
}
await Save("after-dispatch");
var until=DateTimeOffset.UtcNow.AddSeconds(20);var applied=false;
do
{
var value=await Read();status=await nav.CallAsync(new{op="session.status"},token);
if(status.Value.GetProperty("state").GetString()=="MainMenu"&&status.Value.GetProperty("nextState").GetString()=="MainMenu")
{reached=true;break;}
if(value.GetProperty("requestId").GetString()==id)
{
applied=value.GetProperty("applied").GetBoolean();
if(value.GetProperty("failed").GetBoolean())throw new IOException("Original menu request expired or failed; it will not be replayed.");
}
if(action=="ExitOptions"&&applied&&value.GetProperty("state").GetString()=="ExitOptions"
&&value.GetProperty("nextState").GetString()=="ExitOptions"&&value.GetProperty("menuReady").GetBoolean())break;
await Task.Delay(25,token);
}while(DateTimeOffset.UtcNow<until);
attempts[^1]=attempts[^1] with{Outcome=reached?"actual-main-menu":applied?"original-action-applied":"unresolved-no-replay",Receipt=current};
await Save("reconciled");
if(!reached&&(action!="ExitOptions"||!applied))throw new IOException("Original menu outcome is unresolved; no request was replayed.");
}
}
finally{PersistentProtectionProcess.Current=null;}
if(!reached)throw new IOException("Original main menu has not been confirmed; no client was terminated.");
}
}
catch(Exception error){failure=error;}
finally{await Save(failure==null?"finished":"failed");}
Console.WriteLine(JsonSerializer.Serialize(new{pid,loaded,actionSent=attempts.Count>0,actualMainMenu=reached,report,error=failure?.Message},ToolboxSettings.JsonOptions));
if(failure!=null)throw new IOException("Session recovery did not complete; evidence and unresolved intents are preserved.",failure);
}
[UnmanagedFunctionPointer(CallingConvention.Winapi)]private delegate uint Probe();
[UnmanagedFunctionPointer(CallingConvention.Winapi)]private delegate uint Start(nuint thread);
}
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
internal static class SessionRecoveryAcceptance
{
private const string Service="SessionRecovery0514k";
private sealed record Attempt(string RequestId,string Action,ulong MenuSequence,DateTimeOffset At,string Outcome,JsonElement? Receipt=null);
internal static async Task ProbeBridgeAsync(int pid,string report)
{
if(File.Exists(report))throw new IOException("Use a fresh read-only bridge report.");
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(2));
var value=await new NavigationPipeClient(pid,Service).CallAsync(new{op="docking.reset.probe"},timeout.Token);
await File.WriteAllTextAsync(report,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions));
Console.WriteLine(JsonSerializer.Serialize(new{pid,service=Service,bridgePassed=value.GetProperty("bridgePassed").GetBoolean(),stage=value.GetProperty("stage").GetString(),report},ToolboxSettings.JsonOptions));
if(!value.GetProperty("bridgePassed").GetBoolean())throw new IOException("Read-only original navigation bridge failed; no input was sent.");
}
internal static async Task InspectAsync(int pid,string service,string report,bool deactivate)
{
if(service is not("SessionRecovery0514" or "SessionRecovery0514c" or "SessionRecovery0514d" or "SessionRecovery0514e" or "SessionRecovery0514f" or "SessionRecovery0514g" or "SessionRecovery0514h" or "SessionRecovery0514i" or "SessionRecovery0514j" or Service))throw new ArgumentException("Expected a known diagnostic extension.");
if(File.Exists(report))throw new IOException("Use a fresh extension report.");
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var result=await new NavigationPipeClient(pid,service).CallAsync(new{op=deactivate?"deactivate":"read"},timeout.Token);
await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,pid,service,deactivate,result},ToolboxSettings.JsonOptions));
Console.WriteLine(result);
}
internal static async Task RunAsync(int pid,string package,string report,bool leave,string? protectionWorkRoot=null)
{
package=Path.GetFullPath(package);report=Path.GetFullPath(report);
if(File.Exists(report))throw new IOException("Use a fresh recovery report.");
Directory.CreateDirectory(Path.GetDirectoryName(report)!);
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(90));var token=timeout.Token;
using var process=Process.GetProcessById(pid);var started=process.StartTime.ToUniversalTime().Ticks;
var nav=new NavigationPipeClient(pid);var pipe=new NavigationPipeClient(pid,Service);
JsonElement? initial=null,current=null,status=null;var masks=Array.Empty<uint>();var resetMasks=Array.Empty<uint>();var telemetryMasks=Array.Empty<uint>();var startMasks=Array.Empty<uint>();var contextMasks=Array.Empty<uint>();
var attempts=new List<Attempt>();var loaded=false;var reached=false;Exception? failure=null;
async Task Save(string phase)
{
var temp=report+".tmp";
await File.WriteAllTextAsync(temp,JsonSerializer.Serialize(new{pid,started,at=DateTimeOffset.UtcNow,phase,loaded,fixtureMasks=masks,resetFixtureMasks=resetMasks,resetTelemetryFixtureMasks=telemetryMasks,dockingStartFixtureMasks=startMasks,flightContextFixtureMasks=contextMasks,
initial,current,status,attempts,actionSent=attempts.Count>0,actualMainMenu=reached,passed=leave?reached:(bool?)null,
error=failure?.ToString(),clientTerminated=false,gameAxesWritten=false,osInputSent=false,authFieldsWritten=false},ToolboxSettings.JsonOptions));
File.Move(temp,report,true);
}
async Task<JsonElement> Read()
{
process.Refresh();if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=started)throw new IOException("Game identity changed.");
current=await pipe.CallAsync(new{op="read"},token);return current.Value;
}
async Task<JsonElement> WaitReady()
{
var until=DateTimeOffset.UtcNow.AddSeconds(12);
do
{
var value=await Read();
if(value.GetProperty("menuReady").GetBoolean())return value;
await Task.Delay(25,token);
}while(DateTimeOffset.UtcNow<until);
throw new IOException("Original recovery menu is not ready: "+current?.GetProperty("menuReason").GetString());
}
try
{
var hello=await nav.CallAsync(new{op="describe"},token);
if(hello.GetProperty("gameSha256").GetString()!=NativeRelogChannel.SupportedGameSha256||hello.GetProperty("processId").GetInt32()!=pid)
throw new InvalidDataException("Expected the existing pinned game instance.");
// AOT fixture libraries remain loaded until this tool process exits.
var fixture=NativeLibrary.Load(Path.Combine(package,"XFEEDInteraction.dll"));
var selfTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFESessionRecoverySelfTest"));
var start=Marshal.GetDelegateForFunctionPointer<Start>(NativeLibrary.GetExport(fixture,"XFEInteractionStart"));
masks=Enumerable.Range(0,8).Select(_=>selfTest()).ToArray();
if(masks.Any(value=>value!=255)||start(0)!=0||start(nuint.MaxValue)!=0)throw new InvalidDataException("Purpose-built recovery guard/unsupported fixture checks failed.");
var resetTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEDockingResetSelfTest"));
resetMasks=Enumerable.Range(0,8).Select(_=>resetTest()).ToArray();
var telemetryTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEDockingResetTelemetrySelfTest"));
telemetryMasks=Enumerable.Range(0,8).Select(_=>telemetryTest()).ToArray();
if(resetMasks.Any(value=>value!=255)||telemetryMasks.Any(value=>value!=255))throw new InvalidDataException("Original reset guards or actual optional-interface regression failed their isolated self-tests.");
var dockingStartTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEDockingStartSelfTest"));
startMasks=Enumerable.Range(0,8).Select(_=>dockingStartTest()).ToArray();
if(startMasks.Any(value=>value!=255))throw new InvalidDataException("Original docking start counterexample guards failed their isolated self-tests.");
var contextTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEFlightContextSelfTest"));
contextMasks=Enumerable.Range(0,8).Select(_=>contextTest()).ToArray();
if(contextMasks.Any(value=>value!=511))throw new InvalidDataException("Original lowest-priority Flight context counterexample guards failed their isolated self-tests.");
var trialTest=Marshal.GetDelegateForFunctionPointer<Probe>(NativeLibrary.GetExport(fixture,"XFEFlightThrottleTrialSelfTest"));
if(Enumerable.Range(0,8).Any(_=>trialTest()!=255))throw new InvalidDataException("Original persistent throttle trial counterexamples failed their isolated self-tests.");
await RuntimeInteractionModule.LoadAsync(pid,NativeRelogChannel.SupportedGameSha256,hello.GetProperty("owningThread").GetInt32(),package,token);loaded=true;
var observedUntil=DateTimeOffset.UtcNow.AddSeconds(3);
do{var sample=await Read();if(sample.GetProperty("menuReady").GetBoolean())break;await Task.Delay(25,token);}while(DateTimeOffset.UtcNow<observedUntil);
initial=current;await Save("observed");
if(leave)
{
using var exclusivity=new FileStream(Path.Combine(Path.GetDirectoryName(report)!,"docking-fixture-wait.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();var player=journal.State;
if(!player.InGame||player.CurrentShipId!=5||player.CargoCount!=0||NavigationSafety.UnhealthyHull(player)||NavigationSafety.HasExternalThreat(player))
throw new InvalidDataException("Healthy empty diagnostic ship required before leaving through its real menu.");
using var guardian=await PersistentProtectionProcess.StartOrReuseAsync(pid,protectionWorkRoot??Path.GetDirectoryName(report)!,token,allowStart:false);
PersistentProtectionProcess.Current=guardian;
try
{
await using var protection=await FlightTestProtectionScope.ArmAsync(pid,exitForFailedHandoff:false);
// Exactly two permitted original menu steps. Persist every
// intent before dispatch; reconcile unknown acknowledgements
// by ID without submitting the operation again.
for(var step=0;step<2&&!reached;step++)
{
var menu=await WaitReady();
var actions=menu.GetProperty("actions").EnumerateArray().Where(row=>row.GetProperty("enabled").GetBoolean()&&row.GetProperty("recoveryAction").GetBoolean()).ToArray();
if(actions.Length!=1)throw new InvalidDataException("One exact enabled original recovery item required.");
var action=actions[0].GetProperty("name").GetString()!;
var id=Guid.NewGuid().ToString("N");var sequence=menu.GetProperty("menuSequence").GetUInt64();
guardian.RequireReady(pid);
attempts.Add(new(id,action,sequence,DateTimeOffset.UtcNow,"intent-persisted"));await Save("before-dispatch");
try
{
current=await pipe.CallAsync(new{op="session.recovery.leave",requestId=id,action,menuSequence=sequence,ttlMilliseconds=250},token);
attempts[^1]=attempts[^1] with{Outcome="acknowledged",Receipt=current};
}
catch(Exception error)
{
attempts[^1]=attempts[^1] with{Outcome="acknowledgement-unknown: "+error.Message};await Save("reconciling");
}
await Save("after-dispatch");
var until=DateTimeOffset.UtcNow.AddSeconds(20);var applied=false;
do
{
var value=await Read();status=await nav.CallAsync(new{op="session.status"},token);
if(status.Value.GetProperty("state").GetString()=="MainMenu"&&status.Value.GetProperty("nextState").GetString()=="MainMenu")
{reached=true;break;}
if(value.GetProperty("requestId").GetString()==id)
{
applied=value.GetProperty("applied").GetBoolean();
if(value.GetProperty("failed").GetBoolean())throw new IOException("Original menu request expired or failed; it will not be replayed.");
}
if(action=="ExitOptions"&&applied&&value.GetProperty("state").GetString()=="ExitOptions"
&&value.GetProperty("nextState").GetString()=="ExitOptions"&&value.GetProperty("menuReady").GetBoolean())break;
await Task.Delay(25,token);
}while(DateTimeOffset.UtcNow<until);
attempts[^1]=attempts[^1] with{Outcome=reached?"actual-main-menu":applied?"original-action-applied":"unresolved-no-replay",Receipt=current};
await Save("reconciled");
if(!reached&&(action!="ExitOptions"||!applied))throw new IOException("Original menu outcome is unresolved; no request was replayed.");
}
}
finally{PersistentProtectionProcess.Current=null;}
if(!reached)throw new IOException("Original main menu has not been confirmed; no client was terminated.");
}
}
catch(Exception error){failure=error;}
finally{await Save(failure==null?"finished":"failed");}
Console.WriteLine(JsonSerializer.Serialize(new{pid,loaded,actionSent=attempts.Count>0,actualMainMenu=reached,report,error=failure?.Message},ToolboxSettings.JsonOptions));
if(failure!=null)throw new IOException("Session recovery did not complete; evidence and unresolved intents are preserved.",failure);
}
[UnmanagedFunctionPointer(CallingConvention.Winapi)]private delegate uint Probe();
[UnmanagedFunctionPointer(CallingConvention.Winapi)]private delegate uint Start(nuint thread);
}