mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
refactor(launcher): replace LauncherFlowCoordinator with LaunchPipeline and slim App shell
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
282
LanMountainDesktop.Launcher/Shell/LauncherOrchestrator.cs
Normal file
282
LanMountainDesktop.Launcher/Shell/LauncherOrchestrator.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Launcher.Models;
|
||||
using LanMountainDesktop.Launcher.Startup;
|
||||
using LanMountainDesktop.Launcher.Views;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Shell;
|
||||
|
||||
internal sealed class LauncherOrchestrator
|
||||
{
|
||||
private readonly CommandContext _context;
|
||||
private readonly DeploymentLocator _deploymentLocator;
|
||||
private readonly OobeStateService _oobeStateService;
|
||||
private readonly UpdateEngineService _updateEngine;
|
||||
private readonly StartupAttemptRegistry _startupAttemptRegistry;
|
||||
private readonly LauncherCoordinatorIpcServer? _coordinatorIpcServer;
|
||||
private readonly DataLocationResolver _dataLocationResolver;
|
||||
private readonly IReadOnlyList<IOobeStep> _oobeSteps;
|
||||
private readonly LaunchPipeline _pipeline;
|
||||
|
||||
public LauncherOrchestrator(
|
||||
CommandContext context,
|
||||
DeploymentLocator deploymentLocator,
|
||||
OobeStateService oobeStateService,
|
||||
UpdateEngineService updateEngine,
|
||||
StartupAttemptRegistry startupAttemptRegistry,
|
||||
LauncherCoordinatorIpcServer? coordinatorIpcServer = null)
|
||||
{
|
||||
_context = context;
|
||||
_deploymentLocator = deploymentLocator;
|
||||
_oobeStateService = oobeStateService;
|
||||
_updateEngine = updateEngine;
|
||||
_startupAttemptRegistry = startupAttemptRegistry;
|
||||
_coordinatorIpcServer = coordinatorIpcServer;
|
||||
_dataLocationResolver = new DataLocationResolver(deploymentLocator.GetAppRoot());
|
||||
_oobeSteps =
|
||||
[
|
||||
new WelcomeOobeStep(_oobeStateService, _context),
|
||||
new DataLocationOobeStep(_dataLocationResolver)
|
||||
];
|
||||
_pipeline = new LaunchPipeline(
|
||||
[
|
||||
new CleanupDeploymentsPhase(),
|
||||
new ExistingHostProbePhase(),
|
||||
new ApplyPendingUpdatePhase(),
|
||||
new OobeGatePhase(),
|
||||
new LaunchHostPhase(),
|
||||
new MonitorStartupPhase()
|
||||
]);
|
||||
}
|
||||
|
||||
public static string ResolveSuccessPolicyKey(CommandContext context) =>
|
||||
new StartupSuccessTracker(context).PolicyKey;
|
||||
|
||||
public async Task<LauncherResult> RunAsync(SplashWindow? existingSplashWindow = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var oobeDecision = _oobeStateService.Evaluate(_context);
|
||||
if (oobeDecision.ShouldShowOobe)
|
||||
{
|
||||
var legacyInfo = LegacyVersionDetector.DetectLegacyInstallation();
|
||||
if (legacyInfo is not null)
|
||||
{
|
||||
var migrationResult = await LaunchUiPresenter.ShowMigrationPromptAsync(legacyInfo).ConfigureAwait(false);
|
||||
Logger.Info($"Migration prompt completed. Result='{migrationResult}'.");
|
||||
}
|
||||
}
|
||||
|
||||
var splashWindow = existingSplashWindow ?? await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
var window = new SplashWindow();
|
||||
window.Show();
|
||||
return window;
|
||||
});
|
||||
var versionInfo = _deploymentLocator.GetVersionInfo();
|
||||
splashWindow.SetVersionInfo(versionInfo.Version, versionInfo.Codename);
|
||||
var reporter = (ISplashStageReporter)splashWindow;
|
||||
|
||||
LoadingDetailsWindow? loadingDetailsWindow = null;
|
||||
if (_context.IsDebugMode || _context.GetOption("show-loading-details") == "true")
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
loadingDetailsWindow = new LoadingDetailsWindow();
|
||||
loadingDetailsWindow.Show();
|
||||
});
|
||||
}
|
||||
|
||||
var successTcs = new TaskCompletionSource<StartupSuccessState>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var activationFailedTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var lastStage = StartupStage.Initializing;
|
||||
var lastStageMessage = "launcher-started";
|
||||
var startupSuccessTracker = new StartupSuccessTracker(_context);
|
||||
var activationFailureReason = string.Empty;
|
||||
var ipcConnected = false;
|
||||
var softTimeoutShown = false;
|
||||
var attachedToExistingAttempt = false;
|
||||
var windowsClosingByOrchestrator = false;
|
||||
StartupAttemptRecord? trackedAttempt = null;
|
||||
PublicShellStatus? shellStatus = null;
|
||||
var loadingState = new LoadingStateMessage();
|
||||
|
||||
void PublishCoordinatorStatus(bool? hostProcessAliveOverride = null, bool completed = false, bool succeeded = false)
|
||||
{
|
||||
if (_coordinatorIpcServer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
trackedAttempt = _startupAttemptRegistry.GetOwnedAttempt() ?? trackedAttempt;
|
||||
var hostPid = trackedAttempt?.HostPid ?? 0;
|
||||
var hostProcessAlive = hostProcessAliveOverride ??
|
||||
(hostPid > 0 && LaunchResultBuilder.TryGetLiveProcess(hostPid, out _));
|
||||
var status = new LauncherCoordinatorStatus
|
||||
{
|
||||
AttemptId = trackedAttempt?.AttemptId ?? string.Empty,
|
||||
CoordinatorPid = Environment.ProcessId,
|
||||
HostPid = hostPid,
|
||||
HostProcessAlive = hostProcessAlive,
|
||||
LaunchSource = trackedAttempt?.LaunchSource ?? _context.LaunchSource,
|
||||
SuccessPolicy = trackedAttempt?.SuccessPolicy ?? startupSuccessTracker.PolicyKey,
|
||||
LastObservedStage = lastStage,
|
||||
LastObservedMessage = lastStageMessage,
|
||||
PublicIpcConnected = ipcConnected,
|
||||
State = trackedAttempt?.State.ToString() ?? StartupAttemptState.Pending.ToString(),
|
||||
SoftTimeoutShown = softTimeoutShown,
|
||||
Completed = completed,
|
||||
Succeeded = succeeded,
|
||||
ShellStatus = shellStatus,
|
||||
UpdatedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
_coordinatorIpcServer.UpdateStatus(status);
|
||||
_startupAttemptRegistry.UpdateOwnedCoordinatorHeartbeat(status);
|
||||
}
|
||||
|
||||
trackedAttempt = _startupAttemptRegistry.GetOwnedAttempt();
|
||||
PublishCoordinatorStatus();
|
||||
|
||||
EventHandler? splashClosedHandler = null;
|
||||
splashClosedHandler = (_, _) =>
|
||||
{
|
||||
if (windowsClosingByOrchestrator)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_startupAttemptRegistry.MarkOwnedDetachedWaiting();
|
||||
Logger.Warn("Splash window was closed manually. Launcher will continue monitoring the current startup attempt.");
|
||||
};
|
||||
splashWindow.Closed += splashClosedHandler;
|
||||
|
||||
using var ipcClient = new LanMountainDesktopIpcClient();
|
||||
ipcClient.RegisterNotifyHandler<StartupProgressMessage>(IpcRoutedNotifyIds.LauncherStartupProgress, message =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ipcConnected = true;
|
||||
lastStage = message.Stage;
|
||||
lastStageMessage = message.Message ?? message.Stage.ToString();
|
||||
Logger.Info($"IPC stage received. Stage='{message.Stage}'; Message='{message.Message ?? string.Empty}'.");
|
||||
|
||||
loadingState = loadingState with
|
||||
{
|
||||
Stage = message.Stage,
|
||||
OverallProgressPercent = message.ProgressPercent,
|
||||
Message = message.Message,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
reporter.Report(LaunchUiPresenter.MapStartupStageToSplashStage(message.Stage), message.Message ?? message.Stage.ToString());
|
||||
loadingDetailsWindow?.UpdateLoadingState(loadingState);
|
||||
_startupAttemptRegistry.UpdateOwnedStage(message.Stage, message.Message, ipcConnected: true);
|
||||
PublishCoordinatorStatus();
|
||||
|
||||
if (startupSuccessTracker.TryResolve(message.Stage, out var successState))
|
||||
{
|
||||
successTcs.TrySetResult(successState);
|
||||
}
|
||||
|
||||
if (message.Stage == StartupStage.ActivationFailed)
|
||||
{
|
||||
activationFailureReason = message.Message ?? "activation_failed";
|
||||
activationFailedTcs.TrySetResult(message.Message ?? "activation_failed");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("IPC progress callback failed.", ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
ipcClient.RegisterNotifyHandler<LoadingStateMessage>(IpcRoutedNotifyIds.LauncherLoadingState, message =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
loadingState = message;
|
||||
loadingDetailsWindow?.UpdateLoadingState(loadingState);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("IPC loading-state callback failed.", ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var launchContext = new LaunchContext
|
||||
{
|
||||
CommandContext = _context,
|
||||
DeploymentLocator = _deploymentLocator,
|
||||
OobeStateService = _oobeStateService,
|
||||
UpdateEngine = _updateEngine,
|
||||
StartupAttemptRegistry = _startupAttemptRegistry,
|
||||
CoordinatorIpcServer = _coordinatorIpcServer,
|
||||
DataLocationResolver = _dataLocationResolver,
|
||||
OobeSteps = _oobeSteps,
|
||||
SplashWindow = splashWindow,
|
||||
LoadingDetailsWindow = loadingDetailsWindow,
|
||||
Reporter = reporter,
|
||||
IpcClient = ipcClient,
|
||||
SuccessTracker = startupSuccessTracker,
|
||||
SuccessTcs = successTcs,
|
||||
ActivationFailedTcs = activationFailedTcs,
|
||||
LoadingState = loadingState,
|
||||
PublishCoordinatorStatus = PublishCoordinatorStatus,
|
||||
SplashClosedHandler = splashClosedHandler
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _pipeline.ExecuteAsync(launchContext).ConfigureAwait(false);
|
||||
windowsClosingByOrchestrator = launchContext.WindowsClosingByOrchestrator;
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (splashClosedHandler is not null)
|
||||
{
|
||||
splashWindow.Closed -= splashClosedHandler;
|
||||
}
|
||||
|
||||
if (!windowsClosingByOrchestrator && !launchContext.WindowsClosingByOrchestrator)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (splashWindow.IsVisible && splashWindow.IsLoaded)
|
||||
{
|
||||
splashWindow.Close();
|
||||
Logger.Info("Splash window closed in orchestrator cleanup.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Failed to close splash window during orchestrator cleanup.", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Launcher orchestrator failed.", ex);
|
||||
var oobeDecision = _oobeStateService.Evaluate(_context);
|
||||
return LaunchResultBuilder.Build(
|
||||
false,
|
||||
"launch",
|
||||
"exception",
|
||||
ex.Message,
|
||||
LaunchResultBuilder.BuildLauncherContextDetails(_context, oobeDecision, _deploymentLocator.GetAppRoot()),
|
||||
ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user