# Launcher 架构文档 > LanMountainDesktop.Launcher - 应用启动器与版本目录选择 ## 目录 - [概述](#概述) - [职责范围](#职责范围) - [架构设计](#架构设计) - [核心服务](#核心服务) - [版本管理](#版本管理) - [启动流程](#启动流程) - [命令行接口](#命令行接口) - [开发指南](#开发指南) ## 概述 Launcher 是 LanMountainDesktop 的唯一入口点,负责: - 首次体验引导 (OOBE) - 启动动画 (Splash Screen) - 多版本管理和选择 - 不承担更新职责;更新检查、下载、应用与回滚均由主程序负责 - 插件安装和升级维护命令 Air APP 窗口生命周期不再由 Launcher 进程内 broker 承担。Launcher 在正常启动时预启动包根下的 `LanMountainDesktop.AirAppRuntime`,该进程以框架依赖 JIT 方式运行并负责 Air APP IPC、实例表和 AirAppHost 进程管理。 **设计理念**: 参考 ClassIsland 项目,实现原子化的多版本管理和随时版本回退能力。 ## 职责范围 ### 1. OOBE (Out-of-Box Experience) - 首次启动引导 - 欢迎页面 - 初始设置向导 ### 2. Splash Screen - 启动动画 - 加载进度显示 - 品牌展示 ### 3. 版本管理 - 多版本并存 (`app-{version}/` 目录) - 版本选择算法 - 版本标记系统 (`.current`, `.partial`, `.destroy`) - 旧版本自动清理 ### 4. 更新边界 - Host 负责更新检查、频道策略、下载、`deployment.lock` 写入、PLONDS 应用、部署切换和回滚 - Launcher 不提供 `update check` / `update download` / `apply-update` / `rollback` 命令 - Launcher 只按版本目录和 `.current` / `.partial` / `.destroy` 标记选择要启动的 Host ### 5. 插件维护 - `plugin install` / `plugin update` 保留为兼容维护命令 - 应用内插件市场下载、校验和 pending 队列由 Host 负责 ## 架构设计 ### 目录结构 **安装后的目录结构:** ``` C:\Program Files\LanMountainDesktop\ ├── LanMountainDesktop.Launcher.exe ← 唯一入口 ├── LanMountainDesktop.AirAppRuntime.exe ← Air APP 生命周期容器(JIT) ├── app-1.0.0/ ← 版本目录 │ ├── .current ← 当前版本标记 │ ├── LanMountainDesktop.exe │ ├── LanMountainDesktop.AirAppHost.exe │ ├── LanMountainDesktop.dll │ └── ... (所有依赖) ├── app-1.0.1/ ← 新版本 │ ├── .partial ← 下载中标记 │ └── ... ├── app-0.9.9/ ← 旧版本 │ ├── .destroy ← 待删除标记 │ └── ... └── .Launcher/ ← Launcher 数据目录 ├── state/ │ └── first_run_completed ← OOBE 完成标记 ├── update/ │ ├── incoming/ ← 更新缓存 │ │ ├── files.json │ │ ├── files.json.sig │ │ └── update.zip │ └── public-key.pem ← RSA 公钥 └── snapshots/ ← 更新快照 └── {snapshot-id}.json ``` ### 版本标记文件 | 文件名 | 作用 | 创建时机 | 删除时机 | |--------|------|----------|----------| | `.current` | 标记当前使用的版本 | 更新完成后 | 新版本激活时 | | `.partial` | 标记下载未完成的版本 | 开始下载时 | 下载完成验证通过后 | | `.destroy` | 标记待删除的旧版本 | 新版本激活时 | 目录删除后 | ## 核心服务 ### DeploymentLocator **职责**: 扫描和定位版本目录,选择最佳版本 **关键方法**: ```csharp // 查找当前部署目录 string? FindCurrentDeploymentDirectory() // 解析主程序可执行文件路径 string? ResolveHostExecutablePath() // 获取当前版本号 string GetCurrentVersion() // 构建下一个部署目录路径 string BuildNextDeploymentDirectory(string targetVersion) // 清理标记为 .destroy 的目录 void CleanupDestroyedDeployments() ``` **版本选择算法**: 1. 扫描所有 `app-*` 目录 2. 过滤掉带 `.destroy` 或 `.partial` 标记的目录 3. 优先选择带 `.current` 标记的版本 4. 如果没有 `.current`,选择版本号最高的 ### Host UpdateOrchestrator **职责**: 更新检查、频道策略、manifest 解析、下载与安装触发位于 Host 的 `LanMountainDesktop/Services/Update/UpdateOrchestrator.cs`。Launcher 不再提供 `update check` / `update download` CLI。 ### Host UpdateInstallGateway **职责**: 更新应用与回滚入口位于 Host。`UpdateOrchestrator` 下载后调用 `UpdateInstallGateway` 在 Host 进程内应用 PLONDS payload;回滚通过 Host 的 `UpdateRollbackGateway` 执行。 ### LauncherOrchestrator / LaunchPipeline **职责**: 协调完整的启动流程(`Shell/LauncherOrchestrator.cs` + `Startup/LaunchPipeline.cs`) **启动阶段 (ILaunchPhase)**: 1. `CleanupDeploymentsPhase` — 清理旧部署 2. `ExistingHostProbePhase` — 多实例 / 现有 Host 探测 3. `OobeGatePhase` — OOBE 步骤 4. `LaunchHostPhase` — 启动 Host 5. `MonitorStartupPhase` — IPC 启动监控 **GUI 入口**: `Shell/LauncherCompositionRoot` + `Shell/LauncherServiceRegistration`(MS DI 轻量装配) ### ~~LauncherFlowCoordinator~~ (已移除) 已由 `LauncherOrchestrator` + `LaunchPipeline` 替代。 ### OobeStateService **职责**: 管理首次运行状态 **关键方法**: ```csharp // 检查是否首次运行 bool IsFirstRun() // 标记 OOBE 已完成 void MarkCompleted() ``` ### PluginInstallerService **职责**: CLI 维护命令下的插件包安装(`plugin install`)。应用内插件市场安装由 Host 在启动时应用 pending 队列,不经过 Launcher 正常启动流程。 **关键方法**: ```csharp // 安装插件包(CLI 维护) LauncherResult InstallPackage(string sourcePath, string pluginsDirectory) ``` ### PluginUpgradeQueueService **职责**: CLI 维护命令下的待处理插件升级(`plugin update`)。Launcher 正常 GUI 启动流程不再应用 pending 队列;Host 在 `PluginRuntimeService.ApplyPendingPluginOperations()` 中统一处理。 **关键方法**: ```csharp // 应用待处理的插件升级(CLI 维护) LauncherResult ApplyPendingUpgrades(string pluginsDirectory) ``` ## 版本管理 ### 版本选择算法详解 ```csharp public string? FindCurrentDeploymentDirectory() { var candidates = Directory.GetDirectories(rootDir, "app-*"); // 1. 过滤无效版本 var validCandidates = candidates .Where(path => !File.Exists(Path.Combine(path, ".destroy")) && !File.Exists(Path.Combine(path, ".partial"))) .ToList(); // 2. 优先选择带 .current 标记的 var withMarkers = validCandidates .Where(path => File.Exists(Path.Combine(path, ".current"))) .OrderByDescending(path => ParseVersion(path)) .FirstOrDefault(); if (withMarkers != null) return withMarkers; // 3. 选择版本号最高的 return validCandidates .OrderByDescending(path => ParseVersion(path)) .FirstOrDefault(); } ``` ### 版本激活流程 ```csharp private void ActivateDeployment(string fromDeployment, string toDeployment) { // 1. 在新版本添加 .current 标记 File.WriteAllText(Path.Combine(toDeployment, ".current"), string.Empty); // 2. 移除旧版本的 .current 标记 var fromCurrent = Path.Combine(fromDeployment, ".current"); if (File.Exists(fromCurrent)) File.Delete(fromCurrent); // 3. 标记旧版本为待删除 File.WriteAllText(Path.Combine(fromDeployment, ".destroy"), string.Empty); // 4. 移除新版本的 .partial 标记 (如果有) var toPartial = Path.Combine(toDeployment, ".partial"); if (File.Exists(toPartial)) File.Delete(toPartial); } ``` ### 版本清理流程 ```csharp public void CleanupDestroyedDeployments() { var destroyedDirs = Directory.GetDirectories(rootDir) .Where(x => File.Exists(Path.Combine(x, ".destroy"))); foreach (var dir in destroyedDirs) { try { Directory.Delete(dir, recursive: true); } catch { // 忽略删除失败 (可能文件被占用) // 下次启动时再试 } } } ``` ## 启动流程 ### 完整启动流程图 ``` 用户启动 Launcher.exe ↓ 清理旧版本 (.destroy 目录) ↓ 首次运行? ──Yes→ 显示 OOBE 窗口 ↓ No 显示 Splash 窗口 ↓ 检查待处理的更新 ↓ 有更新? ──Yes→ 应用更新 (原子化) ↓ No 处理插件升级队列 ↓ 选择最佳版本 (DeploymentLocator) ↓ 启动主程序 (Process.Start) ↓ 关闭 Splash 窗口 ↓ Launcher 退出 ``` ### 代码流程 **Program.cs**: ```csharp static async Task Main(string[] args) { var commandContext = CommandContext.FromArgs(args); // 处理 CLI 命令 if (commandContext.Command != "launch") return await Commands.RunCliCommandAsync(commandContext); // 启动 Avalonia 应用 LauncherRuntimeContext.Current = commandContext; BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); return Environment.ExitCode; } ``` **App.axaml.cs / Shell 入口**: ```csharp public override void OnFrameworkInitializationCompleted() { var splashWindow = LaunchEntryHandler.CreateSplashWindow(); splashWindow.Show(); _ = LauncherCompositionRoot.RunOrchestratorWithSplashAsync(desktop, context, splashWindow); } ``` **LaunchPipeline**: ```csharp internal sealed class LaunchPipeline { public async Task ExecuteAsync(LaunchContext context, CancellationToken cancellationToken = default) { foreach (var phase in _phases) { var result = await phase.ExecuteAsync(context, cancellationToken); if (result.Status == LaunchPhaseStatus.Completed) { return result.Result!; } } return LaunchResultBuilder.BuildFailure("launch", "pipeline_incomplete", "Launch pipeline finished without producing a result."); } } ``` `LauncherFlowCoordinator` 已删除。GUI 顶层生命周期由 `LauncherGuiCoordinator` 处理,启动阶段由 `LaunchPipeline` 和各 `ILaunchPhase` 承载;更新检查、下载、应用与回滚均由 Host 处理。 ## 命令行接口 ### launch - 启动应用 ```bash LanMountainDesktop.Launcher.exe launch ``` 启动完整流程: OOBE → Splash → 更新 → 插件 → 主程序 ### plugin install - 安装插件 ```bash LanMountainDesktop.Launcher.exe plugin install ``` 维护兼容入口:直接把 `.laapp` 插件包写入指定插件目录。应用内插件市场不再使用 Launcher 做普通插件安装;市场安装会先把包下载到当前用户的 pending 队列,并在下一次 Host 启动、插件发现前应用。 ## 开发指南 ### 本地调试 **直接运行 Launcher:** ```bash dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- launch ``` **调试特定命令:** ```bash # Launcher 不提供更新/回滚 CLI;调试更新请运行主程序并使用设置页或 Host 更新服务。 ``` ### 模拟多版本环境 ```bash # 1. 发布主程序 dotnet publish LanMountainDesktop/LanMountainDesktop.csproj -c Debug -o ./test-deploy/app-1.0.0 # 2. 创建 .current 标记 New-Item -ItemType File -Path ./test-deploy/app-1.0.0/.current # 3. 复制 Launcher 到根目录 Copy-Item LanMountainDesktop.Launcher/bin/Debug/net10.0/* ./test-deploy/ # 4. 运行 Launcher ./test-deploy/LanMountainDesktop.Launcher.exe launch ``` ### 测试更新流程 ```bash # 1. 创建两个版本 dotnet publish LanMountainDesktop/LanMountainDesktop.csproj -o ./test-deploy/app-1.0.0 dotnet publish LanMountainDesktop/LanMountainDesktop.csproj -o ./test-deploy/app-1.0.1 # 2. 生成增量包 pwsh ./scripts/Generate-DeltaPackage.ps1 ` -PreviousVersion "1.0.0" ` -CurrentVersion "1.0.1" ` -PreviousDir "./test-deploy/app-1.0.0" ` -CurrentDir "./test-deploy/app-1.0.1" ` -OutputDir "./test-deploy/.Launcher/update/incoming" # 3. 测试应用更新 # 运行主程序并通过 Host 更新服务触发下载、应用和回滚。 ``` ### 添加新的 OOBE 步骤 1. 实现 `IOobeStep` 接口: ```csharp public class MyOobeStep : IOobeStep { public async Task RunAsync(CancellationToken cancellationToken) { // 显示 OOBE 窗口 // 等待用户完成 } } ``` 2. 在 `Shell/LauncherOrchestrator.cs` 的 OOBE step 组装处注册,或通过后续 `ILaunchPhase`/OOBE 装配点接入: ```csharp _oobeSteps = [ new WelcomeOobeStep(_oobeStateService), new MyOobeStep() // 添加新步骤 ]; ``` 当前内置 OOBE 向导窗口(`OobeWindow`)内步骤顺序包含:开场 → 主题 → **数据保存位置** → **启动与展示** → 隐私与遥测 → 完成。「启动与展示」写入 Host 的 `settings.json`(PascalCase)并在 Windows 下同步 Run 项,实现代码在 `HostAppSettingsOobeMerger.cs` 与 `LauncherWindowsStartupService.cs`,界面与逻辑挂在 `Views/OobeWindow.axaml(.cs)`。 ### 自定义更新源 更新源配置与 manifest provider 位于 Host 更新服务中,优先查看 `LanMountainDesktop/Services/Update/UpdateOrchestrator.cs`、`SettingsUpdateManifestProvider.cs` 与具体 provider。 ## 相关文档 - [更新系统详细文档](UPDATE_SYSTEM.md) - [构建和部署指南](BUILD_AND_DEPLOY.md) - [架构文档](ARCHITECTURE.md) - [开发文档](DEVELOPMENT.md) ## Current OOBE and Elevation Contract - OOBE state is a per-user truth source stored at `%LOCALAPPDATA%\LanMountainDesktop\.launcher\state\oobe-state.json`. - Same-user reinstall or upgrade must not re-enter OOBE. - `first_run_completed` is legacy compatibility data only and should not remain the long-term primary format. - Launch source values are `normal`, `postinstall`, `plugin-install`, and `debug-preview`. - Auto-OOBE is allowed only for normal user-mode startup. - `postinstall` may open OOBE only when the launcher is not elevated and the user state path is available. - `plugin-install` and `debug-preview` must not auto-enter OOBE. - Allowed elevation paths are limited to the installer itself, full installer update application, and user-confirmed legacy uninstall. - Default plugin installation targets the Host data root and must not request elevation when that directory is writable. - The Launcher `plugin install` maintenance command accepts `--app-root` so it can verify the configured data root before writing. It rejects targets outside that root. - In-app market installs are deferred Host-side operations when the data root is writable: download and verify now, apply from the pending queue on the next Host startup. - If portable data is configured under an administrator-protected install path, Host stages the package in a user-writable download directory and invokes the restricted Launcher maintenance command with UAC to copy the package into `Extensions/Plugins`. ## Public IPC Baseline Launcher now consumes Host startup telemetry from the unified public IPC stack: - Host publishes `StartupProgressMessage` via `lanmountain.launcher.startup-progress` - Host publishes `LoadingStateMessage` via `lanmountain.launcher.loading-state` - Launcher connects through `LanMountainDesktopIpcClient` The previous custom length-prefixed named-pipe transport is no longer the primary startup communication path. ## Coordinator Guard Launcher also owns a small per-user local coordinator used only between Launcher processes. It reserves `startup-attempt.json` before host launch, publishes a heartbeat, and exposes a local coordinator pipe for secondary Launchers. A secondary Launcher must attach to that coordinator or activate the existing Host through Public IPC instead of starting another Host process. See [Launcher Coordinator](LAUNCHER_COORDINATOR.md).