Files
LanMountainDesktop/docs/LAUNCHER.md
2026-05-28 15:14:37 +08:00

530 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Launcher 架构文档
> LanMountainDesktop.Launcher - 应用启动器和版本管理系统
## 目录
- [概述](#概述)
- [职责范围](#职责范围)
- [架构设计](#架构设计)
- [核心服务](#核心服务)
- [版本管理](#版本管理)
- [启动流程](#启动流程)
- [命令行接口](#命令行接口)
- [开发指南](#开发指南)
## 概述
Launcher 是 LanMountainDesktop 的唯一入口点,负责:
- 首次体验引导 (OOBE)
- 启动动画 (Splash Screen)
- 多版本管理和选择
- 应用更新 (增量更新、原子化更新)
- 插件安装和升级
- 版本回退
**设计理念**: 参考 ClassIsland 项目,实现原子化的多版本管理和随时版本回退能力。
## 职责范围
### 1. OOBE (Out-of-Box Experience)
- 首次启动引导
- 欢迎页面
- 初始设置向导
### 2. Splash Screen
- 启动动画
- 加载进度显示
- 品牌展示
### 3. 版本管理
- 多版本并存 (`app-{version}/` 目录)
- 版本选择算法
- 版本标记系统 (`.current`, `.partial`, `.destroy`)
- 旧版本自动清理
### 4. 应用更新
- GitHub Release API 集成
- 更新频道管理 (Stable/Preview)
- 增量更新下载
- 原子化更新应用
- 签名验证
- 版本回退
### 5. 插件管理
- 插件安装 (`.laapp` 包)
- 插件更新检查
- 插件升级队列处理
## 架构设计
### 目录结构
**安装后的目录结构:**
```
C:\Program Files\LanMountainDesktop\
├── LanMountainDesktop.Launcher.exe ← 唯一入口
├── app-1.0.0/ ← 版本目录
│ ├── .current ← 当前版本标记
│ ├── LanMountainDesktop.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`,选择版本号最高的
### UpdateCheckService
**职责**: 检查 GitHub Release 更新
**关键方法**:
```csharp
// 检查更新
Task<UpdateCheckResult> CheckForUpdateAsync(
string currentVersion,
UpdateChannel channel,
CancellationToken cancellationToken = default)
```
**更新频道**:
- `Stable` - 只检查 `prerelease=false` 的版本
- `Preview` - 检查所有版本 (包括 `prerelease=true`)
### IUpdateEngine / UpdateEngineFacade
**职责**: `UpdateEngineFacade``IUpdateEngine` 薄门面pending 检测、签名、Legacy/PLONDS apply、快照、checkpoint、回滚和清理分别位于 `Update/` 策略/基础设施类。
**关键方法**:
```csharp
LauncherResult CheckPendingUpdate()
Task<LauncherResult> DownloadAsync(...)
Task<LauncherResult> ApplyPendingUpdateAsync()
LauncherResult RollbackLatest()
void CleanupDestroyedDeployments()
void CleanupIncomingArtifacts()
```
### LauncherOrchestrator / LaunchPipeline
**职责**: 协调完整的启动流程(`Shell/LauncherOrchestrator.cs` + `Startup/LaunchPipeline.cs`
**启动阶段 (ILaunchPhase)**:
1. `CleanupDeploymentsPhase` — 清理旧部署
2. `ExistingHostProbePhase` — 多实例 / 现有 Host 探测
3. `ApplyPendingUpdatePhase` — 应用 pending 更新
4. `OobeGatePhase` — OOBE 步骤
5. `LaunchHostPhase` — 启动 Host
6. `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<int> 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<LauncherResult> 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` 承载;更新 apply 通过 `IUpdateEngine` 门面进入 `Update/` 策略类。
## 命令行接口
### launch - 启动应用
```bash
LanMountainDesktop.Launcher.exe launch
```
启动完整流程: OOBE → Splash → 更新 → 插件 → 主程序
### update check - 检查更新
```bash
LanMountainDesktop.Launcher.exe update check
```
检查 GitHub Release 是否有新版本。
### update download - 下载更新
```bash
LanMountainDesktop.Launcher.exe update download --version 1.0.1
```
下载指定版本的更新包。
### update apply - 应用更新
```bash
LanMountainDesktop.Launcher.exe update apply
```
应用已下载的更新 (原子化操作)。
### update rollback - 版本回退
```bash
LanMountainDesktop.Launcher.exe update rollback
```
回退到上一个有效版本。
### plugin install - 安装插件
```bash
LanMountainDesktop.Launcher.exe plugin install <path-to-plugin.laapp>
```
维护兼容入口:直接把 `.laapp` 插件包写入指定插件目录。应用内插件市场不再使用 Launcher 做普通插件安装;市场安装会先把包下载到当前用户的 pending 队列,并在下一次 Host 启动、插件发现前应用。
## 开发指南
### 本地调试
**直接运行 Launcher:**
```bash
dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- launch
```
**调试特定命令:**
```bash
# 检查更新
dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- update check
# 版本回退
dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- update rollback
```
### 模拟多版本环境
```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. 测试应用更新
./test-deploy/LanMountainDesktop.Launcher.exe update apply
```
### 添加新的 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)`
### 自定义更新源
修改 `App.axaml.cs` 中的 GitHub 仓库信息:
```csharp
var updateCheckService = new UpdateCheckService(
"YourOrg", // GitHub 组织/用户名
"YourRepo" // 仓库名
);
```
## 相关文档
- [更新系统详细文档](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`, `apply-update`, `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.
- `apply-update`, `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 current user's LocalAppData scope and must not request elevation by default.
- In-app market installs are deferred Host-side operations: download and verify now, apply from the per-user pending queue on the next Host startup.
## 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).