using System.Diagnostics;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Services;
internal static class GameSessionAcceptance
{
internal static async Task LeaveAsync(int pid,string report)
{
if(File.Exists(report))throw new IOException("Use a new original-menu exit report.");
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 initial=journal.State;
if(!initial.InGame||string.IsNullOrWhiteSpace(initial.CommanderId)||initial.CurrentShipId!=5||initial.CargoCount!=0||NavigationSafety.HasExternalThreat(initial))
throw new InvalidOperationException("Expected the healthy, empty CG ship before updating its plugin.");
using var process=Process.GetProcessById(pid);var started=process.StartTime.ToUniversalTime();
var channel=new NativeRelogChannel(pid,NativeRelogChannel.SupportedGameSha256,token=>
{
token.ThrowIfCancellationRequested();process.Refresh();
if(process.HasExited||process.StartTime.ToUniversalTime()!=started)throw new IOException("Game process changed during native menu exit.");
return Task.CompletedTask;
});
var owner=Guid.NewGuid().ToString("N");var actions=new List<string>();string? failure=null;var passed=false;string? last=null;
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(100));
try
{
await channel.BeginAsync(owner,timeout.Token);string? pending=null;var requested=DateTimeOffset.MinValue;
while(true)
{
timeout.Token.ThrowIfCancellationRequested();var frame=await channel.ReadAsync(owner,timeout.Token);var now=DateTimeOffset.UtcNow;
if(frame.State!=last){Console.WriteLine("原始返回菜单:"+frame.State);last=frame.State;}
if(frame.Sequence<=0||now-frame.ObservedAt>TimeSpan.FromMilliseconds(350)||frame.ObservedAt>now.AddMilliseconds(100))
{await Task.Delay(80,timeout.Token);continue;}
if(frame.Stable&&frame.State=="MainMenu"){passed=true;break;}
if(pending is not null)
{
if(frame.LastRequestId==pending&&(frame.ActionApplied||frame.ActionFailed)||now-requested>TimeSpan.FromSeconds(3))pending=null;
else{await Task.Delay(80,timeout.Token);continue;}
}
var action=NativeRelogWorkflow.ChooseAction(frame,initial.GameMode,false);
if(action is not null)
{
pending=Guid.NewGuid().ToString("N");requested=now;
try{await channel.SubmitAsync(owner,pending,frame,action,timeout.Token);actions.Add(action);}
catch(NativeSessionStateChangedException){pending=null;}
}
await Task.Delay(80,timeout.Token);
}
}
catch(Exception error){failure=error.ToString();}
finally
{
await channel.EndAsync(owner);
await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{passed,pid,lastState=last,actions,failure,screenUse=false,osInputsSent=0},ToolboxSettings.JsonOptions));
}
if(!passed)throw new InvalidOperationException("Original game menu exit did not complete; no process was terminated.");
}
internal static async Task<JsonElement> StatusAsync(int processId,CancellationToken token=default)
{
var pipe=new NavigationPipeClient(processId);var hello=await pipe.CallAsync(new{op="describe"},token);
if(hello.TryGetProperty("nativeSessionStatus",out var enabled)&&enabled.ValueKind==JsonValueKind.True)
return await pipe.CallAsync(new{op="session.status"},token);
// Compatibility with 0.5.1: ending an unclaimed, freshly generated
// owner returns the status without changing another workflow's lease.
return await pipe.CallAsync(new{op="session.end",owner=Guid.NewGuid().ToString("N")},token);
}
internal static async Task RecoverDisconnectedAsync(int processId,string executable,string package,string report)
{
if(File.Exists(report))throw new IOException("Use a new recovery report path.");
executable=Path.GetFullPath(executable);package=Path.GetFullPath(package);report=Path.GetFullPath(report);
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 driver=new WindowsGameSessionDriver(()=>journal.State,()=>executable);
using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(5));var token=timeout.Token;
var target=await driver.FindLaunchTargetAsync(token);
if(target.ProcessId!=processId)throw new InvalidOperationException("The disconnected game process changed.");
var pipe=new NavigationPipeClient(processId);var hello=await pipe.CallAsync(new{op="describe"},token);var status=await StatusAsync(processId,token);
if(hello.GetProperty("gameSha256").GetString()!=NativeRelogChannel.SupportedGameSha256||hello.GetProperty("inGame").GetBoolean()
||status.GetProperty("state").GetString()!="FailedToConnect"||status.GetProperty("nextState").GetString()!="FailedToConnect")
throw new InvalidOperationException("Only the verified disconnected game state may be restarted by this recovery.");
if(!journal.State.Docked||string.IsNullOrWhiteSpace(journal.State.CommanderId)||journal.State.CurrentShipId!=5||journal.State.CargoCount!=1)
throw new InvalidOperationException("The reserved one-ton ship or last docking identity changed.");
var manager=new GamePluginManager(package);var previous=await manager.InspectAsync(executable,token);
if(!previous.Installed)throw new InvalidOperationException("Recovery may update only the toolbox-owned plugin installation.");
var bundle=manager.Package();
var bundleHash=Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(await File.ReadAllBytesAsync(Path.Combine(package,bundle.Loader),token)));
if(bundleHash!=bundle.Sha256||bundle.SupportedGameSha256!=NativeRelogChannel.SupportedGameSha256)
throw new InvalidDataException("The replacement package failed validation; the disconnected game was left running.");
var stages=new List<string>();string? failure=null;var installed=false;var entered=false;
try
{
await driver.ValidateRestartAsync(target,token);await driver.TerminateAsync(target,token);stages.Add("verified-disconnected-process-terminated");
var watch=Stopwatch.StartNew();while(!await driver.HasExitedAsync(target,token))
{if(watch.Elapsed>TimeSpan.FromSeconds(20))throw new IOException("Old disconnected process did not exit.");await Task.Delay(100,token);}
await manager.InstallAsync(executable,token);installed=true;stages.Add("managed-plugin-updated");
await StartAsync(executable,report+".entry.json");entered=true;stages.Add("original-solo-entry-confirmed");
}
catch(Exception error){failure=error.ToString();}
finally{await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{processId,initial=status,installed,entered,stages,failure,screenUse=false,flightInputsSent=0,maintenanceHoldReleased=false},ToolboxSettings.JsonOptions));}
if(!entered)throw new InvalidOperationException("Disconnected-session recovery has not confirmed entry; maintenance hold remains active. "+failure);
}
public static async Task StartAsync(string executable,string report,int? existingProcessId=null)
{
if(File.Exists(report))throw new IOException("Use a new report path.");
executable=Path.GetFullPath(executable);report=Path.GetFullPath(report);
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 initial=journal.State;
if(!NativeRelogWorkflow.SupportsMode(initial.GameMode)||initial.CommanderId.Length==0)
throw new InvalidOperationException("A known commander and previous Solo/Open mode are required.");
using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(8));var token=timeout.Token;
var driver=new WindowsGameSessionDriver(()=>journal.State,()=>executable);
var sessions=new GameSessionController(driver,()=>false);
sessions.Progress+=p=>{if(p.Message.Length>0)Console.WriteLine(p.Message);};
if(existingProcessId is null)await sessions.RunAsync(SessionOperation.Start,token);
var target=await driver.FindTargetAsync(token);
if(existingProcessId is {} expectedPid&&target.ProcessId!=expectedPid)
throw new InvalidOperationException("The selected existing game process changed; no replacement was started.");
using var process=Process.GetProcessById(target.ProcessId);
if(process.HasExited||!string.Equals(process.MainModule?.FileName,executable,StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Official launch returned another game instance.");
PluginHandshake? hello=null;
while(hello is null)
{
token.ThrowIfCancellationRequested();
try{hello=await GamePluginManager.StartNavigationObservationAsync(target.ProcessId,token);}
catch(Exception error)when(error is IOException or OperationCanceledException&&!token.IsCancellationRequested)
{await Task.Delay(250,token);}
}
if(!hello.Active||!hello.ObservationStarted)throw new InvalidOperationException("Managed plugin observation is not ready.");
var channel=new NativeRelogChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,t=>
{
t.ThrowIfCancellationRequested();process.Refresh();
if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks)throw new IOException("Game process changed during entry.");
return Task.CompletedTask;
},entryOnly:true);
var owner=Guid.NewGuid().ToString("N");var actions=new List<string>();string? pending=null;
var requested=DateTimeOffset.MinValue;string? lastState=null;var passed=false;string? entryFailure=null;
try
{
while(true)
{
token.ThrowIfCancellationRequested();
var connection=await StatusAsync(target.ProcessId,token);
if(connection.GetProperty("state").GetString()=="FailedToConnect")throw new IOException("Original game connection failed; maintenance hold remains active.");
try{await channel.BeginAsync(owner,token);break;}
catch(InvalidOperationException error)when(error.Message.StartsWith("Wait for an idle game",StringComparison.Ordinal))
{await Task.Delay(150,token);}
}
while(true)
{
if(File.Exists(report+".stop"))throw new OperationCanceledException("Entry stopped.");
var frame=await channel.ReadAsync(owner,token);var now=DateTimeOffset.UtcNow;
if(frame.State=="FailedToConnect")throw new IOException("Original game connection failed; maintenance hold remains active.");
if(frame.State!=lastState){Console.WriteLine("原生菜单:"+frame.State);lastState=frame.State;}
if(frame.Sequence<=0||now-frame.ObservedAt>TimeSpan.FromMilliseconds(350)||frame.ObservedAt>now.AddMilliseconds(100))
{await Task.Delay(100,token);continue;}
var current=journal.State;
if(current.CommanderId!=initial.CommanderId)throw new InvalidOperationException("Commander changed during game entry.");
if(frame.Stable&&frame.State=="InGame"&¤t.InGame&¤t.GameMode==initial.GameMode&¤t.LoadGameAt>=initial.LoadGameAt)
{passed=true;break;}
if(pending is not null)
{
if(frame.LastRequestId==pending&&(frame.ActionApplied||frame.ActionFailed))pending=null;
else if(now-requested>TimeSpan.FromSeconds(3))pending=null;
else{await Task.Delay(100,token);continue;}
}
var action=NativeRelogWorkflow.ChooseAction(frame,initial.GameMode,reachedMainMenu:true);
if(action is not null)
{
pending=Guid.NewGuid().ToString("N");requested=now;
try{await channel.SubmitAsync(owner,pending,frame,action,token);actions.Add(action);}
catch(NativeSessionStateChangedException){pending=null;}
}
await Task.Delay(100,token);
}
}
catch(Exception error){entryFailure=error.ToString();throw;}
finally
{
await channel.EndAsync(owner);
await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{passed,failure=entryFailure,lastState,target.ProcessId,hello.Version,actions,
ship=journal.State.Ship,shipId=journal.State.CurrentShipId,capacity=journal.State.CargoCapacity,
system=journal.State.SystemName,station=journal.State.StationName,journal.State.Docked,screenUse=false,flightInputsSent=0},ToolboxSettings.JsonOptions));
}
Console.WriteLine(JsonSerializer.Serialize(new{passed,target.ProcessId,hello.Version,ship=journal.State.Ship,capacity=journal.State.CargoCapacity,screenUse=false}));
}
}
using System.Diagnostics;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Services;
internal static class GameSessionAcceptance
{
internal static async Task LeaveAsync(int pid,string report)
{
if(File.Exists(report))throw new IOException("Use a new original-menu exit report.");
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 initial=journal.State;
if(!initial.InGame||string.IsNullOrWhiteSpace(initial.CommanderId)||initial.CurrentShipId!=5||initial.CargoCount!=0||NavigationSafety.HasExternalThreat(initial))
throw new InvalidOperationException("Expected the healthy, empty CG ship before updating its plugin.");
using var process=Process.GetProcessById(pid);var started=process.StartTime.ToUniversalTime();
var channel=new NativeRelogChannel(pid,NativeRelogChannel.SupportedGameSha256,token=>
{
token.ThrowIfCancellationRequested();process.Refresh();
if(process.HasExited||process.StartTime.ToUniversalTime()!=started)throw new IOException("Game process changed during native menu exit.");
return Task.CompletedTask;
});
var owner=Guid.NewGuid().ToString("N");var actions=new List<string>();string? failure=null;var passed=false;string? last=null;
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(100));
try
{
await channel.BeginAsync(owner,timeout.Token);string? pending=null;var requested=DateTimeOffset.MinValue;
while(true)
{
timeout.Token.ThrowIfCancellationRequested();var frame=await channel.ReadAsync(owner,timeout.Token);var now=DateTimeOffset.UtcNow;
if(frame.State!=last){Console.WriteLine("原始返回菜单:"+frame.State);last=frame.State;}
if(frame.Sequence<=0||now-frame.ObservedAt>TimeSpan.FromMilliseconds(350)||frame.ObservedAt>now.AddMilliseconds(100))
{await Task.Delay(80,timeout.Token);continue;}
if(frame.Stable&&frame.State=="MainMenu"){passed=true;break;}
if(pending is not null)
{
if(frame.LastRequestId==pending&&(frame.ActionApplied||frame.ActionFailed)||now-requested>TimeSpan.FromSeconds(3))pending=null;
else{await Task.Delay(80,timeout.Token);continue;}
}
var action=NativeRelogWorkflow.ChooseAction(frame,initial.GameMode,false);
if(action is not null)
{
pending=Guid.NewGuid().ToString("N");requested=now;
try{await channel.SubmitAsync(owner,pending,frame,action,timeout.Token);actions.Add(action);}
catch(NativeSessionStateChangedException){pending=null;}
}
await Task.Delay(80,timeout.Token);
}
}
catch(Exception error){failure=error.ToString();}
finally
{
await channel.EndAsync(owner);
await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{passed,pid,lastState=last,actions,failure,screenUse=false,osInputsSent=0},ToolboxSettings.JsonOptions));
}
if(!passed)throw new InvalidOperationException("Original game menu exit did not complete; no process was terminated.");
}
internal static async Task<JsonElement> StatusAsync(int processId,CancellationToken token=default)
{
var pipe=new NavigationPipeClient(processId);var hello=await pipe.CallAsync(new{op="describe"},token);
if(hello.TryGetProperty("nativeSessionStatus",out var enabled)&&enabled.ValueKind==JsonValueKind.True)
return await pipe.CallAsync(new{op="session.status"},token);
// Compatibility with 0.5.1: ending an unclaimed, freshly generated
// owner returns the status without changing another workflow's lease.
return await pipe.CallAsync(new{op="session.end",owner=Guid.NewGuid().ToString("N")},token);
}
internal static async Task RecoverDisconnectedAsync(int processId,string executable,string package,string report)
{
if(File.Exists(report))throw new IOException("Use a new recovery report path.");
executable=Path.GetFullPath(executable);package=Path.GetFullPath(package);report=Path.GetFullPath(report);
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 driver=new WindowsGameSessionDriver(()=>journal.State,()=>executable);
using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(5));var token=timeout.Token;
var target=await driver.FindLaunchTargetAsync(token);
if(target.ProcessId!=processId)throw new InvalidOperationException("The disconnected game process changed.");
var pipe=new NavigationPipeClient(processId);var hello=await pipe.CallAsync(new{op="describe"},token);var status=await StatusAsync(processId,token);
if(hello.GetProperty("gameSha256").GetString()!=NativeRelogChannel.SupportedGameSha256||hello.GetProperty("inGame").GetBoolean()
||status.GetProperty("state").GetString()!="FailedToConnect"||status.GetProperty("nextState").GetString()!="FailedToConnect")
throw new InvalidOperationException("Only the verified disconnected game state may be restarted by this recovery.");
if(!journal.State.Docked||string.IsNullOrWhiteSpace(journal.State.CommanderId)||journal.State.CurrentShipId!=5||journal.State.CargoCount!=1)
throw new InvalidOperationException("The reserved one-ton ship or last docking identity changed.");
var manager=new GamePluginManager(package);var previous=await manager.InspectAsync(executable,token);
if(!previous.Installed)throw new InvalidOperationException("Recovery may update only the toolbox-owned plugin installation.");
var bundle=manager.Package();
var bundleHash=Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(await File.ReadAllBytesAsync(Path.Combine(package,bundle.Loader),token)));
if(bundleHash!=bundle.Sha256||bundle.SupportedGameSha256!=NativeRelogChannel.SupportedGameSha256)
throw new InvalidDataException("The replacement package failed validation; the disconnected game was left running.");
var stages=new List<string>();string? failure=null;var installed=false;var entered=false;
try
{
await driver.ValidateRestartAsync(target,token);await driver.TerminateAsync(target,token);stages.Add("verified-disconnected-process-terminated");
var watch=Stopwatch.StartNew();while(!await driver.HasExitedAsync(target,token))
{if(watch.Elapsed>TimeSpan.FromSeconds(20))throw new IOException("Old disconnected process did not exit.");await Task.Delay(100,token);}
await manager.InstallAsync(executable,token);installed=true;stages.Add("managed-plugin-updated");
await StartAsync(executable,report+".entry.json");entered=true;stages.Add("original-solo-entry-confirmed");
}
catch(Exception error){failure=error.ToString();}
finally{await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{processId,initial=status,installed,entered,stages,failure,screenUse=false,flightInputsSent=0,maintenanceHoldReleased=false},ToolboxSettings.JsonOptions));}
if(!entered)throw new InvalidOperationException("Disconnected-session recovery has not confirmed entry; maintenance hold remains active. "+failure);
}
public static async Task StartAsync(string executable,string report,int? existingProcessId=null)
{
if(File.Exists(report))throw new IOException("Use a new report path.");
executable=Path.GetFullPath(executable);report=Path.GetFullPath(report);
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 initial=journal.State;
if(!NativeRelogWorkflow.SupportsMode(initial.GameMode)||initial.CommanderId.Length==0)
throw new InvalidOperationException("A known commander and previous Solo/Open mode are required.");
using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(8));var token=timeout.Token;
var driver=new WindowsGameSessionDriver(()=>journal.State,()=>executable);
var sessions=new GameSessionController(driver,()=>false);
sessions.Progress+=p=>{if(p.Message.Length>0)Console.WriteLine(p.Message);};
if(existingProcessId is null)await sessions.RunAsync(SessionOperation.Start,token);
var target=await driver.FindTargetAsync(token);
if(existingProcessId is {} expectedPid&&target.ProcessId!=expectedPid)
throw new InvalidOperationException("The selected existing game process changed; no replacement was started.");
using var process=Process.GetProcessById(target.ProcessId);
if(process.HasExited||!string.Equals(process.MainModule?.FileName,executable,StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Official launch returned another game instance.");
PluginHandshake? hello=null;
while(hello is null)
{
token.ThrowIfCancellationRequested();
try{hello=await GamePluginManager.StartNavigationObservationAsync(target.ProcessId,token);}
catch(Exception error)when(error is IOException or OperationCanceledException&&!token.IsCancellationRequested)
{await Task.Delay(250,token);}
}
if(!hello.Active||!hello.ObservationStarted)throw new InvalidOperationException("Managed plugin observation is not ready.");
var channel=new NativeRelogChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,t=>
{
t.ThrowIfCancellationRequested();process.Refresh();
if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks)throw new IOException("Game process changed during entry.");
return Task.CompletedTask;
},entryOnly:true);
var owner=Guid.NewGuid().ToString("N");var actions=new List<string>();string? pending=null;
var requested=DateTimeOffset.MinValue;string? lastState=null;var passed=false;string? entryFailure=null;
try
{
while(true)
{
token.ThrowIfCancellationRequested();
var connection=await StatusAsync(target.ProcessId,token);
if(connection.GetProperty("state").GetString()=="FailedToConnect")throw new IOException("Original game connection failed; maintenance hold remains active.");
try{await channel.BeginAsync(owner,token);break;}
catch(InvalidOperationException error)when(error.Message.StartsWith("Wait for an idle game",StringComparison.Ordinal))
{await Task.Delay(150,token);}
}
while(true)
{
if(File.Exists(report+".stop"))throw new OperationCanceledException("Entry stopped.");
var frame=await channel.ReadAsync(owner,token);var now=DateTimeOffset.UtcNow;
if(frame.State=="FailedToConnect")throw new IOException("Original game connection failed; maintenance hold remains active.");
if(frame.State!=lastState){Console.WriteLine("原生菜单:"+frame.State);lastState=frame.State;}
if(frame.Sequence<=0||now-frame.ObservedAt>TimeSpan.FromMilliseconds(350)||frame.ObservedAt>now.AddMilliseconds(100))
{await Task.Delay(100,token);continue;}
var current=journal.State;
if(current.CommanderId!=initial.CommanderId)throw new InvalidOperationException("Commander changed during game entry.");
if(frame.Stable&&frame.State=="InGame"&¤t.InGame&¤t.GameMode==initial.GameMode&¤t.LoadGameAt>=initial.LoadGameAt)
{passed=true;break;}
if(pending is not null)
{
if(frame.LastRequestId==pending&&(frame.ActionApplied||frame.ActionFailed))pending=null;
else if(now-requested>TimeSpan.FromSeconds(3))pending=null;
else{await Task.Delay(100,token);continue;}
}
var action=NativeRelogWorkflow.ChooseAction(frame,initial.GameMode,reachedMainMenu:true);
if(action is not null)
{
pending=Guid.NewGuid().ToString("N");requested=now;
try{await channel.SubmitAsync(owner,pending,frame,action,token);actions.Add(action);}
catch(NativeSessionStateChangedException){pending=null;}
}
await Task.Delay(100,token);
}
}
catch(Exception error){entryFailure=error.ToString();throw;}
finally
{
await channel.EndAsync(owner);
await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{passed,failure=entryFailure,lastState,target.ProcessId,hello.Version,actions,
ship=journal.State.Ship,shipId=journal.State.CurrentShipId,capacity=journal.State.CargoCapacity,
system=journal.State.SystemName,station=journal.State.StationName,journal.State.Docked,screenUse=false,flightInputsSent=0},ToolboxSettings.JsonOptions));
}
Console.WriteLine(JsonSerializer.Serialize(new{passed,target.ProcessId,hello.Version,ship=journal.State.Ship,capacity=journal.State.CargoCapacity,screenUse=false}));
}
}