using System.Diagnostics; using Avalonia.Threading; using LanMountainDesktop.Launcher.Models; using LanMountainDesktop.Launcher.Resources; using LanMountainDesktop.Launcher.Services.Ipc; using LanMountainDesktop.Launcher.Views; using LanMountainDesktop.Shared.Contracts.Launcher; using LanMountainDesktop.Shared.IPC; using LanMountainDesktop.Shared.IPC.Abstractions.Services; namespace LanMountainDesktop.Launcher.Services; internal sealed class LauncherFlowCoordinator { private static readonly TimeSpan StartupSoftTimeout = TimeSpan.FromSeconds(10); private static readonly TimeSpan StartupHardTimeout = TimeSpan.FromSeconds(30); private static readonly string SoftTimeoutStatusMessage = Strings.Coordinator_SlowDeviceMessage; private static readonly string SoftTimeoutDetailsMessage = Strings.Coordinator_RunningHostMessage; private readonly CommandContext _context; private readonly DeploymentLocator _deploymentLocator; private readonly OobeStateService _oobeStateService; private readonly UpdateEngineService _updateEngine; private readonly PluginInstallerService _pluginInstallerService; private readonly StartupAttemptRegistry _startupAttemptRegistry; private readonly LauncherCoordinatorIpcServer? _coordinatorIpcServer; private readonly DataLocationResolver _dataLocationResolver; private readonly IReadOnlyList _oobeSteps; public LauncherFlowCoordinator( CommandContext context, DeploymentLocator deploymentLocator, OobeStateService oobeStateService, UpdateEngineService updateEngine, PluginInstallerService pluginInstallerService, StartupAttemptRegistry? startupAttemptRegistry = null, LauncherCoordinatorIpcServer? coordinatorIpcServer = null) { _context = context; _deploymentLocator = deploymentLocator; _oobeStateService = oobeStateService; _updateEngine = updateEngine; _pluginInstallerService = pluginInstallerService; _startupAttemptRegistry = startupAttemptRegistry ?? new StartupAttemptRegistry(); _coordinatorIpcServer = coordinatorIpcServer; _dataLocationResolver = new DataLocationResolver(deploymentLocator.GetAppRoot()); _oobeSteps = [ new WelcomeOobeStep(_oobeStateService, _context), new DataLocationOobeStep(_dataLocationResolver) ]; } public static string ResolveSuccessPolicyKey(CommandContext context) { return new StartupSuccessTracker(context).PolicyKey; } public async Task RunAsync(SplashWindow? existingSplashWindow = null) { try { _deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3); var oobeDecision = _oobeStateService.Evaluate(_context); var launcherContextDetails = BuildLauncherContextDetails(_context, oobeDecision, _deploymentLocator.GetAppRoot()); if (oobeDecision.ShouldShowOobe) { var legacyInfo = LegacyVersionDetector.DetectLegacyInstallation(); if (legacyInfo is not null) { var migrationResult = await 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 windowsClosingByCoordinator = false; 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(TaskCreationOptions.RunContinuationsAsynchronously); var activationFailedTcs = new TaskCompletionSource(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; StartupAttemptRecord? trackedAttempt = null; PublicShellStatus? shellStatus = null; 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 && 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(); var loadingState = new LoadingStateMessage(); EventHandler? splashClosedHandler = null; splashClosedHandler = (_, _) => { if (windowsClosingByCoordinator) { 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(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(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(IpcRoutedNotifyIds.LauncherLoadingState, message => { Dispatcher.UIThread.Post(() => { try { loadingState = message; loadingDetailsWindow?.UpdateLoadingState(loadingState); } catch (Exception ex) { Logger.Error("IPC loading-state callback failed.", ex); } }); }); try { if (ShouldProbeExistingHostBeforeLaunch(_context)) { var multiInstanceBehavior = LoadMultiInstanceLaunchBehavior(); var existingShellStatus = await TryGetExistingHostStatusAsync(ipcClient, TimeSpan.FromMilliseconds(900)) .ConfigureAwait(false); if (IsExistingHostReadyForLauncherDecision(existingShellStatus)) { ipcConnected = true; shellStatus = existingShellStatus; var decisionResult = await ApplyExistingHostBehaviorAsync( ipcClient, multiInstanceBehavior, existingShellStatus!) .ConfigureAwait(false); shellStatus = decisionResult.ActivationResult?.Status ?? existingShellStatus; var recoverableActivationFailure = decisionResult.ActivationResult is not null && IsRecoverableActivationFailure(decisionResult.ActivationResult); lastStage = decisionResult.Success || recoverableActivationFailure ? StartupStage.ActivationRedirected : StartupStage.ActivationFailed; lastStageMessage = decisionResult.Message; if (decisionResult.Success || recoverableActivationFailure) { _startupAttemptRegistry.MarkOwnedSucceeded(lastStage, lastStageMessage); } else { _startupAttemptRegistry.MarkOwnedFailed(lastStage, lastStageMessage); } PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: true, succeeded: decisionResult.Success); windowsClosingByCoordinator = true; await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: decisionResult.Success, stage: "launch", code: decisionResult.Code, message: decisionResult.Message, details: MergeDetails( launcherContextDetails, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["publicIpcConnected"] = "true", ["multiInstanceBehavior"] = multiInstanceBehavior.ToString(), ["existingHostPid"] = shellStatus?.ProcessId.ToString() ?? string.Empty, ["existingShellState"] = shellStatus?.ShellState ?? string.Empty, ["existingTrayState"] = shellStatus?.Tray.State ?? string.Empty, ["existingTaskbarUsable"] = shellStatus?.Taskbar.IsUsable.ToString() ?? string.Empty, ["activationAccepted"] = decisionResult.ActivationResult?.Accepted.ToString() ?? string.Empty })); } } reporter.Report("update", "Checking updates..."); var updateResult = await _updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false); if (!updateResult.Success) { Logger.Warn($"Update apply failed, will try to launch existing version. Error='{updateResult.Message}'."); reporter.Report("update", "Update failed, launching existing version..."); // Clean up corrupted update files to prevent repeated failures try { _updateEngine.CleanupIncomingArtifacts(); } catch (Exception ex) { Logger.Warn($"Failed to cleanup update artifacts after failed update: {ex.Message}"); } // Continue to launch existing version instead of aborting } reporter.Report("plugins", "Applying plugin upgrades..."); var pluginsDir = _context.GetOption("plugins-dir") ?? Path.Combine(_deploymentLocator.GetAppRoot(), "plugins"); var queueResult = new PluginUpgradeQueueService(_pluginInstallerService).ApplyPendingUpgrades(pluginsDir); if (!queueResult.Success) { Logger.Warn($"Plugin upgrade failed, continuing startup. Error='{queueResult.Message}'."); reporter.Report("plugins", "Plugin upgrade failed, continuing..."); } if (oobeDecision.ShouldShowOobe) { await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Hide()); foreach (var step in _oobeSteps) { await step.RunAsync(CancellationToken.None).ConfigureAwait(false); } await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Show()); } reporter.Report("launch", "Launching desktop..."); var launchOutcome = default(HostLaunchOutcome); var attachableAttempt = _startupAttemptRegistry.TryGetAttachableAttempt(_context.LaunchSource, startupSuccessTracker.PolicyKey); if (attachableAttempt is not null && _startupAttemptRegistry.AdoptAttempt(attachableAttempt.AttemptId) && TryGetLiveProcess(attachableAttempt.HostPid, out var attachedProcess)) { trackedAttempt = attachableAttempt; attachedToExistingAttempt = true; ipcConnected = attachableAttempt.IpcConnected; lastStage = attachableAttempt.LastObservedStage; lastStageMessage = string.IsNullOrWhiteSpace(attachableAttempt.LastObservedMessage) ? "Attached to the existing startup attempt." : attachableAttempt.LastObservedMessage; reporter.Report(MapStartupStageToSplashStage(lastStage), lastStageMessage); PublishCoordinatorStatus(hostProcessAliveOverride: true); if (startupSuccessTracker.TryResolve(lastStage, out var attachedSuccessState)) { windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedSucceeded(attachedSuccessState.Stage, attachedSuccessState.Message); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: attachedSuccessState.Code, message: attachedSuccessState.Message, details: MergeDetails( launcherContextDetails, BuildAttemptDetails( trackedAttempt, attachedToExistingAttempt, ipcConnected, hostProcessAlive: true, lastStage, lastStageMessage, activationFailureReason, softTimeoutShown: false, recoveryActivationAttempted: false))); } if (attachableAttempt.State is StartupAttemptState.SoftTimeout or StartupAttemptState.DetachedWaiting) { softTimeoutShown = true; reporter.Report("delayed", SoftTimeoutStatusMessage); loadingState = BuildDelayedLoadingState( loadingState, SoftTimeoutStatusMessage, SoftTimeoutDetailsMessage, trackedAttempt.StartedAtUtc); loadingDetailsWindow?.UpdateLoadingState(loadingState); } launchOutcome = HostLaunchOutcome.FromProcess( attachedProcess!, BuildResult( true, "launchHost", "attached_attempt", "Attached to an existing startup attempt.", BuildAttemptDetails( trackedAttempt, attachedToExistingAttempt, ipcConnected, hostProcessAlive: true, lastStage, lastStageMessage, activationFailureReason, softTimeoutShown, recoveryActivationAttempted: false)), BuildAttemptDetails( trackedAttempt, attachedToExistingAttempt, ipcConnected, hostProcessAlive: true, lastStage, lastStageMessage, activationFailureReason, softTimeoutShown, recoveryActivationAttempted: false)); } else { launchOutcome = await LaunchHostWithIpcAsync().ConfigureAwait(false); } if (!launchOutcome.Result.Success) { return WithAdditionalDetails(launchOutcome.Result, launcherContextDetails); } if (launchOutcome.ImmediateResult is not null) { await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return WithAdditionalDetails(launchOutcome.ImmediateResult, launcherContextDetails); } if (launchOutcome.Process is null) { return BuildResult( success: false, stage: "launch", code: "host_start_failed", message: "Host launch did not create a process.", details: MergeDetails( launcherContextDetails, MergeDetails( launchOutcome.Details, BuildAttemptDetails( trackedAttempt, attachedToExistingAttempt, ipcConnected, hostProcessAlive: false, lastStage, lastStageMessage, activationFailureReason, softTimeoutShown, recoveryActivationAttempted: false)))); } if (!attachedToExistingAttempt) { var reservedAttempt = _startupAttemptRegistry.GetOwnedAttempt(); trackedAttempt = reservedAttempt is { ReservedBeforeHostStart: true } ? _startupAttemptRegistry.AssignOwnedHostProcess( launchOutcome.Process.Id, lastStage, lastStageMessage) : _startupAttemptRegistry.StartOwnedAttempt( launchOutcome.Process.Id, _context.LaunchSource, startupSuccessTracker.PolicyKey, lastStage, lastStageMessage); PublishCoordinatorStatus(hostProcessAliveOverride: true); } Dictionary ComposeLaunchDetails(bool hostProcessAlive, bool recoveryActivationAttempted = false) { return MergeDetails( launcherContextDetails, MergeDetails( launchOutcome.Details, BuildAttemptDetails( trackedAttempt, attachedToExistingAttempt, ipcConnected, hostProcessAlive, lastStage, lastStageMessage, activationFailureReason, softTimeoutShown, recoveryActivationAttempted))); } async Task RefreshShellStatusAsync(string waitingMessage) { if (!ipcClient.IsConnected) { return null; } ipcConnected = true; _startupAttemptRegistry.MarkOwnedIpcConnected(); shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false); if (startupSuccessTracker.TryResolve(shellStatus, out var successState)) { return successState; } if (shellStatus is { DesktopVisible: false }) { _startupAttemptRegistry.MarkOwnedWaitingForShell(waitingMessage); } PublishCoordinatorStatus(hostProcessAliveOverride: true); return null; } var connected = await TryConnectToPublicIpcAsync(ipcClient, TimeSpan.FromMilliseconds(1200)).ConfigureAwait(false); if (!connected) { Logger.Info("Host public IPC is not ready yet. Launcher will keep monitoring the host process and retry."); } else { var shellSuccess = await RefreshShellStatusAsync("Host public IPC is ready; waiting for desktop shell.") .ConfigureAwait(false); if (shellSuccess is not null) { successTcs.TrySetResult(shellSuccess); } } var processExitTask = launchOutcome.Process.WaitForExitAsync(); var startedAt = trackedAttempt?.StartedAtUtc ?? DateTimeOffset.UtcNow; var softTimeoutAt = startedAt + StartupSoftTimeout; var hardTimeoutAt = startedAt + StartupHardTimeout; var nextReconnectAttemptAt = DateTimeOffset.UtcNow.AddSeconds(2); var nextShellStatusPollAt = DateTimeOffset.UtcNow.AddSeconds(1); var activationRetryAttempted = false; while (true) { if (successTcs.Task.IsCompleted) { var successState = await successTcs.Task.ConfigureAwait(false); windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedSucceeded(successState.Stage, successState.Message); PublishCoordinatorStatus(!launchOutcome.Process.HasExited, completed: true, succeeded: true); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: successState.Code, message: successState.Message, details: ComposeLaunchDetails(!launchOutcome.Process.HasExited)); } if (activationFailedTcs.Task.IsCompleted && !activationRetryAttempted) { activationRetryAttempted = true; activationFailureReason = await activationFailedTcs.Task.ConfigureAwait(false); Logger.Warn($"Activation failure received before startup success. Reason='{activationFailureReason}'."); var activationRecovery = await TryRecoverActivationThroughExistingHostAsync( ipcClient, startupSuccessTracker, TimeSpan.FromSeconds(1)).ConfigureAwait(false); if (activationRecovery is not null) { windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedSucceeded(activationRecovery.Stage, activationRecovery.Message); PublishCoordinatorStatus( hostProcessAliveOverride: !launchOutcome.Process.HasExited, completed: true, succeeded: true); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: activationRecovery.Code, message: activationRecovery.Message, details: ComposeLaunchDetails( !launchOutcome.Process.HasExited, recoveryActivationAttempted: true)); } Logger.Info("Activation failure did not recover through public IPC yet. Launcher will keep monitoring the current host attempt."); } if (processExitTask.IsCompleted) { var exitCode = launchOutcome.Process.ExitCode; Logger.Warn($"Host exited before startup success criteria were met. ExitCode={exitCode}."); windowsClosingByCoordinator = true; if (IsSuccessfulActivationExitCode(exitCode)) { _startupAttemptRegistry.MarkOwnedSucceeded(StartupStage.ActivationRedirected, "Host redirected activation to the existing desktop instance."); PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: true); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: "activation_redirected", message: "Host redirected activation to the existing desktop instance.", details: MergeDetails( ComposeLaunchDetails(hostProcessAlive: false), new Dictionary(StringComparer.OrdinalIgnoreCase) { ["exitCode"] = exitCode.ToString() })); } if (!activationRetryAttempted && IsFailedActivationExitCode(exitCode)) { activationRetryAttempted = true; var activationRecovery = await TryRecoverActivationThroughExistingHostAsync( ipcClient, startupSuccessTracker, TimeSpan.FromSeconds(2)).ConfigureAwait(false); if (activationRecovery is not null) { _startupAttemptRegistry.MarkOwnedSucceeded(activationRecovery.Stage, activationRecovery.Message); PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: true, succeeded: true); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: activationRecovery.Code, message: activationRecovery.Message, details: MergeDetails( ComposeLaunchDetails(hostProcessAlive: true, recoveryActivationAttempted: true), new Dictionary(StringComparer.OrdinalIgnoreCase) { ["exitCode"] = exitCode.ToString() })); } Logger.Info("Activation exit code did not recover through public IPC. Launcher will report the activation failure without launching another host."); } _startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason); PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: false); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: false, stage: "launch", code: IsFailedActivationExitCode(exitCode) ? "activation_failed" : "host_exited_early", message: IsFailedActivationExitCode(exitCode) ? $"Host activation handshake failed before the required startup state was reported. ExitCode={exitCode}." : $"Host exited before the required startup state was reported. ExitCode={exitCode}.", details: MergeDetails( ComposeLaunchDetails(hostProcessAlive: false), new Dictionary(StringComparer.OrdinalIgnoreCase) { ["exitCode"] = exitCode.ToString() })); } var now = DateTimeOffset.UtcNow; if (ipcConnected && !launchOutcome.Process.HasExited && now >= nextShellStatusPollAt) { var shellSuccess = await RefreshShellStatusAsync("Host public IPC is ready; waiting for desktop shell.") .ConfigureAwait(false); if (shellSuccess is not null) { successTcs.TrySetResult(shellSuccess); continue; } nextShellStatusPollAt = DateTimeOffset.UtcNow.AddSeconds(1); } if (!ipcConnected && !launchOutcome.Process.HasExited && now >= nextReconnectAttemptAt) { connected = await TryConnectToPublicIpcAsync(ipcClient, TimeSpan.FromMilliseconds(800)).ConfigureAwait(false); if (connected) { var shellSuccess = await RefreshShellStatusAsync("Host public IPC reconnected; waiting for desktop shell.") .ConfigureAwait(false); if (shellSuccess is not null) { successTcs.TrySetResult(shellSuccess); continue; } } nextReconnectAttemptAt = DateTimeOffset.UtcNow.AddSeconds(2); } if (!softTimeoutShown && now >= softTimeoutAt && (!launchOutcome.Process.HasExited || ipcConnected)) { softTimeoutShown = true; _startupAttemptRegistry.MarkOwnedSoftTimeout(SoftTimeoutStatusMessage); reporter.Report("delayed", SoftTimeoutStatusMessage); loadingState = BuildDelayedLoadingState( loadingState, SoftTimeoutStatusMessage, SoftTimeoutDetailsMessage, trackedAttempt?.StartedAtUtc ?? startedAt); loadingDetailsWindow?.UpdateLoadingState(loadingState); PublishCoordinatorStatus(hostProcessAliveOverride: !launchOutcome.Process.HasExited); } if (now >= hardTimeoutAt) { break; } var nextCheckpointAt = hardTimeoutAt; if (!softTimeoutShown && softTimeoutAt < nextCheckpointAt) { nextCheckpointAt = softTimeoutAt; } var delay = nextCheckpointAt - now; if (delay > TimeSpan.FromSeconds(1)) { delay = TimeSpan.FromSeconds(1); } else if (delay < TimeSpan.FromMilliseconds(100)) { delay = TimeSpan.FromMilliseconds(100); } await Task.WhenAny( successTcs.Task, activationFailedTcs.Task, processExitTask, Task.Delay(delay)).ConfigureAwait(false); } var recoveryActivationAttempted = false; if (!connected && !launchOutcome.Process.HasExited) { connected = await TryConnectToPublicIpcAsync(ipcClient, TimeSpan.FromSeconds(1)).ConfigureAwait(false); if (connected) { var shellSuccess = await RefreshShellStatusAsync("Host public IPC is ready; waiting for desktop shell.") .ConfigureAwait(false); if (shellSuccess is not null) { windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedSucceeded(shellSuccess.Stage, shellSuccess.Message); PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: true, succeeded: true); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: shellSuccess.Code, message: shellSuccess.Message, details: ComposeLaunchDetails(hostProcessAlive: true)); } } } if (connected && !launchOutcome.Process.HasExited) { recoveryActivationAttempted = true; var recoveryOutcome = await TryRecoverWithPublicActivationAsync( ipcClient, launchOutcome.Process, successTcs.Task, startupSuccessTracker).ConfigureAwait(false); if (recoveryOutcome is not null) { windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedSucceeded(recoveryOutcome.Stage, recoveryOutcome.Message); shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false); PublishCoordinatorStatus(!launchOutcome.Process.HasExited, completed: true, succeeded: true); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: recoveryOutcome.Code, message: recoveryOutcome.Message, details: ComposeLaunchDetails( !launchOutcome.Process.HasExited, recoveryActivationAttempted: true)); } } if (connected && !launchOutcome.Process.HasExited) { windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedWaitingForShell("Host process is still running after the launcher wait window."); shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false); if (startupSuccessTracker.TryResolve(shellStatus, out var finalShellSuccess)) { _startupAttemptRegistry.MarkOwnedSucceeded(finalShellSuccess.Stage, finalShellSuccess.Message); PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: true, succeeded: true); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: finalShellSuccess.Code, message: finalShellSuccess.Message, details: ComposeLaunchDetails( hostProcessAlive: true, recoveryActivationAttempted)); } PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: true, succeeded: false); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: false, stage: "launch", code: "shell_not_ready", message: "Host public IPC is connected, but the desktop shell did not create or show the main window in time.", details: ComposeLaunchDetails( hostProcessAlive: true, recoveryActivationAttempted)); } if (!connected && !launchOutcome.Process.HasExited) { windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedWaitingForShell("Host process is still running, but public IPC is not ready yet."); PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: false, succeeded: true); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: true, stage: "launch", code: "startup_pending", message: "Host process is still running; Launcher will not start another process while public IPC finishes startup.", details: ComposeLaunchDetails( hostProcessAlive: true, recoveryActivationAttempted)); } windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason); PublishCoordinatorStatus(!launchOutcome.Process.HasExited, completed: true, succeeded: false); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( success: false, stage: "launch", code: "desktop_not_visible", message: $"Host process started, but it never reached the required startup state within {StartupHardTimeout.TotalSeconds:0} seconds.", details: ComposeLaunchDetails( !launchOutcome.Process.HasExited, recoveryActivationAttempted)); } finally { if (splashClosedHandler is not null) { splashWindow.Closed -= splashClosedHandler; } if (!windowsClosingByCoordinator) { await Dispatcher.UIThread.InvokeAsync(() => { try { if (splashWindow.IsVisible && splashWindow.IsLoaded) { splashWindow.Close(); Logger.Info("Splash window closed in coordinator cleanup."); } } catch (Exception ex) { Logger.Error("Failed to close splash window during coordinator cleanup.", ex); } }); } } } catch (Exception ex) { Logger.Error("Launcher coordinator failed.", ex); return BuildResult( success: false, stage: "launch", code: "exception", message: ex.Message, details: BuildLauncherContextDetails(_context, _oobeStateService.Evaluate(_context), _deploymentLocator.GetAppRoot()), errorMessage: ex.ToString()); } } private static async Task CloseWindowsAsync(SplashWindow splashWindow, LoadingDetailsWindow? loadingDetailsWindow) { try { await Dispatcher.UIThread.InvokeAsync(() => splashWindow.DismissAsync()); } catch (Exception ex) { Logger.Error("Failed to dismiss splash window.", ex); } await Dispatcher.UIThread.InvokeAsync(() => { try { if (loadingDetailsWindow is not null && loadingDetailsWindow.IsVisible) { loadingDetailsWindow.Close(); } } catch (Exception ex) { Logger.Error("Failed to close loading details window.", ex); } }); } private async Task LaunchHostWithIpcAsync(bool forceDirectMode = false, string? retryTag = null) { var resolution = _deploymentLocator.ResolveHostExecutable(_context); if (!resolution.Success || string.IsNullOrWhiteSpace(resolution.ResolvedHostPath)) { var (errorResult, selectedPath) = await ShowHostNotFoundErrorAsync().ConfigureAwait(false); if (errorResult == ErrorWindowResult.Retry) { if (!string.IsNullOrWhiteSpace(selectedPath) && File.Exists(selectedPath)) { return await LaunchHostWithExplicitPathAsync(selectedPath, forceDirectMode, retryTag).ConfigureAwait(false); } return await LaunchHostWithIpcAsync(forceDirectMode, retryTag).ConfigureAwait(false); } return HostLaunchOutcome.FromResult(BuildResult( success: false, stage: "launchHost", code: "host_not_found", message: "LanMountainDesktop host executable was not found.", details: BuildResolutionDetails(resolution, null, null, "resolve"))); } return await LaunchHostWithResolvedPathAsync(resolution, forceDirectMode, retryTag).ConfigureAwait(false); } private Task LaunchHostWithExplicitPathAsync(string hostPath, bool forceDirectMode, string? retryTag) { var resolution = new HostResolutionResult { Success = true, ResolvedHostPath = Path.GetFullPath(hostPath), ResolutionSource = "user_selected_path", AppRoot = _deploymentLocator.GetAppRoot(), ExplicitAppRoot = Path.GetDirectoryName(hostPath), SearchedPaths = [Path.GetFullPath(hostPath)] }; return LaunchHostWithResolvedPathAsync(resolution, forceDirectMode, retryTag); } private async Task LaunchHostWithResolvedPathAsync( HostResolutionResult resolution, bool forceDirectMode, string? retryTag) { var dataRoot = _dataLocationResolver.ResolveDataRoot(); var plan = HostLaunchPlanBuilder.Build(_context, _deploymentLocator, resolution, dataRoot); var hostPath = plan.HostPath; if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { EnsureExecutable(hostPath); } var primaryMode = HostStartMode.Direct; var fallbackMode = !forceDirectMode && OperatingSystem.IsWindows() ? HostStartMode.ShellExecute : (HostStartMode?)null; var firstAttempt = await StartHostProcessAsync(plan, primaryMode, retryTag).ConfigureAwait(false); if (firstAttempt.ProcessCreated && firstAttempt.Process is not null) { var firstDetails = BuildResolutionDetails(resolution, firstAttempt, null, null); return HostLaunchOutcome.FromProcess( firstAttempt.Process, BuildResult(true, "launchHost", "ok", "Host launched.", firstDetails), firstDetails); } if (fallbackMode is null) { return BuildOutcomeFromAttempt(resolution, firstAttempt, null); } Logger.Warn( $"Primary host start attempt failed. Retrying with fallback mode '{fallbackMode}'. " + $"FailureReason='{firstAttempt.FailureReason ?? "unknown"}'; ExitCode='{firstAttempt.ExitCode?.ToString() ?? ""}'."); var secondAttempt = await StartHostProcessAsync(plan, fallbackMode.Value, retryTag).ConfigureAwait(false); if (secondAttempt.ProcessCreated && secondAttempt.Process is not null) { var details = BuildResolutionDetails(resolution, firstAttempt, secondAttempt, null); return HostLaunchOutcome.FromProcess( secondAttempt.Process, BuildResult(true, "launchHost", "ok", "Host launched.", details), details); } return BuildOutcomeFromAttempt(resolution, secondAttempt, firstAttempt); } private static HostLaunchOutcome BuildOutcomeFromAttempt( HostResolutionResult resolution, HostStartAttempt finalAttempt, HostStartAttempt? previousAttempt) { var details = BuildResolutionDetails( resolution, previousAttempt ?? finalAttempt, previousAttempt is null ? null : finalAttempt, !finalAttempt.ProcessCreated ? "start" : finalAttempt.ExitCode is int finalExitCode && IsFailedActivationExitCode(finalExitCode) ? "activation" : "early-exit"); if (!finalAttempt.ProcessCreated) { return HostLaunchOutcome.FromResult(BuildResult( false, "launchHost", "host_start_failed", $"Failed to start host using start mode '{finalAttempt.StartMode}'.", details)); } if (finalAttempt.ExitCode is not null && IsSuccessfulActivationExitCode(finalAttempt.ExitCode.Value)) { return HostLaunchOutcome.FromImmediateResult(BuildResult( true, "launch", "activation_redirected", "Launcher activation was redirected to the existing desktop instance.", details)); } if (finalAttempt.ExitCode is not null && IsFailedActivationExitCode(finalAttempt.ExitCode.Value)) { return HostLaunchOutcome.FromResult(BuildResult( false, "launch", "activation_failed", $"Host activation handshake failed using start mode '{finalAttempt.StartMode}'.", details)); } return HostLaunchOutcome.FromResult(BuildResult( false, "launchHost", "host_exited_early", $"Host exited early using start mode '{finalAttempt.StartMode}'.", details)); } private async Task StartHostProcessAsync( HostLaunchPlan plan, HostStartMode startMode, string? retryTag) { var startInfo = new ProcessStartInfo { FileName = plan.HostPath, WorkingDirectory = plan.WorkingDirectory, UseShellExecute = startMode == HostStartMode.ShellExecute }; if (startMode == HostStartMode.Direct) { foreach (var argument in plan.Arguments) { startInfo.ArgumentList.Add(argument); } foreach (var pair in plan.EnvironmentVariables) { startInfo.EnvironmentVariables[pair.Key] = pair.Value; } } else { startInfo.Arguments = HostLaunchPlanBuilder.FormatArgumentsForLog(plan.Arguments); } try { var process = Process.Start(startInfo); Logger.Info( $"Host launch requested. Mode='{startMode}'; RetryTag='{retryTag ?? ""}'; Path='{plan.HostPath}'; " + $"PackageRoot='{plan.PackageRoot}'; WorkingDir='{plan.WorkingDirectory}'; Pid={(process is null ? -1 : process.Id)}; " + $"Args='{HostLaunchPlanBuilder.FormatArgumentsForLog(plan.Arguments)}'."); if (process is null) { return HostStartAttempt.StartFailed(startMode, "process_start_returned_null", plan); } await Task.Yield(); return HostStartAttempt.Started(startMode, process, plan); } catch (Exception ex) { Logger.Error($"Host start failed. Mode='{startMode}'.", ex); return HostStartAttempt.StartFailed(startMode, ex.GetType().Name, plan); } } private async Task<(ErrorWindowResult Result, string? CustomPath)> ShowHostNotFoundErrorAsync() { ErrorWindow? errorWindow = null; await Dispatcher.UIThread.InvokeAsync(() => { try { errorWindow = new ErrorWindow(); errorWindow.ConfigureForHostNotFound(); errorWindow.SetErrorMessage("LanMountainDesktop host executable was not found."); errorWindow.Show(); Logger.Warn("Host not found. Showing error window."); } catch (Exception ex) { Logger.Error("Failed to show host-not-found error window.", ex); } }); if (errorWindow is null) { return (ErrorWindowResult.Exit, null); } ErrorWindowResult result; string? customPath; try { result = await errorWindow.WaitForChoiceAsync().ConfigureAwait(false); customPath = errorWindow.GetCustomHostPath(); Logger.Info($"Host-not-found window result='{result}'; HasCustomPath={!string.IsNullOrWhiteSpace(customPath)}."); } catch (Exception ex) { Logger.Error("Failed while waiting for host-not-found window result.", ex); result = ErrorWindowResult.Exit; customPath = null; } await Dispatcher.UIThread.InvokeAsync(() => { try { if (errorWindow.IsVisible && errorWindow.IsLoaded) { errorWindow.Close(); } } catch (Exception ex) { Logger.Error("Failed to close host-not-found error window.", ex); } }); return (result, customPath); } private async Task ShowMigrationPromptAsync(LegacyVersionInfo legacyInfo) { MigrationPromptWindow? migrationWindow = null; await Dispatcher.UIThread.InvokeAsync(() => { try { migrationWindow = new MigrationPromptWindow(); migrationWindow.SetLegacyInfo(legacyInfo); migrationWindow.Show(); } catch (Exception ex) { Logger.Error("Failed to show migration prompt window.", ex); } }); if (migrationWindow is null) { return MigrationResult.Skipped; } MigrationResult result; try { result = await migrationWindow.WaitForChoiceAsync().ConfigureAwait(false); } catch (Exception ex) { Logger.Error("Failed while waiting for migration prompt result.", ex); result = MigrationResult.Skipped; } await Dispatcher.UIThread.InvokeAsync(() => { try { if (migrationWindow.IsVisible && migrationWindow.IsLoaded) { migrationWindow.Close(); } } catch (Exception ex) { Logger.Error("Failed to close migration prompt window.", ex); } }); return result; } private static string MapStartupStageToSplashStage(StartupStage stage) => stage switch { StartupStage.Initializing => "initializing", StartupStage.LoadingSettings => "settings", StartupStage.LoadingPlugins => "plugins", StartupStage.TrayReady => "shell", StartupStage.InitializingUI => "ui", StartupStage.ShellInitialized => "shell", StartupStage.BackgroundReady => "ready", StartupStage.DesktopVisible => "ready", StartupStage.ActivationRedirected => "activation", StartupStage.ActivationFailed => "error", StartupStage.Ready => "ready", _ => "launch" }; private static LauncherResult BuildResult( bool success, string stage, string code, string message, Dictionary? details = null, string? errorMessage = null) { Logger.Info($"Launcher result prepared. Success={success}; Stage='{stage}'; Code='{code}'."); return new LauncherResult { Success = success, Stage = stage, Code = code, Message = message, ErrorMessage = errorMessage, Details = details ?? [] }; } private static LauncherResult WithAdditionalDetails(LauncherResult result, Dictionary details) { return new LauncherResult { Success = result.Success, Stage = result.Stage, Code = result.Code, Message = result.Message, CurrentVersion = result.CurrentVersion, TargetVersion = result.TargetVersion, RolledBackTo = result.RolledBackTo, Details = MergeDetails(details, result.Details), InstalledPackagePath = result.InstalledPackagePath, ManifestId = result.ManifestId, ManifestName = result.ManifestName, ErrorMessage = result.ErrorMessage }; } private static Dictionary BuildLauncherContextDetails( CommandContext context, OobeLaunchDecision oobeDecision, string appRoot) { return new Dictionary(StringComparer.OrdinalIgnoreCase) { ["command"] = context.Command, ["launchSource"] = context.LaunchSource, ["isGuiMode"] = context.IsGuiCommand.ToString(), ["isDebugMode"] = context.IsDebugMode.ToString(), ["isElevated"] = oobeDecision.IsElevated.ToString(), ["resolvedAppRoot"] = appRoot, ["oobeStatePath"] = oobeDecision.StatePath, ["oobeStateStatus"] = oobeDecision.Status.ToString(), ["oobeDecision"] = oobeDecision.ShouldShowOobe ? "show" : "skip", ["oobeSuppressionReason"] = oobeDecision.SuppressionReason, ["oobeResultCode"] = oobeDecision.ResultCode, ["userSid"] = oobeDecision.UserSid ?? string.Empty, ["usedLegacyOobeMarker"] = oobeDecision.UsedLegacyMarker.ToString(), ["migratedLegacyOobeMarker"] = oobeDecision.MigratedLegacyMarker.ToString(), ["oobeStateError"] = oobeDecision.ErrorMessage }; } private static Dictionary BuildResolutionDetails( HostResolutionResult resolution, HostStartAttempt? firstAttempt, HostStartAttempt? secondAttempt, string? failureStage) { var details = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["resolvedAppRoot"] = resolution.AppRoot, ["explicitAppRoot"] = resolution.ExplicitAppRoot ?? string.Empty, ["resolvedHostPath"] = resolution.ResolvedHostPath ?? string.Empty, ["resolutionSource"] = resolution.ResolutionSource ?? string.Empty, ["devModeConfigIgnored"] = resolution.DevModeConfigIgnored.ToString(), ["searchedPaths"] = string.Join(" | ", resolution.SearchedPaths), ["failureStage"] = failureStage ?? string.Empty }; if (firstAttempt is not null) { details["startMode"] = firstAttempt.StartMode.ToString(); details["processCreated"] = firstAttempt.ProcessCreated.ToString(); details["hostPid"] = firstAttempt.ProcessId?.ToString() ?? string.Empty; details["packageRoot"] = firstAttempt.PackageRoot ?? string.Empty; details["workingDirectory"] = firstAttempt.WorkingDirectory ?? string.Empty; details["arguments"] = firstAttempt.Arguments ?? string.Empty; details["firstAttemptFailureReason"] = firstAttempt.FailureReason ?? string.Empty; details["firstAttemptExitCode"] = firstAttempt.ExitCode?.ToString() ?? string.Empty; } if (secondAttempt is not null) { details["fallbackStartMode"] = secondAttempt.StartMode.ToString(); details["fallbackProcessCreated"] = secondAttempt.ProcessCreated.ToString(); details["fallbackHostPid"] = secondAttempt.ProcessId?.ToString() ?? string.Empty; details["fallbackPackageRoot"] = secondAttempt.PackageRoot ?? string.Empty; details["fallbackWorkingDirectory"] = secondAttempt.WorkingDirectory ?? string.Empty; details["fallbackArguments"] = secondAttempt.Arguments ?? string.Empty; details["fallbackFailureReason"] = secondAttempt.FailureReason ?? string.Empty; details["fallbackExitCode"] = secondAttempt.ExitCode?.ToString() ?? string.Empty; } return details; } private static Dictionary MergeDetails( Dictionary left, Dictionary right) { var merged = new Dictionary(left, StringComparer.OrdinalIgnoreCase); foreach (var pair in right) { merged[pair.Key] = pair.Value; } return merged; } private static void EnsureExecutable(string path) { if (OperatingSystem.IsWindows()) { return; } try { var mode = File.GetUnixFileMode(path); mode |= UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; File.SetUnixFileMode(path, mode); } catch { } } private static async Task TryConnectToPublicIpcAsync( LanMountainDesktopIpcClient ipcClient, TimeSpan timeout) { if (ipcClient.IsConnected) { return true; } try { var connectTask = ipcClient.ConnectAsync(); var completedTask = await Task.WhenAny(connectTask, Task.Delay(timeout)).ConfigureAwait(false); if (completedTask != connectTask) { return false; } await connectTask.ConfigureAwait(false); return ipcClient.IsConnected; } catch (Exception ex) { Logger.Info($"Public IPC is not ready yet: {ex.Message}"); return false; } } internal static bool ShouldProbeExistingHostBeforeLaunch(CommandContext context) { if (!string.Equals(context.Command, "launch", StringComparison.OrdinalIgnoreCase)) { return false; } if (context.IsPreviewCommand || context.IsMaintenanceCommand) { return false; } return !string.Equals(context.LaunchSource, "restart", StringComparison.OrdinalIgnoreCase); } private MultiInstanceLaunchBehavior LoadMultiInstanceLaunchBehavior() { try { var settingsPath = HostAppSettingsOobeMerger.GetSettingsFilePath(_dataLocationResolver.ResolveDataRoot()); return HostAppSettingsOobeMerger.LoadMultiInstanceLaunchBehavior(settingsPath); } catch (Exception ex) { Logger.Warn($"Failed to load multi-instance launch behavior. Falling back to default. {ex.Message}"); return MultiInstanceLaunchBehavior.NotifyAndOpenDesktop; } } internal static bool IsExistingHostReadyForLauncherDecision(PublicShellStatus? status) { return status is { PublicIpcReady: true, ProcessId: > 0 }; } private static async Task TryGetExistingHostStatusAsync( LanMountainDesktopIpcClient ipcClient, TimeSpan timeout) { try { var connected = ipcClient.IsConnected || await TryConnectToPublicIpcAsync(ipcClient, timeout).ConfigureAwait(false); if (!connected) { return null; } var shellProxy = ipcClient.CreateProxy(); return await shellProxy.GetShellStatusAsync().ConfigureAwait(false); } catch (Exception ex) { Logger.Info($"Existing host status probe did not complete: {ex.Message}"); return null; } } private static async Task ApplyExistingHostBehaviorAsync( LanMountainDesktopIpcClient ipcClient, MultiInstanceLaunchBehavior behavior, PublicShellStatus status) { try { var shellProxy = ipcClient.CreateProxy(); return behavior switch { MultiInstanceLaunchBehavior.OpenDesktopSilently => await ActivateExistingHostForBehaviorAsync( shellProxy, showLauncherNotice: false, successCode: "existing_host_activated", successMessage: "Launcher activated the existing desktop instance.", failureCode: "existing_host_activation_failed").ConfigureAwait(false), MultiInstanceLaunchBehavior.NotifyAndOpenDesktop => await ActivateExistingHostForBehaviorAsync( shellProxy, showLauncherNotice: true, successCode: "existing_host_activated_with_notice", successMessage: "Launcher activated the existing desktop instance and showed the repeated-launch notice.", failureCode: "existing_host_activation_failed").ConfigureAwait(false), MultiInstanceLaunchBehavior.PromptOnly => await ShowPromptOnlyExistingHostAsync( shellProxy, status).ConfigureAwait(false), MultiInstanceLaunchBehavior.RestartApp => await RestartExistingHostAsync(shellProxy).ConfigureAwait(false), _ => await ActivateExistingHostForBehaviorAsync( shellProxy, showLauncherNotice: true, successCode: "existing_host_activated_with_notice", successMessage: "Launcher activated the existing desktop instance and showed the repeated-launch notice.", failureCode: "existing_host_activation_failed").ConfigureAwait(false) }; } catch (Exception ex) { Logger.Warn($"Failed to apply multi-instance behavior '{behavior}': {ex.Message}"); return new ExistingHostBehaviorResult( false, "multi_instance_behavior_failed", $"Failed to apply multi-instance behavior '{behavior}': {ex.Message}", null); } } private static async Task ActivateExistingHostForBehaviorAsync( IPublicShellControlService shellProxy, bool showLauncherNotice, string successCode, string successMessage, string failureCode) { var activation = await shellProxy.ActivateMainWindowWithStatusAsync().ConfigureAwait(false); var success = activation.Accepted || IsRecoverableActivationFailure(activation); if (showLauncherNotice && success) { var promptResult = await ShowMultiInstancePromptAsync(activation.Status).ConfigureAwait(false); if (promptResult == MultiInstancePromptResult.OpenDesktop) { activation = await shellProxy.ActivateMainWindowWithStatusAsync().ConfigureAwait(false); } } return new ExistingHostBehaviorResult( success, activation.Accepted ? successCode : success ? "existing_host_startup_pending" : failureCode, activation.Accepted ? successMessage : activation.Message, activation); } private static async Task RestartExistingHostAsync( IPublicShellControlService shellProxy) { var accepted = await shellProxy.RestartAsync().ConfigureAwait(false); return new ExistingHostBehaviorResult( accepted, accepted ? "existing_host_restart_requested" : "existing_host_restart_failed", accepted ? "Launcher requested the existing desktop instance to restart." : "Launcher could not request restart from the existing desktop instance.", null); } private static async Task ShowPromptOnlyExistingHostAsync( IPublicShellControlService shellProxy, PublicShellStatus status) { var promptResult = await ShowMultiInstancePromptAsync(status).ConfigureAwait(false); if (promptResult == MultiInstancePromptResult.OpenDesktop) { return await ActivateExistingHostForBehaviorAsync( shellProxy, showLauncherNotice: false, successCode: "existing_host_activated_from_prompt", successMessage: "Launcher activated the existing desktop instance from the prompt.", failureCode: "existing_host_activation_failed").ConfigureAwait(false); } return new ExistingHostBehaviorResult( true, "existing_host_prompt_only", "Launcher showed the repeated-launch prompt and did not open the desktop automatically.", null); } private static async Task ShowMultiInstancePromptAsync(PublicShellStatus status) { return await Dispatcher.UIThread.InvokeAsync(async () => { var prompt = new MultiInstancePromptWindow(); prompt.SetDetails(status.ProcessId, status.ShellState); prompt.Show(); return await prompt.WaitForChoiceAsync().ConfigureAwait(true); }); } private static async Task TryActivateExistingHostWithStatusAsync( LanMountainDesktopIpcClient ipcClient, TimeSpan timeout) { try { var connected = ipcClient.IsConnected || await TryConnectToPublicIpcAsync(ipcClient, timeout).ConfigureAwait(false); if (!connected) { return null; } var shellProxy = ipcClient.CreateProxy(); return await shellProxy.ActivateMainWindowWithStatusAsync().ConfigureAwait(false); } catch (Exception ex) { Logger.Info($"Existing host activation probe did not complete: {ex.Message}"); return null; } } private static async Task TryRecoverActivationThroughExistingHostAsync( LanMountainDesktopIpcClient ipcClient, StartupSuccessTracker startupSuccessTracker, TimeSpan timeout) { var activation = await TryActivateExistingHostWithStatusAsync(ipcClient, timeout).ConfigureAwait(false); if (activation is null) { return null; } if (startupSuccessTracker.TryResolve(activation.Status, out var shellSuccess)) { return shellSuccess; } if (activation.Accepted) { return startupSuccessTracker.BuildRecoverySuccessState(); } return IsRecoverableActivationFailure(activation) ? new StartupSuccessState( StartupStage.Ready, "startup_pending", activation.Message) : null; } internal static bool IsRecoverableActivationFailure(PublicShellActivationResult activation) { if (activation.Accepted) { return false; } if (string.Equals(activation.Code, "shutdown_in_progress", StringComparison.OrdinalIgnoreCase)) { return false; } return activation.Status.PublicIpcReady && (!activation.Status.MainWindowOpened || !activation.Status.DesktopVisible || string.Equals(activation.Code, "shell_not_ready", StringComparison.OrdinalIgnoreCase) || string.Equals(activation.Code, "startup_pending", StringComparison.OrdinalIgnoreCase)); } internal static bool IsSuccessfulActivationExitCode(int exitCode) => exitCode == HostExitCodes.SecondaryActivationSucceeded; internal static bool IsFailedActivationExitCode(int exitCode) => exitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired; private static async Task TryGetPublicShellStatusAsync( LanMountainDesktopIpcClient ipcClient) { try { var shellProxy = ipcClient.CreateProxy(); return await shellProxy.GetShellStatusAsync().ConfigureAwait(false); } catch (Exception ex) { Logger.Warn($"Failed to query public shell status: {ex.Message}"); return null; } } private static async Task TryRecoverWithPublicActivationAsync( LanMountainDesktopIpcClient ipcClient, Process hostProcess, Task successTask, StartupSuccessTracker startupSuccessTracker) { try { var shellProxy = ipcClient.CreateProxy(); var activation = await shellProxy.ActivateMainWindowWithStatusAsync().ConfigureAwait(false); if (startupSuccessTracker.TryResolve(activation.Status, out var shellSuccess)) { return shellSuccess; } var completedTask = await Task.WhenAny(successTask, Task.Delay(TimeSpan.FromSeconds(5))).ConfigureAwait(false); if (completedTask == successTask) { return await successTask.ConfigureAwait(false); } if (!hostProcess.HasExited && (activation.Accepted || IsRecoverableActivationFailure(activation))) { return startupSuccessTracker.BuildRecoverySuccessState(); } } catch (Exception ex) { Logger.Warn($"Public activation recovery failed: {ex.Message}"); } return null; } private static LoadingStateMessage BuildDelayedLoadingState( LoadingStateMessage loadingState, string summaryMessage, string detailMessage, DateTimeOffset startedAtUtc) { var delayedItems = loadingState.ActiveItems .Where(item => !string.Equals(item.Id, "launcher-soft-timeout", StringComparison.OrdinalIgnoreCase)) .ToList(); delayedItems.Insert(0, new LoadingItem { Id = "launcher-soft-timeout", Type = LoadingItemType.System, Name = "Startup still in progress", Description = detailMessage, State = LoadingState.Delayed, ProgressPercent = Math.Max(loadingState.OverallProgressPercent, 1), Message = detailMessage, StartTime = startedAtUtc }); return loadingState with { ActiveItems = delayedItems, Message = summaryMessage, Timestamp = DateTimeOffset.UtcNow, TotalCount = Math.Max(loadingState.TotalCount, delayedItems.Count) }; } private static Dictionary BuildAttemptDetails( StartupAttemptRecord? trackedAttempt, bool attachedToExistingAttempt, bool ipcConnected, bool hostProcessAlive, StartupStage lastStage, string lastStageMessage, string? activationFailureReason, bool softTimeoutShown, bool recoveryActivationAttempted) { var details = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["hostProcessAlive"] = hostProcessAlive.ToString(), ["attachedToExistingAttempt"] = attachedToExistingAttempt.ToString(), ["ipcConnected"] = ipcConnected.ToString(), ["ipcStage"] = lastStage.ToString(), ["ipcMessage"] = lastStageMessage, ["activationFailureReason"] = activationFailureReason ?? string.Empty, ["softTimeoutShown"] = softTimeoutShown.ToString(), ["recoveryActivationAttempted"] = recoveryActivationAttempted.ToString() }; if (trackedAttempt is not null) { details["startupAttemptId"] = trackedAttempt.AttemptId; details["startupAttemptState"] = trackedAttempt.State.ToString(); details["startupAttemptStartedAtUtc"] = trackedAttempt.StartedAtUtc.ToString("O"); details["startupAttemptUpdatedAtUtc"] = trackedAttempt.UpdatedAtUtc.ToString("O"); details["startupAttemptHeartbeatAtUtc"] = trackedAttempt.HeartbeatAtUtc.ToString("O"); details["successPolicy"] = trackedAttempt.SuccessPolicy; details["hostPid"] = trackedAttempt.HostPid.ToString(); details["coordinatorPid"] = trackedAttempt.CoordinatorPid.ToString(); details["coordinatorPipeName"] = trackedAttempt.CoordinatorPipeName; details["reservedBeforeHostStart"] = trackedAttempt.ReservedBeforeHostStart.ToString(); details["publicIpcConnected"] = trackedAttempt.PublicIpcConnected.ToString(); details["shellStatus"] = trackedAttempt.ShellStatus; } return details; } private static bool TryGetLiveProcess(int processId, out Process? process) { process = null; if (processId <= 0) { return false; } try { process = Process.GetProcessById(processId); return !process.HasExited; } catch { process?.Dispose(); process = null; return false; } } private enum HostStartMode { ShellExecute, Direct } private sealed record HostStartAttempt( HostStartMode StartMode, bool ProcessCreated, Process? Process, bool ExitedEarly, int? ExitCode, string? FailureReason, string? PackageRoot, string? WorkingDirectory, string? Arguments) { public int? ProcessId => Process?.Id; public static HostStartAttempt Started(HostStartMode startMode, Process process, HostLaunchPlan plan) => new( startMode, true, process, false, null, null, plan.PackageRoot, plan.WorkingDirectory, HostLaunchPlanBuilder.FormatArgumentsForLog(plan.Arguments)); public static HostStartAttempt EarlyExit(HostStartMode startMode, Process process, int exitCode, HostLaunchPlan plan) => new( startMode, true, process, true, exitCode, null, plan.PackageRoot, plan.WorkingDirectory, HostLaunchPlanBuilder.FormatArgumentsForLog(plan.Arguments)); public static HostStartAttempt StartFailed(HostStartMode startMode, string failureReason, HostLaunchPlan? plan = null) => new( startMode, false, null, false, null, failureReason, plan?.PackageRoot, plan?.WorkingDirectory, plan is null ? null : HostLaunchPlanBuilder.FormatArgumentsForLog(plan.Arguments)); } private sealed record ExistingHostBehaviorResult( bool Success, string Code, string Message, PublicShellActivationResult? ActivationResult); private sealed record HostLaunchOutcome( LauncherResult Result, Process? Process, LauncherResult? ImmediateResult, Dictionary Details) { public static HostLaunchOutcome FromResult(LauncherResult result) => new(result, null, result.Success ? result : null, result.Details); public static HostLaunchOutcome FromImmediateResult(LauncherResult result) => new(result, null, result, result.Details); public static HostLaunchOutcome FromProcess(Process process, LauncherResult result, Dictionary details) => new(result, process, null, details); } private sealed class StartupSuccessTracker { private readonly LaunchSuccessPolicy _policy; private bool _trayReady; private bool _backgroundReady; public string PolicyKey => _policy.ToString(); public StartupSuccessTracker(CommandContext context) { var restartPresentation = LauncherRuntimeMetadata.GetRestartPresentationMode(context.RawArgs); var isRestartLaunch = string.Equals(context.LaunchSource, "restart", StringComparison.OrdinalIgnoreCase); _policy = !isRestartLaunch ? LaunchSuccessPolicy.Foreground : restartPresentation switch { RestartPresentationMode.Tray => LaunchSuccessPolicy.RestartTray, RestartPresentationMode.Minimized => LaunchSuccessPolicy.RestartBackground, _ => LaunchSuccessPolicy.Foreground }; } public bool TryResolve(StartupStage stage, out StartupSuccessState successState) { switch (stage) { case StartupStage.ActivationRedirected: successState = new StartupSuccessState( stage, "activation_redirected", "Launcher activation was redirected to the existing desktop instance."); return true; case StartupStage.DesktopVisible: successState = new StartupSuccessState( stage, _policy == LaunchSuccessPolicy.Foreground ? "ok" : "desktop_visible_fallback", _policy == LaunchSuccessPolicy.Foreground ? "Desktop is visible and ready." : "Desktop recovered in a visible state."); return true; case StartupStage.Ready: successState = new StartupSuccessState( stage, _policy == LaunchSuccessPolicy.Foreground ? "ready" : "background_ready", "Desktop reported that startup is ready."); return true; case StartupStage.TrayReady: _trayReady = true; break; case StartupStage.BackgroundReady: _backgroundReady = true; break; } if (_policy == LaunchSuccessPolicy.RestartBackground && _backgroundReady) { successState = new StartupSuccessState( StartupStage.BackgroundReady, "background_ready", "Desktop restart completed in the background."); return true; } if (_policy == LaunchSuccessPolicy.RestartTray && _trayReady && _backgroundReady) { successState = new StartupSuccessState( StartupStage.BackgroundReady, "background_ready", "Desktop restart completed with tray recovery ready."); return true; } successState = default!; return false; } public bool TryResolve(PublicShellStatus? status, out StartupSuccessState successState) { if (status is not null && (status.DesktopVisible || status.MainWindowVisible || status.MainWindowOpened)) { successState = new StartupSuccessState( status.DesktopVisible || status.MainWindowVisible ? StartupStage.DesktopVisible : StartupStage.Ready, _policy == LaunchSuccessPolicy.Foreground ? "ok" : "background_ready", status.DesktopVisible || status.MainWindowVisible ? "Desktop shell is visible and ready." : "Desktop shell window has opened."); return true; } successState = default!; return false; } public StartupSuccessState BuildRecoverySuccessState() { return _policy switch { LaunchSuccessPolicy.RestartTray => new StartupSuccessState( StartupStage.DesktopVisible, "recovery_activation_requested", "Launcher requested a visible recovery because the background restart never confirmed tray readiness."), LaunchSuccessPolicy.RestartBackground => new StartupSuccessState( StartupStage.DesktopVisible, "recovery_activation_requested", "Launcher requested a visible recovery because the background restart never confirmed readiness."), _ => new StartupSuccessState( StartupStage.DesktopVisible, "recovery_activation_requested", "Launcher requested a visible recovery from the running desktop instance.") }; } } private sealed record StartupSuccessState( StartupStage Stage, string Code, string Message); private enum LaunchSuccessPolicy { Foreground, RestartBackground, RestartTray } }