Files
LanMountainDesktop/docs/05-更新与发布/01-更新系统架构.md
2026-06-08 03:54:33 +08:00

475 lines
14 KiB
Markdown
Raw Permalink 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.
# 更新系统架构
本文档描述阑山桌面的更新系统设计,包括增量更新、原子化安装、版本管理和回滚机制。
## 更新系统概览
### 核心特性
-**增量更新** - 只下载变更文件,节省带宽
-**原子化更新** - 保证完整性,失败自动回滚
-**多版本共存** - 支持多版本并存和快速切换
-**签名验证** - RSA 签名确保安全性
-**版本回退** - 一键回退到上一版本
-**更新频道** - Stable/Preview 频道切换
-**后台下载** - 静默下载,不影响使用
### 架构组件
```
┌──────────────────────────────────────────────┐
│ LanMountainDesktop (Host) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ UpdateOrchestrator │ │
│ │ (更新编排 - Host 拥有) │ │
│ │ - 检查更新 │ │
│ │ - 下载更新 │ │
│ │ - 应用更新 │ │
│ │ - 回滚更新 │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │UpdateInstall │ │UpdateRollback │ │
│ │ Gateway │ │ Gateway │ │
│ └────────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────┘
↓ 写入 deployment.lock
┌──────────────────────────────────────────────┐
│ Launcher │
│ (启动器 - 版本选择和清理) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ DeploymentLocator │ │
│ │ 扫描和选择最佳版本 │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
## 目录结构
### 版本目录布局
```
安装根目录/
├── LanMountainDesktop.Launcher.exe # 启动器
├── app-1.0.0/ # 旧版本
│ ├── .destroy # 待删除标记
│ ├── LanMountainDesktop.exe
│ └── ...
├── app-1.0.1/ # 当前版本
│ ├── .current # 当前版本标记
│ ├── LanMountainDesktop.exe
│ └── ...
├── app-1.0.2/ # 新版本(下载中)
│ ├── .partial # 未完成标记
│ └── ...
└── .Launcher/ # 启动器数据
├── state/
│ └── oobe-state.json
├── update/
│ ├── incoming/ # 更新缓存
│ │ ├── deployment.lock # 部署锁
│ │ ├── plonds-filemap.json # 文件清单
│ │ ├── plonds-filemap.sig # RSA 签名
│ │ └── objects/ # 对象文件
│ ├── public-key.pem # 公钥
│ └── snapshots/ # 更新快照
│ └── snapshot-{id}.json
└── logs/
```
### 版本标记文件
| 标记文件 | 含义 | 创建者 | 删除时机 |
|---------|------|--------|---------|
| `.current` | 当前使用版本 | Host UpdateInstallGateway | 切换到新版本时 |
| `.partial` | 下载未完成 | Host UpdateInstallGateway | 安装成功或失败清理时 |
| `.destroy` | 待删除版本 | Host UpdateInstallGateway | Launcher 下次启动清理 |
## 更新流程
### 完整更新流程
```
1. Host 后台检查更新
UpdateOrchestrator.CheckAsync()
- 调用 manifest provider (PLONDS/GitHub)
- 根据更新频道过滤版本
- 对比当前版本和最新版本
2. 发现新版本
用户收到通知
3. Host 下载更新
UpdateOrchestrator.DownloadAsync()
- 下载 plonds-filemap.json
- 下载 plonds-filemap.sig
- 下载对象文件
- 保存到 .Launcher/update/incoming/
4. 写入 deployment.lock
{
"TargetVersion": "1.0.2",
"FromVersion": "1.0.1",
"FileMapPath": "...",
"SignaturePath": "...",
"ArchivePath": "..."
}
5. Host 调用 UpdateInstallGateway
UpdateInstallGateway.InstallAsync()
- 验证签名 (UpdateSignatureVerifier)
- 创建目标目录 app-1.0.2/
- 标记 .partial
- 保存快照到 snapshots/
6. 应用文件操作
PlondsUpdateApplier.ApplyFileMap()
For each file in filemap:
- action="replace" → 从对象解压
- action="reuse" → 从旧版本复制
- action="delete" → 跳过
7. 验证所有文件哈希
For each file:
计算 SHA256
与 filemap 对比
8. 激活新版本
DeploymentActivator.ActivateDeployment()
- 移除 app-1.0.2/.partial
- 添加 app-1.0.2/.current
- 移除 app-1.0.1/.current
- 添加 app-1.0.1/.destroy
9. 更新快照状态 = "applied"
10. 清理 incoming 缓存
IncomingArtifactsCleaner.Clean()
11. 用户重启应用
Launcher 启动 app-1.0.2/
12. Launcher 清理 .destroy 目录
删除 app-1.0.1/
```
### 增量包结构
#### GitHub Release Assets
```
LanMountainDesktop-v1.0.2/
├── LanMountainDesktop-Setup-1.0.2-x64.exe # 完整安装包
├── releases.win.json # VeloPack 清单
├── LanMountainDesktop-1.0.2-win-x64.nupkg # VeloPack 包
├── RELEASES # VeloPack 元数据
└── (legacy) plonds-filemap-1.0.2.json # 旧格式(可选)
```
#### plonds-filemap.json 格式
```json
{
"FromVersion": "1.0.1",
"ToVersion": "1.0.2",
"GeneratedAt": "2026-06-08T10:00:00Z",
"Files": [
{
"Path": "LanMountainDesktop.exe",
"Action": "replace",
"Sha256": "abc123...",
"Size": 1024000,
"ObjectPath": "objects/abc123"
},
{
"Path": "LanMountainDesktop.dll",
"Action": "reuse",
"Sha256": "def456...",
"Size": 512000
},
{
"Path": "OldPlugin.dll",
"Action": "delete"
}
]
}
```
### 文件操作类型
| Action | 说明 | 处理方式 |
|--------|------|----------|
| `replace` | 新增或替换文件 | 从 objects/ 解压 |
| `reuse` | 文件未变更 | 从旧版本目录复制 |
| `delete` | 删除文件 | 不操作(新版本中不存在) |
## 原子化保证
### 失败场景处理
| 失败场景 | 检测机制 | 恢复方式 |
|---------|---------|---------|
| **签名验证失败** | RSA 验证 | 拒绝安装,保留旧版本 |
| **文件哈希不匹配** | SHA256 校验 | 自动回滚到旧版本 |
| **磁盘空间不足** | 写入失败 | 自动回滚到旧版本 |
| **进程崩溃** | .partial 标记 | Launcher 启动时清理 |
| **断电/强制关机** | .partial 标记 | Launcher 启动时清理 |
### 回滚机制
```csharp
private void TryRollbackOnFailure(SnapshotMetadata snapshot)
{
try
{
// 1. 删除未完成的新版本目录
if (Directory.Exists(snapshot.TargetDirectory))
{
Directory.Delete(snapshot.TargetDirectory, true);
}
// 2. 移除旧版本的 .destroy 标记
var destroyMarker = Path.Combine(
snapshot.SourceDirectory,
".destroy"
);
if (File.Exists(destroyMarker))
{
File.Delete(destroyMarker);
}
// 3. 确保旧版本有 .current 标记
var currentMarker = Path.Combine(
snapshot.SourceDirectory,
".current"
);
if (!File.Exists(currentMarker))
{
File.WriteAllText(currentMarker, string.Empty);
}
// 4. 更新快照状态
snapshot.Status = "rolled_back";
SaveSnapshot(snapshotPath, snapshot);
}
catch (Exception ex)
{
_logger.LogError(ex, "Rollback failed");
// 记录错误但不抛出
}
}
```
## 版本管理
### 版本选择算法
Launcher 启动时的版本选择逻辑:
```csharp
public string? FindBestVersion()
{
var appDirs = Directory.GetDirectories(
_installRoot,
"app-*"
);
// 1. 过滤掉标记为 .partial 或 .destroy 的版本
var validDirs = appDirs.Where(dir =>
!File.Exists(Path.Combine(dir, ".partial")) &&
!File.Exists(Path.Combine(dir, ".destroy"))
);
// 2. 优先选择带 .current 标记的版本
var currentDir = validDirs.FirstOrDefault(dir =>
File.Exists(Path.Combine(dir, ".current"))
);
if (currentDir != null)
return currentDir;
// 3. 否则选择版本号最高的
var sortedDirs = validDirs
.Select(dir => new {
Path = dir,
Version = ParseVersion(dir)
})
.Where(x => x.Version != null)
.OrderByDescending(x => x.Version)
.ToList();
return sortedDirs.FirstOrDefault()?.Path;
}
```
### 版本回退
用户可以通过设置页手动回退到上一版本:
```
设置 → 更新 → 版本历史 → 回滚
```
**回退流程**:
```
1. Host 调用 UpdateRollbackGateway
2. 读取最新快照 (snapshots/)
3. 获取 SourceDirectory (旧版本)
4. 激活旧版本
- 移除当前版本的 .current
- 添加 .current 到旧版本
- 移除旧版本的 .destroy
- 添加 .destroy 到当前版本
5. 更新快照状态 = "manual_rollback"
6. 提示用户重启应用
```
## 安全机制
### RSA 签名
#### 签名生成CI 环境)
```powershell
# scripts/Sign-FileMap.ps1
$privateKeyPem = Get-Content -Path $PrivateKeyPath -Raw
$rsa = [System.Security.Cryptography.RSA]::Create()
$rsa.ImportFromPem($privateKeyPem)
$jsonBytes = [System.IO.File]::ReadAllBytes($FilesJsonPath)
$signature = $rsa.SignData(
$jsonBytes,
[System.Security.Cryptography.HashAlgorithmName]::SHA256,
[System.Security.Cryptography.RSASignaturePadding]::Pkcs1
)
$signatureBase64 = [Convert]::ToBase64String($signature)
Set-Content -Path "$FilesJsonPath.sig" -Value $signatureBase64
```
#### 签名验证Launcher
```csharp
private (bool Success, string Message) VerifySignature(
string fileMapPath,
string signaturePath)
{
// 1. 读取公钥
var publicKeyPath = Path.Combine(
_launcherRoot,
"update",
"public-key.pem"
);
using var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(publicKeyPath));
// 2. 读取签名
var signatureBase64 = File.ReadAllText(signaturePath).Trim();
var signature = Convert.FromBase64String(signatureBase64);
// 3. 验证
var jsonBytes = File.ReadAllBytes(fileMapPath);
var isValid = rsa.VerifyData(
jsonBytes,
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1
);
return isValid
? (true, "ok")
: (false, "Signature verification failed");
}
```
### 文件完整性验证
```csharp
// 验证所有文件的 SHA256
foreach (var file in fileMap.Files)
{
if (file.Action == "delete")
continue;
var fullPath = Path.Combine(targetDeployment, file.Path);
var actualHash = ComputeSha256Hex(fullPath);
if (!string.Equals(
actualHash,
file.Sha256,
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"File hash mismatch for '{file.Path}'."
);
}
}
```
### 路径遍历防护
```csharp
private static void EnsurePathWithinRoot(
string targetPath,
string rootPath)
{
var fullTarget = Path.GetFullPath(targetPath);
var fullRoot = Path.GetFullPath(rootPath);
if (!fullTarget.StartsWith(
fullRoot,
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Path traversal detected: {targetPath}"
);
}
}
```
## 更新频道
### 频道类型
| 频道 | 说明 | GitHub Release 过滤 |
|------|------|---------------------|
| **Stable** | 正式版,推荐大多数用户使用 | `prerelease=false` |
| **Preview** | 预览版,包含最新功能和 Bug 修复 | 所有版本(包括 `prerelease=true` |
### 切换频道
```
设置 → 更新 → 更新频道 → 选择频道
```
切换后立即检查更新。
## VeloPack 集成
### 当前状态
- ✅ CI 管道已生成 VeloPack 原生资产(`releases.win.json`, `*.nupkg`, `RELEASES`
- ✅ Host 拥有更新检查/下载/应用/回滚编排
- ✅ Launcher 仅负责版本选择和启动
- ⚠️ 旧的 `files.json` + `update.zip` 生成作为禁用的回退路径保留在 CI 中
### 未来迁移
计划完全迁移到 VeloPack
1. 移除旧格式支持
2. 简化 CI 管道
3. 利用 VeloPack 的增量更新算法
## 相关文档
- [打包与构建](../05-更新与发布/03-打包与构建.md) - 构建流程
- [CI/CD 配置](../05-更新与发布/04-CICD配置.md) - 自动化构建
- [启动器系统](02-启动器系统.md) - 启动器详细设计