Refactor launcher startup, logging & host resolution

Improve launcher startup flow, logging, and host resolution. Key changes: add detailed startup logging and standardized preview messages; unify CLI vs GUI handling and error/result reporting (write result file when requested); refactor DeploymentLocator to a more robust host resolution (new HostResolutionResult, explicit/portable/published/debug resolution paths, legacy fallback); overhaul LauncherFlowCoordinator to better handle IPC stages, activation retries, window lifecycle, plugin/update flows and error reporting; add CommandContext helpers (IsGui/IsPreview/ExplicitAppRoot) and JSON context options; tighten async usage and ConfigureAwait calls; add better UI error handling and consistent exit codes. Several UX/debug conveniences and robustness fixes included.
This commit is contained in:
lincube
2026-04-22 07:31:54 +08:00
parent 5af7ac8b56
commit 703ed7b48a
13 changed files with 1172 additions and 867 deletions

View File

@@ -13,10 +13,13 @@ public partial class App : Application
{ {
public override void Initialize() public override void Initialize()
{ {
// 初始化日志记录器
Logger.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 ?? "<none>"}'.");
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
@@ -24,41 +27,29 @@ public partial class App : Application
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
var context = LauncherRuntimeContext.Current; 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)) if (HandlePreviewCommand(context, desktop))
{ {
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
return; return;
} }
// apply-update 模式:显示 UpdateWindow执行增量更新 + 插件升级
if (string.Equals(context.Command, "apply-update", StringComparison.OrdinalIgnoreCase)) if (string.Equals(context.Command, "apply-update", StringComparison.OrdinalIgnoreCase))
{ {
// 先显示窗口,再启动后台任务
var updateWindow = new UpdateWindow(); var updateWindow = new UpdateWindow();
updateWindow.Show(); updateWindow.Show();
_ = RunApplyUpdateWithWindowAsync(desktop, context, updateWindow); _ = RunApplyUpdateWithWindowAsync(desktop, context, updateWindow);
} }
else else
{ {
// 先显示 Splash 窗口,确保应用程序不会立即退出
var splashWindow = new SplashWindow(); var splashWindow = new SplashWindow();
splashWindow.Show(); splashWindow.Show();
// 在 try-catch 块中实例化所有服务,确保任何异常都能被捕获
_ = RunCoordinatorWithSplashAsync(desktop, context, splashWindow); _ = RunCoordinatorWithSplashAsync(desktop, context, splashWindow);
} }
} }
@@ -66,156 +57,127 @@ public partial class App : Application
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
/// <summary>
/// 处理界面预览命令
/// </summary>
private bool HandlePreviewCommand(CommandContext context, IClassicDesktopStyleApplicationLifetime desktop) private bool HandlePreviewCommand(CommandContext context, IClassicDesktopStyleApplicationLifetime desktop)
{ {
var command = context.Command.ToLowerInvariant(); switch (context.Command.ToLowerInvariant())
switch (command)
{ {
case "preview-splash": case "preview-splash":
Console.WriteLine("[Launcher] Preview mode: SplashWindow"); {
Logger.Info("Preview command: splash.");
var splashWindow = new SplashWindow(); var splashWindow = new SplashWindow();
splashWindow.SetDebugMode(true); splashWindow.SetDebugMode(true);
splashWindow.Show(); splashWindow.Show();
_ = SimulateSplashPreviewAsync(desktop, splashWindow); _ = SimulateSplashPreviewAsync(desktop, splashWindow);
return true; return true;
}
case "preview-error": case "preview-error":
Console.WriteLine("[Launcher] Preview mode: ErrorWindow"); {
Logger.Info("Preview command: error.");
var errorWindow = new ErrorWindow(); var errorWindow = new ErrorWindow();
errorWindow.SetErrorMessage("[预览模式] 这是一个错误页面预览。\n\n用于查看错误页面的样式和布局。"); errorWindow.SetErrorMessage("[Preview] This is the launcher error window preview.");
errorWindow.Show(); errorWindow.Show();
_ = WaitForWindowCloseAsync(desktop, errorWindow); _ = WaitForWindowCloseAsync(desktop, errorWindow);
return true; return true;
}
case "preview-update": case "preview-update":
Console.WriteLine("[Launcher] Preview mode: UpdateWindow"); {
Logger.Info("Preview command: update.");
var updateWindow = new UpdateWindow(); var updateWindow = new UpdateWindow();
updateWindow.SetDebugMode(true); updateWindow.SetDebugMode(true);
updateWindow.Show(); updateWindow.Show();
_ = SimulateUpdatePreviewAsync(desktop, updateWindow); _ = SimulateUpdatePreviewAsync(desktop, updateWindow);
return true; return true;
}
case "preview-oobe": case "preview-oobe":
Console.WriteLine("[Launcher] Preview mode: OobeWindow"); {
Logger.Info("Preview command: oobe.");
var oobeWindow = new OobeWindow(); var oobeWindow = new OobeWindow();
oobeWindow.Show(); oobeWindow.Show();
_ = SimulateOobePreviewAsync(desktop, oobeWindow); _ = SimulateOobePreviewAsync(desktop, oobeWindow);
return true; return true;
}
case "preview-debug": case "preview-debug":
Console.WriteLine("[Launcher] Preview mode: DevDebugWindow"); {
Logger.Info("Preview command: debug window.");
var devDebugWindow = new DevDebugWindow(); var devDebugWindow = new DevDebugWindow();
devDebugWindow.Show(); devDebugWindow.Show();
return true; return true;
}
default: default:
return false; return false;
} }
} }
/// <summary>
/// 模拟 Splash 窗口预览
/// </summary>
private async Task SimulateSplashPreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, SplashWindow window) private async Task SimulateSplashPreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, SplashWindow window)
{ {
var stages = new[] { "initializing", "update", "plugins", "launch", "ready" }; 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; 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]); reporter.Report(stages[i], messages[i]);
await Task.Delay(800); await Task.Delay(800).ConfigureAwait(false);
} }
// 等待5秒后自动关闭 await Task.Delay(5000).ConfigureAwait(false);
await Task.Delay(5000);
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0)); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0));
} }
/// <summary>
/// 模拟 Update 窗口预览
/// </summary>
private async Task SimulateUpdatePreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, UpdateWindow window) private async Task SimulateUpdatePreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, UpdateWindow window)
{ {
var stages = new[] { "verify", "extract", "apply", "plugins", "cleanup" }; 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); window.Report(stages[i], $"Processing {stages[i]}...", (i + 1) * 20);
await Task.Delay(600); await Task.Delay(600).ConfigureAwait(false);
} }
window.ReportComplete(true, null); window.ReportComplete(true, null);
await Task.Delay(3000).ConfigureAwait(false);
// 等待3秒后自动关闭
await Task.Delay(3000);
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0)); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0));
string GetStageName(string stage) => stage switch
{
"verify" => "验证",
"extract" => "解压",
"apply" => "应用",
"plugins" => "升级插件",
"cleanup" => "清理",
_ => stage
};
} }
/// <summary>
/// 模拟 OOBE 窗口预览
/// </summary>
private async Task SimulateOobePreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, OobeWindow window) private async Task SimulateOobePreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, OobeWindow window)
{ {
try try
{ {
// 等待用户点击开始按钮 await window.WaitForEnterAsync().ConfigureAwait(false);
await window.WaitForEnterAsync(); Logger.Info("OOBE preview completed by user.");
Console.WriteLine("[Launcher] OOBE preview completed by user");
} }
catch (Exception ex) 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)); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0));
} }
/// <summary>
/// 等待窗口关闭
/// </summary>
private async Task WaitForWindowCloseAsync(IClassicDesktopStyleApplicationLifetime desktop, Window window) private async Task WaitForWindowCloseAsync(IClassicDesktopStyleApplicationLifetime desktop, Window window)
{ {
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
window.Closed += (s, e) => tcs.TrySetResult(); window.Closed += (_, _) => tcs.TrySetResult();
await tcs.Task; await tcs.Task.ConfigureAwait(false);
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0)); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0));
} }
private static async Task RunCoordinatorWithSplashAsync( private static async Task RunCoordinatorWithSplashAsync(
IClassicDesktopStyleApplicationLifetime desktop, IClassicDesktopStyleApplicationLifetime desktop,
CommandContext context, CommandContext context,
SplashWindow splashWindow) SplashWindow splashWindow)
{ {
LauncherResult result; LauncherResult result;
ErrorWindow? errorWindow = null;
LauncherFlowCoordinator? coordinator = null;
try try
{ {
// 在 try-catch 块中实例化所有服务,确保异常被捕获
var appRoot = Commands.ResolveAppRoot(context); var appRoot = Commands.ResolveAppRoot(context);
Logger.Info(
$"Coordinator start. Command='{context.Command}'; AppRoot='{appRoot}'; " +
$"IsDebugMode={context.IsDebugMode}; ResultPath='{context.GetOption("result") ?? "<none>"}'.");
var deploymentLocator = new DeploymentLocator(appRoot); var deploymentLocator = new DeploymentLocator(appRoot);
var coordinator = new LauncherFlowCoordinator(
// TODO: 从配置读取 GitHub 仓库信息
coordinator = new LauncherFlowCoordinator(
context, context,
deploymentLocator, deploymentLocator,
new OobeStateService(appRoot), new OobeStateService(appRoot),
@@ -226,88 +188,85 @@ public partial class App : Application
} }
catch (Exception ex) catch (Exception ex)
{ {
// 捕获异常并显示错误窗口 Logger.Error("Coordinator threw an unhandled exception.", ex);
result = new LauncherResult result = new LauncherResult
{ {
Success = false, Success = false,
Stage = "launch", Stage = "launch",
Code = "exception", Code = "exception",
Message = $"启动器发生错误: {ex.Message}", Message = $"Launcher failed: {ex.Message}",
ErrorMessage = ex.ToString() 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; Environment.ExitCode = result.Success ? 0 : 1;
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background);
} }
/// <summary> private static async Task WriteLauncherResultAsync(CommandContext context, LauncherResult result)
/// apply-update 模式:执行增量更新和插件升级,完成后自动退出 {
/// </summary> 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( private static async Task RunApplyUpdateWithWindowAsync(
IClassicDesktopStyleApplicationLifetime desktop, IClassicDesktopStyleApplicationLifetime desktop,
CommandContext context, CommandContext context,
@@ -324,8 +283,7 @@ public partial class App : Application
try try
{ {
// 1. 应用增量更新 await Dispatcher.UIThread.InvokeAsync(() => window.Report("verify", "Verifying update...", 10));
await Dispatcher.UIThread.InvokeAsync(() => window.Report("verify", "正在验证更新...", 10));
var updateResult = await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false); var updateResult = await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false);
if (!updateResult.Success) if (!updateResult.Success)
{ {
@@ -333,24 +291,20 @@ public partial class App : Application
errorMessage = updateResult.Message; errorMessage = updateResult.Message;
} }
// 2. 应用待处理的插件升级
if (success) if (success)
{ {
await Dispatcher.UIThread.InvokeAsync(() => window.Report("plugins", "正在升级插件...", 60)); await Dispatcher.UIThread.InvokeAsync(() => window.Report("plugins", "Applying plugin upgrades...", 60));
var pluginsDir = context.GetOption("plugins-dir") var pluginsDir = context.GetOption("plugins-dir") ?? Path.Combine(appRoot, "plugins");
?? Path.Combine(appRoot, "plugins");
var queueResult = pluginUpgrades.ApplyPendingUpgrades(pluginsDir); var queueResult = pluginUpgrades.ApplyPendingUpgrades(pluginsDir);
if (!queueResult.Success && queueResult.Code != "noop") if (!queueResult.Success && queueResult.Code != "noop")
{ {
// 插件升级失败不阻断整体流程,仅记录到控制台 Logger.Error($"Plugin upgrade failed during apply-update: {queueResult.Message}");
Console.Error.WriteLine($"Plugin upgrade had failures: {queueResult.Message}");
} }
} }
// 3. 清理旧版本保留至少3个版本以支持回滚
if (success) 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); deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
} }
} }
@@ -358,21 +312,11 @@ public partial class App : Application
{ {
success = false; success = false;
errorMessage = ex.Message; errorMessage = ex.Message;
Logger.Error("Apply-update flow failed.", ex);
} }
// 显示完成状态,短暂停留后关闭
await Dispatcher.UIThread.InvokeAsync(() => window.ReportComplete(success, errorMessage)); await Dispatcher.UIThread.InvokeAsync(() => window.ReportComplete(success, errorMessage));
await Task.Delay(success ? 1500 : 5000).ConfigureAwait(false);
if (success)
{
// 成功:停留 1.5 秒让用户看到"更新完成"
await Task.Delay(1500);
}
else
{
// 失败:停留 5 秒让用户看到错误信息
await Task.Delay(5000);
}
await Commands.WriteResultIfNeededAsync(context.GetOption("result"), new LauncherResult await Commands.WriteResultIfNeededAsync(context.GetOption("result"), new LauncherResult
{ {
@@ -385,6 +329,4 @@ public partial class App : Application
Environment.ExitCode = success ? 0 : 1; Environment.ExitCode = success ? 0 : 1;
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background); await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background);
} }
} }

