mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
refactor(launcher): replace LauncherFlowCoordinator with LaunchPipeline and slim App shell
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
namespace LanMountainDesktop.Launcher.Startup;
|
||||
|
||||
internal sealed class ApplyPendingUpdatePhase : ILaunchPhase
|
||||
{
|
||||
public string Name => nameof(ApplyPendingUpdatePhase);
|
||||
|
||||
public async Task<LaunchPhaseResult> ExecuteAsync(LaunchContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.Reporter.Report("update", "Checking updates...");
|
||||
var updateResult = await context.UpdateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false);
|
||||
if (!updateResult.Success)
|
||||
{
|
||||
Logger.Warn($"Update apply failed, will try to launch existing version. Error='{updateResult.Message}'.");
|
||||
context.Reporter.Report("update", "Update failed, launching existing version...");
|
||||
try
|
||||
{
|
||||
context.UpdateEngine.CleanupIncomingArtifacts();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Failed to cleanup update artifacts after failed update: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return new LaunchPhaseResult(LaunchPhaseStatus.Continue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace LanMountainDesktop.Launcher.Startup;
|
||||
|
||||
internal sealed class CleanupDeploymentsPhase : ILaunchPhase
|
||||
{
|
||||
public string Name => nameof(CleanupDeploymentsPhase);
|
||||
|
||||
public Task<LaunchPhaseResult> ExecuteAsync(LaunchContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.DeploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
|
||||
context.OobeDecision = context.OobeStateService.Evaluate(context.CommandContext);
|
||||
context.LauncherContextDetails = LaunchResultBuilder.BuildLauncherContextDetails(
|
||||
context.CommandContext,
|
||||
context.OobeDecision,
|
||||
context.DeploymentLocator.GetAppRoot());
|
||||
return Task.FromResult(new LaunchPhaseResult(LaunchPhaseStatus.Continue));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Startup;
|
||||
|
||||
internal sealed class ExistingHostProbePhase : ILaunchPhase
|
||||
{
|
||||
public string Name => nameof(ExistingHostProbePhase);
|
||||
|
||||
public async Task<LaunchPhaseResult> ExecuteAsync(LaunchContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!HostActivationPolicy.ShouldProbeExistingHostBeforeLaunch(context.CommandContext))
|
||||
{
|
||||
return new LaunchPhaseResult(LaunchPhaseStatus.Continue);
|
||||
}
|
||||
|
||||
var multiInstanceBehavior = ExistingHostProbe.LoadMultiInstanceLaunchBehavior(context.DataLocationResolver);
|
||||
var existingShellStatus = await ExistingHostProbe.TryGetExistingHostStatusAsync(
|
||||
context.IpcClient,
|
||||
StartupTimeoutPolicy.ExistingHostProbeTimeout).ConfigureAwait(false);
|
||||
|
||||
if (!HostActivationPolicy.IsExistingHostReadyForLauncherDecision(existingShellStatus))
|
||||
{
|
||||
return new LaunchPhaseResult(LaunchPhaseStatus.Continue);
|
||||
}
|
||||
|
||||
context.IpcConnected = true;
|
||||
context.ShellStatus = existingShellStatus;
|
||||
var decisionResult = await ExistingHostProbe.ApplyExistingHostBehaviorAsync(
|
||||
context.IpcClient,
|
||||
multiInstanceBehavior,
|
||||
existingShellStatus!).ConfigureAwait(false);
|
||||
context.ShellStatus = decisionResult.ActivationResult?.Status ?? existingShellStatus;
|
||||
var recoverableActivationFailure = decisionResult.ActivationResult is not null &&
|
||||
HostActivationPolicy.IsRecoverableActivationFailure(decisionResult.ActivationResult);
|
||||
context.LastStage = decisionResult.Success || recoverableActivationFailure
|
||||
? StartupStage.ActivationRedirected
|
||||
: StartupStage.ActivationFailed;
|
||||
context.LastStageMessage = decisionResult.Message;
|
||||
if (decisionResult.Success || recoverableActivationFailure)
|
||||
{
|
||||
context.StartupAttemptRegistry.MarkOwnedSucceeded(context.LastStage, context.LastStageMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.StartupAttemptRegistry.MarkOwnedFailed(context.LastStage, context.LastStageMessage);
|
||||
}
|
||||
|
||||
context.PublishCoordinatorStatus(true, true, decisionResult.Success);
|
||||
context.WindowsClosingByOrchestrator = true;
|
||||
await LaunchUiPresenter.CloseWindowsAsync(context.SplashWindow, context.LoadingDetailsWindow).ConfigureAwait(false);
|
||||
return new LaunchPhaseResult(
|
||||
LaunchPhaseStatus.Completed,
|
||||
LaunchResultBuilder.Build(
|
||||
decisionResult.Success,
|
||||
"launch",
|
||||
decisionResult.Code,
|
||||
decisionResult.Message,
|
||||
LaunchResultBuilder.MergeDetails(
|
||||
context.LauncherContextDetails,
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["publicIpcConnected"] = "true",
|
||||
["multiInstanceBehavior"] = multiInstanceBehavior.ToString(),
|
||||
["existingHostPid"] = context.ShellStatus?.ProcessId.ToString() ?? string.Empty,
|
||||
["existingShellState"] = context.ShellStatus?.ShellState ?? string.Empty,
|
||||
["existingTrayState"] = context.ShellStatus?.Tray.State ?? string.Empty,
|
||||
["existingTaskbarUsable"] = context.ShellStatus?.Taskbar.IsUsable.ToString() ?? string.Empty,
|
||||
["activationAccepted"] = decisionResult.ActivationResult?.Accepted.ToString() ?? string.Empty
|
||||
})));
|
||||
}
|
||||
}
|
||||
173
LanMountainDesktop.Launcher/Startup/Phases/LaunchHostPhase.cs
Normal file
173
LanMountainDesktop.Launcher/Startup/Phases/LaunchHostPhase.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using LanMountainDesktop.Launcher.Models;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Startup;
|
||||
|
||||
internal sealed class LaunchHostPhase : ILaunchPhase
|
||||
{
|
||||
private static readonly string SoftTimeoutStatusMessage = Resources.Strings.Coordinator_SlowDeviceMessage;
|
||||
private static readonly string SoftTimeoutDetailsMessage = Resources.Strings.Coordinator_RunningHostMessage;
|
||||
|
||||
private readonly HostLaunchService _hostLaunchService = new();
|
||||
|
||||
public string Name => nameof(LaunchHostPhase);
|
||||
|
||||
public async Task<LaunchPhaseResult> ExecuteAsync(LaunchContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.Reporter.Report("launch", "Launching desktop...");
|
||||
var startupSuccessTracker = context.SuccessTracker;
|
||||
var attachableAttempt = context.StartupAttemptRegistry.TryGetAttachableAttempt(
|
||||
context.CommandContext.LaunchSource,
|
||||
startupSuccessTracker.PolicyKey);
|
||||
|
||||
HostLaunchOutcome launchOutcome;
|
||||
if (attachableAttempt is not null &&
|
||||
context.StartupAttemptRegistry.AdoptAttempt(attachableAttempt.AttemptId) &&
|
||||
LaunchResultBuilder.TryGetLiveProcess(attachableAttempt.HostPid, out var attachedProcess))
|
||||
{
|
||||
context.TrackedAttempt = attachableAttempt;
|
||||
context.AttachedToExistingAttempt = true;
|
||||
context.IpcConnected = attachableAttempt.IpcConnected;
|
||||
context.LastStage = attachableAttempt.LastObservedStage;
|
||||
context.LastStageMessage = string.IsNullOrWhiteSpace(attachableAttempt.LastObservedMessage)
|
||||
? "Attached to the existing startup attempt."
|
||||
: attachableAttempt.LastObservedMessage;
|
||||
context.Reporter.Report(
|
||||
LaunchUiPresenter.MapStartupStageToSplashStage(context.LastStage),
|
||||
context.LastStageMessage);
|
||||
context.PublishCoordinatorStatus(true, false, false);
|
||||
|
||||
if (startupSuccessTracker.TryResolve(context.LastStage, out var attachedSuccessState))
|
||||
{
|
||||
context.WindowsClosingByOrchestrator = true;
|
||||
context.StartupAttemptRegistry.MarkOwnedSucceeded(attachedSuccessState.Stage, attachedSuccessState.Message);
|
||||
await LaunchUiPresenter.CloseWindowsAsync(context.SplashWindow, context.LoadingDetailsWindow).ConfigureAwait(false);
|
||||
return new LaunchPhaseResult(
|
||||
LaunchPhaseStatus.Completed,
|
||||
LaunchResultBuilder.Build(
|
||||
true,
|
||||
"launch",
|
||||
attachedSuccessState.Code,
|
||||
attachedSuccessState.Message,
|
||||
LaunchResultBuilder.MergeDetails(
|
||||
context.LauncherContextDetails,
|
||||
LaunchAttemptDetails.Build(
|
||||
context.TrackedAttempt,
|
||||
context.AttachedToExistingAttempt,
|
||||
context.IpcConnected,
|
||||
hostProcessAlive: true,
|
||||
context.LastStage,
|
||||
context.LastStageMessage,
|
||||
context.ActivationFailureReason,
|
||||
softTimeoutShown: false,
|
||||
recoveryActivationAttempted: false))));
|
||||
}
|
||||
|
||||
if (attachableAttempt.State is StartupAttemptState.SoftTimeout or StartupAttemptState.DetachedWaiting)
|
||||
{
|
||||
context.SoftTimeoutShown = true;
|
||||
context.Reporter.Report("delayed", SoftTimeoutStatusMessage);
|
||||
context.LoadingState = HostStartupMonitor.BuildDelayedLoadingState(
|
||||
context.LoadingState,
|
||||
SoftTimeoutStatusMessage,
|
||||
SoftTimeoutDetailsMessage,
|
||||
context.TrackedAttempt!.StartedAtUtc);
|
||||
context.LoadingDetailsWindow?.UpdateLoadingState(context.LoadingState);
|
||||
}
|
||||
|
||||
launchOutcome = HostLaunchOutcome.FromProcess(
|
||||
attachedProcess!,
|
||||
LaunchResultBuilder.Build(
|
||||
true,
|
||||
"launchHost",
|
||||
"attached_attempt",
|
||||
"Attached to an existing startup attempt.",
|
||||
LaunchAttemptDetails.Build(
|
||||
context.TrackedAttempt,
|
||||
context.AttachedToExistingAttempt,
|
||||
context.IpcConnected,
|
||||
hostProcessAlive: true,
|
||||
context.LastStage,
|
||||
context.LastStageMessage,
|
||||
context.ActivationFailureReason,
|
||||
context.SoftTimeoutShown,
|
||||
recoveryActivationAttempted: false)),
|
||||
LaunchAttemptDetails.Build(
|
||||
context.TrackedAttempt,
|
||||
context.AttachedToExistingAttempt,
|
||||
context.IpcConnected,
|
||||
hostProcessAlive: true,
|
||||
context.LastStage,
|
||||
context.LastStageMessage,
|
||||
context.ActivationFailureReason,
|
||||
context.SoftTimeoutShown,
|
||||
recoveryActivationAttempted: false));
|
||||
}
|
||||
else
|
||||
{
|
||||
launchOutcome = await _hostLaunchService.LaunchAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
context.LaunchOutcome = launchOutcome;
|
||||
|
||||
if (!launchOutcome.Result.Success)
|
||||
{
|
||||
return new LaunchPhaseResult(
|
||||
LaunchPhaseStatus.Completed,
|
||||
LaunchResultBuilder.WithAdditionalDetails(launchOutcome.Result, context.LauncherContextDetails));
|
||||
}
|
||||
|
||||
if (launchOutcome.ImmediateResult is not null)
|
||||
{
|
||||
context.WindowsClosingByOrchestrator = true;
|
||||
await LaunchUiPresenter.CloseWindowsAsync(context.SplashWindow, context.LoadingDetailsWindow).ConfigureAwait(false);
|
||||
return new LaunchPhaseResult(
|
||||
LaunchPhaseStatus.Completed,
|
||||
LaunchResultBuilder.WithAdditionalDetails(launchOutcome.ImmediateResult, context.LauncherContextDetails));
|
||||
}
|
||||
|
||||
if (launchOutcome.Process is null)
|
||||
{
|
||||
return new LaunchPhaseResult(
|
||||
LaunchPhaseStatus.Completed,
|
||||
LaunchResultBuilder.Build(
|
||||
success: false,
|
||||
stage: "launch",
|
||||
code: "host_start_failed",
|
||||
message: "Host launch did not create a process.",
|
||||
details: LaunchResultBuilder.MergeDetails(
|
||||
context.LauncherContextDetails,
|
||||
LaunchResultBuilder.MergeDetails(
|
||||
launchOutcome.Details,
|
||||
LaunchAttemptDetails.Build(
|
||||
context.TrackedAttempt,
|
||||
context.AttachedToExistingAttempt,
|
||||
context.IpcConnected,
|
||||
hostProcessAlive: false,
|
||||
context.LastStage,
|
||||
context.LastStageMessage,
|
||||
context.ActivationFailureReason,
|
||||
context.SoftTimeoutShown,
|
||||
recoveryActivationAttempted: false)))));
|
||||
}
|
||||
|
||||
if (!context.AttachedToExistingAttempt)
|
||||
{
|
||||
var reservedAttempt = context.StartupAttemptRegistry.GetOwnedAttempt();
|
||||
context.TrackedAttempt = reservedAttempt is { ReservedBeforeHostStart: true }
|
||||
? context.StartupAttemptRegistry.AssignOwnedHostProcess(
|
||||
launchOutcome.Process.Id,
|
||||
context.LastStage,
|
||||
context.LastStageMessage)
|
||||
: context.StartupAttemptRegistry.StartOwnedAttempt(
|
||||
launchOutcome.Process.Id,
|
||||
context.CommandContext.LaunchSource,
|
||||
startupSuccessTracker.PolicyKey,
|
||||
context.LastStage,
|
||||
context.LastStageMessage);
|
||||
context.PublishCoordinatorStatus(true, false, false);
|
||||
}
|
||||
|
||||
return new LaunchPhaseResult(LaunchPhaseStatus.Continue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
namespace LanMountainDesktop.Launcher.Startup;
|
||||
|
||||
internal sealed class MonitorStartupPhase : ILaunchPhase
|
||||
{
|
||||
public string Name => nameof(MonitorStartupPhase);
|
||||
|
||||
public async Task<LaunchPhaseResult> ExecuteAsync(LaunchContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var launchOutcome = context.LaunchOutcome
|
||||
?? throw new InvalidOperationException("LaunchHostPhase must run before MonitorStartupPhase.");
|
||||
|
||||
if (launchOutcome.Process is null)
|
||||
{
|
||||
return new LaunchPhaseResult(
|
||||
LaunchPhaseStatus.Completed,
|
||||
LaunchResultBuilder.BuildFailure("launch", "host_start_failed", "Host process is missing."));
|
||||
}
|
||||
|
||||
Dictionary<string, string> ComposeLaunchDetails(bool hostProcessAlive, bool recoveryActivationAttempted = false) =>
|
||||
LaunchResultBuilder.MergeDetails(
|
||||
context.LauncherContextDetails,
|
||||
LaunchResultBuilder.MergeDetails(
|
||||
launchOutcome.Details,
|
||||
LaunchAttemptDetails.Build(
|
||||
context.TrackedAttempt,
|
||||
context.AttachedToExistingAttempt,
|
||||
context.IpcConnected,
|
||||
hostProcessAlive,
|
||||
context.LastStage,
|
||||
context.LastStageMessage,
|
||||
context.ActivationFailureReason,
|
||||
context.SoftTimeoutShown,
|
||||
recoveryActivationAttempted)));
|
||||
|
||||
var monitor = new HostStartupMonitor();
|
||||
var monitorOutcome = await monitor.MonitorUntilCompleteAsync(new HostStartupMonitor.Request(
|
||||
launchOutcome.Process,
|
||||
context.IpcClient,
|
||||
context.SuccessTracker,
|
||||
context.StartupAttemptRegistry,
|
||||
context.TrackedAttempt,
|
||||
context.AttachedToExistingAttempt,
|
||||
context.LauncherContextDetails,
|
||||
context.SuccessTcs,
|
||||
context.ActivationFailedTcs,
|
||||
context.Reporter,
|
||||
context.LoadingDetailsWindow,
|
||||
context.LoadingState,
|
||||
context.LastStage,
|
||||
context.LastStageMessage,
|
||||
context.IpcConnected,
|
||||
context.ActivationFailureReason,
|
||||
context.SoftTimeoutShown,
|
||||
context.PublishCoordinatorStatus,
|
||||
ComposeLaunchDetails)).ConfigureAwait(false);
|
||||
|
||||
context.WindowsClosingByOrchestrator = true;
|
||||
await LaunchUiPresenter.CloseWindowsAsync(context.SplashWindow, context.LoadingDetailsWindow).ConfigureAwait(false);
|
||||
return new LaunchPhaseResult(
|
||||
LaunchPhaseStatus.Completed,
|
||||
LaunchResultBuilder.Build(
|
||||
monitorOutcome.Success,
|
||||
"launch",
|
||||
monitorOutcome.Code,
|
||||
monitorOutcome.Message,
|
||||
monitorOutcome.Details));
|
||||
}
|
||||
}
|
||||
24
LanMountainDesktop.Launcher/Startup/Phases/OobeGatePhase.cs
Normal file
24
LanMountainDesktop.Launcher/Startup/Phases/OobeGatePhase.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Startup;
|
||||
|
||||
internal sealed class OobeGatePhase : ILaunchPhase
|
||||
{
|
||||
public string Name => nameof(OobeGatePhase);
|
||||
|
||||
public async Task<LaunchPhaseResult> ExecuteAsync(LaunchContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (context.OobeDecision.ShouldShowOobe)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => context.SplashWindow.Hide());
|
||||
foreach (var step in context.OobeSteps)
|
||||
{
|
||||
await step.RunAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() => context.SplashWindow.Show());
|
||||
}
|
||||
|
||||
return new LaunchPhaseResult(LaunchPhaseStatus.Continue);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user