diff --git a/LanMountainDesktop.Launcher/App.axaml.cs b/LanMountainDesktop.Launcher/App.axaml.cs index ce77610..78b4219 100644 --- a/LanMountainDesktop.Launcher/App.axaml.cs +++ b/LanMountainDesktop.Launcher/App.axaml.cs @@ -13,10 +13,13 @@ public partial class App : Application { public override void Initialize() { - // 初始化日志记录器 Logger.Initialize(); - Logger.Info("Launcher starting..."); - + var context = LauncherRuntimeContext.Current; + Logger.Info( + $"Launcher App initialize. Command='{context.Command}'; IsGuiMode={context.IsGuiCommand}; " + + $"IsPreview={context.IsPreviewCommand}; IsDebugMode={context.IsDebugMode}; " + + $"ExplicitAppRoot='{context.ExplicitAppRoot ?? ""}'."); + AvaloniaXamlLoader.Load(this); } @@ -24,41 +27,29 @@ public partial class App : Application { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var context = LauncherRuntimeContext.Current; + Logger.Info( + $"Framework initialization completed. Command='{context.Command}'; IsPreview={context.IsPreviewCommand}; " + + $"IsDebugMode={context.IsDebugMode}."); - // 调试模式:显示开发调试窗口 - if (context.IsDebugMode) - { - var devDebugWindow = new DevDebugWindow(); - devDebugWindow.Show(); - - // 调试模式下不自动启动正常流程,由开发者通过调试窗口控制 - base.OnFrameworkInitializationCompleted(); - return; - } - - // 处理各界面的预览命令 if (HandlePreviewCommand(context, desktop)) { base.OnFrameworkInitializationCompleted(); return; } - // apply-update 模式:显示 UpdateWindow,执行增量更新 + 插件升级 if (string.Equals(context.Command, "apply-update", StringComparison.OrdinalIgnoreCase)) { - // 先显示窗口,再启动后台任务 var updateWindow = new UpdateWindow(); updateWindow.Show(); _ = RunApplyUpdateWithWindowAsync(desktop, context, updateWindow); } else { - // 先显示 Splash 窗口,确保应用程序不会立即退出 var splashWindow = new SplashWindow(); splashWindow.Show(); - - // 在 try-catch 块中实例化所有服务,确保任何异常都能被捕获 _ = RunCoordinatorWithSplashAsync(desktop, context, splashWindow); } } @@ -66,156 +57,127 @@ public partial class App : Application base.OnFrameworkInitializationCompleted(); } - /// - /// 处理界面预览命令 - /// private bool HandlePreviewCommand(CommandContext context, IClassicDesktopStyleApplicationLifetime desktop) { - var command = context.Command.ToLowerInvariant(); - - switch (command) + switch (context.Command.ToLowerInvariant()) { case "preview-splash": - Console.WriteLine("[Launcher] Preview mode: SplashWindow"); + { + Logger.Info("Preview command: splash."); var splashWindow = new SplashWindow(); splashWindow.SetDebugMode(true); splashWindow.Show(); _ = SimulateSplashPreviewAsync(desktop, splashWindow); return true; - + } case "preview-error": - Console.WriteLine("[Launcher] Preview mode: ErrorWindow"); + { + Logger.Info("Preview command: error."); var errorWindow = new ErrorWindow(); - errorWindow.SetErrorMessage("[预览模式] 这是一个错误页面预览。\n\n用于查看错误页面的样式和布局。"); + errorWindow.SetErrorMessage("[Preview] This is the launcher error window preview."); errorWindow.Show(); _ = WaitForWindowCloseAsync(desktop, errorWindow); return true; - + } case "preview-update": - Console.WriteLine("[Launcher] Preview mode: UpdateWindow"); + { + Logger.Info("Preview command: update."); var updateWindow = new UpdateWindow(); updateWindow.SetDebugMode(true); updateWindow.Show(); _ = SimulateUpdatePreviewAsync(desktop, updateWindow); return true; - + } case "preview-oobe": - Console.WriteLine("[Launcher] Preview mode: OobeWindow"); + { + Logger.Info("Preview command: oobe."); var oobeWindow = new OobeWindow(); oobeWindow.Show(); _ = SimulateOobePreviewAsync(desktop, oobeWindow); return true; - + } case "preview-debug": - Console.WriteLine("[Launcher] Preview mode: DevDebugWindow"); + { + Logger.Info("Preview command: debug window."); var devDebugWindow = new DevDebugWindow(); devDebugWindow.Show(); return true; - + } default: return false; } } - /// - /// 模拟 Splash 窗口预览 - /// private async Task SimulateSplashPreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, SplashWindow window) { var stages = new[] { "initializing", "update", "plugins", "launch", "ready" }; - var messages = new[] { "初始化...", "检查更新...", "检查插件...", "正在启动...", "就绪" }; + var messages = new[] { "Initializing...", "Checking updates...", "Checking plugins...", "Launching host...", "Ready" }; var reporter = (ISplashStageReporter)window; - - for (int i = 0; i < stages.Length; i++) + + for (var i = 0; i < stages.Length; i++) { reporter.Report(stages[i], messages[i]); - await Task.Delay(800); + await Task.Delay(800).ConfigureAwait(false); } - - // 等待5秒后自动关闭 - await Task.Delay(5000); + + await Task.Delay(5000).ConfigureAwait(false); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0)); } - /// - /// 模拟 Update 窗口预览 - /// private async Task SimulateUpdatePreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, UpdateWindow window) { var stages = new[] { "verify", "extract", "apply", "plugins", "cleanup" }; - - for (int i = 0; i < stages.Length; i++) + + for (var i = 0; i < stages.Length; i++) { - window.Report(stages[i], $"正在{GetStageName(stages[i])}...", (i + 1) * 20); - await Task.Delay(600); + window.Report(stages[i], $"Processing {stages[i]}...", (i + 1) * 20); + await Task.Delay(600).ConfigureAwait(false); } - + window.ReportComplete(true, null); - - // 等待3秒后自动关闭 - await Task.Delay(3000); + await Task.Delay(3000).ConfigureAwait(false); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0)); - - string GetStageName(string stage) => stage switch - { - "verify" => "验证", - "extract" => "解压", - "apply" => "应用", - "plugins" => "升级插件", - "cleanup" => "清理", - _ => stage - }; } - /// - /// 模拟 OOBE 窗口预览 - /// private async Task SimulateOobePreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, OobeWindow window) { try { - // 等待用户点击开始按钮 - await window.WaitForEnterAsync(); - Console.WriteLine("[Launcher] OOBE preview completed by user"); + await window.WaitForEnterAsync().ConfigureAwait(false); + Logger.Info("OOBE preview completed by user."); } catch (Exception ex) { - Console.Error.WriteLine($"[Launcher] OOBE preview error: {ex.Message}"); + Logger.Error("OOBE preview failed.", ex); } - - // 用户点击后关闭应用程序 + await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0)); } - /// - /// 等待窗口关闭 - /// private async Task WaitForWindowCloseAsync(IClassicDesktopStyleApplicationLifetime desktop, Window window) { - var tcs = new TaskCompletionSource(); - window.Closed += (s, e) => tcs.TrySetResult(); - await tcs.Task; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + window.Closed += (_, _) => tcs.TrySetResult(); + await tcs.Task.ConfigureAwait(false); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0)); } - + private static async Task RunCoordinatorWithSplashAsync( IClassicDesktopStyleApplicationLifetime desktop, CommandContext context, SplashWindow splashWindow) { LauncherResult result; - ErrorWindow? errorWindow = null; - LauncherFlowCoordinator? coordinator = null; - + try { - // 在 try-catch 块中实例化所有服务,确保异常被捕获 var appRoot = Commands.ResolveAppRoot(context); + Logger.Info( + $"Coordinator start. Command='{context.Command}'; AppRoot='{appRoot}'; " + + $"IsDebugMode={context.IsDebugMode}; ResultPath='{context.GetOption("result") ?? ""}'."); + var deploymentLocator = new DeploymentLocator(appRoot); - - // TODO: 从配置读取 GitHub 仓库信息 - - coordinator = new LauncherFlowCoordinator( + var coordinator = new LauncherFlowCoordinator( context, deploymentLocator, new OobeStateService(appRoot), @@ -226,88 +188,85 @@ public partial class App : Application } catch (Exception ex) { - // 捕获异常并显示错误窗口 + Logger.Error("Coordinator threw an unhandled exception.", ex); result = new LauncherResult { Success = false, Stage = "launch", Code = "exception", - Message = $"启动器发生错误: {ex.Message}", + Message = $"Launcher failed: {ex.Message}", ErrorMessage = ex.ToString() }; - - Console.Error.WriteLine($"[Launcher] Exception caught: {ex}"); - - // 在 UI 线程显示错误窗口 - 使用更健壮的方式 - try - { - await Dispatcher.UIThread.InvokeAsync(() => - { - try - { - // 安全关闭 Splash 窗口 - if (splashWindow.IsVisible && splashWindow.IsLoaded) - { - splashWindow.Close(); - } - } - catch (Exception closeEx) - { - Console.Error.WriteLine($"[Launcher] Error closing splash window: {closeEx.Message}"); - } - - // 创建并显示错误窗口 - try - { - errorWindow = new ErrorWindow(); - errorWindow.SetErrorMessage($"启动器发生错误:\n{ex.Message}\n\n请检查应用安装是否完整,或尝试重新安装。"); - errorWindow.Show(); - Console.WriteLine("[Launcher] ErrorWindow shown successfully"); - } - catch (Exception windowEx) - { - Console.Error.WriteLine($"[Launcher] Failed to show ErrorWindow: {windowEx.Message}"); - } - }); - - // 如果错误窗口成功显示,等待它关闭 - if (errorWindow != null) - { - try - { - // 等待用户选择或窗口关闭 - var errorResult = await errorWindow.WaitForChoiceAsync(); - Console.WriteLine($"[Launcher] ErrorWindow result: {errorResult}"); - } - catch (Exception waitEx) - { - Console.Error.WriteLine($"[Launcher] Error waiting for ErrorWindow: {waitEx.Message}"); - // 如果等待失败,至少给用户5秒时间看到错误信息 - await Task.Delay(5000); - } - } - else - { - // 错误窗口未能显示,等待5秒让用户看到控制台输出 - await Task.Delay(5000); - } - } - catch (Exception uiEx) - { - // 最后的兜底:记录到控制台 - Console.Error.WriteLine($"[Launcher] Critical error in UI thread: {uiEx.Message}"); - await Task.Delay(3000); - } } - - await Commands.WriteResultIfNeededAsync(LauncherRuntimeContext.Current.GetOption("result"), result).ConfigureAwait(false); + + Logger.Info($"Coordinator completed. Success={result.Success}; Stage='{result.Stage}'; Code='{result.Code}'."); + await WriteLauncherResultAsync(context, result).ConfigureAwait(false); + + if (!result.Success && + result.Code is not "host_not_found" && + (string.Equals(result.Stage, "launch", StringComparison.OrdinalIgnoreCase) || + string.Equals(result.Stage, "launchHost", StringComparison.OrdinalIgnoreCase))) + { + await ShowFailureWindowAsync(result).ConfigureAwait(false); + } + Environment.ExitCode = result.Success ? 0 : 1; await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background); } - /// - /// apply-update 模式:执行增量更新和插件升级,完成后自动退出 - /// + 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 ShowFailureWindowAsync(LauncherResult result) + { + ErrorWindow? errorWindow = null; + + await Dispatcher.UIThread.InvokeAsync(() => + { + try + { + errorWindow = new ErrorWindow(); + 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; + } + + try + { + await errorWindow.WaitForChoiceAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error("Failure window closed unexpectedly.", ex); + } + } + private static async Task RunApplyUpdateWithWindowAsync( IClassicDesktopStyleApplicationLifetime desktop, CommandContext context, @@ -324,8 +283,7 @@ public partial class App : Application try { - // 1. 应用增量更新 - await Dispatcher.UIThread.InvokeAsync(() => window.Report("verify", "正在验证更新...", 10)); + await Dispatcher.UIThread.InvokeAsync(() => window.Report("verify", "Verifying update...", 10)); var updateResult = await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false); if (!updateResult.Success) { @@ -333,24 +291,20 @@ public partial class App : Application errorMessage = updateResult.Message; } - // 2. 应用待处理的插件升级 if (success) { - await Dispatcher.UIThread.InvokeAsync(() => window.Report("plugins", "正在升级插件...", 60)); - var pluginsDir = context.GetOption("plugins-dir") - ?? Path.Combine(appRoot, "plugins"); + await Dispatcher.UIThread.InvokeAsync(() => window.Report("plugins", "Applying plugin upgrades...", 60)); + var pluginsDir = context.GetOption("plugins-dir") ?? Path.Combine(appRoot, "plugins"); var queueResult = pluginUpgrades.ApplyPendingUpgrades(pluginsDir); if (!queueResult.Success && queueResult.Code != "noop") { - // 插件升级失败不阻断整体流程,仅记录到控制台 - Console.Error.WriteLine($"Plugin upgrade had failures: {queueResult.Message}"); + Logger.Error($"Plugin upgrade failed during apply-update: {queueResult.Message}"); } } - // 3. 清理旧版本,保留至少3个版本以支持回滚 if (success) { - await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", "正在清理...", 90)); + await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", "Cleaning up old deployments...", 90)); deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3); } } @@ -358,21 +312,11 @@ public partial class App : Application { success = false; errorMessage = ex.Message; + Logger.Error("Apply-update flow failed.", ex); } - // 显示完成状态,短暂停留后关闭 await Dispatcher.UIThread.InvokeAsync(() => window.ReportComplete(success, errorMessage)); - - if (success) - { - // 成功:停留 1.5 秒让用户看到"更新完成" - await Task.Delay(1500); - } - else - { - // 失败:停留 5 秒让用户看到错误信息 - await Task.Delay(5000); - } + await Task.Delay(success ? 1500 : 5000).ConfigureAwait(false); await Commands.WriteResultIfNeededAsync(context.GetOption("result"), new LauncherResult { @@ -385,6 +329,4 @@ public partial class App : Application Environment.ExitCode = success ? 0 : 1; await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background); } - - } diff --git a/LanMountainDesktop.Launcher/AppJsonContext.cs b/LanMountainDesktop.Launcher/AppJsonContext.cs index 26b229a..6d2380d 100644 --- a/LanMountainDesktop.Launcher/AppJsonContext.cs +++ b/LanMountainDesktop.Launcher/AppJsonContext.cs @@ -6,7 +6,10 @@ using LanMountainDesktop.Shared.Contracts.Launcher; namespace LanMountainDesktop.Launcher; -[JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSourceGenerationOptions( + WriteIndented = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true)] [JsonSerializable(typeof(SignedFileMap))] [JsonSerializable(typeof(UpdateFileEntry))] [JsonSerializable(typeof(PlondsUpdateMetadata))] diff --git a/LanMountainDesktop.Launcher/CommandContext.cs b/LanMountainDesktop.Launcher/CommandContext.cs index 1af1487..81db418 100644 --- a/LanMountainDesktop.Launcher/CommandContext.cs +++ b/LanMountainDesktop.Launcher/CommandContext.cs @@ -4,6 +4,17 @@ namespace LanMountainDesktop.Launcher; internal sealed class CommandContext { + private static readonly string[] GuiCommands = + [ + "launch", + "apply-update", + "preview-splash", + "preview-error", + "preview-update", + "preview-oobe", + "preview-debug" + ]; + public string Command { get; } public string SubCommand { get; } @@ -28,6 +39,14 @@ internal sealed class CommandContext Options.ContainsKey("debug") || System.Diagnostics.Debugger.IsAttached; + public bool IsPreviewCommand => + Command.StartsWith("preview-", StringComparison.OrdinalIgnoreCase); + + public bool IsGuiCommand => + GuiCommands.Contains(Command, StringComparer.OrdinalIgnoreCase); + + public string? ExplicitAppRoot => GetOption("app-root"); + private CommandContext(string command, string subCommand, Dictionary options, string[] rawArgs) { Command = command; diff --git a/LanMountainDesktop.Launcher/Program.cs b/LanMountainDesktop.Launcher/Program.cs index 410b2bd..fa18a83 100644 --- a/LanMountainDesktop.Launcher/Program.cs +++ b/LanMountainDesktop.Launcher/Program.cs @@ -10,32 +10,54 @@ internal static class Program private static async Task Main(string[] args) { var commandContext = CommandContext.FromArgs(args); + Logger.Initialize(); + Logger.Info( + $"Program entry. Command='{commandContext.Command}'; SubCommand='{commandContext.SubCommand}'; " + + $"IsGuiMode={commandContext.IsGuiCommand}; IsDebugMode={commandContext.IsDebugMode}; " + + $"HasResultPath={!string.IsNullOrWhiteSpace(commandContext.GetOption("result"))}; " + + $"ExplicitAppRoot='{commandContext.ExplicitAppRoot ?? ""}'."); - // 处理遗留插件安装命令 - if (commandContext.IsLegacyPluginInstall) + try { - var installer = new PluginInstallerService(); - return await Commands.RunLegacyPluginInstallAsync(commandContext, installer).ConfigureAwait(false); - } + if (commandContext.IsLegacyPluginInstall) + { + var installer = new PluginInstallerService(); + return await Commands.RunLegacyPluginInstallAsync(commandContext, installer).ConfigureAwait(false); + } + + if (!commandContext.IsGuiCommand) + { + return await Commands.RunCliCommandAsync(commandContext).ConfigureAwait(false); + } - // apply-update 命令:启动 Avalonia GUI 显示更新进度窗口 - if (string.Equals(commandContext.Command, "apply-update", StringComparison.OrdinalIgnoreCase)) - { LauncherRuntimeContext.Current = commandContext; BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); return Environment.ExitCode; } - - // 处理其他 CLI 命令 (update, plugin, rollback 等) - if (!string.Equals(commandContext.Command, "launch", StringComparison.OrdinalIgnoreCase)) + catch (Exception ex) { - return await Commands.RunCliCommandAsync(commandContext).ConfigureAwait(false); - } + Logger.Error("Launcher failed before GUI flow completed.", ex); - // 主启动流程: OOBE -> Splash -> 版本选择 -> 启动主程序 - LauncherRuntimeContext.Current = commandContext; - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - return Environment.ExitCode; + var result = new LauncherResult + { + Success = false, + Stage = "launcher", + Code = "launcher_bootstrap_failed", + Message = ex.Message, + ErrorMessage = ex.ToString(), + Details = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["command"] = commandContext.Command, + ["subCommand"] = commandContext.SubCommand, + ["isGuiMode"] = commandContext.IsGuiCommand.ToString(), + ["isDebugMode"] = commandContext.IsDebugMode.ToString(), + ["explicitAppRoot"] = commandContext.ExplicitAppRoot ?? string.Empty + } + }; + + await Commands.WriteResultIfNeededAsync(commandContext.GetOption("result"), result).ConfigureAwait(false); + return 1; + } } private static AppBuilder BuildAvaloniaApp() diff --git a/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs b/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs index 3e1ab99..d5c47bf 100644 --- a/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs +++ b/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs @@ -33,7 +33,6 @@ internal sealed class DeploymentLocator var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly); Console.WriteLine($"[DeploymentLocator] Found {candidates.Length} app-* directories"); - // ClassIsland 风格的查询:先筛选,后排序 var validInstallations = candidates .Where(path => { @@ -79,38 +78,199 @@ internal sealed class DeploymentLocator } } - public string? ResolveHostExecutablePath() + public HostResolutionResult ResolveHostExecutable(CommandContext context) { - // 使用新的灵活定位器 - var options = new HostDiscoveryOptions - { - ExecutableName = "LanMountainDesktop", - PreferDevModeConfig = true, - RecursiveSearch = false, // 默认不启用递归搜索以提高性能 - AdditionalSearchPaths = new List - { - // 可以通过配置文件或环境变量添加更多路径 - "${AppRoot}", - "${AppRoot}/..", - "${BaseDirectory}/../..", - } - }; + ArgumentNullException.ThrowIfNull(context); - var locator = new FlexibleHostLocator(_appRoot, options); - var result = locator.ResolveHostExecutablePath(); - - if (result != null) + var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop"; + var searchedPaths = new List(); + var explicitAppRoot = context.ExplicitAppRoot; + var devModeConfigIgnored = !context.IsDebugMode && Views.ErrorWindow.CheckDevModeEnabled(); + + string? resolvedPath; + string? source; + + if (!string.IsNullOrWhiteSpace(explicitAppRoot)) { - return result; + var explicitRoot = Path.GetFullPath(explicitAppRoot); + resolvedPath = TryResolveExplicitAppRoot(explicitRoot, executable, searchedPaths, out source); + } + else + { + resolvedPath = TryResolvePublishedOrPortableHost(executable, searchedPaths, out source); } - // 回退到旧逻辑(作为备选) + if (resolvedPath is null && context.IsDebugMode) + { + resolvedPath = TryResolveDebugHost(executable, searchedPaths, out source); + } + + if (resolvedPath is null) + { + resolvedPath = ResolveHostExecutablePathLegacy(); + if (!string.IsNullOrWhiteSpace(resolvedPath)) + { + searchedPaths.Add(Path.GetFullPath(resolvedPath)); + source = "legacy_fallback"; + } + } + + return new HostResolutionResult + { + Success = !string.IsNullOrWhiteSpace(resolvedPath), + ResolvedHostPath = resolvedPath, + ResolutionSource = source, + AppRoot = _appRoot, + ExplicitAppRoot = explicitAppRoot, + DevModeConfigIgnored = devModeConfigIgnored, + SearchedPaths = searchedPaths + .Where(path => !string.IsNullOrWhiteSpace(path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList() + }; + } + + public string? ResolveHostExecutablePath() + { return ResolveHostExecutablePathLegacy(); } - /// - /// 传统的主程序路径解析(作为备选) - /// + private string? TryResolveExplicitAppRoot( + string explicitRoot, + string executable, + List searchedPaths, + out string? source) + { + var directPath = Path.Combine(explicitRoot, executable); + searchedPaths.Add(directPath); + if (File.Exists(directPath)) + { + source = "explicit_app_root_direct"; + return directPath; + } + + var deployment = FindBestDeploymentHost(explicitRoot, executable, searchedPaths); + if (deployment is not null) + { + source = "explicit_app_root_deployment"; + return deployment; + } + + source = null; + return null; + } + + private string? TryResolvePublishedOrPortableHost( + string executable, + List searchedPaths, + out string? source) + { + var deployment = FindBestDeploymentHost(_appRoot, executable, searchedPaths); + if (deployment is not null) + { + source = "published_deployment"; + return deployment; + } + + var portableCandidates = new[] + { + Path.Combine(_appRoot, executable), + Path.Combine(AppContext.BaseDirectory, executable) + }; + + foreach (var candidate in portableCandidates + .Select(Path.GetFullPath) + .Distinct(StringComparer.OrdinalIgnoreCase)) + { + searchedPaths.Add(candidate); + if (File.Exists(candidate)) + { + source = "portable_host"; + return candidate; + } + } + + source = null; + return null; + } + + private string? TryResolveDebugHost( + string executable, + List searchedPaths, + out string? source) + { + if (Views.ErrorWindow.CheckDevModeEnabled()) + { + var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath(); + if (!string.IsNullOrWhiteSpace(savedCustomPath)) + { + var fullSavedPath = Path.GetFullPath(savedCustomPath); + searchedPaths.Add(fullSavedPath); + if (File.Exists(fullSavedPath)) + { + source = "debug_saved_custom_path"; + return fullSavedPath; + } + } + } + + foreach (var devPath in GetDevelopmentPaths(executable)) + { + var fullPath = Path.GetFullPath(devPath); + searchedPaths.Add(fullPath); + if (File.Exists(fullPath)) + { + source = "debug_build_output"; + return fullPath; + } + } + + source = null; + return null; + } + + private static string? FindBestDeploymentHost( + string root, + string executable, + List searchedPaths) + { + if (!Directory.Exists(root)) + { + searchedPaths.Add(Path.Combine(root, "app-*", executable)); + return null; + } + + var appDirs = Directory.GetDirectories(root, "app-*", SearchOption.TopDirectoryOnly) + .Where(path => !File.Exists(Path.Combine(path, ".destroy"))) + .Where(path => !File.Exists(Path.Combine(path, ".partial"))) + .Select(path => new + { + Path = path, + HostPath = Path.Combine(path, executable), + HasCurrent = File.Exists(Path.Combine(path, ".current")), + Version = ParseVersionFromDirectory(path) + }) + .OrderByDescending(item => item.HasCurrent) + .ThenByDescending(item => item.Version) + .ToList(); + + foreach (var candidate in appDirs) + { + searchedPaths.Add(candidate.HostPath); + if (File.Exists(candidate.HostPath)) + { + return candidate.HostPath; + } + } + + if (appDirs.Count == 0) + { + searchedPaths.Add(Path.Combine(root, "app-*", executable)); + } + + return null; + } + private string? ResolveHostExecutablePathLegacy() { var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop"; @@ -126,14 +286,12 @@ internal sealed class DeploymentLocator } } - // 2. 查找 Launcher 所在目录(开发环境 - 直接运行) var inRoot = Path.Combine(_appRoot, executable); if (File.Exists(inRoot)) { return inRoot; } - // 3. 查找父目录(开发环境 - 从 Launcher 项目运行) var parent = Path.GetFullPath(Path.Combine(_appRoot, "..")); var inParent = Path.Combine(parent, executable); if (File.Exists(inParent)) @@ -144,14 +302,12 @@ internal sealed class DeploymentLocator // 4. 开发模式:如果启用了开发模式,优先使用保存的自定义路径 if (Views.ErrorWindow.CheckDevModeEnabled()) { - // 4.1 首先检查保存的自定义路径 var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath(); if (!string.IsNullOrWhiteSpace(savedCustomPath) && File.Exists(savedCustomPath)) { return savedCustomPath; } - // 4.2 扫描开发路径 var devPath = ScanDevelopmentPaths(executable); if (!string.IsNullOrWhiteSpace(devPath)) { @@ -179,7 +335,7 @@ internal sealed class DeploymentLocator { var possiblePaths = new[] { - // 从 Launcher 项目运行 + // ?Launcher 项目运行 Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable), Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable), @@ -203,17 +359,14 @@ internal sealed class DeploymentLocator } /// - /// 获取开发环境可能的主程序路径 - /// + /// 获取开发环境可能的主程序路? /// private static IEnumerable GetDevelopmentPaths(string executable) { - // 获取 Launcher 所在目录 var launcherDir = AppContext.BaseDirectory; - // 可能的开发目录结构 var possiblePaths = new[] { - // 从 Launcher 项目运行:..\LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe + // ?Launcher 项目运行?.\LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable), Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable), @@ -221,7 +374,7 @@ internal sealed class DeploymentLocator Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable), Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable), - // 从 dev-test 目录运行 + // ?dev-test 目录运行 Path.Combine(launcherDir, "..", "dev-test", "app-1.0.0-dev", executable), }; @@ -256,9 +409,8 @@ internal sealed class DeploymentLocator } /// - /// 清理旧版本部署,保留最近的N个版本 - /// - /// 最少保留版本数,默认3个 + /// 清理旧版本部署,保留最近的N个版? /// + /// 最少保留版本数,默??/param> public void CleanupOldDeployments(int minVersionsToKeep = 3) { try @@ -272,7 +424,6 @@ internal sealed class DeploymentLocator var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly); - // 过滤掉无效部署目录(排除partial),按版本排序 var validDeployments = candidates .Where(path => !File.Exists(Path.Combine(path, ".partial"))) .Select(path => new @@ -349,7 +500,6 @@ internal sealed class DeploymentLocator { if (versionsToKeep.Contains(deployment.Path)) { - // 保留此版本,如果之前标记了destroy则取消标记 if (deployment.IsDestroyed) { try @@ -365,7 +515,6 @@ internal sealed class DeploymentLocator continue; } - // 如果还没标记destroy的,先标记 if (!deployment.IsDestroyed) { try @@ -387,7 +536,7 @@ internal sealed class DeploymentLocator } catch { - // 忽略删除失败(可能文件被占用),下次启动再试 + // 忽略删除失败(可能文件被占?,下次启动再试 Console.WriteLine($"[DeploymentLocator] Failed to delete (will retry later): {deployment.Path}"); } } @@ -400,7 +549,7 @@ internal sealed class DeploymentLocator } /// - /// 仅清理已标记为.destroy的部署(兼容旧方法) + /// 仅清理已标记?destroy的部署(兼容旧方法) /// [Obsolete("Use CleanupOldDeployments instead")] public void CleanupDestroyedDeployments() @@ -432,8 +581,7 @@ internal sealed class DeploymentLocator } /// - /// 从部署目录读取版本信息 - /// + /// 从部署目录读取版本信? /// public AppVersionInfo GetVersionInfo() { var deploymentDir = FindCurrentDeploymentDirectory(); @@ -453,16 +601,16 @@ internal sealed class DeploymentLocator } catch { - // 忽略读取失败,回退到默认值 } } } - // 回退:从目录名解析版本,使用默认开发代号 return new AppVersionInfo { Version = GetCurrentVersion(), - Codename = "Administrate" // 默认开发代号 + Codename = "Administrate" }; - } +} + + } diff --git a/LanMountainDesktop.Launcher/Services/HostResolutionResult.cs b/LanMountainDesktop.Launcher/Services/HostResolutionResult.cs new file mode 100644 index 0000000..c42e377 --- /dev/null +++ b/LanMountainDesktop.Launcher/Services/HostResolutionResult.cs @@ -0,0 +1,18 @@ +namespace LanMountainDesktop.Launcher.Services; + +internal sealed class HostResolutionResult +{ + public bool Success { get; init; } + + public string? ResolvedHostPath { get; init; } + + public string? ResolutionSource { get; init; } + + public string AppRoot { get; init; } = string.Empty; + + public string? ExplicitAppRoot { get; init; } + + public bool DevModeConfigIgnored { get; init; } + + public List SearchedPaths { get; init; } = []; +} diff --git a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs index d44bcc8..6fa700b 100644 --- a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs +++ b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs @@ -12,6 +12,7 @@ internal sealed class LauncherFlowCoordinator private static readonly string[] LauncherOnlyOptions = [ "debug", "show-loading-details", "plugins-dir", "source", "result", + "app-root", LauncherIpcConstants.LauncherPidEnvVar, LauncherIpcConstants.PackageRootEnvVar, LauncherIpcConstants.VersionEnvVar, @@ -44,32 +45,26 @@ internal sealed class LauncherFlowCoordinator { try { - // 清理旧版本,保留至少3个版本 _deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3); - // 检测老版本安装(首次运行时) if (_oobeStateService.IsFirstRun()) { var legacyInfo = LegacyVersionDetector.DetectLegacyInstallation(); - if (legacyInfo != null) + if (legacyInfo is not null) { - var migrationResult = await ShowMigrationPromptAsync(legacyInfo); - // 无论用户选择什么,都继续启动流程 - Console.WriteLine($"[LauncherFlowCoordinator] Migration prompt result: {migrationResult}"); + var migrationResult = await ShowMigrationPromptAsync(legacyInfo).ConfigureAwait(false); + Logger.Info($"Migration prompt completed. Result='{migrationResult}'."); } } - // 使用传入的 Splash 窗口或创建新的 var splashWindow = existingSplashWindow ?? await Dispatcher.UIThread.InvokeAsync(() => { var window = new SplashWindow(); window.Show(); return window; }); - var reporter = (ISplashStageReporter)splashWindow; - - // 创建加载详情窗口(可选,用于显示详细加载状态) + LoadingDetailsWindow? loadingDetailsWindow = null; if (_context.IsDebugMode || _context.GetOption("show-loading-details") == "true") { @@ -79,49 +74,48 @@ internal sealed class LauncherFlowCoordinator loadingDetailsWindow.Show(); }); } - - // 跟踪主程序是否已就绪,就绪后自动关闭 Splash 窗口 - var hostReadyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - // 加载状态管理 + + var visibilityTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var activationFailedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var lastStage = StartupStage.Initializing; + var lastStageMessage = "launcher-started"; + var loadingState = new LoadingStateMessage(); - - // 启动 IPC 服务端监听主程序进度 - using var ipcServer = new LauncherIpcServer(msg => + using var ipcServer = new LauncherIpcServer(message => { Dispatcher.UIThread.Post(() => { try { - // 更新加载状态 + lastStage = message.Stage; + lastStageMessage = message.Message ?? string.Empty; + Logger.Info($"IPC stage received. Stage='{message.Stage}'; Message='{message.Message ?? string.Empty}'."); + loadingState = loadingState with { - Stage = msg.Stage, - OverallProgressPercent = msg.ProgressPercent, - Message = msg.Message, + Stage = message.Stage, + OverallProgressPercent = message.ProgressPercent, + Message = message.Message, Timestamp = DateTimeOffset.UtcNow }; - - // 报告到 Splash 窗口 - reporter.Report(msg.Stage.ToString().ToLower(), msg.Message ?? ""); - - // 更新加载详情窗口 + + reporter.Report(MapStartupStageToSplashStage(message.Stage), message.Message ?? message.Stage.ToString()); loadingDetailsWindow?.UpdateLoadingState(loadingState); - - // 主程序报告就绪后,关闭 Splash 窗口和加载详情窗口 - if (msg.Stage == StartupStage.Ready) + + switch (message.Stage) { - if (splashWindow.IsVisible && splashWindow.IsLoaded) - { - splashWindow.Close(); - } - loadingDetailsWindow?.Close(); - hostReadyTcs.TrySetResult(); + case StartupStage.DesktopVisible: + case StartupStage.ActivationRedirected: + visibilityTcs.TrySetResult(message.Stage); + break; + case StartupStage.ActivationFailed: + activationFailedTcs.TrySetResult(message.Message ?? "activation_failed"); + break; } } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Error in IPC callback: {ex.Message}"); + Logger.Error("IPC progress callback failed.", ex); } }); }); @@ -129,25 +123,21 @@ internal sealed class LauncherFlowCoordinator try { - // 检查并安装待处理的更新(主程序下载的) - reporter.Report("update", "检查更新..."); + reporter.Report("update", "Checking updates..."); var updateResult = await _updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false); if (!updateResult.Success) { return updateResult; } - // 检查并安装待处理的插件更新 - reporter.Report("plugins", "检查插件更新..."); - var pluginsDir = _context.GetOption("plugins-dir") - ?? Path.Combine(_deploymentLocator.GetAppRoot(), "plugins"); + 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) { return queueResult; } - // OOBE(首次运行引导) if (_oobeStateService.IsFirstRun()) { await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Hide()); @@ -155,116 +145,108 @@ internal sealed class LauncherFlowCoordinator { await step.RunAsync(CancellationToken.None).ConfigureAwait(false); } + await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Show()); } - // 启动主程序 - reporter.Report("launch", "正在启动..."); - var (hostResult, hostProcess) = await LaunchHostWithIpcAsync(splashWindow); - if (!hostResult.Success) + reporter.Report("launch", "Launching desktop..."); + var launchOutcome = await LaunchHostWithIpcAsync().ConfigureAwait(false); + if (!launchOutcome.Result.Success) { - return hostResult; + return launchOutcome.Result; } - // 等待主程序进程退出。Launcher 作为后台守护进程保持运行, - // 维持 IPC 管道服务端供主程序报告启动进度。 - if (hostProcess is not null) + if (launchOutcome.ImmediateResult is not null) { - var processExitTask = hostProcess.WaitForExitAsync(); - - // 等待主程序就绪或进程退出(取先发生者) - // 30 秒超时,宿主端有 10 秒兜底机制确保 Ready 信号发送 - var readyOrTimeoutOrExit = Task.WhenAny( - hostReadyTcs.Task, - processExitTask, - Task.Delay(TimeSpan.FromSeconds(30))); - - var completedTask = await readyOrTimeoutOrExit; - - // Host process exited before reporting Ready. - if (completedTask == processExitTask) - { - var exitCode = hostProcess.ExitCode; - Console.Error.WriteLine($"[LauncherFlowCoordinator] Host process exited before Ready. ExitCode={exitCode}."); + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return launchOutcome.ImmediateResult; + } - var recoveryResult = await TryRecoverFromEarlyHostExitAsync( - exitCode, - hostReadyTcs, - splashWindow, - loadingDetailsWindow).ConfigureAwait(false); - if (recoveryResult is not null) + if (launchOutcome.Process is null) + { + return BuildResult( + success: false, + stage: "launch", + code: "host_start_failed", + message: "Host launch did not create a process.", + details: launchOutcome.Details); + } + + var processExitTask = launchOutcome.Process.WaitForExitAsync(); + var completedTask = await Task.WhenAny( + visibilityTcs.Task, + activationFailedTcs.Task, + processExitTask, + Task.Delay(TimeSpan.FromSeconds(30))).ConfigureAwait(false); + + if (completedTask == visibilityTcs.Task) + { + var stage = await visibilityTcs.Task.ConfigureAwait(false); + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return BuildResult( + success: true, + stage: "launch", + code: stage == StartupStage.ActivationRedirected ? "activation_redirected" : "ok", + message: stage == StartupStage.ActivationRedirected + ? "Launcher activation was redirected to the existing desktop instance." + : "Desktop is visible and ready.", + details: launchOutcome.Details); + } + + if (completedTask == activationFailedTcs.Task) + { + Logger.Warn($"Activation failure received before desktop visibility. Reason='{await activationFailedTcs.Task.ConfigureAwait(false)}'."); + var retryOutcome = await RetryActivationAfterEarlyFailureAsync().ConfigureAwait(false); + if (retryOutcome is not null) + { + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return retryOutcome; + } + } + + if (completedTask == processExitTask) + { + var exitCode = launchOutcome.Process.ExitCode; + Logger.Warn($"Host exited before desktop became visible. ExitCode={exitCode}."); + + if (exitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired) + { + var retryOutcome = await RetryActivationAfterEarlyFailureAsync().ConfigureAwait(false); + if (retryOutcome is not null) { - return recoveryResult; + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return retryOutcome; } + } - // Close Splash window for unrecoverable early exits. - await Dispatcher.UIThread.InvokeAsync(() => + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return BuildResult( + success: false, + stage: "launch", + code: exitCode == HostExitCodes.SecondaryActivationSucceeded ? "activation_redirected" : "host_exited_early", + message: exitCode == HostExitCodes.SecondaryActivationSucceeded + ? "Host redirected activation to the existing desktop instance." + : $"Host exited before the desktop became visible. ExitCode={exitCode}.", + details: MergeDetails(launchOutcome.Details, new Dictionary { - try - { - if (splashWindow.IsVisible && splashWindow.IsLoaded) - { - splashWindow.Close(); - } - } - catch (Exception ex) - { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing splash window: {ex.Message}"); - } - }); - - return new LauncherResult - { - Success = false, - Stage = "launch", - Code = "host_crashed", - Message = $"主程序异常退出,退出代码: {exitCode}" - }; - } - - // 如果 Splash 窗口仍然打开(超时情况),关闭它 - if (splashWindow.IsVisible) - { - Console.WriteLine("[LauncherFlowCoordinator] Timeout waiting for Ready signal, closing splash window..."); - await Dispatcher.UIThread.InvokeAsync(() => - { - try - { - if (splashWindow.IsVisible && splashWindow.IsLoaded) - { - splashWindow.Close(); - } - } - catch (Exception ex) - { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing splash window on timeout: {ex.Message}"); - } - }); - } - - // 继续等待主程序进程退出(如果它还在运行) - if (!hostProcess.HasExited) - { - await processExitTask; - } - } - else - { - // 如果无法获取进程引用,退回到有限等待 - await Task.Delay(TimeSpan.FromSeconds(30)); + ["exitCode"] = exitCode.ToString() + })); } - return new LauncherResult - { - Success = true, - Stage = "exit", - Code = "ok", - Message = "Launcher completed successfully." - }; + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return BuildResult( + success: false, + stage: "launch", + code: "desktop_not_visible", + message: "Host process started, but the desktop never became visible within 30 seconds.", + details: MergeDetails(launchOutcome.Details, new Dictionary + { + ["ipcStage"] = lastStage.ToString(), + ["ipcMessage"] = lastStageMessage + })); } finally { - // Splash 窗口可能已由 IPC Ready 回调关闭,这里做安全清理 await Dispatcher.UIThread.InvokeAsync(() => { try @@ -272,124 +254,74 @@ internal sealed class LauncherFlowCoordinator if (splashWindow.IsVisible && splashWindow.IsLoaded) { splashWindow.Close(); - Console.WriteLine("[LauncherFlowCoordinator] Splash window closed in finally block"); + Logger.Info("Splash window closed in coordinator cleanup."); } } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing splash window in finally: {ex.Message}"); + Logger.Error("Failed to close splash window during coordinator cleanup.", ex); } }); } } catch (Exception ex) { - return new LauncherResult - { - Success = false, - Stage = "launch", - Code = "exception", - Message = ex.Message, - ErrorMessage = ex.ToString() - }; + Logger.Error("Launcher coordinator failed.", ex); + return BuildResult( + success: false, + stage: "launch", + code: "exception", + message: ex.Message, + errorMessage: ex.ToString()); } } - private async Task TryRecoverFromEarlyHostExitAsync( - int exitCode, - TaskCompletionSource hostReadyTcs, - SplashWindow splashWindow, - LoadingDetailsWindow? loadingDetailsWindow) + private async Task RetryActivationAfterEarlyFailureAsync() { - if (exitCode == HostExitCodes.SecondaryActivationSucceeded) + Logger.Warn("Attempting one explicit activation retry after host early failure."); + var retryOutcome = await LaunchHostWithIpcAsync(forceDirectMode: true, retryTag: "explicit-activation-retry").ConfigureAwait(false); + if (!retryOutcome.Result.Success) { - Console.WriteLine("[LauncherFlowCoordinator] Host redirected activation to an existing primary instance."); - await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); - return new LauncherResult + return retryOutcome.Result; + } + + if (retryOutcome.ImmediateResult is not null) + { + return retryOutcome.ImmediateResult; + } + + if (retryOutcome.Process is not null) + { + var retryExitTask = retryOutcome.Process.WaitForExitAsync(); + var completed = await Task.WhenAny(retryExitTask, Task.Delay(TimeSpan.FromSeconds(15))).ConfigureAwait(false); + + if (completed != retryExitTask) { - Success = true, - Stage = "launch", - Code = "activated_existing_instance", - Message = "Detected existing running instance and activation was acknowledged." - }; - } - - if (exitCode is not HostExitCodes.SecondaryActivationFailed and not HostExitCodes.RestartLockNotAcquired) - { - return null; - } - - Console.Error.WriteLine( - $"[LauncherFlowCoordinator] Activation handshake failed with exit code {exitCode}. Retrying explicit activation once..."); - - var (retryLaunchResult, retryProcess) = await LaunchHostWithIpcAsync(splashWindow).ConfigureAwait(false); - if (!retryLaunchResult.Success) - { - return retryLaunchResult; - } - - if (retryProcess is null) - { - return new LauncherResult - { - Success = false, - Stage = "launch", - Code = "activation_retry_start_failed", - Message = "Explicit activation retry failed because no host process was created." - }; - } - - Console.WriteLine($"[LauncherFlowCoordinator] Explicit activation retry started. RetryPid={retryProcess.Id}."); - var retryExitTask = retryProcess.WaitForExitAsync(); - var retryCompleted = await Task.WhenAny( - hostReadyTcs.Task, - retryExitTask, - Task.Delay(TimeSpan.FromSeconds(15))).ConfigureAwait(false); - - if (retryCompleted == hostReadyTcs.Task) - { - Console.WriteLine("[LauncherFlowCoordinator] Host reported Ready after explicit activation retry."); - await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); - return new LauncherResult - { - Success = true, - Stage = "launch", - Code = "activation_retry_ready", - Message = "Explicit activation retry succeeded and host reported Ready." - }; - } - - if (retryCompleted == retryExitTask) - { - var retryExitCode = retryProcess.ExitCode; - if (retryExitCode == HostExitCodes.SecondaryActivationSucceeded) - { - await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); - return new LauncherResult - { - Success = true, - Stage = "launch", - Code = "activation_retry_redirected", - Message = "Explicit activation retry redirected to the existing primary instance." - }; + return BuildResult( + success: true, + stage: "launch", + code: "activation_retry_started", + message: "Activation retry started the host successfully.", + details: retryOutcome.Details); } - return new LauncherResult + if (retryOutcome.Process.ExitCode == HostExitCodes.SecondaryActivationSucceeded) { - Success = false, - Stage = "launch", - Code = "activation_retry_failed", - Message = $"Explicit activation retry failed. ExitCode={retryExitCode}. 请结束残留后台进程后重试。" - }; + return BuildResult( + success: true, + stage: "launch", + code: "activation_redirected", + message: "Activation retry redirected to the existing desktop instance.", + details: retryOutcome.Details); + } } - return new LauncherResult - { - Success = false, - Stage = "launch", - Code = "activation_retry_timeout", - Message = "Explicit activation retry timed out before host became ready. 请结束残留后台进程后重试。" - }; + return BuildResult( + success: false, + stage: "launch", + code: "activation_failed", + message: "Activation retry failed to make the desktop visible.", + details: retryOutcome.Details); } private static async Task CloseWindowsAsync(SplashWindow splashWindow, LoadingDetailsWindow? loadingDetailsWindow) @@ -405,7 +337,7 @@ internal sealed class LauncherFlowCoordinator } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Failed to close splash window: {ex.Message}"); + Logger.Error("Failed to close splash window.", ex); } try @@ -417,159 +349,307 @@ internal sealed class LauncherFlowCoordinator } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Failed to close loading details window: {ex.Message}"); + Logger.Error("Failed to close loading details window.", ex); } }); } - private async Task<(LauncherResult Result, Process? Process)> LaunchHostWithIpcAsync(SplashWindow? splashWindow = null, string? customHostPath = null) + private async Task LaunchHostWithIpcAsync(bool forceDirectMode = false, string? retryTag = null) { - // 优先使用自定义路径(调试模式选择的路径) - var hostPath = customHostPath ?? _deploymentLocator.ResolveHostExecutablePath(); - - if (string.IsNullOrWhiteSpace(hostPath)) + var resolution = _deploymentLocator.ResolveHostExecutable(_context); + if (!resolution.Success || string.IsNullOrWhiteSpace(resolution.ResolvedHostPath)) { - // 关闭 Splash 窗口 - // 显示错误窗口而不是直接退出 - var (errorResult, selectedPath) = await ShowHostNotFoundErrorAsync(); - + var (errorResult, selectedPath) = await ShowHostNotFoundErrorAsync().ConfigureAwait(false); if (errorResult == ErrorWindowResult.Retry) { - // 用户选择重试,如果有选择路径则使用,否则重新尝试 - if (!string.IsNullOrWhiteSpace(selectedPath)) + if (!string.IsNullOrWhiteSpace(selectedPath) && File.Exists(selectedPath)) { - return await LaunchHostWithIpcAsync(splashWindow, selectedPath); + return await LaunchHostWithExplicitPathAsync(selectedPath, forceDirectMode, retryTag).ConfigureAwait(false); } - return await LaunchHostWithIpcAsync(splashWindow); + + return await LaunchHostWithIpcAsync(forceDirectMode, retryTag).ConfigureAwait(false); } - - // 用户选择退出 - return (new LauncherResult - { - Success = false, - Stage = "launchHost", - Code = "host_not_found", - Message = "LanMountainDesktop host executable not found." - }, null); + + 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 hostPath = resolution.ResolvedHostPath!; if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { EnsureExecutable(hostPath); } - var hostWorkingDir = Path.GetDirectoryName(hostPath) ?? _deploymentLocator.GetAppRoot(); + var hostWorkingDirectory = Path.GetDirectoryName(hostPath) ?? _deploymentLocator.GetAppRoot(); var versionInfo = _deploymentLocator.GetVersionInfo(); + var forwardedArguments = BuildForwardedArguments(versionInfo); - // 构建命令行参数:转发用户参数 + IPC 环境信息通过命令行传递 - // UseShellExecute = true 确保 Shell 启动子进程,使其正确关联到交互式桌面窗口站(WinSta0), - // 避免子进程窗口创建成功但不可见的问题。 + var primaryMode = forceDirectMode || !OperatingSystem.IsWindows() + ? HostStartMode.Direct + : HostStartMode.ShellExecute; + var fallbackMode = primaryMode == HostStartMode.ShellExecute + ? HostStartMode.Direct + : (HostStartMode?)null; + + var firstAttempt = await StartHostProcessAsync(hostPath, hostWorkingDirectory, forwardedArguments, versionInfo, primaryMode, retryTag).ConfigureAwait(false); + if (firstAttempt.ProcessCreated && !firstAttempt.ExitedEarly && 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(hostPath, hostWorkingDirectory, forwardedArguments, versionInfo, fallbackMode.Value, retryTag).ConfigureAwait(false); + if (secondAttempt.ProcessCreated && !secondAttempt.ExitedEarly && 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 HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired + ? "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 == HostExitCodes.SecondaryActivationSucceeded) + { + return HostLaunchOutcome.FromImmediateResult(BuildResult( + true, + "launch", + "activation_redirected", + "Launcher activation was redirected to the existing desktop instance.", + details)); + } + + if (finalAttempt.ExitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired) + { + 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( + string hostPath, + string hostWorkingDirectory, + string arguments, + AppVersionInfo versionInfo, + HostStartMode startMode, + string? retryTag) + { + var startInfo = new ProcessStartInfo + { + FileName = hostPath, + WorkingDirectory = hostWorkingDirectory, + Arguments = arguments, + UseShellExecute = startMode == HostStartMode.ShellExecute + }; + + if (startMode == HostStartMode.Direct) + { + startInfo.EnvironmentVariables[LauncherIpcConstants.LauncherPidEnvVar] = Environment.ProcessId.ToString(); + startInfo.EnvironmentVariables[LauncherIpcConstants.PackageRootEnvVar] = _deploymentLocator.GetAppRoot(); + startInfo.EnvironmentVariables[LauncherIpcConstants.VersionEnvVar] = versionInfo.Version; + startInfo.EnvironmentVariables[LauncherIpcConstants.CodenameEnvVar] = versionInfo.Codename; + } + + try + { + var process = Process.Start(startInfo); + Logger.Info( + $"Host launch requested. Mode='{startMode}'; RetryTag='{retryTag ?? ""}'; Path='{hostPath}'; " + + $"WorkingDir='{hostWorkingDirectory}'; Pid={(process is null ? -1 : process.Id)}; Args='{startInfo.Arguments}'."); + + if (process is null) + { + return HostStartAttempt.StartFailed(startMode, "process_start_returned_null"); + } + + var exitTask = process.WaitForExitAsync(); + var completed = await Task.WhenAny(exitTask, Task.Delay(TimeSpan.FromSeconds(2))).ConfigureAwait(false); + if (completed == exitTask) + { + return HostStartAttempt.EarlyExit(startMode, process, process.ExitCode); + } + + return HostStartAttempt.Started(startMode, process); + } + catch (Exception ex) + { + Logger.Error($"Host start failed. Mode='{startMode}'.", ex); + return HostStartAttempt.StartFailed(startMode, ex.GetType().Name); + } + } + + private string BuildForwardedArguments(AppVersionInfo versionInfo) + { var arguments = new System.Text.StringBuilder(); - // 转发命令行参数给主程序(排除 Launcher 自己的命令和选项) - // 只过滤 Launcher 专属的选项,保留宿主程序需要的参数(如 --restart-parent-pid) - foreach (var arg in _context.RawArgs) + for (var index = 0; index < _context.RawArgs.Count; index++) { + var arg = _context.RawArgs[index]; + if (arg == _context.Command || arg == _context.SubCommand) + { continue; - - if (arg.StartsWith("--")) + } + + if (arg.StartsWith("--", StringComparison.Ordinal)) { var key = arg[2..]; var equalsIndex = key.IndexOf('='); - if (equalsIndex >= 0) key = key[..equalsIndex]; - + if (equalsIndex >= 0) + { + key = key[..equalsIndex]; + } + if (LauncherOnlyOptions.Contains(key, StringComparer.OrdinalIgnoreCase)) + { + if (equalsIndex < 0 && + index + 1 < _context.RawArgs.Count && + !_context.RawArgs[index + 1].StartsWith("--", StringComparison.Ordinal)) + { + index++; + } + continue; + } } - - if (arguments.Length > 0) arguments.Append(' '); + + if (arguments.Length > 0) + { + arguments.Append(' '); + } + arguments.Append(QuoteArgument(arg)); } - // 通过命令行参数传递 IPC 连接信息(UseShellExecute=true 时不支持 EnvironmentVariables) - if (arguments.Length > 0) arguments.Append(' '); + if (arguments.Length > 0) + { + arguments.Append(' '); + } + arguments.Append($"--{LauncherIpcConstants.LauncherPidEnvVar}={Environment.ProcessId}"); arguments.Append($" --{LauncherIpcConstants.PackageRootEnvVar}={QuoteArgument(_deploymentLocator.GetAppRoot())}"); arguments.Append($" --{LauncherIpcConstants.VersionEnvVar}={versionInfo.Version}"); - arguments.Append($" --{LauncherIpcConstants.CodenameEnvVar}={versionInfo.Codename}"); + arguments.Append($" --{LauncherIpcConstants.CodenameEnvVar}={QuoteArgument(versionInfo.Codename)}"); - var processStartInfo = new ProcessStartInfo - { - FileName = hostPath, - UseShellExecute = true, - WorkingDirectory = hostWorkingDir, - Arguments = arguments.ToString() - }; - - // 同时设置环境变量作为备选(当 UseShellExecute=true 时 EnvironmentVariables 仍会被子进程继承) - processStartInfo.EnvironmentVariables[LauncherIpcConstants.LauncherPidEnvVar] = - Environment.ProcessId.ToString(); - processStartInfo.EnvironmentVariables[LauncherIpcConstants.PackageRootEnvVar] = - _deploymentLocator.GetAppRoot(); - processStartInfo.EnvironmentVariables[LauncherIpcConstants.VersionEnvVar] = versionInfo.Version; - processStartInfo.EnvironmentVariables[LauncherIpcConstants.CodenameEnvVar] = versionInfo.Codename; - - var hostProcess = Process.Start(processStartInfo); - Console.WriteLine( - $"[LauncherFlowCoordinator] Host launch requested. Path='{hostPath}'; WorkingDir='{hostWorkingDir}'; " + - $"Pid={(hostProcess is null ? -1 : hostProcess.Id)}; Args='{processStartInfo.Arguments}'."); - return (new LauncherResult - { - Success = true, - Stage = "launchHost", - Code = "ok", - Message = "Host launched." - }, hostProcess); + return arguments.ToString(); } - /// - /// 显示找不到主程序的错误窗口 - /// private async Task<(ErrorWindowResult Result, string? CustomPath)> ShowHostNotFoundErrorAsync() { ErrorWindow? errorWindow = null; - - // 在 UI 线程创建并显示错误窗口 + await Dispatcher.UIThread.InvokeAsync(() => { try { errorWindow = new ErrorWindow(); - errorWindow.SetErrorMessage("找不到阑山桌面应用程序。"); + errorWindow.SetErrorMessage("LanMountainDesktop host executable was not found."); errorWindow.Show(); - Console.WriteLine("[LauncherFlowCoordinator] ErrorWindow shown for host not found"); + Logger.Warn("Host not found. Showing error window."); } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Failed to show ErrorWindow: {ex.Message}"); + Logger.Error("Failed to show host-not-found error window.", ex); } }); - + if (errorWindow is null) { - Console.Error.WriteLine("[LauncherFlowCoordinator] ErrorWindow is null, cannot wait for choice"); return (ErrorWindowResult.Exit, null); } - - // 等待用户选择 + ErrorWindowResult result; string? customPath; - try { - result = await errorWindow.WaitForChoiceAsync(); + result = await errorWindow.WaitForChoiceAsync().ConfigureAwait(false); customPath = errorWindow.GetCustomHostPath(); - Console.WriteLine($"[LauncherFlowCoordinator] ErrorWindow result: {result}, customPath: {customPath != null}"); + Logger.Info($"Host-not-found window result='{result}'; HasCustomPath={!string.IsNullOrWhiteSpace(customPath)}."); } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Error waiting for choice: {ex.Message}"); + Logger.Error("Failed while waiting for host-not-found window result.", ex); result = ErrorWindowResult.Exit; customPath = null; } - - // 安全关闭错误窗口 + await Dispatcher.UIThread.InvokeAsync(() => { try @@ -577,26 +657,21 @@ internal sealed class LauncherFlowCoordinator if (errorWindow.IsVisible && errorWindow.IsLoaded) { errorWindow.Close(); - Console.WriteLine("[LauncherFlowCoordinator] ErrorWindow closed successfully"); } } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing ErrorWindow: {ex.Message}"); + Logger.Error("Failed to close host-not-found error window.", ex); } }); - + return (result, customPath); } - /// - /// 显示迁移提示窗口 - /// private async Task ShowMigrationPromptAsync(LegacyVersionInfo legacyInfo) { MigrationPromptWindow? migrationWindow = null; - // 在 UI 线程创建并显示迁移提示窗口 await Dispatcher.UIThread.InvokeAsync(() => { try @@ -604,35 +679,29 @@ internal sealed class LauncherFlowCoordinator migrationWindow = new MigrationPromptWindow(); migrationWindow.SetLegacyInfo(legacyInfo); migrationWindow.Show(); - Console.WriteLine("[LauncherFlowCoordinator] MigrationPromptWindow shown"); } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Failed to show MigrationPromptWindow: {ex.Message}"); + Logger.Error("Failed to show migration prompt window.", ex); } }); if (migrationWindow is null) { - Console.Error.WriteLine("[LauncherFlowCoordinator] MigrationPromptWindow is null, skipping migration prompt"); return MigrationResult.Skipped; } - // 等待用户选择 MigrationResult result; - try { - result = await migrationWindow.WaitForChoiceAsync(); - Console.WriteLine($"[LauncherFlowCoordinator] MigrationPromptWindow result: {result}"); + result = await migrationWindow.WaitForChoiceAsync().ConfigureAwait(false); } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Error waiting for migration choice: {ex.Message}"); + Logger.Error("Failed while waiting for migration prompt result.", ex); result = MigrationResult.Skipped; } - // 安全关闭窗口 await Dispatcher.UIThread.InvokeAsync(() => { try @@ -640,18 +709,102 @@ internal sealed class LauncherFlowCoordinator if (migrationWindow.IsVisible && migrationWindow.IsLoaded) { migrationWindow.Close(); - Console.WriteLine("[LauncherFlowCoordinator] MigrationPromptWindow closed successfully"); } } catch (Exception ex) { - Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing MigrationPromptWindow: {ex.Message}"); + 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.InitializingUI => "ui", + StartupStage.ShellInitialized => "shell", + 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 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["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["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 string QuoteArgument(string value) { if (string.IsNullOrEmpty(value)) @@ -700,84 +853,45 @@ internal sealed class LauncherFlowCoordinator } } - private sealed class WelcomeOobeStep : IOobeStep + private enum HostStartMode { - private readonly OobeStateService _stateService; + ShellExecute, + Direct + } - public WelcomeOobeStep(OobeStateService stateService) - { - _stateService = stateService; - } + private sealed record HostStartAttempt( + HostStartMode StartMode, + bool ProcessCreated, + Process? Process, + bool ExitedEarly, + int? ExitCode, + string? FailureReason) + { + public int? ProcessId => Process?.Id; - public async Task RunAsync(CancellationToken cancellationToken) - { - OobeWindow? window = null; - - try - { - window = await Dispatcher.UIThread.InvokeAsync(() => - { - try - { - var oobeWindow = new OobeWindow(); - oobeWindow.Show(); - Console.WriteLine("[WelcomeOobeStep] OOBE window shown"); - return oobeWindow; - } - catch (Exception ex) - { - Console.Error.WriteLine($"[WelcomeOobeStep] Failed to show OOBE window: {ex.Message}"); - return null; - } - }); + public static HostStartAttempt Started(HostStartMode startMode, Process process) => + new(startMode, true, process, false, null, null); - if (window is null) - { - Console.Error.WriteLine("[WelcomeOobeStep] OOBE window is null, skipping OOBE"); - _stateService.MarkCompleted(); - return; - } + public static HostStartAttempt EarlyExit(HostStartMode startMode, Process process, int exitCode) => + new(startMode, true, process, true, exitCode, null); - using var _ = cancellationToken.Register(() => - { - try - { - if (window.IsVisible && window.IsLoaded) - { - window.Close(); - } - } - catch (Exception ex) - { - Console.Error.WriteLine($"[WelcomeOobeStep] Error closing OOBE window on cancel: {ex.Message}"); - } - }); - - await window.WaitForEnterAsync().ConfigureAwait(false); - Console.WriteLine("[WelcomeOobeStep] OOBE completed by user"); - _stateService.MarkCompleted(); - } - finally - { - if (window is not null) - { - await Dispatcher.UIThread.InvokeAsync(() => - { - try - { - if (window.IsVisible && window.IsLoaded) - { - window.Close(); - Console.WriteLine("[WelcomeOobeStep] OOBE window closed in finally"); - } - } - catch (Exception ex) - { - Console.Error.WriteLine($"[WelcomeOobeStep] Error closing OOBE window in finally: {ex.Message}"); - } - }); - } - } - } + public static HostStartAttempt StartFailed(HostStartMode startMode, string failureReason) => + new(startMode, false, null, false, null, failureReason); + } + + 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); } } diff --git a/LanMountainDesktop.Launcher/Services/WelcomeOobeStep.cs b/LanMountainDesktop.Launcher/Services/WelcomeOobeStep.cs new file mode 100644 index 0000000..810d83b --- /dev/null +++ b/LanMountainDesktop.Launcher/Services/WelcomeOobeStep.cs @@ -0,0 +1,42 @@ +using Avalonia.Threading; +using LanMountainDesktop.Launcher.Views; + +namespace LanMountainDesktop.Launcher.Services; + +internal sealed class WelcomeOobeStep : IOobeStep +{ + private readonly OobeStateService _oobeStateService; + + public WelcomeOobeStep(OobeStateService oobeStateService) + { + _oobeStateService = oobeStateService; + } + + public async Task RunAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + OobeWindow? window = null; + await Dispatcher.UIThread.InvokeAsync(() => + { + window = new OobeWindow(); + window.Show(); + }); + + if (window is null) + { + return; + } + + await window.WaitForEnterAsync().ConfigureAwait(false); + _oobeStateService.MarkCompleted(); + + await Dispatcher.UIThread.InvokeAsync(() => + { + if (window.IsVisible) + { + window.Close(); + } + }); + } +} diff --git a/LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml.cs b/LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml.cs index 23d45df..ea4da60 100644 --- a/LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml.cs +++ b/LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml.cs @@ -23,14 +23,12 @@ public partial class LoadingDetailsWindow : Window { AvaloniaXamlLoader.Load(this); - // 初始化列表 var itemsList = this.FindControl("LoadingItemsList"); if (itemsList != null) { itemsList.ItemsSource = _items; } - // 创建更新定时器 _updateTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) @@ -59,8 +57,7 @@ public partial class LoadingDetailsWindow : Window } /// - /// 更新加载状态 - /// + /// 更新加载状? /// public void UpdateLoadingState(LoadingStateMessage state) { Dispatcher.UIThread.Post(() => @@ -73,7 +70,6 @@ public partial class LoadingDetailsWindow : Window // 更新整体进度 UpdateOverallProgress(state); - // 更新当前活动项 UpdateCurrentItem(state); // 更新列表 @@ -124,8 +120,7 @@ public partial class LoadingDetailsWindow : Window } /// - /// 更新当前活动项 - /// + /// 更新当前活动? /// private void UpdateCurrentItem(LoadingStateMessage state) { var currentItem = state.ActiveItems.FirstOrDefault(); @@ -162,7 +157,6 @@ public partial class LoadingDetailsWindow : Window /// private void UpdateItemsList(LoadingStateMessage state) { - // 同步列表项 foreach (var item in state.ActiveItems) { var existing = _items.FirstOrDefault(i => i.Id == item.Id); @@ -187,7 +181,7 @@ public partial class LoadingDetailsWindow : Window } } - // 按状态排序:进行中 -> 等待中 -> 已完成 -> 失败 + // 按状态排序:进行?-> 等待?-> 已完?-> 失败 var sortedItems = _items.OrderBy(i => GetStatePriority(i.State)).ToList(); _items.Clear(); foreach (var item in sortedItems) @@ -240,17 +234,20 @@ public partial class LoadingDetailsWindow : Window /// private static string GetStageDescription(StartupStage stage) => stage switch { - StartupStage.Initializing => "正在初始化系统...", - StartupStage.LoadingSettings => "正在加载设置...", - StartupStage.LoadingPlugins => "正在加载插件...", - StartupStage.InitializingUI => "正在初始化界面...", - StartupStage.Ready => "加载完成", - _ => "正在加载..." + StartupStage.Initializing => "ڳʼϵͳ...", + StartupStage.LoadingSettings => "ڼ...", + StartupStage.LoadingPlugins => "ڼز...", + StartupStage.InitializingUI => "ڳʼ...", + StartupStage.ShellInitialized => "ѳʼ", + StartupStage.DesktopVisible => "Ѿɼ", + StartupStage.ActivationRedirected => "Ѽʵ", + StartupStage.ActivationFailed => "ʵʧ", + StartupStage.Ready => "", + _ => "ڼ..." }; /// - /// 获取项描述 - /// + /// 获取项描? /// private static string GetItemDescription(LoadingItem item) { if (!string.IsNullOrEmpty(item.Description)) @@ -268,8 +265,7 @@ public partial class LoadingDetailsWindow : Window } /// - /// 获取项图标 - /// + /// 获取项图? /// private static string GetItemIcon(LoadingItemType type) => type switch { LoadingItemType.Plugin => "\uE768", @@ -298,8 +294,7 @@ public partial class LoadingDetailsWindow : Window } /// -/// 加载项视图模型 -/// +/// 加载项视图模?/// public class LoadingItemViewModel : INotifyPropertyChanged { public string Id { get; } @@ -394,3 +389,4 @@ public class LoadingItemViewModel : INotifyPropertyChanged _ => new SolidColorBrush(Color.Parse("#616161")) }; } + diff --git a/LanMountainDesktop.Shared.Contracts/Launcher/LauncherIpc.cs b/LanMountainDesktop.Shared.Contracts/Launcher/LauncherIpc.cs index dbe7f71..c35ae94 100644 --- a/LanMountainDesktop.Shared.Contracts/Launcher/LauncherIpc.cs +++ b/LanMountainDesktop.Shared.Contracts/Launcher/LauncherIpc.cs @@ -1,89 +1,38 @@ namespace LanMountainDesktop.Shared.Contracts.Launcher; -/// -/// 启动阶段枚举 -/// public enum StartupStage { - /// - /// 初始化中 - /// Initializing, - - /// - /// 加载设置中 - /// LoadingSettings, - - /// - /// 加载插件中 - /// LoadingPlugins, - - /// - /// 初始化界面中 - /// InitializingUI, - - /// - /// 就绪 - /// + ShellInitialized, + DesktopVisible, + ActivationRedirected, + ActivationFailed, Ready } -/// -/// 启动进度消息 -/// public record StartupProgressMessage { - /// - /// 当前阶段 - /// public StartupStage Stage { get; init; } - - /// - /// 进度百分比 (0-100) - /// + public int ProgressPercent { get; init; } - - /// - /// 状态消息 - /// + public string? Message { get; init; } - - /// - /// 时间戳 - /// + public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow; } -/// -/// Launcher IPC 常量 -/// public static class LauncherIpcConstants { - /// - /// 命名管道名称 - /// public const string PipeName = "LanMountainDesktop_Launcher"; - - /// - /// Launcher 进程 ID 环境变量 - /// + public const string LauncherPidEnvVar = "LMD_LAUNCHER_PID"; - - /// - /// 包根目录环境变量 - /// + public const string PackageRootEnvVar = "LMD_PACKAGE_ROOT"; - - /// - /// 版本环境变量 - /// + public const string VersionEnvVar = "LMD_VERSION"; - - /// - /// 开发代号环境变量 - /// + public const string CodenameEnvVar = "LMD_CODENAME"; } diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 8effc69..1cbf1fd 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; @@ -79,6 +80,10 @@ public partial class App : Application private LoadingStateReporter? _loadingStateReporter; private bool _singleInstanceReleased; private int _forcedExitScheduled; + private bool _mainWindowOpened; + private bool _trayInitialized; + private readonly object _launcherProgressLock = new(); + private readonly List _pendingLauncherProgressMessages = []; internal static SingleInstanceService? CurrentSingleInstanceService { get; set; } internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle => @@ -86,10 +91,9 @@ public partial class App : Application internal static INotificationService? CurrentNotificationService => (Current as App)?._notificationService; - // 隐私政策查看事件 + // 闅愮鏀跨瓥鏌ョ湅浜嬩欢 public static event Action? CurrentPrivacyPolicyViewRequested; - // 触发隐私政策查看事件的方法 public static void RaisePrivacyPolicyViewRequested() { CurrentPrivacyPolicyViewRequested?.Invoke(); @@ -156,6 +160,7 @@ public partial class App : Application RegisterUiUnhandledExceptionGuard(); LinuxDesktopEntryInstaller.EnsureInstalled(); + _ = InitializeLauncherIpcAsync(); DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell); if (!Design.IsDesignMode && OperatingSystem.IsWindows()) @@ -164,37 +169,43 @@ public partial class App : Application } base.OnFrameworkInitializationCompleted(); - - // IPC 初始化移到窗口创建之后,避免 async void 中的 await 导致窗口创建延迟 - // 使用 fire-and-forget 模式,不阻塞主流程 - _ = InitializeLauncherIpcAsync(); } private async Task InitializeLauncherIpcAsync() { if (!LauncherIpcClient.IsLaunchedByLauncher()) return; - + try { _launcherIpcClient = new LauncherIpcClient(); var connected = await _launcherIpcClient.ConnectAsync(); - - if (connected) + if (!connected) { - AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server."); - - // 初始化加载状态管理器 - _loadingStateManager = new LoadingStateManager(); - _loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient); - _loadingStateReporter.Start(); - - // 注册系统初始化加载项 - _loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "系统初始化", "初始化系统核心组件"); - _loadingStateManager.StartItem("system.init", "已连接启动器"); - - ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化..."); - ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置..."); + return; + } + + AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server."); + + bool hadBufferedMessages; + lock (_launcherProgressLock) + { + hadBufferedMessages = _pendingLauncherProgressMessages.Count > 0; + } + + await FlushPendingLauncherProgressAsync(); + + _loadingStateManager = new LoadingStateManager(); + _loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient); + _loadingStateReporter.Start(); + + _loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "System Initialization", "Initialize core application services."); + _loadingStateManager.StartItem("system.init", "Launcher IPC connected."); + + if (!hadBufferedMessages) + { + ReportStartupProgress(StartupStage.Initializing, 10, "Initializing application..."); + ReportStartupProgress(StartupStage.LoadingSettings, 20, "Loading settings..."); } } catch (Exception ex) @@ -203,67 +214,86 @@ public partial class App : Application } } - /// - /// 向 Launcher 报告启动进度(fire-and-forget,不阻塞主流程) - /// private void ReportStartupProgress(StartupStage stage, int percent, string message) { - if (_launcherIpcClient is null) - return; - - _ = Task.Run(async () => + QueueOrSendLauncherProgress(new StartupProgressMessage { - try - { - await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage - { - Stage = stage, - ProgressPercent = percent, - Message = message - }); - } - catch (Exception ex) - { - AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}"); - } - }); + Stage = stage, + ProgressPercent = percent, + Message = message, + Timestamp = DateTimeOffset.UtcNow + }, logSuccess: false); } - /// - /// 向 Launcher 报告关键启动进度,使用后台线程避免阻塞 UI - /// 用于 Ready 等关键状态报告 - /// private void ReportStartupProgressSync(StartupStage stage, int percent, string message) { - if (_launcherIpcClient is null) - return; + QueueOrSendLauncherProgress(new StartupProgressMessage + { + Stage = stage, + ProgressPercent = percent, + Message = message, + Timestamp = DateTimeOffset.UtcNow + }, logSuccess: true); + } + private void QueueOrSendLauncherProgress(StartupProgressMessage message, bool logSuccess) + { + var ipcClient = _launcherIpcClient; + if (ipcClient is null || !ipcClient.IsConnected) + { + lock (_launcherProgressLock) + { + _pendingLauncherProgressMessages.Add(message); + } + + AppLogger.Info("LauncherIpc", $"Buffered launcher stage '{message.Stage}' because IPC is not connected yet."); + return; + } + + _ = SendLauncherProgressAsync(ipcClient, message, logSuccess); + } + + private async Task FlushPendingLauncherProgressAsync() + { + var ipcClient = _launcherIpcClient; + if (ipcClient is null || !ipcClient.IsConnected) + { + return; + } + + StartupProgressMessage[] pendingMessages; + lock (_launcherProgressLock) + { + pendingMessages = _pendingLauncherProgressMessages.ToArray(); + _pendingLauncherProgressMessages.Clear(); + } + + foreach (var pendingMessage in pendingMessages) + { + await SendLauncherProgressAsync(ipcClient, pendingMessage, logSuccess: false); + } + } + + private async Task SendLauncherProgressAsync(LauncherIpcClient ipcClient, StartupProgressMessage message, bool logSuccess) + { try { - _ = Task.Run(async () => + await ipcClient.ReportProgressAsync(message); + if (logSuccess) { - try - { - await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage - { - Stage = stage, - ProgressPercent = percent, - Message = message - }); - AppLogger.Info("LauncherIpc", $"Successfully reported stage: {stage}"); - } - catch (Exception ex) - { - AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}"); - } - }); + AppLogger.Info("LauncherIpc", $"Successfully reported stage: {message.Stage}"); + } } catch (Exception ex) { - AppLogger.Warn("LauncherIpc", $"Failed to launch progress report task: {ex.Message}"); + AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}"); + + lock (_launcherProgressLock) + { + _pendingLauncherProgressMessages.Add(message); + } } } - private void ApplyDesignTimeTheme() { RequestedThemeVariant = ThemeVariant.Light; @@ -289,7 +319,7 @@ public partial class App : Application // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins DisableAvaloniaDataAnnotationValidation(); desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown; - ReportStartupProgress(StartupStage.InitializingUI, 60, "正在初始化界面..."); + ReportStartupProgress(StartupStage.InitializingUI, 60, "姝e湪鍒濆鍖栫晫闈?.."); CreateAndAssignMainWindow(desktop, "FrameworkInitialization"); }, OnDesktopLifetimeExit, @@ -337,25 +367,21 @@ public partial class App : Application _ = sender; _ = e; - // 仅在 Windows 上支持融合桌面功能 if (!OperatingSystem.IsWindows()) { AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows."); return; } - // 切换进入编辑模式,隐藏常态零散的小部件 FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode(); - // 确保透明覆盖层窗口存在并显示 + // 纭繚閫忔槑瑕嗙洊灞傜獥鍙e瓨鍦ㄥ苟鏄剧ず EnsureTransparentOverlayWindow(); - // 打开融合桌面组件库窗口 Dispatcher.UIThread.Post(() => { try { - // 确保覆盖层窗口已显示(组件要渲染在上面,必须先 Show) if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible) { _transparentOverlayWindow.Show(); @@ -368,16 +394,15 @@ public partial class App : Application window.SetOverlayWindow(_transparentOverlayWindow); } - // 当组件库关闭时,退出编辑态 - window.Closed += (s, ev) => + window.Closed += (s, ev) => { if (_transparentOverlayWindow is not null) { - // 触发画布保存,并隐藏画布 + // 瑙﹀彂鐢诲竷淇濆瓨锛屽苟闅愯棌鐢诲竷 _transparentOverlayWindow.SaveLayoutAndHide(); } - // 让管理器根据已存储的最新快照重建生成所有实体小组件 + // 璁╃鐞嗗櫒鏍规嵁宸插瓨鍌ㄧ殑鏈€鏂板揩鐓ч噸寤虹敓鎴愭墍鏈夊疄浣撳皬缁勪欢 FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode(); }; @@ -434,7 +459,7 @@ public partial class App : Application private void InitializePluginRuntime() { - ReportStartupProgress(StartupStage.LoadingPlugins, 30, "正在加载插件..."); + ReportStartupProgress(StartupStage.LoadingPlugins, 30, "姝e湪鍔犺浇鎻掍欢..."); try { _pluginRuntimeService?.Dispose(); @@ -489,9 +514,12 @@ public partial class App : Application } RefreshTrayIconContent(); + _trayInitialized = true; + AppLogger.Info("TrayIcon", $"Tray initialized successfully. Pid={Environment.ProcessId}."); } catch (Exception ex) { + _trayInitialized = false; AppLogger.Warn("TrayIcon", "Failed to initialize tray icon.", ex); } } @@ -537,14 +565,12 @@ public partial class App : Application return; } - // 仅在 Windows 上支持融合桌面功能 if (!OperatingSystem.IsWindows()) { _trayComponentLibraryMenuItem.IsVisible = false; return; } - // 检查融合桌面功能是否启用 var appSnapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App); _trayComponentLibraryMenuItem.IsVisible = appSnapshot.EnableFusedDesktop; @@ -855,13 +881,12 @@ public partial class App : Application if (languageChanged) { - // 清除本地化缓存,强制重新加载语言文件 + // 娓呴櫎鏈湴鍖栫紦瀛橈紝寮哄埗閲嶆柊鍔犺浇璇█鏂囦欢 _localizationService.ClearCache(); ApplyCurrentCultureFromSettings(); RefreshTrayIconContent(); } - // 检查融合桌面设置是否变更 var fusedDesktopChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFusedDesktop), StringComparer.OrdinalIgnoreCase); @@ -1076,64 +1101,49 @@ public partial class App : Application ShowInTaskbar = true }; + _mainWindowOpened = false; AttachMainWindow(mainWindow); desktop.MainWindow = mainWindow; AppLogger.Info("App", $"Main window created. Reason='{reason}'. LogFile={AppLogger.LogFilePath}"); LogBrowserStartupDiagnostics(); SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"MainWindowCreated:{reason}"); - - // 延迟报告 Ready 直到窗口实际打开并可见 - // 使用 Opened 事件确保所有资源已加载完毕 + ReportStartupProgress(StartupStage.ShellInitialized, 85, "Desktop shell initialized."); + AppLogger.Info( + "App", + $"Shell initialized. Reason='{reason}'; TrayInitialized={_trayInitialized}; MainWindowVisible={mainWindow.IsVisible}."); + mainWindow.Opened += OnMainWindowOpened; - // 手动显示窗口,因为在 ShutdownMode.OnExplicitShutdown 模式下框架不会自动调用 Show if (!mainWindow.IsVisible) { mainWindow.Show(); } - - // 兜底机制:如果 Opened 事件 10 秒内未触发,强制发送 Ready 信号 - // 防止因渲染问题导致 Opened 不触发,启动器 Splash 窗口一直显示 + _ = Task.Run(async () => { - await Task.Delay(TimeSpan.FromSeconds(10)); - if (_launcherIpcClient is not null && _launcherIpcClient.IsConnected) + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); + if (!_mainWindowOpened) { - try - { - await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage - { - Stage = StartupStage.Ready, - ProgressPercent = 100, - Message = "就绪" - }); - AppLogger.Warn("App", "Ready signal sent via fallback (Opened event did not fire within 10s)"); - } - catch { } + AppLogger.Warn("App", "Main window Opened event did not fire within 10 seconds. DesktopVisible was not reported."); } }); - + return mainWindow; } - - /// - /// 主窗口打开完成事件 - 此时所有组件、资源及功能模块均已完全加载 - /// private void OnMainWindowOpened(object? sender, EventArgs e) { if (sender is MainWindow mainWindow) { mainWindow.Opened -= OnMainWindowOpened; - - AppLogger.Info("App", "Main window opened and ready. Reporting Ready to Launcher..."); - - // 完成系统初始化加载项 - _loadingStateManager?.CompleteItem("system.init", "系统初始化完成"); - - // 报告 Ready 状态,启动器可以安全关闭 Splash 窗口 - ReportStartupProgressSync(StartupStage.Ready, 100, "就绪"); - - // 停止加载状态上报 + _mainWindowOpened = true; + + AppLogger.Info( + "App", + $"Main window opened. Reporting DesktopVisible. TrayInitialized={_trayInitialized}; ShellState='{_desktopShellState}'."); + + _loadingStateManager?.CompleteItem("system.init", "System initialization completed."); + ReportStartupProgressSync(StartupStage.DesktopVisible, 100, "Desktop visible."); + ReportStartupProgressSync(StartupStage.Ready, 100, "Ready."); _loadingStateReporter?.Stop(); } } @@ -1327,3 +1337,5 @@ public partial class App : Application return _localizationService.GetString(languageCode, key, fallback); } } + + diff --git a/LanMountainDesktop/Program.cs b/LanMountainDesktop/Program.cs index c477e68..685fd75 100644 --- a/LanMountainDesktop/Program.cs +++ b/LanMountainDesktop/Program.cs @@ -8,6 +8,7 @@ using LanMountainDesktop.DesktopHost; using LanMountainDesktop.Models; using LanMountainDesktop.Plugins; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Launcher; using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Shared.Contracts.Launcher; @@ -33,6 +34,7 @@ public sealed class Program AppLogger.Warn( "Startup", $"Restart relaunch could not acquire the single-instance lock. pid={restartParentProcessId.Value}. Suppressing multi-open activation prompt."); + ReportLauncherStageBeforeExit(StartupStage.ActivationFailed, "Restart relaunch could not acquire the single-instance lock."); Environment.ExitCode = HostExitCodes.RestartLockNotAcquired; return; } @@ -43,6 +45,7 @@ public sealed class Program AppLogger.Info( "Startup", $"Secondary launch forwarded to primary instance successfully. Acked={activationAcknowledged}; Pid={Environment.ProcessId}."); + ReportLauncherStageBeforeExit(StartupStage.ActivationRedirected, "Secondary launch forwarded to the primary instance."); Environment.ExitCode = HostExitCodes.SecondaryActivationSucceeded; } else @@ -50,6 +53,9 @@ public sealed class Program AppLogger.Warn( "Startup", $"Secondary launch failed to activate the primary instance. Acked={activationAcknowledged}; Reason='{failureReason ?? "unknown"}'; Pid={Environment.ProcessId}."); + ReportLauncherStageBeforeExit( + StartupStage.ActivationFailed, + $"Secondary launch failed to activate the primary instance. Reason='{failureReason ?? "unknown"}'."); Environment.ExitCode = HostExitCodes.SecondaryActivationFailed; } @@ -247,6 +253,35 @@ public sealed class Program }; } + private static void ReportLauncherStageBeforeExit(StartupStage stage, string message) + { + if (!LauncherIpcClient.IsLaunchedByLauncher()) + { + return; + } + + try + { + using var launcherIpcClient = new LauncherIpcClient(); + var connected = launcherIpcClient.ConnectAsync().GetAwaiter().GetResult(); + if (!connected) + { + return; + } + + launcherIpcClient.ReportProgressAsync(new StartupProgressMessage + { + Stage = stage, + ProgressPercent = 100, + Message = message + }).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + AppLogger.Warn("LauncherIpc", $"Failed to report early launcher stage '{stage}'.", ex); + } + } + private static void InitializeTelemetryIdentity() { try diff --git a/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs b/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs index f05c200..675398a 100644 --- a/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs +++ b/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs @@ -13,6 +13,11 @@ namespace LanMountainDesktop.Services.Launcher; /// public class LauncherIpcClient : IDisposable { + private static readonly JsonSerializerOptions StartupProgressJsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + private NamedPipeClientStream? _pipeClient; private bool _isConnected; private readonly object _writeLock = new(); @@ -65,7 +70,7 @@ public class LauncherIpcClient : IDisposable try { - var json = JsonSerializer.Serialize(message); + var json = JsonSerializer.Serialize(message, StartupProgressJsonOptions); var payload = System.Text.Encoding.UTF8.GetBytes(json); // 长度前缀协议:[4字节长度][消息正文]