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 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? 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 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 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.UtcNow0&&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)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)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 HasSavingSceneAsync(int pid,CancellationToken token) =>await SavingSceneRecovery.ReadAsync(pid,NativeRelogChannel.SupportedGameSha256,token).ConfigureAwait(false); private async Task 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=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}; } }