View File

@@ -6,7 +6,10 @@ using LanMountainDesktop.Shared.Contracts.Launcher;
namespace LanMountainDesktop.Launcher; namespace LanMountainDesktop.Launcher;
[JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(SignedFileMap))] [JsonSerializable(typeof(SignedFileMap))]
[JsonSerializable(typeof(UpdateFileEntry))] [JsonSerializable(typeof(UpdateFileEntry))]
[JsonSerializable(typeof(PlondsUpdateMetadata))] [JsonSerializable(typeof(PlondsUpdateMetadata))]

View File

@@ -4,6 +4,17 @@ namespace LanMountainDesktop.Launcher;
internal sealed class CommandContext 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 Command { get; }
public string SubCommand { get; } public string SubCommand { get; }
@@ -28,6 +39,14 @@ internal sealed class CommandContext
Options.ContainsKey("debug") || Options.ContainsKey("debug") ||
System.Diagnostics.Debugger.IsAttached; 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<string, string> options, string[] rawArgs) private CommandContext(string command, string subCommand, Dictionary<string, string> options, string[] rawArgs)
{ {
Command = command; Command = command;

View File

@@ -10,32 +10,54 @@ internal static class Program
private static async Task<int> Main(string[] args) private static async Task<int> Main(string[] args)
{ {
var commandContext = CommandContext.FromArgs(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 ?? "<none>"}'.");
// 处理遗留插件安装命令 try
if (commandContext.IsLegacyPluginInstall)
{ {
var installer = new PluginInstallerService(); if (commandContext.IsLegacyPluginInstall)
return await Commands.RunLegacyPluginInstallAsync(commandContext, installer).ConfigureAwait(false); {
} 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; LauncherRuntimeContext.Current = commandContext;
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
return Environment.ExitCode; return Environment.ExitCode;
} }
catch (Exception ex)
// 处理其他 CLI 命令 (update, plugin, rollback 等)
if (!string.Equals(commandContext.Command, "launch", StringComparison.OrdinalIgnoreCase))
{ {
return await Commands.RunCliCommandAsync(commandContext).ConfigureAwait(false); Logger.Error("Launcher failed before GUI flow completed.", ex);
}
// 主启动流程: OOBE -> Splash -> 版本选择 -> 启动主程序 var result = new LauncherResult
LauncherRuntimeContext.Current = commandContext; {
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); Success = false,
return Environment.ExitCode; Stage = "launcher",
Code = "launcher_bootstrap_failed",
Message = ex.Message,
ErrorMessage = ex.ToString(),
Details = new Dictionary<string, string>(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() private static AppBuilder BuildAvaloniaApp()

View File

@@ -33,7 +33,6 @@ internal sealed class DeploymentLocator
var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly); var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly);
Console.WriteLine($"[DeploymentLocator] Found {candidates.Length} app-* directories"); Console.WriteLine($"[DeploymentLocator] Found {candidates.Length} app-* directories");
// ClassIsland 风格的查询:先筛选,后排序
var validInstallations = candidates var validInstallations = candidates
.Where(path => .Where(path =>
{ {
@@ -79,38 +78,199 @@ internal sealed class DeploymentLocator
} }
} }
public string? ResolveHostExecutablePath() public HostResolutionResult ResolveHostExecutable(CommandContext context)
{ {
// 使用新的灵活定位器 ArgumentNullException.ThrowIfNull(context);
var options = new HostDiscoveryOptions
{
ExecutableName = "LanMountainDesktop",
PreferDevModeConfig = true,
RecursiveSearch = false, // 默认不启用递归搜索以提高性能
AdditionalSearchPaths = new List<string>
{
// 可以通过配置文件或环境变量添加更多路径
"${AppRoot}",
"${AppRoot}/..",
"${BaseDirectory}/../..",
}
};
var locator = new FlexibleHostLocator(_appRoot, options); var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
var result = locator.ResolveHostExecutablePath(); var searchedPaths = new List<string>();
var explicitAppRoot = context.ExplicitAppRoot;
if (result != null) 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(); return ResolveHostExecutablePathLegacy();
} }
/// <summary> private string? TryResolveExplicitAppRoot(
/// 传统的主程序路径解析(作为备选) string explicitRoot,
/// </summary> string executable,
List<string> 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<string> 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<string> 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<string> 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() private string? ResolveHostExecutablePathLegacy()
{ {
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop"; var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
@@ -126,14 +286,12 @@ internal sealed class DeploymentLocator
} }
} }
// 2. 查找 Launcher 所在目录(开发环境 - 直接运行)
var inRoot = Path.Combine(_appRoot, executable); var inRoot = Path.Combine(_appRoot, executable);
if (File.Exists(inRoot)) if (File.Exists(inRoot))
{ {
return inRoot; return inRoot;
} }
// 3. 查找父目录(开发环境 - 从 Launcher 项目运行)
var parent = Path.GetFullPath(Path.Combine(_appRoot, "..")); var parent = Path.GetFullPath(Path.Combine(_appRoot, ".."));
var inParent = Path.Combine(parent, executable); var inParent = Path.Combine(parent, executable);
if (File.Exists(inParent)) if (File.Exists(inParent))
@@ -144,14 +302,12 @@ internal sealed class DeploymentLocator
// 4. å¼€å<E282AC>模å¼<C3A5>ï¼šå¦æžœå<C593>¯ç”¨äº†å¼€å<E282AC>模å¼<C3A5>,优先使用ä¿<C3A4>存的自定义路径 // 4. å¼€å<E282AC>模å¼<C3A5>ï¼šå¦æžœå<C593>¯ç”¨äº†å¼€å<E282AC>模å¼<C3A5>,优先使用ä¿<C3A4>存的自定义路径
if (Views.ErrorWindow.CheckDevModeEnabled()) if (Views.ErrorWindow.CheckDevModeEnabled())
{ {
// 4.1 首先检查保存的自定义路径
var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath(); var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath();
if (!string.IsNullOrWhiteSpace(savedCustomPath) && File.Exists(savedCustomPath)) if (!string.IsNullOrWhiteSpace(savedCustomPath) && File.Exists(savedCustomPath))
{ {
return savedCustomPath; return savedCustomPath;
} }
// 4.2 扫描开发路径
var devPath = ScanDevelopmentPaths(executable); var devPath = ScanDevelopmentPaths(executable);
if (!string.IsNullOrWhiteSpace(devPath)) if (!string.IsNullOrWhiteSpace(devPath))
{ {
@@ -179,7 +335,7 @@ internal sealed class DeploymentLocator
{ {
var possiblePaths = new[] var possiblePaths = new[]
{ {
// Launcher 项目运行 // ä»?Launcher 项ç®è¿<EFBFBD>行
Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable), Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable), Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
@@ -203,17 +359,14 @@ internal sealed class DeploymentLocator
} }
/// <summary> /// <summary>
/// 获取开发环境可能的主程序路径 /// 获å<EFBFBD>å¼€å<EFBFBD>环境å<EFBFBD>¯èƒ½çš„主ç¨åº<EFBFBD>è·¯å¾? /// </summary>
/// </summary>
private static IEnumerable<string> GetDevelopmentPaths(string executable) private static IEnumerable<string> GetDevelopmentPaths(string executable)
{ {
// 获取 Launcher 所在目录
var launcherDir = AppContext.BaseDirectory; var launcherDir = AppContext.BaseDirectory;
// 可能的开发目录结构
var possiblePaths = new[] var possiblePaths = new[]
{ {
// Launcher 项目运行:..\LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe // ä»?Launcher 项ç®è¿<EFBFBD>行ï¼?.\LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable), Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Release", "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", "Debug", "net10.0", executable),
Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable), Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
// dev-test 目录运行 // ä»?dev-test ç®å½•è¿<EFBFBD>行
Path.Combine(launcherDir, "..", "dev-test", "app-1.0.0-dev", executable), Path.Combine(launcherDir, "..", "dev-test", "app-1.0.0-dev", executable),
}; };
@@ -256,9 +409,8 @@ internal sealed class DeploymentLocator
} }
/// <summary> /// <summary>
/// 清理旧版本部署保留最近的N个版本 /// 清ç<EFBFBD>†æ—§ç‰ˆæœ¬éƒ¨ç½²ï¼Œä¿<EFBFBD>留最è¿çš„N个版æœ? /// </summary>
/// </summary> /// <param name="minVersionsToKeep">最å°ä¿<C3A4>留版本数,默è®?ä¸?/param>
/// <param name="minVersionsToKeep">最少保留版本数默认3个</param>
public void CleanupOldDeployments(int minVersionsToKeep = 3) public void CleanupOldDeployments(int minVersionsToKeep = 3)
{ {
try try
@@ -272,7 +424,6 @@ internal sealed class DeploymentLocator
var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly); var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly);
// 过滤掉无效部署目录排除partial按版本排序
var validDeployments = candidates var validDeployments = candidates
.Where(path => !File.Exists(Path.Combine(path, ".partial"))) .Where(path => !File.Exists(Path.Combine(path, ".partial")))
.Select(path => new .Select(path => new
@@ -349,7 +500,6 @@ internal sealed class DeploymentLocator
{ {
if (versionsToKeep.Contains(deployment.Path)) if (versionsToKeep.Contains(deployment.Path))
{ {
// 保留此版本如果之前标记了destroy则取消标记
if (deployment.IsDestroyed) if (deployment.IsDestroyed)
{ {
try try
@@ -365,7 +515,6 @@ internal sealed class DeploymentLocator
continue; continue;
} }
// 如果还没标记destroy的先标记
if (!deployment.IsDestroyed) if (!deployment.IsDestroyed)
{ {
try try
@@ -387,7 +536,7 @@ internal sealed class DeploymentLocator
} }
catch catch
{ {
// 忽略删除失败(可能文件被占用),下次启动再试 // 忽略删除失败(å<>¯èƒ½æ‡ä»¶è¢«å<C2AB> ç”?,䏿¬¡å<C2A1>¯åЍå†<C3A5>试
Console.WriteLine($"[DeploymentLocator] Failed to delete (will retry later): {deployment.Path}"); Console.WriteLine($"[DeploymentLocator] Failed to delete (will retry later): {deployment.Path}");
} }
} }
@@ -400,7 +549,7 @@ internal sealed class DeploymentLocator
} }
/// <summary> /// <summary>
/// 仅清理已标记为.destroy的部署兼容旧方法 /// 仅清ç<EFBFBD>†å·²æ ‡è®°ä¸?destroyçš„éƒ¨ç½²ï¼ˆå…¼å®¹æ—§æ¹æ³•)
/// </summary> /// </summary>
[Obsolete("Use CleanupOldDeployments instead")] [Obsolete("Use CleanupOldDeployments instead")]
public void CleanupDestroyedDeployments() public void CleanupDestroyedDeployments()
@@ -432,8 +581,7 @@ internal sealed class DeploymentLocator
} }
/// <summary> /// <summary>
/// 从部署目录读取版本信息 /// 从部署ç®å½•读å<EFBFBD>版本信æ<EFBFBD>? /// </summary>
/// </summary>
public AppVersionInfo GetVersionInfo() public AppVersionInfo GetVersionInfo()
{ {
var deploymentDir = FindCurrentDeploymentDirectory(); var deploymentDir = FindCurrentDeploymentDirectory();
@@ -453,16 +601,16 @@ internal sealed class DeploymentLocator
} }
catch catch
{ {
// 忽略读取失败,回退到默认值
} }
} }
} }
// 回退:从目录名解析版本,使用默认开发代号
return new AppVersionInfo return new AppVersionInfo
{ {
Version = GetCurrentVersion(), Version = GetCurrentVersion(),
Codename = "Administrate" // 默认开发代号 Codename = "Administrate"
}; };
} }
} }

