Files
LanMountainDesktop/docs/LAUNCHER.md

550 lines
13 KiB
Markdown
Raw Normal View History

Launcher (#4) * 激进的更新 * 试试 * fix.可爱的我一直在修CI( * fix.启动器一定要能够启动 * feat.尝试弄了AOT的启动器。 * fix.修CI,好像是因为Linux那边有个问题,反正修就对了。 * fix.ci难修,为什么liunx跑不起来呢? * Update build.yml * Update LanMountainDesktop.csproj * changed.调整了启动逻辑,优化了更新页面。 * changed.优化了更新体验 * feat.依旧试增量更新这一块,看看velopack * fix.我们试验性地修复了启动器无法正常启动的问题,原因可能是这个画面没有启动,就GUI没显示。然后还把编译问题修了一下。 * fix.继续修ci,ci怎么天天炸 * changed.velopack,试试rust * fix.修ci,修融合桌面,修启动器 * fix.GitHub Action工作流怎么天天出问题 * feat.引入velopack,不好,是rust(至少内存很安全了。 * chore: migrate release pipeline to signed filemap and wire rainyun s3 * fix: make optional s3 upload step workflow-parse safe * fix: make delta pack generation robust for empty diffs and linux paths * chore: rotate launcher update public key for pdc signing * fix: restore stable launcher update public key * fix: sync launcher public key with update signing secret * fix: normalize PEM line endings in signing key validation * fix: rotate launcher public key to match ci signing secret * fix: compare signing keys by SPKI instead of PEM text * refactor update backend to host-managed PDC pipeline * fix release workflow env key collisions * relax publish-pdc precheck to require S3 only * set GH_TOKEN for PDCC installer step * ci: add local pdc mock fallback for release publish * ci: fix pdc mock process log redirection * ci: fallback pdcc signing key to update private key * ci: ensure pdcc signing passphrase env is always set * ci: create pdcc publish root before invoking client * ci: set pdcc version variable from release version * ci: decouple pdcc installer version from publish config version * ci: package pdcc subchannels with generated filemap and changelog * ci: make local pdc mock diff return empty for fast fallback * ci: fix pdcc variable mapping and pdc signing prechecks * Update App.axaml.cs * ci: wire aws cli credentials for rainyun s3 * ci: pin pdcc client version separately from app version * ci: harden local pdc mock transport handling * ci: publish pdcc subchannels in one pass * ci: add pdcc publish heartbeat and timeout * ci: fix pdcc publish workdir bootstrap * feat.Penguin Logistics Online Network Distribution System * ci: fix plonds s3 probe and signing fallback * ci: validate signing key and quiet missing baselines * ci: relax aws checksum mode for rainyun s3 * ci: avoid multipart uploads to rainyun s3 * ci: handle empty plonds baselines safely * ci.plonds * Rebuild release pipeline around PLONDS and DDSS * Fix Windows installer script path in release workflow
2026-04-21 20:59:52 +08:00
# 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`)
### UpdateEngineService
**职责**: 下载、验证、应用更新
**关键方法**:
```csharp
// 检查待处理的更新
LauncherResult CheckPendingUpdate()
// 下载更新
Task<LauncherResult> DownloadAsync(
string manifestUrl,
string signatureUrl,
string archiveUrl,
CancellationToken cancellationToken)
// 应用待处理的更新
LauncherResult ApplyPendingUpdate()
// 回退到上一个版本
LauncherResult RollbackLatest()
// 清理待删除的部署
void CleanupDestroyedDeployments()
```
### LauncherFlowCoordinator
**职责**: 协调完整的启动流程
**启动流程**:
1. 清理待删除的旧版本
2. 检查是否首次运行,显示 OOBE
3. 显示 Splash 窗口
4. 应用待处理的更新
5. 处理插件升级队列
6. 启动主程序
7. 关闭 Splash 窗口
### OobeStateService
**职责**: 管理首次运行状态
**关键方法**:
```csharp
// 检查是否首次运行
bool IsFirstRun()
// 标记 OOBE 已完成
void MarkCompleted()
```
### PluginInstallerService
**职责**: 处理插件安装
**关键方法**:
```csharp
// 安装插件包
Task<PluginInstallResult> InstallAsync(
string packagePath,
string targetDirectory,
CancellationToken cancellationToken = default)
```
### PluginUpgradeQueueService
**职责**: 批量处理插件升级队列
**关键方法**:
```csharp
// 应用待处理的插件升级
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**:
```csharp
public override void OnFrameworkInitializationCompleted()
{
var appRoot = Commands.ResolveAppRoot(context);
var deploymentLocator = new DeploymentLocator(appRoot);
var updateCheckService = new UpdateCheckService("owner", "repo");
var coordinator = new LauncherFlowCoordinator(
context,
deploymentLocator,
new OobeStateService(appRoot),
new UpdateEngineService(deploymentLocator),
updateCheckService,
new PluginInstallerService());
_ = RunCoordinatorAsync(desktop, coordinator);
}
```
**LauncherFlowCoordinator.RunAsync()**:
```csharp
public async Task<LauncherResult> RunAsync()
{
// 1. 清理旧版本
_deploymentLocator.CleanupDestroyedDeployments();
// 2. OOBE
if (_oobeStateService.IsFirstRun())
{
foreach (var step in _oobeSteps)
await step.RunAsync(CancellationToken.None);
}
// 3. Splash
var splashWindow = await Dispatcher.UIThread.InvokeAsync(() =>
{
var window = new SplashWindow();
window.Show();
return window;
});
try
{
// 4. 应用更新
var updateResult = _updateEngine.ApplyPendingUpdate();
if (!updateResult.Success)
return updateResult;
// 5. 插件升级
var pluginsDir = Path.Combine(_deploymentLocator.GetAppRoot(), "plugins");
var queueResult = new PluginUpgradeQueueService(_pluginInstallerService)
.ApplyPendingUpgrades(pluginsDir);
if (!queueResult.Success)
return queueResult;
// 6. 启动主程序
var hostResult = LaunchHost();
if (!hostResult.Success)
return hostResult;
return new LauncherResult { Success = true };
}
finally
{
await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Close());
}
}
```
## 命令行接口
### 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:**
```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.`LauncherFlowCoordinator` 中注册:
```csharp
_oobeSteps = [
new WelcomeOobeStep(_oobeStateService),
new MyOobeStep() // 添加新步骤
];
```
### 自定义更新源
修改 `App.axaml.cs` 中的 GitHub 仓库信息:
```csharp
var updateCheckService = new UpdateCheckService(
"YourOrg", // GitHub 组织/用户名
"YourRepo" // 仓库名
);
```
## 相关文档
- [更新系统详细文档](UPDATE_SYSTEM.md)
- [构建和部署指南](BUILD_AND_DEPLOY.md)
- [架构文档](ARCHITECTURE.md)
- [开发文档](DEVELOPMENT.md)