using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.Services;
public sealed class WindowsTradingBootstrap(Func<PlayerState> player,string executable,string packageDirectory,string? operationDirectory=null)
{
public bool ReconcileOriginalOrders{get;init;}
public bool ResumeExistingCargo{get;init;}
public GameSessionTarget? ExpectedConnectionFailureClient{get;init;}
public FlightConnectionBinding? ExpectedFlightConnection{get;init;}
private bool originalFailedClientExited;
public event Action<string>? Progress;
public Task UpgradeAndEnterAsync(CancellationToken token)
{
var expected=player();var lifecycle=new WindowsGameSessionDriver(player,()=>executable);
var dumpDirectory=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),"AppData","Local","Temp","Frontier Developments","EliteDangerous","CrashDumps");
Task AttemptAsync(Action<GameSessionTarget> connectionOwner,CancellationToken cancellation)=>StartupCrashRecovery.RunAsync(
(owned,working)=>UpgradeAndEnterCoreAsync(expected,client=>{connectionOwner(client);owned(client);},working,
originalFailedClientExited?null:ExpectedConnectionFailureClient),lifecycle.HasExitedAsync,
target=>StartupCrashRecovery.Find(dumpDirectory,target,DateTimeOffset.UtcNow),
async(crash,count,crashCancellation)=>
{
Progress?.Invoke($"已确认新启动客户端崩溃(0x{crash.ExceptionCode:x8}),正在通过官方启动器恢复原模式({count}/2)。凭据:{crash.Path}");
await OwnedStartupCrashReporter.CloseAsync(executable,crash,crashCancellation).ConfigureAwait(false);
},cancellation);
return StartupConnectionRecovery.RunAsync(AttemptAsync,async(client,failure,owned,count,cancellation)=>
{
await RecoverConnectionFailureAsync(client,expected,lifecycle,owned,count,cancellation).ConfigureAwait(false);
// Only the fresh-evidence recovery above may release this exact
// inherited instance binding, after its real exit was observed.
originalFailedClientExited=true;
},token,allowVerifiedExisting:true);
}
private async Task UpgradeAndEnterCoreAsync(PlayerState expected,Action<GameSessionTarget> ownLaunch,CancellationToken token,GameSessionTarget? expectedFailedClient=null)
{
var lifecycle=new WindowsGameSessionDriver(player,()=>executable);
var nextNoticeCheck=DateTimeOffset.MinValue;
async Task VerifyEntryAsync(GameSessionTarget client,CancellationToken cancellation)
{
if(await lifecycle.HasExitedAsync(client,cancellation).ConfigureAwait(false))throw new StartupClientExitedException(client.ProcessId);
}
Task PrepareEntryAsync(GameSessionTarget client,CancellationToken cancellation)
{
cancellation.ThrowIfCancellationRequested();
if(DateTimeOffset.UtcNow<nextNoticeCheck)return Task.CompletedTask;
nextNoticeCheck=DateTimeOffset.UtcNow.AddSeconds(1);
if(KnownTranslationStartupNotice.TryDismiss(client,cancellation))
Progress?.Invoke("已核对并关闭当前客户端的汉化加载提示,正在继续原始主菜单载入。");
return Task.CompletedTask;
}
var target=await lifecycle.FindLaunchTargetAsync(token).ConfigureAwait(false);
if(expectedFailedClient is {} failedClient&&(target.ProcessId!=failedClient.ProcessId||target.StartedUtcTicks!=failedClient.StartedUtcTicks
||!string.Equals(Path.GetFullPath(target.Executable),Path.GetFullPath(failedClient.Executable),StringComparison.OrdinalIgnoreCase)))
throw new InvalidDataException("飞行连接恢复的原实例已改变,未接管另一个游戏客户端。");
var original=expected;var mode=original.GameMode;var commander=original.CommanderId;
if(!NativeRelogWorkflow.SupportsMode(mode))throw new NotSupportedException("请先确认原来的单人或公开游戏模式。");
await lifecycle.ValidateLaunchAsync(target,token).ConfigureAwait(false);
if(target.ProcessId>0&&await lifecycle.HasExitedAsync(target,token))target=target with{ProcessId=0,StartedUtcTicks=0};
if(target.ProcessId>0)
{
try{await ThrowForFailedConnectionAsync(target,original,lifecycle,token).ConfigureAwait(false);}
catch(StartupClientExitedException)
{
if(!await lifecycle.HasExitedAsync(target,token))throw;
target=target with{ProcessId=0,StartedUtcTicks=0};
}
}
target=await RecoverStalledSceneAsync(target,original,lifecycle,token).ConfigureAwait(false);
if(target.ProcessId>0)
{
var existing=await GamePluginManager.ProbeAsync(target.ProcessId,token:token).ConfigureAwait(false);
if(existing.Version==new GamePluginManager(packageDirectory).Package().Version&&!ReconcileOriginalOrders)
{
if(!existing.ObservationStarted)await GamePluginManager.StartNavigationObservationAsync(target.ProcessId,token).ConfigureAwait(false);
var started=new DateTimeOffset(new DateTime(target.StartedUtcTicks,DateTimeKind.Utc));
var actual=await new NativeRelogChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>VerifyEntryAsync(target,c),entryOnly:true).ReadEntryStatusAsync(token);
if(!NativeSessionStartupReadiness.Ready(player(),actual,DateTimeOffset.UtcNow,started))
{
Progress?.Invoke("新版插件已加载,正在恢复现有游戏进程");
var entry=new NativeSessionEntryWorkflow(new NativeRelogChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>VerifyEntryAsync(target,c),entryOnly:true,prepareEntry:c=>PrepareEntryAsync(target,c)),player,mode,commander)
{ProcessStartedAt=started,ExpectedShipId=original.CurrentShipId};
entry.Progress+=text=>Progress?.Invoke(text);
try{await entry.RunAsync(token).ConfigureAwait(false);}catch(NativeSessionConnectionFailedException failure){throw new NativeSessionConnectionFailedException(failure.Frame,target);}
}
if(player().CommanderId!=commander||player().CurrentShipId!=original.CurrentShipId||!ResumeExistingCargo&&player().CargoCount!=0||player().GameMode!=mode||!player().CargoReliable
||ResumeExistingCargo&&player().CargoCount!=original.CargoCount)
throw new InvalidDataException("恢复后的指挥官、飞船、空舱或游戏模式不一致,未继续自动贸易。");
return;
}
}
if(target.ProcessId>0)
{
if(original.InGame&&(!original.CargoReliable||!ReconcileOriginalOrders&&original.CargoCount!=0||original.CurrentShipId is null))
throw new InvalidOperationException("更新前需要确认停靠和空货舱;当前游戏已保留。");
if(original.InGame&&!original.Docked)
{
Progress?.Invoke("正在连续确认空舱、静止、无威胁及原生健康状态,再通过原始菜单更新插件");
var gate=new SessionMaintenanceFlightGate(original);var pipe=new NavigationPipeClient(target.ProcessId);
var maintenanceUntil=DateTimeOffset.UtcNow.AddSeconds(30);
while(true)
{
var raw=await pipe.CallAsync(new{op="read"},token);var local=await pipe.CallAsync(new{op="local.read"},token);
if(gate.Observe(player(),raw,local,DateTimeOffset.UtcNow))break;
if(DateTimeOffset.UtcNow>=maintenanceUntil)throw new InvalidOperationException("未连续确认可更新的空舱静止状态,当前游戏已保留。");
await Task.Delay(100,token);
}
}
if(operationDirectory is not null)
await NativeUnsubmittedTradeIntents.ReconcileAsync(operationDirectory,new NativeCommerceChannel(target.ProcessId),original,token).ConfigureAwait(false);
using var game=Process.GetProcessById(target.ProcessId);
if(game.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks||!string.Equals(game.MainModule?.FileName,target.Executable,StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("准备更新的游戏实例已改变。");
Progress?.Invoke("正在通过游戏原始菜单退出当前会话以更新插件");
var recovery=new NativeSessionRecoveryChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,Path.Combine(packageDirectory,"DockingControl"));
var owner=Guid.NewGuid().ToString("N");var endedAtMenu=false;
recovery.Progress+=text=>Progress?.Invoke(text);
try
{
await recovery.BeginAsync(owner,token).ConfigureAwait(false);
var frame=await recovery.ReadAsync(owner,token).ConfigureAwait(false);
SessionMaintenanceExit.RequireMainMenu(frame,DateTimeOffset.UtcNow);
Progress?.Invoke("已确认游戏主菜单,正在结束客户端以加载新版插件");
await lifecycle.TerminateAsync(target,token).ConfigureAwait(false);
endedAtMenu=true;
}
finally{await SessionMaintenanceExit.ReleaseAfterExitAsync(()=>recovery.EndAsync(owner),()=>endedAtMenu&&game.HasExited).ConfigureAwait(false);}
using var menuDeadline=CancellationTokenSource.CreateLinkedTokenSource(token);menuDeadline.CancelAfter(TimeSpan.FromSeconds(15));
await game.WaitForExitAsync(menuDeadline.Token).ConfigureAwait(false);
}
var manager=new GamePluginManager(packageDirectory);
if(!ReconcileOriginalOrders)
{
Progress?.Invoke("正在安装已验证的通用原生插件");
var installed=await manager.InstallAsync(executable,token).ConfigureAwait(false);
if(!installed.Installed||installed.Installation?.Version!=manager.Package().Version)throw new IOException("原生插件安装结果未核对。");
}
await lifecycle.LaunchAsync(target,token).ConfigureAwait(false);
GameSessionTarget? replacement=null;var until=DateTimeOffset.UtcNow.AddMinutes(3);
while(replacement is null&&DateTimeOffset.UtcNow<until)
{replacement=await lifecycle.FindReplacementAsync(target,token).ConfigureAwait(false);if(replacement is null)await Task.Delay(150,token);}
if(replacement is null)throw new TimeoutException("官方启动器尚未建立新的游戏实例。");
var current=replacement;ownLaunch(current);until=DateTimeOffset.UtcNow.AddMinutes(4);
while(true)
{
token.ThrowIfCancellationRequested();
await VerifyEntryAsync(current,token).ConfigureAwait(false);
await PrepareEntryAsync(current,token).ConfigureAwait(false);
try
{
var hello=await GamePluginManager.ProbeAsync(current.ProcessId,token:token).ConfigureAwait(false);
if(hello.Version!=manager.Package().Version)throw new InvalidDataException("新进程没有加载刚安装的插件版本。");
if(!hello.ObservationStarted)hello=await GamePluginManager.StartNavigationObservationAsync(current.ProcessId,token).ConfigureAwait(false);
if(hello.ObservationStarted&&hello.SupportsRelog)break;
}
catch(Exception error)when(!token.IsCancellationRequested&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or OperationCanceledException){}
if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("新游戏进程的原生菜单接口未就绪。");
await Task.Delay(100,token).ConfigureAwait(false);
}
var startedCurrent=new DateTimeOffset(new DateTime(current.StartedUtcTicks,DateTimeKind.Utc));
var nativeCurrent=await new NativeRelogChannel(current.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>VerifyEntryAsync(current,c),entryOnly:true).ReadEntryStatusAsync(token);
if(!NativeSessionStartupReadiness.Ready(player(),nativeCurrent,DateTimeOffset.UtcNow,startedCurrent))
{
var entry=new NativeSessionEntryWorkflow(new NativeRelogChannel(current.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>VerifyEntryAsync(current,c),entryOnly:true,prepareEntry:c=>PrepareEntryAsync(current,c)),player,mode,commander)
{ProcessStartedAt=startedCurrent,ExpectedShipId=original.CurrentShipId};
entry.Progress+=text=>Progress?.Invoke(text);
try{await entry.RunAsync(token).ConfigureAwait(false);}catch(NativeSessionConnectionFailedException failure){throw new NativeSessionConnectionFailedException(failure.Frame,current);}
}
if(player().CommanderId!=commander||player().CurrentShipId!=original.CurrentShipId||!ResumeExistingCargo&&!ReconcileOriginalOrders&&player().CargoCount!=0||player().GameMode!=mode||!player().CargoReliable
||ResumeExistingCargo&&player().CargoCount!=original.CargoCount)
throw new InvalidDataException("恢复后的指挥官、飞船、空舱或游戏模式不一致,未继续自动贸易。");
Progress?.Invoke("已进入原游戏模式,正在核对原行程。");
}
private static void RequireOriginalIdentity(PlayerState expected,PlayerState current)
{
if(current.CommanderId.Length>0&¤t.CommanderId!=expected.CommanderId
||current.CurrentShipId is {} ship&&ship!=expected.CurrentShipId||current.GameMode.Length>0&¤t.GameMode!=expected.GameMode)
throw new InvalidDataException("连接失败恢复期间身份、飞船或原模式改变,当前客户端已保留。");
}
private void RequireConnectionRecoveryIdentity(PlayerState expected,CancellationToken token)
{
token.ThrowIfCancellationRequested();var current=player();RequireOriginalIdentity(expected,current);
// Before the inherited failed process has really exited, no new
// Journal session or changed cargo can replace the original flight.
// Its official replacement is validated as a new session on entry.
if(!originalFailedClientExited&&ExpectedFlightConnection is {} binding)
FlightConnectionRecovery.RequireCurrentIdentity(binding,current,token);
}
private async Task ThrowForFailedConnectionAsync(GameSessionTarget client,PlayerState expected,WindowsGameSessionDriver lifecycle,CancellationToken token)
{
var observer=new NativeConnectionFailureObserver();var until=DateTimeOffset.UtcNow.AddSeconds(5);
var channel=new NativeRelogChannel(client.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>lifecycle.ValidateExitAsync(client,c),entryOnly:true);
while(DateTimeOffset.UtcNow<until)
{
RequireConnectionRecoveryIdentity(expected,token);NativeSessionFrame frame;
try{frame=await channel.ReadEntryStatusAsync(token);}
catch(Exception error)when(!token.IsCancellationRequested&&error is not StartupClientExitedException&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or InvalidOperationException or OperationCanceledException){return;}
if(!frame.Available||!NativeMenuExitSafety.Fresh(frame,DateTimeOffset.UtcNow)||!frame.Stable||frame.State!="FailedToConnect")return;
if(observer.Observe(frame,DateTimeOffset.UtcNow))throw new NativeSessionConnectionFailedException(frame,client);
await Task.Delay(100,token);
}
}
private async Task RecoverConnectionFailureAsync(GameSessionTarget client,PlayerState expected,WindowsGameSessionDriver lifecycle,bool owned,int count,CancellationToken token)
{
await lifecycle.ValidateRestartAsync(client,token);
await using(var image=File.OpenRead(client.Executable))
if(Convert.ToHexStringLower(await SHA256.HashDataAsync(image,token))!=NativeRelogChannel.SupportedGameSha256)
throw new InvalidDataException("连接失败客户端的游戏构建不匹配,未重启。");
var pipe=new NavigationPipeClient(client.ProcessId);var observer=new NativeConnectionFailureObserver();var until=DateTimeOffset.UtcNow.AddSeconds(5);
JsonElement lastMenu=default,lastNavigation=default;
while(true)
{
RequireConnectionRecoveryIdentity(expected,token);await lifecycle.ValidateExitAsync(client,token);
lastMenu=await pipe.CallAsync(new{op="session.status"},token);lastNavigation=await pipe.CallAsync(new{op="describe"},token);
RequireConnectionRecoveryIdentity(expected,token);await lifecycle.ValidateExitAsync(client,token);
if(!NativeSessionStartupReadiness.FailedWithoutLiveFlight(lastMenu,lastNavigation,client.ProcessId,NativeRelogChannel.SupportedGameSha256,DateTimeOffset.UtcNow))
throw new InvalidDataException("原版连接失败或无驾驶场景的证据已改变,未重启客户端。");
if(observer.Observe(NativeRelogChannel.Parse(lastMenu),DateTimeOffset.UtcNow))break;
if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("未连续确认连接失败,客户端已保留。");
await Task.Delay(100,token);
}
var directory=operationDirectory??Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","logs","startup-recovery");
Directory.CreateDirectory(directory);
var evidence=new{at=DateTimeOffset.UtcNow,ownership=owned?"owned-new":"adopted-verified-failed-client",client,count,expected.CommanderId,expected.CurrentShipId,expected.GameMode,
originalOrdersUnchanged=true,session=lastMenu,navigation=lastNavigation};
await File.WriteAllTextAsync(Path.Combine(directory,$"connection-recovery-{client.ProcessId}-{client.StartedUtcTicks}-{count}.json"),JsonSerializer.Serialize(evidence),token);
RequireConnectionRecoveryIdentity(expected,token);await lifecycle.ValidateExitAsync(client,token);
Progress?.Invoke($"原版连接失败已持续确认,正在通过官方启动器恢复原模式({count}/2);保留原货物与交易回执。");
await StartupConnectionRecovery.TerminateWithFreshEvidenceAsync(async cancellation=>
{
RequireConnectionRecoveryIdentity(expected,cancellation);await lifecycle.ValidateExitAsync(client,cancellation);
var menu=await pipe.CallAsync(new{op="session.status"},cancellation);
var navigation=await pipe.CallAsync(new{op="describe"},cancellation);
RequireConnectionRecoveryIdentity(expected,cancellation);await lifecycle.ValidateExitAsync(client,cancellation);
return NativeSessionStartupReadiness.FailedWithoutLiveFlight(menu,navigation,client.ProcessId,NativeRelogChannel.SupportedGameSha256,DateTimeOffset.UtcNow);
},cancellation=>lifecycle.TerminateAsync(client,cancellation),token);
var exitUntil=DateTimeOffset.UtcNow.AddSeconds(15);
while(!await lifecycle.HasExitedAsync(client,token))
{if(DateTimeOffset.UtcNow>=exitUntil)throw new TimeoutException("连接失败客户端尚未确认退出,未启动第二个实例。");await Task.Delay(100,token);}
}
internal static async Task<bool> HasSavingSceneAsync(int pid,CancellationToken token)
=>await SavingSceneRecovery.ReadAsync(pid,NativeRelogChannel.SupportedGameSha256,token).ConfigureAwait(false);
private async Task<GameSessionTarget> RecoverStalledSceneAsync(GameSessionTarget target,PlayerState expected,WindowsGameSessionDriver lifecycle,CancellationToken token)
{
if(target.ProcessId<=0||!await HasSavingSceneAsync(target.ProcessId,token).ConfigureAwait(false))return target;
var began=DateTimeOffset.UtcNow;
Progress?.Invoke("游戏正在保存抵达位置;保持原航程并观察原版场景,暂不发送驾驶或交易指令。");
while(DateTimeOffset.UtcNow-began<TimeSpan.FromSeconds(90))
{
token.ThrowIfCancellationRequested();var current=player();
if(current.CommanderId!=expected.CommanderId||current.CurrentShipId!=expected.CurrentShipId||current.GameMode!=expected.GameMode||current.CargoCount!=expected.CargoCount)
throw new InvalidOperationException("保存场景等待期间身份、飞船或货舱变化,未自动重启。");
if(!await HasSavingSceneAsync(target.ProcessId,token).ConfigureAwait(false))return target;
await Task.Delay(1000,token).ConfigureAwait(false);
}
// No gameplay is permitted by this original transition state. Exiting
// the verified client does not authorize another order; official entry
// below must re-establish commander, ship, mode and actual cargo first.
Progress?.Invoke("原版保存抵达位置持续 90 秒未完成,正在重启官方客户端;原订单和回执保持不变。");
await lifecycle.TerminateAsync(target,token).ConfigureAwait(false);
var until=DateTimeOffset.UtcNow.AddSeconds(15);
while(!await lifecycle.HasExitedAsync(target,token).ConfigureAwait(false))
{if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("原客户端尚未确认退出,未启动新实例。");await Task.Delay(100,token).ConfigureAwait(false);}
// A terminated process can briefly remain in process enumeration. Keep
// its verified installation, but never probe that dead pipe again.
return target with{ProcessId=0,StartedUtcTicks=0};
}
}
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.Services;
public sealed class WindowsTradingBootstrap(Func<PlayerState> player,string executable,string packageDirectory,string? operationDirectory=null)
{
public bool ReconcileOriginalOrders{get;init;}
public bool ResumeExistingCargo{get;init;}
public GameSessionTarget? ExpectedConnectionFailureClient{get;init;}
public FlightConnectionBinding? ExpectedFlightConnection{get;init;}
private bool originalFailedClientExited;
public event Action<string>? Progress;
public Task UpgradeAndEnterAsync(CancellationToken token)
{
var expected=player();var lifecycle=new WindowsGameSessionDriver(player,()=>executable);
var dumpDirectory=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),"AppData","Local","Temp","Frontier Developments","EliteDangerous","CrashDumps");
Task AttemptAsync(Action<GameSessionTarget> connectionOwner,CancellationToken cancellation)=>StartupCrashRecovery.RunAsync(
(owned,working)=>UpgradeAndEnterCoreAsync(expected,client=>{connectionOwner(client);owned(client);},working,
originalFailedClientExited?null:ExpectedConnectionFailureClient),lifecycle.HasExitedAsync,
target=>StartupCrashRecovery.Find(dumpDirectory,target,DateTimeOffset.UtcNow),
async(crash,count,crashCancellation)=>
{
Progress?.Invoke($"已确认新启动客户端崩溃(0x{crash.ExceptionCode:x8}),正在通过官方启动器恢复原模式({count}/2)。凭据:{crash.Path}");
await OwnedStartupCrashReporter.CloseAsync(executable,crash,crashCancellation).ConfigureAwait(false);
},cancellation);
return StartupConnectionRecovery.RunAsync(AttemptAsync,async(client,failure,owned,count,cancellation)=>
{
await RecoverConnectionFailureAsync(client,expected,lifecycle,owned,count,cancellation).ConfigureAwait(false);
// Only the fresh-evidence recovery above may release this exact
// inherited instance binding, after its real exit was observed.
originalFailedClientExited=true;
},token,allowVerifiedExisting:true);
}
private async Task UpgradeAndEnterCoreAsync(PlayerState expected,Action<GameSessionTarget> ownLaunch,CancellationToken token,GameSessionTarget? expectedFailedClient=null)
{
var lifecycle=new WindowsGameSessionDriver(player,()=>executable);
var nextNoticeCheck=DateTimeOffset.MinValue;
async Task VerifyEntryAsync(GameSessionTarget client,CancellationToken cancellation)
{
if(await lifecycle.HasExitedAsync(client,cancellation).ConfigureAwait(false))throw new StartupClientExitedException(client.ProcessId);
}
Task PrepareEntryAsync(GameSessionTarget client,CancellationToken cancellation)
{
cancellation.ThrowIfCancellationRequested();
if(DateTimeOffset.UtcNow<nextNoticeCheck)return Task.CompletedTask;
nextNoticeCheck=DateTimeOffset.UtcNow.AddSeconds(1);
if(KnownTranslationStartupNotice.TryDismiss(client,cancellation))
Progress?.Invoke("已核对并关闭当前客户端的汉化加载提示,正在继续原始主菜单载入。");
return Task.CompletedTask;
}
var target=await lifecycle.FindLaunchTargetAsync(token).ConfigureAwait(false);
if(expectedFailedClient is {} failedClient&&(target.ProcessId!=failedClient.ProcessId||target.StartedUtcTicks!=failedClient.StartedUtcTicks
||!string.Equals(Path.GetFullPath(target.Executable),Path.GetFullPath(failedClient.Executable),StringComparison.OrdinalIgnoreCase)))
throw new InvalidDataException("飞行连接恢复的原实例已改变,未接管另一个游戏客户端。");
var original=expected;var mode=original.GameMode;var commander=original.CommanderId;
if(!NativeRelogWorkflow.SupportsMode(mode))throw new NotSupportedException("请先确认原来的单人或公开游戏模式。");
await lifecycle.ValidateLaunchAsync(target,token).ConfigureAwait(false);
if(target.ProcessId>0&&await lifecycle.HasExitedAsync(target,token))target=target with{ProcessId=0,StartedUtcTicks=0};
if(target.ProcessId>0)
{
try{await ThrowForFailedConnectionAsync(target,original,lifecycle,token).ConfigureAwait(false);}
catch(StartupClientExitedException)
{
if(!await lifecycle.HasExitedAsync(target,token))throw;
target=target with{ProcessId=0,StartedUtcTicks=0};
}
}
target=await RecoverStalledSceneAsync(target,original,lifecycle,token).ConfigureAwait(false);
if(target.ProcessId>0)
{
var existing=await GamePluginManager.ProbeAsync(target.ProcessId,token:token).ConfigureAwait(false);
if(existing.Version==new GamePluginManager(packageDirectory).Package().Version&&!ReconcileOriginalOrders)
{
if(!existing.ObservationStarted)await GamePluginManager.StartNavigationObservationAsync(target.ProcessId,token).ConfigureAwait(false);
var started=new DateTimeOffset(new DateTime(target.StartedUtcTicks,DateTimeKind.Utc));
var actual=await new NativeRelogChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>VerifyEntryAsync(target,c),entryOnly:true).ReadEntryStatusAsync(token);
if(!NativeSessionStartupReadiness.Ready(player(),actual,DateTimeOffset.UtcNow,started))
{
Progress?.Invoke("新版插件已加载,正在恢复现有游戏进程");
var entry=new NativeSessionEntryWorkflow(new NativeRelogChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>VerifyEntryAsync(target,c),entryOnly:true,prepareEntry:c=>PrepareEntryAsync(target,c)),player,mode,commander)
{ProcessStartedAt=started,ExpectedShipId=original.CurrentShipId};
entry.Progress+=text=>Progress?.Invoke(text);
try{await entry.RunAsync(token).ConfigureAwait(false);}catch(NativeSessionConnectionFailedException failure){throw new NativeSessionConnectionFailedException(failure.Frame,target);}
}
if(player().CommanderId!=commander||player().CurrentShipId!=original.CurrentShipId||!ResumeExistingCargo&&player().CargoCount!=0||player().GameMode!=mode||!player().CargoReliable
||ResumeExistingCargo&&player().CargoCount!=original.CargoCount)
throw new InvalidDataException("恢复后的指挥官、飞船、空舱或游戏模式不一致,未继续自动贸易。");
return;
}
}
if(target.ProcessId>0)
{
if(original.InGame&&(!original.CargoReliable||!ReconcileOriginalOrders&&original.CargoCount!=0||original.CurrentShipId is null))
throw new InvalidOperationException("更新前需要确认停靠和空货舱;当前游戏已保留。");
if(original.InGame&&!original.Docked)
{
Progress?.Invoke("正在连续确认空舱、静止、无威胁及原生健康状态,再通过原始菜单更新插件");
var gate=new SessionMaintenanceFlightGate(original);var pipe=new NavigationPipeClient(target.ProcessId);
var maintenanceUntil=DateTimeOffset.UtcNow.AddSeconds(30);
while(true)
{
var raw=await pipe.CallAsync(new{op="read"},token);var local=await pipe.CallAsync(new{op="local.read"},token);
if(gate.Observe(player(),raw,local,DateTimeOffset.UtcNow))break;
if(DateTimeOffset.UtcNow>=maintenanceUntil)throw new InvalidOperationException("未连续确认可更新的空舱静止状态,当前游戏已保留。");
await Task.Delay(100,token);
}
}
if(operationDirectory is not null)
await NativeUnsubmittedTradeIntents.ReconcileAsync(operationDirectory,new NativeCommerceChannel(target.ProcessId),original,token).ConfigureAwait(false);
using var game=Process.GetProcessById(target.ProcessId);
if(game.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks||!string.Equals(game.MainModule?.FileName,target.Executable,StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("准备更新的游戏实例已改变。");
Progress?.Invoke("正在通过游戏原始菜单退出当前会话以更新插件");
var recovery=new NativeSessionRecoveryChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,Path.Combine(packageDirectory,"DockingControl"));
var owner=Guid.NewGuid().ToString("N");var endedAtMenu=false;
recovery.Progress+=text=>Progress?.Invoke(text);
try
{
await recovery.BeginAsync(owner,token).ConfigureAwait(false);
var frame=await recovery.ReadAsync(owner,token).ConfigureAwait(false);
SessionMaintenanceExit.RequireMainMenu(frame,DateTimeOffset.UtcNow);
Progress?.Invoke("已确认游戏主菜单,正在结束客户端以加载新版插件");
await lifecycle.TerminateAsync(target,token).ConfigureAwait(false);
endedAtMenu=true;
}
finally{await SessionMaintenanceExit.ReleaseAfterExitAsync(()=>recovery.EndAsync(owner),()=>endedAtMenu&&game.HasExited).ConfigureAwait(false);}
using var menuDeadline=CancellationTokenSource.CreateLinkedTokenSource(token);menuDeadline.CancelAfter(TimeSpan.FromSeconds(15));
await game.WaitForExitAsync(menuDeadline.Token).ConfigureAwait(false);
}
var manager=new GamePluginManager(packageDirectory);
if(!ReconcileOriginalOrders)
{
Progress?.Invoke("正在安装已验证的通用原生插件");
var installed=await manager.InstallAsync(executable,token).ConfigureAwait(false);
if(!installed.Installed||installed.Installation?.Version!=manager.Package().Version)throw new IOException("原生插件安装结果未核对。");
}
await lifecycle.LaunchAsync(target,token).ConfigureAwait(false);
GameSessionTarget? replacement=null;var until=DateTimeOffset.UtcNow.AddMinutes(3);
while(replacement is null&&DateTimeOffset.UtcNow<until)
{replacement=await lifecycle.FindReplacementAsync(target,token).ConfigureAwait(false);if(replacement is null)await Task.Delay(150,token);}
if(replacement is null)throw new TimeoutException("官方启动器尚未建立新的游戏实例。");
var current=replacement;ownLaunch(current);until=DateTimeOffset.UtcNow.AddMinutes(4);
while(true)
{
token.ThrowIfCancellationRequested();
await VerifyEntryAsync(current,token).ConfigureAwait(false);
await PrepareEntryAsync(current,token).ConfigureAwait(false);
try
{
var hello=await GamePluginManager.ProbeAsync(current.ProcessId,token:token).ConfigureAwait(false);
if(hello.Version!=manager.Package().Version)throw new InvalidDataException("新进程没有加载刚安装的插件版本。");
if(!hello.ObservationStarted)hello=await GamePluginManager.StartNavigationObservationAsync(current.ProcessId,token).ConfigureAwait(false);
if(hello.ObservationStarted&&hello.SupportsRelog)break;
}
catch(Exception error)when(!token.IsCancellationRequested&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or OperationCanceledException){}
if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("新游戏进程的原生菜单接口未就绪。");
await Task.Delay(100,token).ConfigureAwait(false);
}
var startedCurrent=new DateTimeOffset(new DateTime(current.StartedUtcTicks,DateTimeKind.Utc));
var nativeCurrent=await new NativeRelogChannel(current.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>VerifyEntryAsync(current,c),entryOnly:true).ReadEntryStatusAsync(token);
if(!NativeSessionStartupReadiness.Ready(player(),nativeCurrent,DateTimeOffset.UtcNow,startedCurrent))
{
var entry=new NativeSessionEntryWorkflow(new NativeRelogChannel(current.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>VerifyEntryAsync(current,c),entryOnly:true,prepareEntry:c=>PrepareEntryAsync(current,c)),player,mode,commander)
{ProcessStartedAt=startedCurrent,ExpectedShipId=original.CurrentShipId};
entry.Progress+=text=>Progress?.Invoke(text);
try{await entry.RunAsync(token).ConfigureAwait(false);}catch(NativeSessionConnectionFailedException failure){throw new NativeSessionConnectionFailedException(failure.Frame,current);}
}
if(player().CommanderId!=commander||player().CurrentShipId!=original.CurrentShipId||!ResumeExistingCargo&&!ReconcileOriginalOrders&&player().CargoCount!=0||player().GameMode!=mode||!player().CargoReliable
||ResumeExistingCargo&&player().CargoCount!=original.CargoCount)
throw new InvalidDataException("恢复后的指挥官、飞船、空舱或游戏模式不一致,未继续自动贸易。");
Progress?.Invoke("已进入原游戏模式,正在核对原行程。");
}
private static void RequireOriginalIdentity(PlayerState expected,PlayerState current)
{
if(current.CommanderId.Length>0&¤t.CommanderId!=expected.CommanderId
||current.CurrentShipId is {} ship&&ship!=expected.CurrentShipId||current.GameMode.Length>0&¤t.GameMode!=expected.GameMode)
throw new InvalidDataException("连接失败恢复期间身份、飞船或原模式改变,当前客户端已保留。");
}
private void RequireConnectionRecoveryIdentity(PlayerState expected,CancellationToken token)
{
token.ThrowIfCancellationRequested();var current=player();RequireOriginalIdentity(expected,current);
// Before the inherited failed process has really exited, no new
// Journal session or changed cargo can replace the original flight.
// Its official replacement is validated as a new session on entry.
if(!originalFailedClientExited&&ExpectedFlightConnection is {} binding)
FlightConnectionRecovery.RequireCurrentIdentity(binding,current,token);
}
private async Task ThrowForFailedConnectionAsync(GameSessionTarget client,PlayerState expected,WindowsGameSessionDriver lifecycle,CancellationToken token)
{
var observer=new NativeConnectionFailureObserver();var until=DateTimeOffset.UtcNow.AddSeconds(5);
var channel=new NativeRelogChannel(client.ProcessId,NativeRelogChannel.SupportedGameSha256,c=>lifecycle.ValidateExitAsync(client,c),entryOnly:true);
while(DateTimeOffset.UtcNow<until)
{
RequireConnectionRecoveryIdentity(expected,token);NativeSessionFrame frame;
try{frame=await channel.ReadEntryStatusAsync(token);}
catch(Exception error)when(!token.IsCancellationRequested&&error is not StartupClientExitedException&&!NavigationRecovery.HasInvalidDataCause(error)&&error is IOException or InvalidOperationException or OperationCanceledException){return;}
if(!frame.Available||!NativeMenuExitSafety.Fresh(frame,DateTimeOffset.UtcNow)||!frame.Stable||frame.State!="FailedToConnect")return;
if(observer.Observe(frame,DateTimeOffset.UtcNow))throw new NativeSessionConnectionFailedException(frame,client);
await Task.Delay(100,token);
}
}
private async Task RecoverConnectionFailureAsync(GameSessionTarget client,PlayerState expected,WindowsGameSessionDriver lifecycle,bool owned,int count,CancellationToken token)
{
await lifecycle.ValidateRestartAsync(client,token);
await using(var image=File.OpenRead(client.Executable))
if(Convert.ToHexStringLower(await SHA256.HashDataAsync(image,token))!=NativeRelogChannel.SupportedGameSha256)
throw new InvalidDataException("连接失败客户端的游戏构建不匹配,未重启。");
var pipe=new NavigationPipeClient(client.ProcessId);var observer=new NativeConnectionFailureObserver();var until=DateTimeOffset.UtcNow.AddSeconds(5);
JsonElement lastMenu=default,lastNavigation=default;
while(true)
{
RequireConnectionRecoveryIdentity(expected,token);await lifecycle.ValidateExitAsync(client,token);
lastMenu=await pipe.CallAsync(new{op="session.status"},token);lastNavigation=await pipe.CallAsync(new{op="describe"},token);
RequireConnectionRecoveryIdentity(expected,token);await lifecycle.ValidateExitAsync(client,token);
if(!NativeSessionStartupReadiness.FailedWithoutLiveFlight(lastMenu,lastNavigation,client.ProcessId,NativeRelogChannel.SupportedGameSha256,DateTimeOffset.UtcNow))
throw new InvalidDataException("原版连接失败或无驾驶场景的证据已改变,未重启客户端。");
if(observer.Observe(NativeRelogChannel.Parse(lastMenu),DateTimeOffset.UtcNow))break;
if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("未连续确认连接失败,客户端已保留。");
await Task.Delay(100,token);
}
var directory=operationDirectory??Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","logs","startup-recovery");
Directory.CreateDirectory(directory);
var evidence=new{at=DateTimeOffset.UtcNow,ownership=owned?"owned-new":"adopted-verified-failed-client",client,count,expected.CommanderId,expected.CurrentShipId,expected.GameMode,
originalOrdersUnchanged=true,session=lastMenu,navigation=lastNavigation};
await File.WriteAllTextAsync(Path.Combine(directory,$"connection-recovery-{client.ProcessId}-{client.StartedUtcTicks}-{count}.json"),JsonSerializer.Serialize(evidence),token);
RequireConnectionRecoveryIdentity(expected,token);await lifecycle.ValidateExitAsync(client,token);
Progress?.Invoke($"原版连接失败已持续确认,正在通过官方启动器恢复原模式({count}/2);保留原货物与交易回执。");
await StartupConnectionRecovery.TerminateWithFreshEvidenceAsync(async cancellation=>
{
RequireConnectionRecoveryIdentity(expected,cancellation);await lifecycle.ValidateExitAsync(client,cancellation);
var menu=await pipe.CallAsync(new{op="session.status"},cancellation);
var navigation=await pipe.CallAsync(new{op="describe"},cancellation);
RequireConnectionRecoveryIdentity(expected,cancellation);await lifecycle.ValidateExitAsync(client,cancellation);
return NativeSessionStartupReadiness.FailedWithoutLiveFlight(menu,navigation,client.ProcessId,NativeRelogChannel.SupportedGameSha256,DateTimeOffset.UtcNow);
},cancellation=>lifecycle.TerminateAsync(client,cancellation),token);
var exitUntil=DateTimeOffset.UtcNow.AddSeconds(15);
while(!await lifecycle.HasExitedAsync(client,token))
{if(DateTimeOffset.UtcNow>=exitUntil)throw new TimeoutException("连接失败客户端尚未确认退出,未启动第二个实例。");await Task.Delay(100,token);}
}
internal static async Task<bool> HasSavingSceneAsync(int pid,CancellationToken token)
=>await SavingSceneRecovery.ReadAsync(pid,NativeRelogChannel.SupportedGameSha256,token).ConfigureAwait(false);
private async Task<GameSessionTarget> RecoverStalledSceneAsync(GameSessionTarget target,PlayerState expected,WindowsGameSessionDriver lifecycle,CancellationToken token)
{
if(target.ProcessId<=0||!await HasSavingSceneAsync(target.ProcessId,token).ConfigureAwait(false))return target;
var began=DateTimeOffset.UtcNow;
Progress?.Invoke("游戏正在保存抵达位置;保持原航程并观察原版场景,暂不发送驾驶或交易指令。");
while(DateTimeOffset.UtcNow-began<TimeSpan.FromSeconds(90))
{
token.ThrowIfCancellationRequested();var current=player();
if(current.CommanderId!=expected.CommanderId||current.CurrentShipId!=expected.CurrentShipId||current.GameMode!=expected.GameMode||current.CargoCount!=expected.CargoCount)
throw new InvalidOperationException("保存场景等待期间身份、飞船或货舱变化,未自动重启。");
if(!await HasSavingSceneAsync(target.ProcessId,token).ConfigureAwait(false))return target;
await Task.Delay(1000,token).ConfigureAwait(false);
}
// No gameplay is permitted by this original transition state. Exiting
// the verified client does not authorize another order; official entry
// below must re-establish commander, ship, mode and actual cargo first.
Progress?.Invoke("原版保存抵达位置持续 90 秒未完成,正在重启官方客户端;原订单和回执保持不变。");
await lifecycle.TerminateAsync(target,token).ConfigureAwait(false);
var until=DateTimeOffset.UtcNow.AddSeconds(15);
while(!await lifecycle.HasExitedAsync(target,token).ConfigureAwait(false))
{if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("原客户端尚未确认退出,未启动新实例。");await Task.Delay(100,token).ConfigureAwait(false);}
// A terminated process can briefly remain in process enumeration. Keep
// its verified installation, but never probe that dead pipe again.
return target with{ProcessId=0,StartedUtcTicks=0};
}
}