View File

@@ -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<string> SearchedPaths { get; init; } = [];
}

View File

@@ -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();
}
});
}
}

View File

@@ -23,14 +23,12 @@ public partial class LoadingDetailsWindow : Window
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
// 初始化列表
var itemsList = this.FindControl<ItemsControl>("LoadingItemsList"); var itemsList = this.FindControl<ItemsControl>("LoadingItemsList");
if (itemsList != null) if (itemsList != null)
{ {
itemsList.ItemsSource = _items; itemsList.ItemsSource = _items;
} }
// 创建更新定时器
_updateTimer = new DispatcherTimer _updateTimer = new DispatcherTimer
{ {
Interval = TimeSpan.FromMilliseconds(100) Interval = TimeSpan.FromMilliseconds(100)
@@ -59,8 +57,7 @@ public partial class LoadingDetailsWindow : Window
} }
/// <summary> /// <summary>
/// 更新加载状态 /// 鏇存柊鍔犺浇鐘舵€? /// </summary>
/// </summary>
public void UpdateLoadingState(LoadingStateMessage state) public void UpdateLoadingState(LoadingStateMessage state)
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
@@ -73,7 +70,6 @@ public partial class LoadingDetailsWindow : Window
// 鏇存柊鏁翠綋杩涘害 // 鏇存柊鏁翠綋杩涘害
UpdateOverallProgress(state); UpdateOverallProgress(state);
// 更新当前活动项
UpdateCurrentItem(state); UpdateCurrentItem(state);
// 鏇存柊鍒楄〃 // 鏇存柊鍒楄〃
@@ -124,8 +120,7 @@ public partial class LoadingDetailsWindow : Window
} }
/// <summary> /// <summary>
/// 更新当前活动项 /// 鏇存柊褰撳墠娲诲姩椤? /// </summary>
/// </summary>
private void UpdateCurrentItem(LoadingStateMessage state) private void UpdateCurrentItem(LoadingStateMessage state)
{ {
var currentItem = state.ActiveItems.FirstOrDefault(); var currentItem = state.ActiveItems.FirstOrDefault();
@@ -162,7 +157,6 @@ public partial class LoadingDetailsWindow : Window
/// </summary> /// </summary>
private void UpdateItemsList(LoadingStateMessage state) private void UpdateItemsList(LoadingStateMessage state)
{ {
// 同步列表项
foreach (var item in state.ActiveItems) foreach (var item in state.ActiveItems)
{ {
var existing = _items.FirstOrDefault(i => i.Id == item.Id); var existing = _items.FirstOrDefault(i => i.Id == item.Id);
@@ -187,7 +181,7 @@ public partial class LoadingDetailsWindow : Window
} }
} }
// 按状态排序:进行中 -> 等待中 -> 已完成 -> 失败 // 鎸夌姸鎬佹帓搴忥細杩涜<EFBFBD>涓?-> 绛夊緟涓?-> 宸插畬鎴?-> 澶辫触
var sortedItems = _items.OrderBy(i => GetStatePriority(i.State)).ToList(); var sortedItems = _items.OrderBy(i => GetStatePriority(i.State)).ToList();
_items.Clear(); _items.Clear();
foreach (var item in sortedItems) foreach (var item in sortedItems)
@@ -240,17 +234,20 @@ public partial class LoadingDetailsWindow : Window
/// </summary> /// </summary>
private static string GetStageDescription(StartupStage stage) => stage switch private static string GetStageDescription(StartupStage stage) => stage switch
{ {
StartupStage.Initializing => "正在初始化系统...", StartupStage.Initializing => "正在初始化系统...",
StartupStage.LoadingSettings => "正在加载设置...", StartupStage.LoadingSettings => "正在加载设置...",
StartupStage.LoadingPlugins => "正在加载插件...", StartupStage.LoadingPlugins => "正在加载插件...",
StartupStage.InitializingUI => "正在初始化界面...", StartupStage.InitializingUI => "正在初始化界面...",
StartupStage.Ready => "加载完成", StartupStage.ShellInitialized => "桌面外壳已初始化",
_ => "正在加载..." StartupStage.DesktopVisible => "桌面已经可见",
StartupStage.ActivationRedirected => "已激活现有实例",
StartupStage.ActivationFailed => "现有实例激活失败",
StartupStage.Ready => "加载完成",
_ => "正在加载..."
}; };
/// <summary> /// <summary>
/// 获取项描述 /// 鑾峰彇椤规弿杩? /// </summary>
/// </summary>
private static string GetItemDescription(LoadingItem item) private static string GetItemDescription(LoadingItem item)
{ {
if (!string.IsNullOrEmpty(item.Description)) if (!string.IsNullOrEmpty(item.Description))
@@ -268,8 +265,7 @@ public partial class LoadingDetailsWindow : Window
} }
/// <summary> /// <summary>
/// 获取项图标 /// 鑾峰彇椤瑰浘鏍? /// </summary>
/// </summary>
private static string GetItemIcon(LoadingItemType type) => type switch private static string GetItemIcon(LoadingItemType type) => type switch
{ {
LoadingItemType.Plugin => "\uE768", LoadingItemType.Plugin => "\uE768",
@@ -298,8 +294,7 @@ public partial class LoadingDetailsWindow : Window
} }
/// <summary> /// <summary>
/// 加载项视图模型 /// 鍔犺浇椤硅<EFBFBD>鍥炬ā鍨?/// </summary>
/// </summary>
public class LoadingItemViewModel : INotifyPropertyChanged public class LoadingItemViewModel : INotifyPropertyChanged
{ {
public string Id { get; } public string Id { get; }
@@ -394,3 +389,4 @@ public class LoadingItemViewModel : INotifyPropertyChanged
_ => new SolidColorBrush(Color.Parse("#616161")) _ => new SolidColorBrush(Color.Parse("#616161"))
}; };
} }

