mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-23 01:44:26 +08:00
feat.文档更新
This commit is contained in:
474
docs/05-更新与发布/01-更新系统架构.md
Normal file
474
docs/05-更新与发布/01-更新系统架构.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# 更新系统架构
|
||||
|
||||
本文档描述阑山桌面的更新系统设计,包括增量更新、原子化安装、版本管理和回滚机制。
|
||||
|
||||
## 更新系统概览
|
||||
|
||||
### 核心特性
|
||||
|
||||
- ✅ **增量更新** - 只下载变更文件,节省带宽
|
||||
- ✅ **原子化更新** - 保证完整性,失败自动回滚
|
||||
- ✅ **多版本共存** - 支持多版本并存和快速切换
|
||||
- ✅ **签名验证** - 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) - 启动器详细设计
|
||||
Reference in New Issue
Block a user