2026-05-28 11:03:49 +08:00
|
|
|
using System.Diagnostics;
|
|
|
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
|
|
|
using Avalonia.Threading;
|
|
|
|
|
using LanMountainDesktop.Launcher.Models;
|
|
|
|
|
using LanMountainDesktop.Launcher.Resources;
|
|
|
|
|
using LanMountainDesktop.Launcher.Views;
|
|
|
|
|
using LanMountainDesktop.Shared.Contracts.Launcher;
|
|
|
|
|
using LanMountainDesktop.Shared.IPC;
|
|
|
|
|
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
|
|
|
|
|
|
|
|
namespace LanMountainDesktop.Launcher.Shell;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Launcher GUI 入口装配:创建编排器并驱动启动流程。
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal static class LauncherCompositionRoot
|
|
|
|
|
{
|
|
|
|
|
public static LauncherOrchestrator CreateOrchestrator(
|
|
|
|
|
CommandContext context,
|
|
|
|
|
string appRoot,
|
|
|
|
|
StartupAttemptRegistry startupAttemptRegistry,
|
2026-05-28 11:13:14 +08:00
|
|
|
LauncherCoordinatorIpcServer coordinatorServer) =>
|
|
|
|
|
LauncherServiceRegistration.CreateOrchestrator(context, startupAttemptRegistry, coordinatorServer);
|
2026-05-28 11:03:49 +08:00
|
|
|
|
|
|
|
|
public static async Task RunOrchestratorWithSplashAsync(
|
|
|
|
|
IClassicDesktopStyleApplicationLifetime desktop,
|
|
|
|
|
CommandContext context,
|
|
|
|
|
SplashWindow splashWindow)
|
|
|
|
|
{
|
|
|
|
|
LauncherResult result;
|
|
|
|
|
SplashWindow? currentSplashWindow = splashWindow;
|
|
|
|
|
var appRoot = Commands.ResolveAppRoot(context);
|
|
|
|
|
var dataLocationResolver = new DataLocationResolver(appRoot);
|
|
|
|
|
var startupAttemptRegistry = new StartupAttemptRegistry();
|
|
|
|
|
var coordinatorPipeName = LauncherCoordinatorIpcServer.CreatePipeName();
|
|
|
|
|
var successPolicy = LauncherOrchestrator.ResolveSuccessPolicyKey(context);
|
|
|
|
|
if (!startupAttemptRegistry.TryReserveCoordinator(
|
|
|
|
|
context.LaunchSource,
|
|
|
|
|
successPolicy,
|
|
|
|
|
coordinatorPipeName,
|
|
|
|
|
out var reservedAttempt,
|
|
|
|
|
out var activeCoordinatorAttempt))
|
|
|
|
|
{
|
|
|
|
|
result = await AttachToExistingCoordinatorAsync(
|
|
|
|
|
context,
|
|
|
|
|
currentSplashWindow,
|
|
|
|
|
activeCoordinatorAttempt).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
Logger.Info($"Secondary launcher completed. Success={result.Success}; Code='{result.Code}'.");
|
|
|
|
|
await WriteLauncherResultAsync(context, result).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
Environment.ExitCode = result.Success ? 0 : 1;
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using var airAppIpcHost = new LauncherAirAppLifecycleIpcHost(
|
|
|
|
|
new LauncherAirAppLifecycleService(
|
|
|
|
|
new AirAppProcessStarter(
|
|
|
|
|
new AirAppHostLocator(),
|
|
|
|
|
() => appRoot,
|
|
|
|
|
() => null,
|
|
|
|
|
() => dataLocationResolver.ResolveDataRoot())));
|
|
|
|
|
airAppIpcHost.Start();
|
|
|
|
|
|
|
|
|
|
using var coordinatorServer = new LauncherCoordinatorIpcServer(
|
|
|
|
|
coordinatorPipeName,
|
|
|
|
|
BuildCoordinatorStatusFromAttempt(reservedAttempt),
|
|
|
|
|
HandleCoordinatorRequestAsync,
|
|
|
|
|
startupAttemptRegistry.UpdateOwnedCoordinatorHeartbeat);
|
|
|
|
|
coordinatorServer.Start();
|
|
|
|
|
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Logger.Info(
|
|
|
|
|
$"Coordinator start. Command='{context.Command}'; AppRoot='{appRoot}'; " +
|
|
|
|
|
$"IsDebugMode={context.IsDebugMode}; LaunchSource='{context.LaunchSource}'; " +
|
|
|
|
|
$"ResultPath='{context.GetOption("result") ?? "<none>"}'.");
|
|
|
|
|
|
|
|
|
|
var orchestrator = CreateOrchestrator(
|
|
|
|
|
context,
|
|
|
|
|
appRoot,
|
|
|
|
|
startupAttemptRegistry,
|
|
|
|
|
coordinatorServer);
|
|
|
|
|
|
|
|
|
|
result = await orchestrator.RunAsync(currentSplashWindow).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.Error("Coordinator threw an unhandled exception.", ex);
|
|
|
|
|
result = new LauncherResult
|
|
|
|
|
{
|
|
|
|
|
Success = false,
|
|
|
|
|
Stage = "launch",
|
|
|
|
|
Code = "exception",
|
|
|
|
|
Message = $"Launcher failed: {ex.Message}",
|
|
|
|
|
ErrorMessage = ex.ToString()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.Success ||
|
|
|
|
|
result.Code == "host_not_found" ||
|
|
|
|
|
(!string.Equals(result.Stage, "launch", StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
!string.Equals(result.Stage, "launchHost", StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var failureAction = await ShowFailureWindowAsync(result).ConfigureAwait(false);
|
|
|
|
|
if (failureAction == ErrorWindowResult.Exit)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (failureAction == ErrorWindowResult.ActivateExisting &&
|
|
|
|
|
await TryActivateExistingInstanceAsync().ConfigureAwait(false))
|
|
|
|
|
{
|
|
|
|
|
result = new LauncherResult
|
|
|
|
|
{
|
|
|
|
|
Success = true,
|
|
|
|
|
Stage = "launch",
|
|
|
|
|
Code = "activation_requested",
|
|
|
|
|
Message = "Launcher activated the existing desktop instance.",
|
|
|
|
|
Details = result.Details
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentSplashWindow = CreateSplashWindow();
|
|
|
|
|
currentSplashWindow.Show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Logger.Info($"Coordinator completed. Success={result.Success}; Stage='{result.Stage}'; Code='{result.Code}'.");
|
|
|
|
|
await WriteLauncherResultAsync(context, result).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
Environment.ExitCode = result.Success ? 0 : 1;
|
|
|
|
|
if (result.Success)
|
|
|
|
|
{
|
|
|
|
|
var hostPid = ResolveManagedHostPid(result, startupAttemptRegistry.GetOwnedAttempt()?.HostPid ?? 0);
|
|
|
|
|
await WaitForManagedProcessesToExitAsync(hostPid, airAppIpcHost.LifecycleService).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async Task RunApplyUpdateWithWindowAsync(
|
|
|
|
|
IClassicDesktopStyleApplicationLifetime desktop,
|
|
|
|
|
CommandContext context,
|
|
|
|
|
UpdateWindow window)
|
|
|
|
|
{
|
|
|
|
|
var appRoot = Commands.ResolveAppRoot(context);
|
|
|
|
|
var deploymentLocator = new DeploymentLocator(appRoot);
|
2026-05-28 11:13:14 +08:00
|
|
|
var updateEngine = new UpdateEngineFacade(deploymentLocator);
|
2026-05-28 11:03:49 +08:00
|
|
|
var pluginInstaller = new PluginInstallerService();
|
|
|
|
|
var pluginUpgrades = new PluginUpgradeQueueService(pluginInstaller);
|
|
|
|
|
|
|
|
|
|
var success = true;
|
|
|
|
|
string? errorMessage = null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => window.Report("verify", Strings.Update_Verifying, 10));
|
|
|
|
|
var updateResult = await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false);
|
|
|
|
|
if (!updateResult.Success)
|
|
|
|
|
{
|
|
|
|
|
success = false;
|
|
|
|
|
errorMessage = updateResult.Message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (success)
|
|
|
|
|
{
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => window.Report("plugins", Strings.Update_ApplyingPlugins, 60));
|
|
|
|
|
var pluginsDir = context.GetOption("plugins-dir") ?? Path.Combine(appRoot, "plugins");
|
|
|
|
|
var queueResult = pluginUpgrades.ApplyPendingUpgrades(pluginsDir);
|
|
|
|
|
if (!queueResult.Success && queueResult.Code != "noop")
|
|
|
|
|
{
|
|
|
|
|
Logger.Error($"Plugin upgrade failed during apply-update: {queueResult.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (success)
|
|
|
|
|
{
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", Strings.Update_CleaningUp, 90));
|
|
|
|
|
deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
success = false;
|
|
|
|
|
errorMessage = ex.Message;
|
|
|
|
|
Logger.Error("Apply-update flow failed.", ex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => window.ReportComplete(success, errorMessage));
|
|
|
|
|
await Task.Delay(success ? 1500 : 5000).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
await Commands.WriteResultIfNeededAsync(context.GetOption("result"), new LauncherResult
|
|
|
|
|
{
|
|
|
|
|
Success = success,
|
|
|
|
|
Stage = "apply-update",
|
|
|
|
|
Code = success ? "ok" : "failed",
|
|
|
|
|
Message = success ? "Update applied successfully." : (errorMessage ?? "Unknown error"),
|
|
|
|
|
Details = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
{
|
|
|
|
|
["command"] = context.Command,
|
|
|
|
|
["launchSource"] = context.LaunchSource
|
|
|
|
|
}
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
Environment.ExitCode = success ? 0 : 1;
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static SplashWindow CreateSplashWindow()
|
|
|
|
|
{
|
|
|
|
|
var window = new SplashWindow();
|
|
|
|
|
TrySetSplashVersionInfo(window, LauncherRuntimeContext.Current);
|
|
|
|
|
return window;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void TrySetSplashVersionInfo(SplashWindow window, CommandContext context)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var appRoot = Commands.ResolveAppRoot(context);
|
|
|
|
|
var versionInfo = new DeploymentLocator(appRoot).GetVersionInfo();
|
|
|
|
|
window.SetVersionInfo(versionInfo.Version, versionInfo.Codename);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.Warn($"Failed to set splash version info before coordinator start: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int ResolveManagedHostPid(LauncherResult result, int fallbackHostPid)
|
|
|
|
|
{
|
|
|
|
|
if (result.Details.TryGetValue("hostPid", out var hostPidText) &&
|
|
|
|
|
int.TryParse(hostPidText, out var hostPid))
|
|
|
|
|
{
|
|
|
|
|
return hostPid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.Details.TryGetValue("existingHostPid", out var existingHostPidText) &&
|
|
|
|
|
int.TryParse(existingHostPidText, out var existingHostPid))
|
|
|
|
|
{
|
|
|
|
|
return existingHostPid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fallbackHostPid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task WaitForManagedProcessesToExitAsync(
|
|
|
|
|
int hostPid,
|
|
|
|
|
LauncherAirAppLifecycleService airAppLifecycleService)
|
|
|
|
|
{
|
|
|
|
|
Logger.Info($"Launcher entering managed background lifetime. HostPid={hostPid}.");
|
|
|
|
|
while (TryGetLiveProcess(hostPid) || airAppLifecycleService.HasLiveAirApps())
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Logger.Info("Launcher managed background lifetime completed; no host or Air APP process remains.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<LauncherResult> AttachToExistingCoordinatorAsync(
|
|
|
|
|
CommandContext context,
|
|
|
|
|
SplashWindow? splashWindow,
|
|
|
|
|
StartupAttemptRecord? activeCoordinatorAttempt)
|
|
|
|
|
{
|
|
|
|
|
var reporter = splashWindow as ISplashStageReporter;
|
|
|
|
|
reporter?.Report("activation", Strings.Preview_ActivationConnecting);
|
|
|
|
|
|
|
|
|
|
if (activeCoordinatorAttempt is not null &&
|
|
|
|
|
!string.IsNullOrWhiteSpace(activeCoordinatorAttempt.CoordinatorPipeName))
|
|
|
|
|
{
|
|
|
|
|
var command = string.Equals(context.LaunchSource, "restart", StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
? LauncherCoordinatorCommands.Attach
|
|
|
|
|
: LauncherCoordinatorCommands.ActivateDesktop;
|
|
|
|
|
var request = new LauncherCoordinatorRequest
|
|
|
|
|
{
|
|
|
|
|
Command = command,
|
|
|
|
|
LaunchSource = context.LaunchSource,
|
|
|
|
|
SuccessPolicy = LauncherOrchestrator.ResolveSuccessPolicyKey(context)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var response = await new LauncherCoordinatorIpcClient()
|
|
|
|
|
.SendAsync(activeCoordinatorAttempt.CoordinatorPipeName, request, TimeSpan.FromSeconds(2))
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (response is not null)
|
|
|
|
|
{
|
|
|
|
|
reporter?.Report("activation", response.Message);
|
|
|
|
|
await DismissSplashIfNeededAsync(splashWindow).ConfigureAwait(false);
|
|
|
|
|
var success = response.Accepted ||
|
|
|
|
|
IsRecoverableActivationFailure(response.ActivationResult, response.Status);
|
|
|
|
|
return new LauncherResult
|
|
|
|
|
{
|
|
|
|
|
Success = success,
|
|
|
|
|
Stage = "launch",
|
|
|
|
|
Code = success && !response.Accepted ? "attached_to_launcher_coordinator" : response.Code,
|
|
|
|
|
Message = success && !response.Accepted
|
|
|
|
|
? "Attached to the active Launcher coordinator; desktop startup is still in progress."
|
|
|
|
|
: response.Message,
|
|
|
|
|
Details = BuildCoordinatorResultDetails(response.Status, response.ActivationResult)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var activation = await TryActivateExistingInstanceWithStatusAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
|
|
|
|
if (activation is not null)
|
|
|
|
|
{
|
|
|
|
|
reporter?.Report("activation", activation.Message);
|
|
|
|
|
await DismissSplashIfNeededAsync(splashWindow).ConfigureAwait(false);
|
|
|
|
|
var success = activation.Accepted || IsRecoverableActivationFailure(activation, null);
|
|
|
|
|
return new LauncherResult
|
|
|
|
|
{
|
|
|
|
|
Success = success,
|
|
|
|
|
Stage = "launch",
|
|
|
|
|
Code = activation.Accepted
|
|
|
|
|
? "existing_host_activated"
|
|
|
|
|
: success
|
|
|
|
|
? "existing_host_startup_pending"
|
|
|
|
|
: "existing_host_activation_failed",
|
|
|
|
|
Message = success && !activation.Accepted
|
|
|
|
|
? "Existing desktop process is still starting; Launcher attached without starting another process."
|
|
|
|
|
: activation.Message,
|
|
|
|
|
Details = BuildCoordinatorResultDetails(null, activation)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await DismissSplashIfNeededAsync(splashWindow).ConfigureAwait(false);
|
|
|
|
|
return new LauncherResult
|
|
|
|
|
{
|
|
|
|
|
Success = false,
|
|
|
|
|
Stage = "launch",
|
|
|
|
|
Code = "launcher_coordinator_unavailable",
|
|
|
|
|
Message = "Another Launcher is coordinating startup, but it did not respond in time.",
|
|
|
|
|
Details = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
{
|
|
|
|
|
["activeCoordinatorPid"] = activeCoordinatorAttempt?.CoordinatorPid.ToString() ?? string.Empty,
|
|
|
|
|
["activeCoordinatorPipeName"] = activeCoordinatorAttempt?.CoordinatorPipeName ?? string.Empty,
|
|
|
|
|
["activeAttemptId"] = activeCoordinatorAttempt?.AttemptId ?? string.Empty,
|
|
|
|
|
["activeHostPid"] = activeCoordinatorAttempt?.HostPid.ToString() ?? string.Empty
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<LauncherCoordinatorResponse> HandleCoordinatorRequestAsync(
|
|
|
|
|
LauncherCoordinatorRequest request,
|
|
|
|
|
LauncherCoordinatorStatus status)
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(request.Command, LauncherCoordinatorCommands.ActivateDesktop, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
var activation = await TryActivateExistingInstanceWithStatusAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
|
|
|
|
if (activation is not null)
|
|
|
|
|
{
|
|
|
|
|
if (!activation.Accepted && IsRecoverableActivationFailure(activation, status))
|
|
|
|
|
{
|
|
|
|
|
return new LauncherCoordinatorResponse
|
|
|
|
|
{
|
|
|
|
|
Accepted = true,
|
|
|
|
|
Code = "attached_to_launcher_coordinator",
|
|
|
|
|
Message = "Attached to the active Launcher coordinator; desktop startup is still in progress.",
|
|
|
|
|
Status = status,
|
|
|
|
|
ActivationResult = activation
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new LauncherCoordinatorResponse
|
|
|
|
|
{
|
|
|
|
|
Accepted = activation.Accepted,
|
|
|
|
|
Code = activation.Accepted ? "existing_host_activated" : "existing_host_activation_failed",
|
|
|
|
|
Message = activation.Message,
|
|
|
|
|
Status = status,
|
|
|
|
|
ActivationResult = activation
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new LauncherCoordinatorResponse
|
|
|
|
|
{
|
|
|
|
|
Accepted = true,
|
|
|
|
|
Code = "attached_to_launcher_coordinator",
|
|
|
|
|
Message = "Attached to the active Launcher coordinator; desktop startup is still in progress.",
|
|
|
|
|
Status = status
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new LauncherCoordinatorResponse
|
|
|
|
|
{
|
|
|
|
|
Accepted = true,
|
|
|
|
|
Code = "attached_to_launcher_coordinator",
|
|
|
|
|
Message = "Attached to the active Launcher coordinator.",
|
|
|
|
|
Status = status
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static LauncherCoordinatorStatus BuildCoordinatorStatusFromAttempt(StartupAttemptRecord attempt)
|
|
|
|
|
{
|
|
|
|
|
return new LauncherCoordinatorStatus
|
|
|
|
|
{
|
|
|
|
|
AttemptId = attempt.AttemptId,
|
|
|
|
|
CoordinatorPid = Environment.ProcessId,
|
|
|
|
|
HostPid = attempt.HostPid,
|
|
|
|
|
HostProcessAlive = TryGetLiveProcess(attempt.HostPid),
|
|
|
|
|
LaunchSource = attempt.LaunchSource,
|
|
|
|
|
SuccessPolicy = attempt.SuccessPolicy,
|
|
|
|
|
LastObservedStage = attempt.LastObservedStage,
|
|
|
|
|
LastObservedMessage = attempt.LastObservedMessage,
|
|
|
|
|
PublicIpcConnected = attempt.PublicIpcConnected || attempt.IpcConnected,
|
|
|
|
|
State = attempt.State.ToString(),
|
|
|
|
|
SoftTimeoutShown = attempt.State is StartupAttemptState.SoftTimeout or StartupAttemptState.DetachedWaiting,
|
|
|
|
|
Completed = attempt.State is StartupAttemptState.Succeeded or StartupAttemptState.Failed,
|
|
|
|
|
Succeeded = attempt.State == StartupAttemptState.Succeeded,
|
|
|
|
|
UpdatedAtUtc = attempt.UpdatedAtUtc
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsRecoverableActivationFailure(
|
|
|
|
|
PublicShellActivationResult? activation,
|
|
|
|
|
LauncherCoordinatorStatus? status)
|
|
|
|
|
{
|
|
|
|
|
if (activation is { Accepted: true })
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status is { Completed: false, HostProcessAlive: true })
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var shellStatus = activation?.Status;
|
|
|
|
|
if (shellStatus is null || !shellStatus.PublicIpcReady)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return !shellStatus.MainWindowOpened ||
|
|
|
|
|
!shellStatus.DesktopVisible ||
|
|
|
|
|
string.Equals(activation?.Code, "shell_not_ready", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(activation?.Code, "startup_pending", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Dictionary<string, string> BuildCoordinatorResultDetails(
|
|
|
|
|
LauncherCoordinatorStatus? status,
|
|
|
|
|
PublicShellActivationResult? activation)
|
|
|
|
|
{
|
|
|
|
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
{
|
|
|
|
|
["coordinatorPid"] = status?.CoordinatorPid.ToString() ?? string.Empty,
|
|
|
|
|
["coordinatorAttemptId"] = status?.AttemptId ?? string.Empty,
|
|
|
|
|
["hostPid"] = status?.HostPid.ToString() ?? activation?.Status.ProcessId.ToString() ?? string.Empty,
|
|
|
|
|
["hostProcessAlive"] = status?.HostProcessAlive.ToString() ?? string.Empty,
|
|
|
|
|
["publicIpcConnected"] = (status?.PublicIpcConnected ?? activation is not null).ToString(),
|
|
|
|
|
["startupStage"] = status?.LastObservedStage.ToString() ?? string.Empty,
|
|
|
|
|
["startupState"] = status?.State ?? string.Empty,
|
|
|
|
|
["activationAccepted"] = activation?.Accepted.ToString() ?? string.Empty,
|
|
|
|
|
["shellState"] = activation?.Status.ShellState ?? status?.ShellStatus?.ShellState ?? string.Empty,
|
|
|
|
|
["trayState"] = activation?.Status.Tray.State ?? status?.ShellStatus?.Tray.State ?? string.Empty,
|
|
|
|
|
["taskbarUsable"] = activation?.Status.Taskbar.IsUsable.ToString() ?? status?.ShellStatus?.Taskbar.IsUsable.ToString() ?? string.Empty
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task DismissSplashIfNeededAsync(SplashWindow? splashWindow)
|
|
|
|
|
{
|
|
|
|
|
if (splashWindow is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await splashWindow.DismissAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.Warn($"Failed to dismiss splash after coordinator attach: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task WriteLauncherResultAsync(CommandContext context, LauncherResult result)
|
|
|
|
|
{
|
|
|
|
|
var resultPath = context.GetOption("result");
|
|
|
|
|
if (string.IsNullOrWhiteSpace(resultPath))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await Commands.WriteResultIfNeededAsync(resultPath, result).ConfigureAwait(false);
|
|
|
|
|
Logger.Info($"Launcher result written to '{Path.GetFullPath(resultPath)}'.");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.Error($"Failed to write launcher result to '{resultPath}'.", ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<ErrorWindowResult> ShowFailureWindowAsync(LauncherResult result)
|
|
|
|
|
{
|
|
|
|
|
ErrorWindow? errorWindow = null;
|
|
|
|
|
var hostProcessAlive = result.Details.TryGetValue("hostProcessAlive", out var hostProcessAliveText) &&
|
|
|
|
|
bool.TryParse(hostProcessAliveText, out var hostProcessAliveValue) &&
|
|
|
|
|
hostProcessAliveValue;
|
|
|
|
|
var hostPid = result.Details.TryGetValue("hostPid", out var hostPidText) &&
|
|
|
|
|
int.TryParse(hostPidText, out var parsedPid)
|
|
|
|
|
? parsedPid
|
|
|
|
|
: (int?)null;
|
|
|
|
|
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
errorWindow = new ErrorWindow();
|
|
|
|
|
if (hostProcessAlive)
|
|
|
|
|
{
|
|
|
|
|
errorWindow.ConfigureForRunningHostFailure(hostPid);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
errorWindow.ConfigureForGenericFailure(allowRetry: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errorWindow.SetErrorMessage(
|
|
|
|
|
$"Failed to start LanMountainDesktop.\n\nStage: {result.Stage}\nCode: {result.Code}\n\n{result.Message}");
|
|
|
|
|
errorWindow.Show();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.Error("Failed to show launcher failure window.", ex);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (errorWindow is null)
|
|
|
|
|
{
|
|
|
|
|
return ErrorWindowResult.Exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await errorWindow.WaitForChoiceAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.Error("Failure window closed unexpectedly.", ex);
|
|
|
|
|
return ErrorWindowResult.Exit;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<bool> TryActivateExistingInstanceAsync()
|
|
|
|
|
{
|
|
|
|
|
var activation = await TryActivateExistingInstanceWithStatusAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
|
|
|
|
return activation?.Accepted == true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<PublicShellActivationResult?> TryActivateExistingInstanceWithStatusAsync(TimeSpan timeout)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using var ipcClient = new LanMountainDesktopIpcClient();
|
|
|
|
|
var connectTask = ipcClient.ConnectAsync();
|
|
|
|
|
var completedTask = await Task.WhenAny(connectTask, Task.Delay(timeout)).ConfigureAwait(false);
|
|
|
|
|
if (completedTask != connectTask)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await connectTask.ConfigureAwait(false);
|
|
|
|
|
if (!ipcClient.IsConnected)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var shellProxy = ipcClient.CreateProxy<IPublicShellControlService>();
|
|
|
|
|
var activationTask = shellProxy.ActivateMainWindowWithStatusAsync();
|
|
|
|
|
completedTask = await Task.WhenAny(activationTask, Task.Delay(timeout)).ConfigureAwait(false);
|
|
|
|
|
if (completedTask != activationTask)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await activationTask.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.Warn($"Failed to activate the existing desktop instance: {ex.Message}");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool TryGetLiveProcess(int processId)
|
|
|
|
|
{
|
|
|
|
|
if (processId <= 0)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using var process = Process.GetProcessById(processId);
|
|
|
|
|
return !process.HasExited;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|