View File

@@ -1,89 +1,38 @@
namespace LanMountainDesktop.Shared.Contracts.Launcher; namespace LanMountainDesktop.Shared.Contracts.Launcher;
/// <summary>
/// 启动阶段枚举
/// </summary>
public enum StartupStage public enum StartupStage
{ {
/// <summary>
/// 初始化中
/// </summary>
Initializing, Initializing,
/// <summary>
/// 加载设置中
/// </summary>
LoadingSettings, LoadingSettings,
/// <summary>
/// 加载插件中
/// </summary>
LoadingPlugins, LoadingPlugins,
/// <summary>
/// 初始化界面中
/// </summary>
InitializingUI, InitializingUI,
ShellInitialized,
/// <summary> DesktopVisible,
/// 就绪 ActivationRedirected,
/// </summary> ActivationFailed,
Ready Ready
} }
/// <summary>
/// 启动进度消息
/// </summary>
public record StartupProgressMessage public record StartupProgressMessage
{ {
/// <summary>
/// 当前阶段
/// </summary>
public StartupStage Stage { get; init; } public StartupStage Stage { get; init; }
/// <summary>
/// 进度百分比 (0-100)
/// </summary>
public int ProgressPercent { get; init; } public int ProgressPercent { get; init; }
/// <summary>
/// 状态消息
/// </summary>
public string? Message { get; init; } public string? Message { get; init; }
/// <summary>
/// 时间戳
/// </summary>
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow; public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
} }
/// <summary>
/// Launcher IPC 常量
/// </summary>
public static class LauncherIpcConstants public static class LauncherIpcConstants
{ {
/// <summary>
/// 命名管道名称
/// </summary>
public const string PipeName = "LanMountainDesktop_Launcher"; public const string PipeName = "LanMountainDesktop_Launcher";
/// <summary>
/// Launcher 进程 ID 环境变量
/// </summary>
public const string LauncherPidEnvVar = "LMD_LAUNCHER_PID"; public const string LauncherPidEnvVar = "LMD_LAUNCHER_PID";
/// <summary>
/// 包根目录环境变量
/// </summary>
public const string PackageRootEnvVar = "LMD_PACKAGE_ROOT"; public const string PackageRootEnvVar = "LMD_PACKAGE_ROOT";
/// <summary>
/// 版本环境变量
/// </summary>
public const string VersionEnvVar = "LMD_VERSION"; public const string VersionEnvVar = "LMD_VERSION";
/// <summary>
/// 开发代号环境变量
/// </summary>
public const string CodenameEnvVar = "LMD_CODENAME"; public const string CodenameEnvVar = "LMD_CODENAME";
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -79,6 +80,10 @@ public partial class App : Application
private LoadingStateReporter? _loadingStateReporter; private LoadingStateReporter? _loadingStateReporter;
private bool _singleInstanceReleased; private bool _singleInstanceReleased;
private int _forcedExitScheduled; private int _forcedExitScheduled;
private bool _mainWindowOpened;
private bool _trayInitialized;
private readonly object _launcherProgressLock = new();
private readonly List<StartupProgressMessage> _pendingLauncherProgressMessages = [];
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; } internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle => internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
@@ -86,10 +91,9 @@ public partial class App : Application
internal static INotificationService? CurrentNotificationService => internal static INotificationService? CurrentNotificationService =>
(Current as App)?._notificationService; (Current as App)?._notificationService;
// 隐私政策查看事件 // 闅愮鏀跨瓥鏌ョ湅浜嬩欢
public static event Action? CurrentPrivacyPolicyViewRequested; public static event Action? CurrentPrivacyPolicyViewRequested;
// 触发隐私政策查看事件的方法
public static void RaisePrivacyPolicyViewRequested() public static void RaisePrivacyPolicyViewRequested()
{ {
CurrentPrivacyPolicyViewRequested?.Invoke(); CurrentPrivacyPolicyViewRequested?.Invoke();
@@ -156,6 +160,7 @@ public partial class App : Application
RegisterUiUnhandledExceptionGuard(); RegisterUiUnhandledExceptionGuard();
LinuxDesktopEntryInstaller.EnsureInstalled(); LinuxDesktopEntryInstaller.EnsureInstalled();
_ = InitializeLauncherIpcAsync();
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell); DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
if (!Design.IsDesignMode && OperatingSystem.IsWindows()) if (!Design.IsDesignMode && OperatingSystem.IsWindows())
@@ -164,37 +169,43 @@ public partial class App : Application
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
// IPC 初始化移到窗口创建之后,避免 async void 中的 await 导致窗口创建延迟
// 使用 fire-and-forget 模式,不阻塞主流程
_ = InitializeLauncherIpcAsync();
} }
private async Task InitializeLauncherIpcAsync() private async Task InitializeLauncherIpcAsync()
{ {
if (!LauncherIpcClient.IsLaunchedByLauncher()) if (!LauncherIpcClient.IsLaunchedByLauncher())
return; return;
try try
{ {
_launcherIpcClient = new LauncherIpcClient(); _launcherIpcClient = new LauncherIpcClient();
var connected = await _launcherIpcClient.ConnectAsync(); var connected = await _launcherIpcClient.ConnectAsync();
if (!connected)
if (connected)
{ {
AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server."); return;
}
// 初始化加载状态管理器
_loadingStateManager = new LoadingStateManager(); AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server.");
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient);
_loadingStateReporter.Start(); bool hadBufferedMessages;
lock (_launcherProgressLock)
// 注册系统初始化加载项 {
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "系统初始化", "初始化系统核心组件"); hadBufferedMessages = _pendingLauncherProgressMessages.Count > 0;
_loadingStateManager.StartItem("system.init", "已连接启动器"); }
ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化..."); await FlushPendingLauncherProgressAsync();
ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置...");
_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) catch (Exception ex)
@@ -203,67 +214,86 @@ public partial class App : Application
} }
} }
/// <summary>
/// 向 Launcher 报告启动进度fire-and-forget不阻塞主流程
/// </summary>
private void ReportStartupProgress(StartupStage stage, int percent, string message) private void ReportStartupProgress(StartupStage stage, int percent, string message)
{ {
if (_launcherIpcClient is null) QueueOrSendLauncherProgress(new StartupProgressMessage
return;
_ = Task.Run(async () =>
{ {
try Stage = stage,
{ ProgressPercent = percent,
await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage Message = message,
{ Timestamp = DateTimeOffset.UtcNow
Stage = stage, }, logSuccess: false);
ProgressPercent = percent,
Message = message
});
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
}
});
} }
/// <summary>
/// 向 Launcher 报告关键启动进度,使用后台线程避免阻塞 UI
/// 用于 Ready 等关键状态报告
/// </summary>
private void ReportStartupProgressSync(StartupStage stage, int percent, string message) private void ReportStartupProgressSync(StartupStage stage, int percent, string message)
{ {
if (_launcherIpcClient is null) QueueOrSendLauncherProgress(new StartupProgressMessage
return; {
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 try
{ {
_ = Task.Run(async () => await ipcClient.ReportProgressAsync(message);
if (logSuccess)
{ {
try AppLogger.Info("LauncherIpc", $"Successfully reported stage: {message.Stage}");
{ }
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}");
}
});
} }
catch (Exception ex) 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() private void ApplyDesignTimeTheme()
{ {
RequestedThemeVariant = ThemeVariant.Light; 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 // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation(); DisableAvaloniaDataAnnotationValidation();
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown; desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
ReportStartupProgress(StartupStage.InitializingUI, 60, "正在初始化界面..."); ReportStartupProgress(StartupStage.InitializingUI, 60, "姝e湪鍒濆鍖栫晫闈?..");
CreateAndAssignMainWindow(desktop, "FrameworkInitialization"); CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
}, },
OnDesktopLifetimeExit, OnDesktopLifetimeExit,
@@ -337,25 +367,21 @@ public partial class App : Application
_ = sender; _ = sender;
_ = e; _ = e;
// 仅在 Windows 上支持融合桌面功能
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
{ {
AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows."); AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows.");
return; return;
} }
// 切换进入编辑模式,隐藏常态零散的小部件
FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode(); FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode();
// 确保透明覆盖层窗口存在并显示 // 纭繚閫忔槑瑕嗙洊灞傜獥鍙e瓨鍦ㄥ苟鏄剧ず
EnsureTransparentOverlayWindow(); EnsureTransparentOverlayWindow();
// 打开融合桌面组件库窗口
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
try try
{ {
// 确保覆盖层窗口已显示(组件要渲染在上面,必须先 Show
if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible) if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible)
{ {
_transparentOverlayWindow.Show(); _transparentOverlayWindow.Show();
@@ -368,16 +394,15 @@ public partial class App : Application
window.SetOverlayWindow(_transparentOverlayWindow); window.SetOverlayWindow(_transparentOverlayWindow);
} }
// 当组件库关闭时,退出编辑态 window.Closed += (s, ev) =>
window.Closed += (s, ev) =>
{ {
if (_transparentOverlayWindow is not null) if (_transparentOverlayWindow is not null)
{ {
// 触发画布保存,并隐藏画布 // 瑙﹀彂鐢诲竷淇濆瓨锛屽苟闅愯棌鐢诲竷
_transparentOverlayWindow.SaveLayoutAndHide(); _transparentOverlayWindow.SaveLayoutAndHide();
} }
// 让管理器根据已存储的最新快照重建生成所有实体小组件 // 璁╃鐞嗗櫒鏍规嵁宸插瓨鍌ㄧ殑鏈€鏂板揩鐓ч噸寤虹敓鎴愭墍鏈夊疄浣撳皬缁勪欢
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode(); FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
}; };
@@ -434,7 +459,7 @@ public partial class App : Application
private void InitializePluginRuntime() private void InitializePluginRuntime()
{ {
ReportStartupProgress(StartupStage.LoadingPlugins, 30, "正在加载插件..."); ReportStartupProgress(StartupStage.LoadingPlugins, 30, "姝e湪鍔犺浇鎻掍欢...");
try try
{ {
_pluginRuntimeService?.Dispose(); _pluginRuntimeService?.Dispose();
@@ -489,9 +514,12 @@ public partial class App : Application
} }
RefreshTrayIconContent(); RefreshTrayIconContent();
_trayInitialized = true;
AppLogger.Info("TrayIcon", $"Tray initialized successfully. Pid={Environment.ProcessId}.");
} }
catch (Exception ex) catch (Exception ex)
{ {
_trayInitialized = false;
AppLogger.Warn("TrayIcon", "Failed to initialize tray icon.", ex); AppLogger.Warn("TrayIcon", "Failed to initialize tray icon.", ex);
} }
} }
@@ -537,14 +565,12 @@ public partial class App : Application
return; return;
} }
// 仅在 Windows 上支持融合桌面功能
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
{ {
_trayComponentLibraryMenuItem.IsVisible = false; _trayComponentLibraryMenuItem.IsVisible = false;
return; return;
} }
// 检查融合桌面功能是否启用
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App); var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
_trayComponentLibraryMenuItem.IsVisible = appSnapshot.EnableFusedDesktop; _trayComponentLibraryMenuItem.IsVisible = appSnapshot.EnableFusedDesktop;
@@ -855,13 +881,12 @@ public partial class App : Application
if (languageChanged) if (languageChanged)
{ {
// 清除本地化缓存,强制重新加载语言文件 // 娓呴櫎鏈湴鍖栫紦瀛橈紝寮哄埗閲嶆柊鍔犺浇璇█鏂囦欢
_localizationService.ClearCache(); _localizationService.ClearCache();
ApplyCurrentCultureFromSettings(); ApplyCurrentCultureFromSettings();
RefreshTrayIconContent(); RefreshTrayIconContent();
} }
// 检查融合桌面设置是否变更
var fusedDesktopChanged = var fusedDesktopChanged =
refreshAll || refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFusedDesktop), StringComparer.OrdinalIgnoreCase); changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFusedDesktop), StringComparer.OrdinalIgnoreCase);
@@ -1076,64 +1101,49 @@ public partial class App : Application
ShowInTaskbar = true ShowInTaskbar = true
}; };
_mainWindowOpened = false;
AttachMainWindow(mainWindow); AttachMainWindow(mainWindow);
desktop.MainWindow = mainWindow; desktop.MainWindow = mainWindow;
AppLogger.Info("App", $"Main window created. Reason='{reason}'. LogFile={AppLogger.LogFilePath}"); AppLogger.Info("App", $"Main window created. Reason='{reason}'. LogFile={AppLogger.LogFilePath}");
LogBrowserStartupDiagnostics(); LogBrowserStartupDiagnostics();
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"MainWindowCreated:{reason}"); SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"MainWindowCreated:{reason}");
ReportStartupProgress(StartupStage.ShellInitialized, 85, "Desktop shell initialized.");
// 延迟报告 Ready 直到窗口实际打开并可见 AppLogger.Info(
// 使用 Opened 事件确保所有资源已加载完毕 "App",
$"Shell initialized. Reason='{reason}'; TrayInitialized={_trayInitialized}; MainWindowVisible={mainWindow.IsVisible}.");
mainWindow.Opened += OnMainWindowOpened; mainWindow.Opened += OnMainWindowOpened;
// 手动显示窗口,因为在 ShutdownMode.OnExplicitShutdown 模式下框架不会自动调用 Show
if (!mainWindow.IsVisible) if (!mainWindow.IsVisible)
{ {
mainWindow.Show(); mainWindow.Show();
} }
// 兜底机制:如果 Opened 事件 10 秒内未触发,强制发送 Ready 信号
// 防止因渲染问题导致 Opened 不触发,启动器 Splash 窗口一直显示
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
await Task.Delay(TimeSpan.FromSeconds(10)); await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
if (_launcherIpcClient is not null && _launcherIpcClient.IsConnected) if (!_mainWindowOpened)
{ {
try AppLogger.Warn("App", "Main window Opened event did not fire within 10 seconds. DesktopVisible was not reported.");
{
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 { }
} }
}); });
return mainWindow; return mainWindow;
} }
/// <summary>
/// 主窗口打开完成事件 - 此时所有组件、资源及功能模块均已完全加载
/// </summary>
private void OnMainWindowOpened(object? sender, EventArgs e) private void OnMainWindowOpened(object? sender, EventArgs e)
{ {
if (sender is MainWindow mainWindow) if (sender is MainWindow mainWindow)
{ {
mainWindow.Opened -= OnMainWindowOpened; mainWindow.Opened -= OnMainWindowOpened;
_mainWindowOpened = true;
AppLogger.Info("App", "Main window opened and ready. Reporting Ready to Launcher...");
AppLogger.Info(
// 完成系统初始化加载项 "App",
_loadingStateManager?.CompleteItem("system.init", "系统初始化完成"); $"Main window opened. Reporting DesktopVisible. TrayInitialized={_trayInitialized}; ShellState='{_desktopShellState}'.");
// 报告 Ready 状态,启动器可以安全关闭 Splash 窗口 _loadingStateManager?.CompleteItem("system.init", "System initialization completed.");
ReportStartupProgressSync(StartupStage.Ready, 100, "就绪"); ReportStartupProgressSync(StartupStage.DesktopVisible, 100, "Desktop visible.");
ReportStartupProgressSync(StartupStage.Ready, 100, "Ready.");
// 停止加载状态上报
_loadingStateReporter?.Stop(); _loadingStateReporter?.Stop();
} }
} }
@@ -1327,3 +1337,5 @@ public partial class App : Application
return _localizationService.GetString(languageCode, key, fallback); return _localizationService.GetString(languageCode, key, fallback);
} }
} }

View File

@@ -8,6 +8,7 @@ using LanMountainDesktop.DesktopHost;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Plugins; using LanMountainDesktop.Plugins;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Launcher;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Shared.Contracts.Launcher; using LanMountainDesktop.Shared.Contracts.Launcher;
@@ -33,6 +34,7 @@ public sealed class Program
AppLogger.Warn( AppLogger.Warn(
"Startup", "Startup",
$"Restart relaunch could not acquire the single-instance lock. pid={restartParentProcessId.Value}. Suppressing multi-open activation prompt."); $"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; Environment.ExitCode = HostExitCodes.RestartLockNotAcquired;
return; return;
} }
@@ -43,6 +45,7 @@ public sealed class Program
AppLogger.Info( AppLogger.Info(
"Startup", "Startup",
$"Secondary launch forwarded to primary instance successfully. Acked={activationAcknowledged}; Pid={Environment.ProcessId}."); $"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; Environment.ExitCode = HostExitCodes.SecondaryActivationSucceeded;
} }
else else
@@ -50,6 +53,9 @@ public sealed class Program
AppLogger.Warn( AppLogger.Warn(
"Startup", "Startup",
$"Secondary launch failed to activate the primary instance. Acked={activationAcknowledged}; Reason='{failureReason ?? "unknown"}'; Pid={Environment.ProcessId}."); $"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; 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() private static void InitializeTelemetryIdentity()
{ {
try try

View File

@@ -13,6 +13,11 @@ namespace LanMountainDesktop.Services.Launcher;
/// </summary> /// </summary>
public class LauncherIpcClient : IDisposable public class LauncherIpcClient : IDisposable
{ {
private static readonly JsonSerializerOptions StartupProgressJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
private NamedPipeClientStream? _pipeClient; private NamedPipeClientStream? _pipeClient;
private bool _isConnected; private bool _isConnected;
private readonly object _writeLock = new(); private readonly object _writeLock = new();
@@ -65,7 +70,7 @@ public class LauncherIpcClient : IDisposable
try try
{ {
var json = JsonSerializer.Serialize(message); var json = JsonSerializer.Serialize(message, StartupProgressJsonOptions);
var payload = System.Text.Encoding.UTF8.GetBytes(json); var payload = System.Text.Encoding.UTF8.GetBytes(json);
// 长度前缀协议:[4字节长度][消息正文] // 长度前缀协议:[4字节长度][消息正文]