mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
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
This commit is contained in:
178
docs/AOT_PUBLISH.md
Normal file
178
docs/AOT_PUBLISH.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Launcher AOT 单文件发布指南
|
||||
|
||||
## 什么是 AOT?
|
||||
|
||||
AOT(Ahead-of-Time)编译将 .NET 代码在构建时直接编译为本地机器码,而不是在运行时通过 JIT 编译。
|
||||
|
||||
### AOT 的优势
|
||||
|
||||
| 特性 | JIT 模式 | AOT 模式 |
|
||||
|------|---------|---------|
|
||||
| 启动速度 | 慢(需要编译) | 快(直接执行) |
|
||||
| 依赖文件 | 多(.dll, runtimeconfig.json) | 少(单文件) |
|
||||
| 需要 .NET Runtime | 是 | 否 |
|
||||
| 文件体积 | 小 | 稍大(但单文件更方便) |
|
||||
| 反编译难度 | 容易 | 困难 |
|
||||
|
||||
## 发布方式
|
||||
|
||||
### 方式一:使用 PowerShell 脚本(推荐)
|
||||
|
||||
```powershell
|
||||
# 默认发布(win-x64,单文件,自包含)
|
||||
.\scripts\Publish-AOT.ps1
|
||||
|
||||
# 指定运行时
|
||||
.\scripts\Publish-AOT.ps1 -RuntimeIdentifier win-x64
|
||||
|
||||
# 不压缩(体积更大但启动更快)
|
||||
.\scripts\Publish-AOT.ps1 -Compress:$false
|
||||
```
|
||||
|
||||
### 方式二:使用 dotnet CLI
|
||||
|
||||
```bash
|
||||
# 基本 AOT 发布
|
||||
dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj `
|
||||
-c Release `
|
||||
-r win-x64 `
|
||||
--self-contained `
|
||||
-p:PublishAot=true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:EnableCompressionInSingleFile=true
|
||||
|
||||
# 输出目录
|
||||
# bin/Release/net10.0/win-x64/publish/
|
||||
```
|
||||
|
||||
### 方式三:使用 MSBuild
|
||||
|
||||
```bash
|
||||
msbuild LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj `
|
||||
/t:Publish `
|
||||
/p:Configuration=Release `
|
||||
/p:RuntimeIdentifier=win-x64 `
|
||||
/p:PublishAot=true `
|
||||
/p:PublishSingleFile=true
|
||||
```
|
||||
|
||||
## 支持的运行时
|
||||
|
||||
| 运行时标识符 | 说明 |
|
||||
|-------------|------|
|
||||
| `win-x64` | Windows 64位(推荐) |
|
||||
| `win-x86` | Windows 32位 |
|
||||
| `win-arm64` | Windows ARM64 |
|
||||
| `linux-x64` | Linux 64位 |
|
||||
| `linux-arm64` | Linux ARM64 |
|
||||
| `osx-x64` | macOS 64位 |
|
||||
| `osx-arm64` | macOS ARM64 (Apple Silicon) |
|
||||
|
||||
## 文件体积对比
|
||||
|
||||
### 普通发布(非 AOT)
|
||||
```
|
||||
LanMountainDesktop.Launcher.exe 150 KB
|
||||
LanMountainDesktop.Launcher.dll 200 KB
|
||||
Avalonia.dll 1.2 MB
|
||||
...(数十个依赖文件)
|
||||
总计: ~15 MB
|
||||
```
|
||||
|
||||
### AOT 单文件发布
|
||||
```
|
||||
LanMountainDesktop.Launcher.exe 8-12 MB(单文件,包含所有依赖)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 修剪(Trimming)
|
||||
|
||||
AOT 会自动移除未使用的代码以减小体积。某些反射代码可能需要特殊处理:
|
||||
|
||||
```csharp
|
||||
// 如果类型被反射使用,需要保留
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public class MyClass { }
|
||||
```
|
||||
|
||||
### 2. Avalonia 兼容性
|
||||
|
||||
- ✅ Avalonia 11.x 完全支持 AOT
|
||||
- ✅ 使用 Compiled Bindings(已在项目中启用)
|
||||
- ✅ 避免动态 XAML 加载
|
||||
|
||||
### 3. Json 序列化
|
||||
|
||||
使用 `JsonSerializer` 时需要源生成器:
|
||||
|
||||
```csharp
|
||||
[JsonSerializable(typeof(MyType))]
|
||||
internal partial class MyJsonContext : JsonSerializerContext { }
|
||||
```
|
||||
|
||||
### 4. 单文件特殊处理
|
||||
|
||||
某些文件需要嵌入到单文件中:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Assets\logo.ico" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 发布失败
|
||||
|
||||
1. **检查 .NET SDK 版本**
|
||||
```bash
|
||||
dotnet --version # 需要 10.0 或更高
|
||||
```
|
||||
|
||||
2. **安装 AOT 工作负载**
|
||||
```bash
|
||||
dotnet workload install wasm-tools # 如果需要 WebAssembly AOT
|
||||
```
|
||||
|
||||
3. **Visual Studio 要求**
|
||||
- 需要 VS 2022 17.8+ 或 VS Code + C# Dev Kit
|
||||
|
||||
### 运行时错误
|
||||
|
||||
1. **缺少类型**
|
||||
- 在 `.csproj` 中添加 `<TrimmerRootAssembly>`
|
||||
|
||||
2. **反射失败**
|
||||
- 使用 `[DynamicallyAccessedMembers]` 标记
|
||||
|
||||
3. **DllNotFoundException**
|
||||
- 确保所有 native 库都包含在发布中
|
||||
|
||||
## 性能对比
|
||||
|
||||
| 指标 | JIT | AOT | 提升 |
|
||||
|------|-----|-----|------|
|
||||
| 启动时间 | 2-3 秒 | 0.5-1 秒 | 2-3x |
|
||||
| 内存占用 | 较高 | 较低 | 20-30% |
|
||||
| 首次响应 | 慢 | 快 | 显著 |
|
||||
|
||||
## 推荐配置
|
||||
|
||||
对于 Launcher 项目,推荐使用以下配置:
|
||||
|
||||
```xml
|
||||
<PublishAot>true</PublishAot>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>partial</TrimMode>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
```
|
||||
|
||||
这样发布的结果:
|
||||
- ✅ 单文件可执行
|
||||
- ✅ 无需 .NET Runtime
|
||||
- ✅ 启动速度快
|
||||
- ✅ 文件体积合理(8-12 MB)
|
||||
@@ -7,6 +7,7 @@
|
||||
| 路径 | 角色 |
|
||||
| --- | --- |
|
||||
| `LanMountainDesktop/` | 主桌面宿主应用,包含 UI、服务、组件系统、插件运行时接入 |
|
||||
| **`LanMountainDesktop.Launcher/`** | **启动器 - 负责 OOBE、Splash、版本管理、增量更新、插件安装** |
|
||||
| `LanMountainDesktop.PluginSdk/` | 官方插件 SDK,定义插件可依赖的公开接口与打包行为 |
|
||||
| `LanMountainDesktop.Shared.Contracts/` | 宿主与插件共享的稳定契约类型 |
|
||||
| `LanMountainDesktop.Appearance/` | 主题、圆角、外观资源相关基础设施 |
|
||||
@@ -14,12 +15,24 @@
|
||||
| `LanMountainDesktop.DesktopHost/` | 桌面宿主流程与生命周期相关逻辑 |
|
||||
| `LanMountainDesktop.DesktopComponents.Runtime/` | 组件运行时支撑能力 |
|
||||
| `LanMountainDesktop.Host.Abstractions/` | 宿主侧抽象接口 |
|
||||
| `LanMountainDesktop.PluginsInstallHelper/` | 插件安装辅助程序与发布输出配套 |
|
||||
| `LanMountainDesktop.PluginTemplate/` | `dotnet new lmd-plugin` 官方模板 |
|
||||
| `LanMountainDesktop.Tests/` | 宿主与 SDK 的测试项目 |
|
||||
|
||||
### 宿主启动主线
|
||||
|
||||
**生产环境启动流程 (通过 Launcher):**
|
||||
|
||||
1. 用户启动 `LanMountainDesktop.Launcher.exe`
|
||||
2. Launcher 扫描 `app-*` 目录,选择最佳版本 (优先 `.current` 标记,然后按版本号降序)
|
||||
3. 首次启动显示 OOBE 引导 (`OobeWindow`)
|
||||
4. 显示 Splash 启动动画 (`SplashWindow`)
|
||||
5. 检查并应用待处理的更新 (`UpdateEngineService.ApplyPendingUpdate`)
|
||||
6. 处理插件升级队列 (`PluginUpgradeQueueService`)
|
||||
7. 启动主程序 `app-{version}/LanMountainDesktop.exe`
|
||||
8. 清理标记为 `.destroy` 的旧版本
|
||||
|
||||
**主程序启动流程 (LanMountainDesktop.exe):**
|
||||
|
||||
启动入口在 `LanMountainDesktop/Program.cs`:
|
||||
|
||||
1. 初始化日志、单实例锁和启动诊断
|
||||
@@ -60,17 +73,130 @@
|
||||
### 测试边界
|
||||
|
||||
`LanMountainDesktop.Tests/` 当前主要覆盖:
|
||||
|
||||
- 圆角与外观相关基线
|
||||
- 组件放置与编辑数学
|
||||
- 圆角与外观相关基础
|
||||
- 组件放置与编辑数据
|
||||
- 组件设置服务
|
||||
- UI 异常防护
|
||||
- 白板笔记持久化
|
||||
|
||||
涉及宿主行为、SDK 契约、布局计算或设置持久化的改动,应优先补对应测试。
|
||||
|
||||
### Launcher 架构详解
|
||||
|
||||
#### 职责范围
|
||||
|
||||
`LanMountainDesktop.Launcher/` 作为应用的唯一入口,负责:
|
||||
|
||||
1. **OOBE (首次体验)** - 首次启动引导和欢迎页面
|
||||
2. **Splash Screen** - 启动动画和加载进度显示
|
||||
3. **版本管理** - 多版本并存、版本选择、版本回退
|
||||
4. **应用更新** - 增量更新、静默更新、原子化更新
|
||||
5. **插件管理** - 插件安装、插件更新队列处理
|
||||
|
||||
#### 核心服务
|
||||
|
||||
| 服务 | 职责 |
|
||||
|------|------|
|
||||
| `DeploymentLocator` | 扫描和定位 `app-*` 版本目录,选择最佳版本 |
|
||||
| `UpdateCheckService` | 调用 GitHub Release API 检查更新,支持 Stable/Preview 频道 |
|
||||
| `UpdateEngineService` | 下载、验证、应用增量更新,支持原子化更新和回滚 |
|
||||
| `LauncherFlowCoordinator` | 协调 OOBE → Splash → 更新 → 插件 → 启动主程序的完整流程 |
|
||||
| `OobeStateService` | 管理首次运行状态 |
|
||||
| `PluginInstallerService` | 处理 `.laapp` 插件包安装 |
|
||||
| `PluginUpgradeQueueService` | 批量处理插件升级队列 |
|
||||
|
||||
#### 版本管理机制
|
||||
|
||||
**目录结构:**
|
||||
```
|
||||
安装根目录/
|
||||
├── LanMountainDesktop.Launcher.exe ← 唯一入口
|
||||
├── app-1.0.0/ ← 版本目录
|
||||
│ ├── .current ← 当前版本标记
|
||||
│ ├── LanMountainDesktop.exe
|
||||
│ └── ...
|
||||
├── app-1.0.1/ ← 新版本
|
||||
│ ├── .partial ← 下载中标记
|
||||
│ └── ...
|
||||
└── .launcher/ ← Launcher 数据
|
||||
├── state/ ← OOBE 状态
|
||||
├── update/incoming/ ← 更新缓存
|
||||
└── snapshots/ ← 更新快照
|
||||
```
|
||||
|
||||
**版本选择算法:**
|
||||
1. 扫描所有 `app-*` 目录
|
||||
2. 过滤掉带 `.destroy` 或 `.partial` 标记的目录
|
||||
3. 优先选择带 `.current` 标记的版本
|
||||
4. 如果没有 `.current`,选择版本号最高的
|
||||
|
||||
**版本标记文件:**
|
||||
- `.current` - 标记当前使用的版本
|
||||
- `.partial` - 标记下载未完成的版本 (更新失败时自动清理)
|
||||
- `.destroy` - 标记待删除的旧版本 (下次启动时清理)
|
||||
|
||||
#### 更新流程
|
||||
|
||||
**增量更新:**
|
||||
1. `UpdateCheckService` 调用 GitHub Release API
|
||||
2. 根据更新频道 (Stable/Preview) 过滤版本
|
||||
3. 下载 `delta-{old}-to-{new}.zip` 和 `files-{new}.json`
|
||||
4. 创建 `app-{new}/` 目录并标记 `.partial`
|
||||
5. 解压增量包,从旧版本复用未变更文件
|
||||
6. 验证所有文件 SHA256
|
||||
7. 删除 `.partial`,添加 `.current` 到新版本
|
||||
8. 标记旧版本 `.destroy`
|
||||
9. 保存更新快照到 `.launcher/snapshots/`
|
||||
|
||||
**原子化保证:**
|
||||
- 更新过程中保持 `.partial` 标记
|
||||
- 任何失败都会触发回滚
|
||||
- 旧版本保留直到新版本验证通过
|
||||
- 快照记录允许手动回退
|
||||
|
||||
**版本回退:**
|
||||
```bash
|
||||
LanMountainDesktop.Launcher.exe update rollback
|
||||
```
|
||||
回退会:
|
||||
1. 读取最新的更新快照
|
||||
2. 移除当前版本的 `.current` 标记
|
||||
3. 添加 `.current` 到上一个版本
|
||||
4. 标记当前版本为 `.destroy`
|
||||
|
||||
#### CI/CD 集成
|
||||
|
||||
**发布产物结构:**
|
||||
```
|
||||
GitHub Release Assets:
|
||||
├── LanMountainDesktop-Setup-1.0.1-x64.exe (安装包)
|
||||
├── app-1.0.1.zip (完整应用包)
|
||||
├── delta-1.0.0-to-1.0.1.zip (增量包)
|
||||
├── files-1.0.1.json (文件清单)
|
||||
└── files-1.0.1.json.sig (RSA 签名)
|
||||
```
|
||||
|
||||
**增量包生成:**
|
||||
- `scripts/Generate-DeltaPackage.ps1` - 对比两个版本生成增量包
|
||||
- `scripts/Sign-FileMap.ps1` - 对 `files.json` 进行 RSA 签名
|
||||
- `.github/workflows/release.yml` - 自动生成并上传增量包
|
||||
|
||||
**安装器集成:**
|
||||
- Inno Setup 脚本修改为安装 Launcher 到根目录
|
||||
- 主程序安装到 `app-{version}/` 子目录
|
||||
- 快捷方式指向 `LanMountainDesktop.Launcher.exe`
|
||||
- 安装后验证 Launcher 和 app 目录存在
|
||||
|
||||
## English
|
||||
|
||||
This repository is organized around a desktop host app plus a host-side plugin ecosystem. `LanMountainDesktop/` contains the application entry points, UI, services, component system, and plugin runtime integration. The surrounding projects provide the public SDK, shared contracts, appearance infrastructure, settings primitives, host abstractions, runtime support, and tests.
|
||||
|
||||
The runtime flow starts in `Program.cs`, proceeds into `App.axaml.cs`, initializes settings/theme/localization services, then boots the desktop shell, tray, windows, and plugin runtime. The most important behavior boundaries are component registration, plugin activation, appearance resources, and settings persistence.
|
||||
**Launcher Architecture**: `LanMountainDesktop.Launcher/` serves as the single entry point, managing OOBE, splash screen, multi-version deployment, incremental updates, and plugin installation. It uses a version directory structure (`app-{version}/`) with marker files (`.current`, `.partial`, `.destroy`) to enable atomic updates and rollback capabilities. See the Chinese section above for detailed architecture documentation.
|
||||
|
||||
The runtime flow starts with the Launcher selecting the best version, then proceeds into `Program.cs`, into `App.axaml.cs`, initializes settings/theme/localization services, then boots the desktop shell, tray, windows, and plugin runtime. The most important behavior boundaries are component registration, plugin activation, appearance resources, and settings persistence.
|
||||
|
||||
## VeloPack Integration Note
|
||||
|
||||
- Incremental package build/publish has moved to VeloPack native assets (
|
||||
eleases.win.json + *.nupkg).
|
||||
- Launcher runtime responsibilities are unchanged: OOBE, startup orchestration, update apply, and rollback.
|
||||
|
||||
335
docs/BUILD_AND_DEPLOY.md
Normal file
335
docs/BUILD_AND_DEPLOY.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# 构建和部署指南
|
||||
|
||||
> LanMountainDesktop 完整构建、打包和发布流程
|
||||
|
||||
## 目录
|
||||
|
||||
- [本地构建](#本地构建)
|
||||
- [发布构建](#发布构建)
|
||||
- [生成安装包](#生成安装包)
|
||||
- [CI/CD 流程](#cicd-流程)
|
||||
- [手动发布](#手动发布)
|
||||
|
||||
## 本地构建
|
||||
|
||||
### 环境要求
|
||||
|
||||
- .NET SDK 10.0 或更高版本
|
||||
- Windows 10/11 (推荐)
|
||||
- Inno Setup 6 (仅生成安装包时需要)
|
||||
|
||||
### 快速构建
|
||||
|
||||
```bash
|
||||
# 1. 还原依赖
|
||||
dotnet restore LanMountainDesktop.slnx
|
||||
|
||||
# 2. 构建 Debug 版本
|
||||
dotnet build LanMountainDesktop.slnx -c Debug
|
||||
|
||||
# 3. 运行主程序
|
||||
dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj
|
||||
```
|
||||
|
||||
### 构建 Release 版本
|
||||
|
||||
```bash
|
||||
dotnet build LanMountainDesktop.slnx -c Release
|
||||
```
|
||||
|
||||
## 发布构建
|
||||
|
||||
### Windows (x64, 自包含)
|
||||
|
||||
```bash
|
||||
dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
|
||||
-c Release `
|
||||
-o ./publish/windows-x64 `
|
||||
--self-contained `
|
||||
-r win-x64 `
|
||||
-p:PublishSingleFile=false `
|
||||
-p:DebugType=none `
|
||||
-p:DebugSymbols=false
|
||||
```
|
||||
|
||||
**发布后的目录结构:**
|
||||
```
|
||||
publish/windows-x64/
|
||||
├── LanMountainDesktop.Launcher.exe ← 入口
|
||||
├── app-{version}/ ← 主程序
|
||||
│ ├── .current
|
||||
│ ├── LanMountainDesktop.exe
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
### Linux (x64)
|
||||
|
||||
```bash
|
||||
dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
|
||||
-c Release `
|
||||
-o ./publish/linux-x64 `
|
||||
--self-contained `
|
||||
-r linux-x64
|
||||
```
|
||||
|
||||
### macOS (arm64)
|
||||
|
||||
```bash
|
||||
dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
|
||||
-c Release `
|
||||
-o ./publish/osx-arm64 `
|
||||
--self-contained `
|
||||
-r osx-arm64
|
||||
```
|
||||
|
||||
## 生成安装包
|
||||
|
||||
### Windows 安装包 (Inno Setup)
|
||||
|
||||
**前提条件:**
|
||||
```powershell
|
||||
# 安装 Inno Setup
|
||||
choco install innosetup -y
|
||||
```
|
||||
|
||||
**生成安装包:**
|
||||
```powershell
|
||||
# 1. 发布应用
|
||||
dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
|
||||
-c Release `
|
||||
-o ./publish/windows-x64 `
|
||||
--self-contained `
|
||||
-r win-x64
|
||||
|
||||
# 2. 运行 Inno Setup 编译器
|
||||
$version = "1.0.0"
|
||||
$arch = "x64"
|
||||
|
||||
iscc.exe `
|
||||
/DMyAppVersion=$version `
|
||||
/DMyAppArch=$arch `
|
||||
/DPublishDir="publish\windows-x64" `
|
||||
/DMyOutputDir="build-installer" `
|
||||
LanMountainDesktop\installer\LanMountainDesktop.iss
|
||||
```
|
||||
|
||||
**输出:**
|
||||
```
|
||||
build-installer/
|
||||
└── LanMountainDesktop-Setup-1.0.0-x64.exe
|
||||
```
|
||||
|
||||
### Linux 包 (.deb)
|
||||
|
||||
```bash
|
||||
# TODO: 添加 .deb 打包脚本
|
||||
```
|
||||
|
||||
### macOS 包 (.dmg)
|
||||
|
||||
```bash
|
||||
# TODO: 添加 .dmg 打包脚本
|
||||
```
|
||||
|
||||
## CI/CD 流程
|
||||
|
||||
### GitHub Actions 工作流
|
||||
|
||||
项目使用 GitHub Actions 自动化构建和发布。
|
||||
|
||||
**触发条件:**
|
||||
- 推送 `v*` 标签 (例如: `v1.0.0`)
|
||||
- 手动触发 (workflow_dispatch)
|
||||
|
||||
**工作流文件:** `.github/workflows/release.yml`
|
||||
|
||||
### 发布流程
|
||||
|
||||
```
|
||||
1. prepare job
|
||||
├─ 解析版本号
|
||||
└─ 设置构建变量
|
||||
|
||||
2. build-windows job
|
||||
├─ 构建 x64 和 x86 版本
|
||||
├─ 重组为 app-{version} 结构
|
||||
├─ 生成增量包
|
||||
├─ 生成 Inno Setup 安装包
|
||||
└─ 上传 artifacts
|
||||
|
||||
3. build-linux job
|
||||
├─ 构建 x64 版本
|
||||
├─ 生成 .deb 包
|
||||
└─ 上传 artifacts
|
||||
|
||||
4. build-macos job
|
||||
├─ 构建 arm64 和 x64 版本
|
||||
├─ 生成 .dmg 包
|
||||
└─ 上传 artifacts
|
||||
|
||||
5. release job
|
||||
├─ 下载所有 artifacts
|
||||
├─ 创建 GitHub Release
|
||||
└─ 上传所有安装包和增量包
|
||||
```
|
||||
|
||||
### 发布产物
|
||||
|
||||
**GitHub Release Assets:**
|
||||
```
|
||||
LanMountainDesktop-v1.0.0/
|
||||
├── LanMountainDesktop-Setup-1.0.0-x64.exe # Windows 安装包
|
||||
├── LanMountainDesktop-Setup-1.0.0-x86.exe
|
||||
├── LanMountainDesktop-1.0.0-linux-x64.deb # Linux 包
|
||||
├── LanMountainDesktop-1.0.0-macos-arm64.dmg # macOS 包
|
||||
├── app-1.0.0.zip # 完整应用包
|
||||
├── delta-0.9.9-to-1.0.0.zip # 增量包
|
||||
├── files-1.0.0.json # 文件清单
|
||||
└── files-1.0.0.json.sig # RSA 签名
|
||||
```
|
||||
|
||||
## 手动发布
|
||||
|
||||
### 1. 准备发布
|
||||
|
||||
```bash
|
||||
# 1. 更新版本号
|
||||
# 编辑 Directory.Build.props 中的 <Version>
|
||||
|
||||
# 2. 更新 CHANGELOG.md
|
||||
# 记录本次发布的变更
|
||||
|
||||
# 3. 提交变更
|
||||
git add .
|
||||
git commit -m "chore: prepare release v1.0.0"
|
||||
git push
|
||||
```
|
||||
|
||||
### 2. 创建 Release 标签
|
||||
|
||||
```bash
|
||||
# 创建标签
|
||||
git tag v1.0.0
|
||||
|
||||
# 推送标签 (触发 CI)
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
### 3. 等待 CI 完成
|
||||
|
||||
访问 GitHub Actions 页面,等待构建完成:
|
||||
```
|
||||
https://github.com/YourOrg/LanMountainDesktop/actions
|
||||
```
|
||||
|
||||
### 4. 验证 Release
|
||||
|
||||
1. 访问 Releases 页面
|
||||
2. 检查所有安装包是否上传成功
|
||||
3. 下载并测试安装包
|
||||
4. 验证增量更新功能
|
||||
|
||||
### 5. 发布公告
|
||||
|
||||
- 在 GitHub Release 中编辑发布说明
|
||||
- 发布到社区/论坛
|
||||
- 更新官网下载链接
|
||||
|
||||
## 增量包生成
|
||||
|
||||
### 手动生成增量包
|
||||
|
||||
```powershell
|
||||
# 1. 准备两个版本的发布目录
|
||||
dotnet publish ... -o ./publish/app-1.0.0
|
||||
dotnet publish ... -o ./publish/app-1.0.1
|
||||
|
||||
# 2. 生成增量包
|
||||
./scripts/Generate-DeltaPackage.ps1 `
|
||||
-PreviousVersion "1.0.0" `
|
||||
-CurrentVersion "1.0.1" `
|
||||
-PreviousDir "./publish/app-1.0.0" `
|
||||
-CurrentDir "./publish/app-1.0.1" `
|
||||
-OutputDir "./delta-output"
|
||||
|
||||
# 3. 签名文件清单
|
||||
./scripts/Sign-FileMap.ps1 `
|
||||
-FilesJsonPath "./delta-output/files-1.0.1.json" `
|
||||
-PrivateKeyPath "./private-key.pem"
|
||||
```
|
||||
|
||||
**输出:**
|
||||
```
|
||||
delta-output/
|
||||
├── delta-1.0.0-to-1.0.1.zip
|
||||
├── files-1.0.1.json
|
||||
└── files-1.0.1.json.sig
|
||||
```
|
||||
|
||||
### 生成 RSA 密钥对
|
||||
|
||||
```powershell
|
||||
# 生成私钥
|
||||
openssl genrsa -out private-key.pem 2048
|
||||
|
||||
# 提取公钥
|
||||
openssl rsa -in private-key.pem -pubout -out public-key.pem
|
||||
```
|
||||
|
||||
**重要:**
|
||||
- 私钥保存在安全位置 (GitHub Secrets)
|
||||
- 公钥打包到 Launcher 中 (`.launcher/update/public-key.pem`)
|
||||
|
||||
## 版本号规范
|
||||
|
||||
遵循 [Semantic Versioning 2.0.0](https://semver.org/):
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
|
||||
|
||||
例如:
|
||||
- 1.0.0 (正式版)
|
||||
- 1.0.1 (补丁版本)
|
||||
- 1.1.0 (新功能)
|
||||
- 2.0.0 (破坏性变更)
|
||||
- 1.0.0-beta.1 (预览版)
|
||||
- 1.0.0-rc.1 (候选版本)
|
||||
```
|
||||
|
||||
### 版本号更新规则
|
||||
|
||||
- **MAJOR**: 破坏性 API 变更
|
||||
- **MINOR**: 新功能,向后兼容
|
||||
- **PATCH**: Bug 修复,向后兼容
|
||||
- **PRERELEASE**: 预览版标识 (alpha, beta, rc)
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 构建失败
|
||||
|
||||
**问题**: `error NU1102: Unable to find package`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
dotnet restore --force
|
||||
dotnet nuget locals all --clear
|
||||
```
|
||||
|
||||
### 发布失败
|
||||
|
||||
**问题**: Launcher 目录不存在
|
||||
|
||||
**解决**: 检查 `LanMountainDesktop.csproj` 中的 `CopyLauncherToPublish` 目标是否正确执行。
|
||||
|
||||
### 安装包生成失败
|
||||
|
||||
**问题**: Inno Setup 找不到文件
|
||||
|
||||
**解决**: 确保 `PublishDir` 路径正确,且包含 `app-{version}/` 目录结构。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [开发文档](DEVELOPMENT.md)
|
||||
- [Launcher 架构](LAUNCHER.md)
|
||||
- [更新系统](UPDATE_SYSTEM.md)
|
||||
- [故障排除](TROUBLESHOOTING.md)
|
||||
@@ -20,10 +20,32 @@ dotnet build LanMountainDesktop.slnx -c Debug
|
||||
|
||||
#### 运行桌面宿主
|
||||
|
||||
**开发模式 (直接运行主程序,跳过 Launcher):**
|
||||
```bash
|
||||
dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj
|
||||
```
|
||||
|
||||
**生产模式 (通过 Launcher 启动):**
|
||||
```bash
|
||||
# 先构建 Launcher
|
||||
dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug
|
||||
|
||||
# 通过 Launcher 启动主程序
|
||||
dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- launch
|
||||
```
|
||||
|
||||
**Launcher 其他命令:**
|
||||
```bash
|
||||
# 检查更新
|
||||
dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- update check
|
||||
|
||||
# 安装插件
|
||||
dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- plugin install <path-to-plugin.laapp>
|
||||
|
||||
# 版本回退
|
||||
dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- update rollback
|
||||
```
|
||||
|
||||
#### 运行测试
|
||||
|
||||
```bash
|
||||
@@ -33,13 +55,18 @@ dotnet test LanMountainDesktop.slnx -c Debug
|
||||
### 常见工作区域
|
||||
|
||||
- 宿主应用:`LanMountainDesktop/`
|
||||
- **Launcher (启动器):`LanMountainDesktop.Launcher/`**
|
||||
- Plugin SDK:`LanMountainDesktop.PluginSdk/`
|
||||
- 共享契约:`LanMountainDesktop.Shared.Contracts/`
|
||||
- 测试:`LanMountainDesktop.Tests/`
|
||||
- 插件打包脚本:`scripts/Pack-PluginPackages.ps1`
|
||||
- **增量更新脚本:`scripts/Generate-DeltaPackage.ps1`, `scripts/Sign-FileMap.ps1`**
|
||||
|
||||
### 调试建议
|
||||
|
||||
- **Launcher 启动问题优先看 `LanMountainDesktop.Launcher/Program.cs` 和 `Services/LauncherFlowCoordinator.cs`**
|
||||
- **版本管理问题优先看 `LanMountainDesktop.Launcher/Services/DeploymentLocator.cs`**
|
||||
- **更新系统问题优先看 `LanMountainDesktop.Launcher/Services/UpdateEngineService.cs` 和 `UpdateCheckService.cs`**
|
||||
- 启动问题优先看 `LanMountainDesktop/Program.cs` 和 `LanMountainDesktop/App.axaml.cs`
|
||||
- 设置窗口和设置页问题优先看 `LanMountainDesktop/Views/`、`ViewModels/` 与相关 `Services/`
|
||||
- 插件加载与安装问题优先看 `LanMountainDesktop/plugins/`
|
||||
@@ -74,8 +101,75 @@ dotnet test LanMountainDesktop.slnx -c Debug
|
||||
- 需求与实施拆解更新到 `.trae/specs/`
|
||||
- AI 协作入口和代码地图更新到 `AGENTS.md` 与 `docs/ai/`
|
||||
|
||||
### Launcher 架构说明
|
||||
|
||||
LanMountainDesktop 使用 Launcher 作为唯一入口,负责版本管理、更新和启动主程序。
|
||||
|
||||
#### 目录结构
|
||||
|
||||
安装后的目录结构:
|
||||
```
|
||||
C:\Program Files\LanMountainDesktop\
|
||||
├── LanMountainDesktop.Launcher.exe ← 唯一入口
|
||||
├── app-1.0.0/ ← 版本目录
|
||||
│ ├── .current ← 当前版本标记
|
||||
│ ├── LanMountainDesktop.exe
|
||||
│ └── ... (所有依赖)
|
||||
├── app-1.0.1/ ← 新版本
|
||||
│ ├── .partial ← 下载中标记
|
||||
│ └── ...
|
||||
└── .launcher/ ← Launcher 数据
|
||||
├── state/ ← OOBE 状态
|
||||
├── update/incoming/ ← 更新缓存
|
||||
└── snapshots/ ← 更新快照
|
||||
```
|
||||
|
||||
#### 版本标记文件
|
||||
|
||||
- `.current` - 标记当前使用的版本
|
||||
- `.partial` - 标记下载未完成的版本
|
||||
- `.destroy` - 标记待删除的旧版本
|
||||
|
||||
#### 启动流程
|
||||
|
||||
1. 用户启动 `LanMountainDesktop.Launcher.exe`
|
||||
2. Launcher 扫描 `app-*` 目录,选择最佳版本
|
||||
3. 如果是首次启动,显示 OOBE 引导
|
||||
4. 显示 Splash 启动动画
|
||||
5. 检查并应用待处理的更新
|
||||
6. 处理插件升级队列
|
||||
7. 启动主程序 `app-{version}/LanMountainDesktop.exe`
|
||||
8. 清理标记为 `.destroy` 的旧版本
|
||||
|
||||
#### 更新流程
|
||||
|
||||
1. Launcher 调用 GitHub Release API 检查更新
|
||||
2. 根据更新频道(Stable/Preview)过滤版本
|
||||
3. 下载增量包到 `app-{new_version}/` 并标记 `.partial`
|
||||
4. 验证文件完整性(SHA256)
|
||||
5. 删除 `.partial`,添加 `.current` 到新版本
|
||||
6. 标记旧版本 `.destroy`
|
||||
7. 下次启动时自动清理
|
||||
|
||||
#### 版本回退
|
||||
|
||||
```bash
|
||||
dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- update rollback
|
||||
```
|
||||
|
||||
回退会切换到上一个有效版本,并保留快照记录。
|
||||
|
||||
## English
|
||||
|
||||
Use `LanMountainDesktop.slnx` as the workspace entry point. The standard loop is `dotnet restore`, `dotnet build LanMountainDesktop.slnx -c Debug`, `dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj`, and `dotnet test LanMountainDesktop.slnx -c Debug`.
|
||||
|
||||
For packaging, see `LanMountainDesktop/PACKAGING.md`. For plugin package generation or local feed workflows, use `scripts/Pack-PluginPackages.ps1`.
|
||||
|
||||
**Launcher Architecture**: LanMountainDesktop uses a Launcher as the single entry point, responsible for version management, updates, and launching the main application. See the Chinese section above for detailed architecture documentation.
|
||||
|
||||
## VeloPack Release Assets
|
||||
|
||||
- Windows incremental release packaging now uses VeloPack native outputs (
|
||||
eleases.win.json, *.nupkg).
|
||||
- Launcher still performs update apply/rollback; VeloPack is used for package generation.
|
||||
- Legacy delta script flow is retained behind a disabled fallback switch in CI.
|
||||
|
||||
78
docs/HOST_DISCOVERY.md
Normal file
78
docs/HOST_DISCOVERY.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 主程序发现配置指南
|
||||
|
||||
Launcher 支持灵活的主程序发现机制,可以通过多种方式配置主程序路径。
|
||||
|
||||
## 发现优先级
|
||||
|
||||
1. **环境变量** (`LMD_HOST_PATH`) - 最高优先级
|
||||
2. **配置文件** (`host-discovery.json`)
|
||||
3. **开发模式保存的路径** - 通过调试窗口选择
|
||||
4. **部署目录** (`app-*`)
|
||||
5. **开发路径** - 自动搜索解决方案中的 bin 目录
|
||||
6. **额外配置路径** - 自定义搜索路径
|
||||
7. **递归搜索** - 如果启用
|
||||
|
||||
## 配置方式
|
||||
|
||||
### 1. 环境变量
|
||||
|
||||
设置 `LMD_HOST_PATH` 环境变量指向主程序可执行文件:
|
||||
|
||||
```powershell
|
||||
$env:LMD_HOST_PATH = "C:\MyApp\LanMountainDesktop.exe"
|
||||
```
|
||||
|
||||
### 2. 配置文件
|
||||
|
||||
在应用根目录创建 `host-discovery.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"HostPath": "C:\\Custom\\Path\\LanMountainDesktop.exe",
|
||||
"AdditionalPaths": [
|
||||
"${AppRoot}/custom",
|
||||
"${UserProfile}/dev/build",
|
||||
"C:/Program Files/LanMountainDesktop/*"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 开发模式
|
||||
|
||||
在错误窗口中按 `Ctrl+Shift+D` 打开调试窗口,启用开发模式并选择自定义路径。路径会自动保存,下次启动时优先使用。
|
||||
|
||||
## 路径变量
|
||||
|
||||
配置文件支持以下变量:
|
||||
|
||||
- `${AppRoot}` - 应用根目录
|
||||
- `${BaseDirectory}` - Launcher 所在目录
|
||||
- `${UserProfile}` - 用户主目录
|
||||
- `${LocalAppData}` - 本地应用数据目录
|
||||
|
||||
## 通配符支持
|
||||
|
||||
`AdditionalPaths` 支持通配符:
|
||||
|
||||
```json
|
||||
{
|
||||
"AdditionalPaths": [
|
||||
"C:/Builds/*/LanMountainDesktop.exe",
|
||||
"${AppRoot}/versions/*/app.exe"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 递归搜索
|
||||
|
||||
启用递归搜索可以自动在子目录中查找主程序:
|
||||
|
||||
```csharp
|
||||
var options = new HostDiscoveryOptions
|
||||
{
|
||||
RecursiveSearch = true,
|
||||
MaxRecursionDepth = 3
|
||||
};
|
||||
```
|
||||
|
||||
注意:递归搜索可能影响启动性能,建议仅在必要时启用。
|
||||
549
docs/LAUNCHER.md
Normal file
549
docs/LAUNCHER.md
Normal file
@@ -0,0 +1,549 @@
|
||||
# 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)
|
||||
129
docs/LAUNCHER_DISTRIBUTION.md
Normal file
129
docs/LAUNCHER_DISTRIBUTION.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Launcher 打包分发指南
|
||||
|
||||
## 目录结构
|
||||
|
||||
打包给用户的 Launcher 应该包含以下结构:
|
||||
|
||||
```
|
||||
LanMountainDesktop/
|
||||
├── LanMountainDesktop.Launcher.exe # 启动器可执行文件
|
||||
├── LanMountainDesktop.Launcher.dll # 启动器依赖
|
||||
├── ... # 其他启动器依赖文件
|
||||
├── app-1.0.0/ # 主程序部署目录
|
||||
│ ├── LanMountainDesktop.exe # 主程序可执行文件
|
||||
│ ├── LanMountainDesktop.dll # 主程序依赖
|
||||
│ ├── version.json # 版本信息文件
|
||||
│ └── .current # 当前版本标记文件
|
||||
└── plugins/ # 插件目录(可选)
|
||||
```
|
||||
|
||||
## 打包步骤
|
||||
|
||||
### 1. 构建 Launcher
|
||||
|
||||
```bash
|
||||
dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Release
|
||||
```
|
||||
|
||||
### 2. 构建主程序
|
||||
|
||||
```bash
|
||||
dotnet build LanMountainDesktop/LanMountainDesktop.csproj -c Release
|
||||
```
|
||||
|
||||
### 3. 创建部署目录
|
||||
|
||||
```powershell
|
||||
# 创建版本目录
|
||||
New-Item -ItemType Directory -Path "dist/app-1.0.0" -Force
|
||||
|
||||
# 复制主程序文件
|
||||
Copy-Item "LanMountainDesktop/bin/Release/net10.0/*" "dist/app-1.0.0/" -Recurse
|
||||
|
||||
# 创建版本标记
|
||||
New-Item -ItemType File -Path "dist/app-1.0.0/.current" -Force
|
||||
```
|
||||
|
||||
### 4. 复制 Launcher
|
||||
|
||||
```powershell
|
||||
# 复制启动器文件
|
||||
Copy-Item "LanMountainDesktop.Launcher/bin/Release/net10.0/*" "dist/" -Recurse
|
||||
```
|
||||
|
||||
### 5. 创建安装包
|
||||
|
||||
可以使用以下工具创建安装包:
|
||||
- **Inno Setup** - Windows 安装程序
|
||||
- **WiX Toolset** - Windows Installer
|
||||
- **MSIX** - Windows 应用包
|
||||
- **Zip** - 便携版
|
||||
|
||||
## 用户数据存储位置
|
||||
|
||||
Launcher 会将用户配置存储在以下位置:
|
||||
|
||||
```
|
||||
%LOCALAPPDATA%\LanMountainDesktop\.launcher\
|
||||
├── devmode.config # 开发模式状态
|
||||
└── custom-host-path.config # 自定义主程序路径
|
||||
```
|
||||
|
||||
这些文件:
|
||||
- **不会**随应用更新而删除
|
||||
- **不会**随应用卸载而删除(除非用户手动清理)
|
||||
- 在重装应用后会自动恢复之前的配置
|
||||
|
||||
## 生产环境行为
|
||||
|
||||
### 正常启动流程
|
||||
|
||||
1. 用户双击 `LanMountainDesktop.Launcher.exe`
|
||||
2. Launcher 查找 `app-*` 目录中的主程序
|
||||
3. 启动主程序并传递版本信息
|
||||
4. 主程序显示正确的版本和开发代号
|
||||
|
||||
### 更新流程
|
||||
|
||||
1. 新版本下载到 `app-{new-version}/`
|
||||
2. 创建 `.current` 标记指向新版本
|
||||
3. 旧版本标记为 `.destroy`
|
||||
4. 下次启动时自动使用新版本
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
### 启用开发模式
|
||||
|
||||
1. 启动 Launcher,如果找不到主程序会显示错误窗口
|
||||
2. 按 `Ctrl+Shift+D` 打开调试窗口
|
||||
3. 勾选"启用开发模式"
|
||||
4. 选择自定义主程序路径
|
||||
5. 关闭窗口,配置会自动保存
|
||||
|
||||
### 开发模式优先级
|
||||
|
||||
开发模式的配置**不会**影响生产环境:
|
||||
- 生产环境优先使用 `app-*` 目录
|
||||
- 开发模式仅在找不到部署目录时生效
|
||||
- 开发模式配置保存在用户数据目录,不影响其他用户
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Launcher 找不到主程序
|
||||
|
||||
1. 检查 `app-*` 目录是否存在
|
||||
2. 检查 `.current` 标记文件是否存在
|
||||
3. 检查主程序可执行文件是否存在
|
||||
4. 查看 `%LOCALAPPDATA%\LanMountainDesktop\.launcher\` 下的配置
|
||||
|
||||
### 版本信息不正确
|
||||
|
||||
1. 检查 `app-*/version.json` 是否存在
|
||||
2. 检查 `version.json` 内容是否正确
|
||||
3. 重新构建主程序生成新的 `version.json`
|
||||
|
||||
### 开发模式配置丢失
|
||||
|
||||
1. 检查 `%LOCALAPPDATA%\LanMountainDesktop\.launcher\` 目录权限
|
||||
2. 检查磁盘空间是否充足
|
||||
3. 手动删除配置目录后重新配置
|
||||
686
docs/PLUGIN_DEVELOPMENT.md
Normal file
686
docs/PLUGIN_DEVELOPMENT.md
Normal file
@@ -0,0 +1,686 @@
|
||||
# 插件开发指南
|
||||
|
||||
> 为 LanMountainDesktop 开发自定义插件
|
||||
|
||||
## 目录
|
||||
|
||||
- [快速开始](#快速开始)
|
||||
- [插件架构](#插件架构)
|
||||
- [创建插件](#创建插件)
|
||||
- [插件生命周期](#插件生命周期)
|
||||
- [添加组件](#添加组件)
|
||||
- [添加设置页](#添加设置页)
|
||||
- [使用服务](#使用服务)
|
||||
- [打包和发布](#打包和发布)
|
||||
- [最佳实践](#最佳实践)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装插件模板
|
||||
|
||||
```bash
|
||||
# 安装官方插件模板
|
||||
dotnet new install LanMountainDesktop.PluginTemplate
|
||||
|
||||
# 查看可用模板
|
||||
dotnet new list | findstr lmd
|
||||
```
|
||||
|
||||
### 创建新插件
|
||||
|
||||
```bash
|
||||
# 创建插件项目
|
||||
dotnet new lmd-plugin -n MyAwesomePlugin
|
||||
|
||||
# 进入项目目录
|
||||
cd MyAwesomePlugin
|
||||
|
||||
# 还原依赖
|
||||
dotnet restore
|
||||
|
||||
# 构建插件
|
||||
dotnet build
|
||||
```
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
MyAwesomePlugin/
|
||||
├── MyAwesomePlugin.csproj # 项目文件
|
||||
├── Plugin.cs # 插件入口
|
||||
├── Components/ # 组件目录
|
||||
│ └── MyComponent.cs
|
||||
├── Views/ # 视图目录
|
||||
│ └── MyComponentView.axaml
|
||||
├── ViewModels/ # 视图模型
|
||||
│ └── MyComponentViewModel.cs
|
||||
├── Settings/ # 设置页
|
||||
│ └── MySettingsPage.axaml
|
||||
└── plugin.json # 插件清单
|
||||
```
|
||||
|
||||
## 插件架构
|
||||
|
||||
### 插件 SDK 版本
|
||||
|
||||
当前 SDK 版本: **4.0.1**
|
||||
|
||||
```xml
|
||||
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="4.0.1" />
|
||||
<PackageReference Include="LanMountainDesktop.Shared.Contracts" Version="4.0.1" />
|
||||
```
|
||||
|
||||
### 插件清单 (plugin.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "com.example.myawesomeplugin",
|
||||
"Name": "My Awesome Plugin",
|
||||
"Version": "1.0.0",
|
||||
"Author": "Your Name",
|
||||
"Description": "A plugin that does awesome things",
|
||||
"MinHostVersion": "1.0.0",
|
||||
"Dependencies": [],
|
||||
"Permissions": [
|
||||
"FileSystem.Read",
|
||||
"Network.Access"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 核心接口
|
||||
|
||||
**IPlugin** - 插件入口接口:
|
||||
```csharp
|
||||
public interface IPlugin
|
||||
{
|
||||
string Id { get; }
|
||||
string Name { get; }
|
||||
string Version { get; }
|
||||
|
||||
Task InitializeAsync(IPluginContext context);
|
||||
Task ShutdownAsync();
|
||||
}
|
||||
```
|
||||
|
||||
**IPluginContext** - 插件上下文:
|
||||
```csharp
|
||||
public interface IPluginContext
|
||||
{
|
||||
string PluginDirectory { get; }
|
||||
IServiceProvider Services { get; }
|
||||
ILogger Logger { get; }
|
||||
ISettingsService Settings { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## 创建插件
|
||||
|
||||
### 1. 实现插件入口
|
||||
|
||||
```csharp
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
|
||||
namespace MyAwesomePlugin;
|
||||
|
||||
public class Plugin : IPlugin
|
||||
{
|
||||
public string Id => "com.example.myawesomeplugin";
|
||||
public string Name => "My Awesome Plugin";
|
||||
public string Version => "1.0.0";
|
||||
|
||||
private IPluginContext? _context;
|
||||
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
// 注册组件
|
||||
var componentRegistry = context.Services.GetService<IComponentRegistry>();
|
||||
componentRegistry?.RegisterComponent<MyComponent>();
|
||||
|
||||
// 注册设置页
|
||||
var settingsRegistry = context.Services.GetService<ISettingsPageRegistry>();
|
||||
settingsRegistry?.RegisterPage<MySettingsPage>("我的插件设置");
|
||||
|
||||
// 初始化逻辑
|
||||
context.Logger.LogInformation("Plugin initialized");
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
// 清理资源
|
||||
_context?.Logger.LogInformation("Plugin shutting down");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 配置项目文件
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!-- 插件元数据 -->
|
||||
<PluginId>com.example.myawesomeplugin</PluginId>
|
||||
<PluginName>My Awesome Plugin</PluginName>
|
||||
<PluginVersion>1.0.0</PluginVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="4.0.1" />
|
||||
<PackageReference Include="LanMountainDesktop.Shared.Contracts" Version="4.0.1" />
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 复制 plugin.json 到输出目录 -->
|
||||
<ItemGroup>
|
||||
<None Update="plugin.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## 插件生命周期
|
||||
|
||||
### 生命周期阶段
|
||||
|
||||
```
|
||||
1. 发现 (Discovery)
|
||||
↓
|
||||
2. 加载 (Load)
|
||||
├─ 加载程序集
|
||||
├─ 验证依赖
|
||||
└─ 创建插件实例
|
||||
↓
|
||||
3. 初始化 (Initialize)
|
||||
├─ 调用 InitializeAsync()
|
||||
├─ 注册组件
|
||||
├─ 注册设置页
|
||||
└─ 初始化服务
|
||||
↓
|
||||
4. 运行 (Running)
|
||||
├─ 组件渲染
|
||||
├─ 事件处理
|
||||
└─ 服务调用
|
||||
↓
|
||||
5. 关闭 (Shutdown)
|
||||
├─ 调用 ShutdownAsync()
|
||||
├─ 清理资源
|
||||
└─ 卸载程序集
|
||||
```
|
||||
|
||||
### 生命周期钩子
|
||||
|
||||
```csharp
|
||||
public class Plugin : IPlugin
|
||||
{
|
||||
// 插件加载后立即调用
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
// 注册组件、服务、设置页
|
||||
// 初始化资源
|
||||
}
|
||||
|
||||
// 插件卸载前调用
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
// 保存状态
|
||||
// 释放资源
|
||||
// 取消订阅
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 添加组件
|
||||
|
||||
### 1. 定义组件类
|
||||
|
||||
```csharp
|
||||
using LanMountainDesktop.PluginSdk.Components;
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
|
||||
namespace MyAwesomePlugin.Components;
|
||||
|
||||
[Component(
|
||||
Id = "com.example.myawesomeplugin.mycomponent",
|
||||
Name = "我的组件",
|
||||
Description = "一个很棒的组件",
|
||||
Category = "工具",
|
||||
Icon = "avares://MyAwesomePlugin/Assets/icon.png"
|
||||
)]
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
public override string Id => "com.example.myawesomeplugin.mycomponent";
|
||||
public override string Name => "我的组件";
|
||||
|
||||
// 组件设置
|
||||
private string _message = "Hello, World!";
|
||||
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
// 组件初始化
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
// 加载设置
|
||||
Message = Settings.GetValue("Message", "Hello, World!");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// 组件更新 (定时调用)
|
||||
public override Task UpdateAsync()
|
||||
{
|
||||
// 更新组件数据
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建组件视图
|
||||
|
||||
**MyComponentView.axaml:**
|
||||
```xml
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:MyAwesomePlugin.ViewModels"
|
||||
x:Class="MyAwesomePlugin.Views.MyComponentView"
|
||||
x:DataType="vm:MyComponentViewModel">
|
||||
<Border Background="{DynamicResource CardBackgroundBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
Padding="16">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{Binding Component.Name}"
|
||||
FontSize="18"
|
||||
FontWeight="Bold" />
|
||||
|
||||
<TextBlock Text="{Binding Component.Message}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<Button Content="点击我"
|
||||
Command="{Binding ClickCommand}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
```
|
||||
|
||||
**MyComponentView.axaml.cs:**
|
||||
```csharp
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace MyAwesomePlugin.Views;
|
||||
|
||||
public partial class MyComponentView : UserControl
|
||||
{
|
||||
public MyComponentView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 创建视图模型
|
||||
|
||||
```csharp
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace MyAwesomePlugin.ViewModels;
|
||||
|
||||
public partial class MyComponentViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private MyComponent _component;
|
||||
|
||||
public MyComponentViewModel(MyComponent component)
|
||||
{
|
||||
_component = component;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Click()
|
||||
{
|
||||
Component.Message = "按钮被点击了!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 注册组件
|
||||
|
||||
```csharp
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
var componentRegistry = context.Services.GetService<IComponentRegistry>();
|
||||
|
||||
// 注册组件
|
||||
componentRegistry?.RegisterComponent<MyComponent>(
|
||||
componentFactory: () => new MyComponent(),
|
||||
viewFactory: (component) => new MyComponentView
|
||||
{
|
||||
DataContext = new MyComponentViewModel((MyComponent)component)
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 添加设置页
|
||||
|
||||
### 1. 创建设置页视图
|
||||
|
||||
**MySettingsPage.axaml:**
|
||||
```xml
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="MyAwesomePlugin.Settings.MySettingsPage">
|
||||
<StackPanel Spacing="16" Margin="24">
|
||||
<TextBlock Text="我的插件设置"
|
||||
FontSize="24"
|
||||
FontWeight="Bold" />
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="消息内容:" />
|
||||
<TextBox x:Name="MessageTextBox"
|
||||
Watermark="输入消息..." />
|
||||
</StackPanel>
|
||||
|
||||
<Button Content="保存"
|
||||
Click="SaveButton_Click" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
```
|
||||
|
||||
**MySettingsPage.axaml.cs:**
|
||||
```csharp
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace MyAwesomePlugin.Settings;
|
||||
|
||||
public partial class MySettingsPage : UserControl
|
||||
{
|
||||
private readonly ISettingsService _settings;
|
||||
|
||||
public MySettingsPage(ISettingsService settings)
|
||||
{
|
||||
InitializeComponent();
|
||||
_settings = settings;
|
||||
|
||||
// 加载设置
|
||||
MessageTextBox.Text = _settings.GetValue("Message", "Hello, World!");
|
||||
}
|
||||
|
||||
private void SaveButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// 保存设置
|
||||
_settings.SetValue("Message", MessageTextBox.Text);
|
||||
|
||||
// 显示提示
|
||||
// TODO: 显示保存成功提示
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 注册设置页
|
||||
|
||||
```csharp
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
var settingsRegistry = context.Services.GetService<ISettingsPageRegistry>();
|
||||
|
||||
settingsRegistry?.RegisterPage(
|
||||
title: "我的插件",
|
||||
category: "插件",
|
||||
pageFactory: () => new MySettingsPage(context.Settings)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 使用服务
|
||||
|
||||
### 可用服务
|
||||
|
||||
**ILogger** - 日志服务:
|
||||
```csharp
|
||||
context.Logger.LogInformation("信息日志");
|
||||
context.Logger.LogWarning("警告日志");
|
||||
context.Logger.LogError("错误日志");
|
||||
```
|
||||
|
||||
**ISettingsService** - 设置服务:
|
||||
```csharp
|
||||
// 读取设置
|
||||
var value = context.Settings.GetValue("Key", "DefaultValue");
|
||||
|
||||
// 写入设置
|
||||
context.Settings.SetValue("Key", "NewValue");
|
||||
|
||||
// 监听设置变化
|
||||
context.Settings.SettingChanged += (sender, e) =>
|
||||
{
|
||||
if (e.Key == "Key")
|
||||
{
|
||||
// 设置已变更
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**INotificationService** - 通知服务:
|
||||
```csharp
|
||||
var notificationService = context.Services.GetService<INotificationService>();
|
||||
|
||||
notificationService?.ShowNotification(
|
||||
title: "通知标题",
|
||||
message: "通知内容",
|
||||
type: NotificationType.Information
|
||||
);
|
||||
```
|
||||
|
||||
**IHttpClientFactory** - HTTP 客户端:
|
||||
```csharp
|
||||
var httpFactory = context.Services.GetService<IHttpClientFactory>();
|
||||
var httpClient = httpFactory?.CreateClient();
|
||||
|
||||
var response = await httpClient.GetStringAsync("https://api.example.com/data");
|
||||
```
|
||||
|
||||
## 打包和发布
|
||||
|
||||
### 1. 构建插件
|
||||
|
||||
```bash
|
||||
dotnet build -c Release
|
||||
```
|
||||
|
||||
### 2. 打包为 .laapp
|
||||
|
||||
```bash
|
||||
# 使用官方打包脚本
|
||||
pwsh ./scripts/Pack-PluginPackages.ps1 -PluginProject ./MyAwesomePlugin/MyAwesomePlugin.csproj
|
||||
|
||||
# 或手动打包
|
||||
cd MyAwesomePlugin/bin/Release/net10.0
|
||||
zip -r MyAwesomePlugin-1.0.0.laapp *
|
||||
```
|
||||
|
||||
### 3. 测试插件
|
||||
|
||||
```bash
|
||||
# 安装插件
|
||||
LanMountainDesktop.Launcher.exe plugin install MyAwesomePlugin-1.0.0.laapp
|
||||
|
||||
# 启动应用测试
|
||||
LanMountainDesktop.Launcher.exe launch
|
||||
```
|
||||
|
||||
### 4. 发布插件
|
||||
|
||||
**选项 1: GitHub Release**
|
||||
1. 创建 GitHub 仓库
|
||||
2. 上传 `.laapp` 文件到 Release
|
||||
3. 用户可以手动下载安装
|
||||
|
||||
**选项 2: 插件市场** (如果可用)
|
||||
1. 提交插件到官方市场
|
||||
2. 等待审核
|
||||
3. 用户可以在应用内浏览和安装
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. **避免阻塞 UI 线程:**
|
||||
```csharp
|
||||
// 错误
|
||||
public override Task UpdateAsync()
|
||||
{
|
||||
Thread.Sleep(1000); // 阻塞!
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// 正确
|
||||
public override async Task UpdateAsync()
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
```
|
||||
|
||||
2. **使用异步 API:**
|
||||
```csharp
|
||||
// 使用 async/await
|
||||
var data = await httpClient.GetStringAsync(url);
|
||||
```
|
||||
|
||||
3. **缓存数据:**
|
||||
```csharp
|
||||
private string? _cachedData;
|
||||
private DateTime _cacheTime;
|
||||
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
if (_cachedData != null && DateTime.Now - _cacheTime < TimeSpan.FromMinutes(5))
|
||||
return _cachedData;
|
||||
|
||||
_cachedData = await FetchDataAsync();
|
||||
_cacheTime = DateTime.Now;
|
||||
return _cachedData;
|
||||
}
|
||||
```
|
||||
|
||||
### 资源管理
|
||||
|
||||
1. **实现 IDisposable:**
|
||||
```csharp
|
||||
public class MyComponent : ComponentBase, IDisposable
|
||||
{
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **取消订阅事件:**
|
||||
```csharp
|
||||
public override Task ShutdownAsync()
|
||||
{
|
||||
context.Settings.SettingChanged -= OnSettingChanged;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
1. **捕获异常:**
|
||||
```csharp
|
||||
public override async Task UpdateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await FetchDataAsync();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to fetch data");
|
||||
// 显示错误提示给用户
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **验证输入:**
|
||||
```csharp
|
||||
public void SetUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
throw new ArgumentException("URL cannot be empty", nameof(url));
|
||||
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||
throw new ArgumentException("Invalid URL format", nameof(url));
|
||||
|
||||
_url = url;
|
||||
}
|
||||
```
|
||||
|
||||
### 本地化
|
||||
|
||||
1. **使用资源文件:**
|
||||
```csharp
|
||||
// Resources/Strings.resx
|
||||
// Name: ComponentName, Value: My Component
|
||||
|
||||
public override string Name => Resources.Strings.ComponentName;
|
||||
```
|
||||
|
||||
2. **支持多语言:**
|
||||
```xml
|
||||
<!-- Resources/Strings.zh-CN.resx -->
|
||||
<data name="ComponentName" xml:space="preserve">
|
||||
<value>我的组件</value>
|
||||
</data>
|
||||
```
|
||||
|
||||
### 安全性
|
||||
|
||||
1. **验证用户输入:**
|
||||
```csharp
|
||||
// 防止路径遍历
|
||||
var safePath = Path.GetFullPath(Path.Combine(pluginDirectory, userInput));
|
||||
if (!safePath.StartsWith(pluginDirectory))
|
||||
throw new SecurityException("Invalid path");
|
||||
```
|
||||
|
||||
2. **使用 HTTPS:**
|
||||
```csharp
|
||||
// 强制使用 HTTPS
|
||||
if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
throw new SecurityException("Only HTTPS URLs are allowed");
|
||||
```
|
||||
|
||||
## 示例插件
|
||||
|
||||
查看官方示例插件:
|
||||
- **天气组件** - 显示天气信息
|
||||
- **倒计时组件** - 倒计时功能
|
||||
- **RSS 阅读器** - 订阅和显示 RSS 源
|
||||
|
||||
仓库: https://github.com/YourOrg/LanMountainDesktop.SamplePlugin
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Plugin SDK v4 迁移指南](PLUGIN_SDK_V4_MIGRATION.md)
|
||||
- [组件开发指南](COMPONENT_DEVELOPMENT.md)
|
||||
- [API 参考](API_REFERENCE.md)
|
||||
- [架构文档](ARCHITECTURE.md)
|
||||
644
docs/TROUBLESHOOTING.md
Normal file
644
docs/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,644 @@
|
||||
# 故障排除指南
|
||||
|
||||
> LanMountainDesktop 常见问题和解决方案
|
||||
|
||||
## 目录
|
||||
|
||||
- [构建问题](#构建问题)
|
||||
- [运行时问题](#运行时问题)
|
||||
- [Launcher 问题](#launcher-问题)
|
||||
- [更新问题](#更新问题)
|
||||
- [插件问题](#插件问题)
|
||||
- [性能问题](#性能问题)
|
||||
- [平台特定问题](#平台特定问题)
|
||||
|
||||
## 构建问题
|
||||
|
||||
### 问题: 编译错误 - 找不到 Windows.Win32 命名空间
|
||||
|
||||
**症状:**
|
||||
```
|
||||
error CS0246: The type or namespace name 'Windows' could not be found
|
||||
```
|
||||
|
||||
**原因:** CsWin32 尚未生成 P/Invoke 代码
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 清理并重新构建
|
||||
dotnet clean
|
||||
dotnet restore
|
||||
dotnet build
|
||||
```
|
||||
|
||||
首次构建时 CsWin32 会自动生成代码,第二次构建应该成功。
|
||||
|
||||
---
|
||||
|
||||
### 问题: NuGet 包还原失败
|
||||
|
||||
**症状:**
|
||||
```
|
||||
error NU1102: Unable to find package 'XXX' with version (>= X.X.X)
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 清理 NuGet 缓存
|
||||
dotnet nuget locals all --clear
|
||||
|
||||
# 强制还原
|
||||
dotnet restore --force
|
||||
|
||||
# 重新构建
|
||||
dotnet build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题: 构建时提示 SDK 版本不匹配
|
||||
|
||||
**症状:**
|
||||
```
|
||||
error NETSDK1045: The current .NET SDK does not support targeting .NET 10.0
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 检查当前 SDK 版本
|
||||
dotnet --version
|
||||
|
||||
# 应该显示 10.x.x
|
||||
# 如果不是,请安装 .NET 10 SDK
|
||||
```
|
||||
|
||||
下载地址: https://dotnet.microsoft.com/download/dotnet/10.0
|
||||
|
||||
---
|
||||
|
||||
### 问题: Avalonia 设计器无法加载
|
||||
|
||||
**症状:** XAML 预览显示错误或空白
|
||||
|
||||
**解决方案:**
|
||||
1. 重启 IDE
|
||||
2. 清理并重新构建项目
|
||||
3. 检查 Avalonia 版本是否一致
|
||||
|
||||
```bash
|
||||
dotnet clean
|
||||
dotnet build
|
||||
```
|
||||
|
||||
## 运行时问题
|
||||
|
||||
### 问题: 应用启动后立即崩溃
|
||||
|
||||
**诊断步骤:**
|
||||
|
||||
1. **检查日志文件:**
|
||||
```
|
||||
Windows: %LOCALAPPDATA%\LanMountainDesktop\logs\
|
||||
Linux: ~/.local/share/LanMountainDesktop/logs/
|
||||
macOS: ~/Library/Application Support/LanMountainDesktop/logs/
|
||||
```
|
||||
|
||||
2. **以调试模式运行:**
|
||||
```bash
|
||||
dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj
|
||||
```
|
||||
|
||||
3. **检查依赖:**
|
||||
```bash
|
||||
# Windows: 确保安装了 .NET 10 Desktop Runtime
|
||||
# Linux: 确保安装了必要的图形库
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题: 窗口无法显示或黑屏
|
||||
|
||||
**可能原因:**
|
||||
- 显卡驱动问题
|
||||
- 渲染模式不兼容
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **切换渲染模式** (编辑配置文件):
|
||||
```json
|
||||
{
|
||||
"Win32RenderingMode": 1 // 尝试不同的值: 0, 1, 2, 3, 4
|
||||
}
|
||||
```
|
||||
|
||||
2. **禁用硬件加速:**
|
||||
```bash
|
||||
# 设置环境变量
|
||||
set AVALONIA_RENDERING_MODE=Software
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题: 单实例锁定失败
|
||||
|
||||
**症状:** 提示"应用已在运行"但实际没有
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# Windows
|
||||
taskkill /F /IM LanMountainDesktop.exe
|
||||
|
||||
# Linux/macOS
|
||||
pkill -9 LanMountainDesktop
|
||||
```
|
||||
|
||||
如果问题持续,删除锁文件:
|
||||
```
|
||||
Windows: %TEMP%\LanMountainDesktop.lock
|
||||
Linux: /tmp/LanMountainDesktop.lock
|
||||
macOS: /tmp/LanMountainDesktop.lock
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题: 设置无法保存
|
||||
|
||||
**症状:** 修改设置后重启应用,设置恢复默认
|
||||
|
||||
**诊断:**
|
||||
```bash
|
||||
# 检查设置文件是否存在
|
||||
# Windows: %LOCALAPPDATA%\LanMountainDesktop\settings.json
|
||||
# Linux: ~/.local/share/LanMountainDesktop/settings.json
|
||||
# macOS: ~/Library/Application Support/LanMountainDesktop/settings.json
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
1. 检查文件权限
|
||||
2. 检查磁盘空间
|
||||
3. 删除损坏的设置文件 (会重置为默认)
|
||||
|
||||
## Launcher 问题
|
||||
|
||||
### 问题: Launcher 找不到主程序
|
||||
|
||||
**症状:**
|
||||
```
|
||||
找不到有效的 LanMountainDesktop 版本,可能是安装已损坏。
|
||||
```
|
||||
|
||||
**诊断:**
|
||||
```bash
|
||||
# 检查目录结构
|
||||
ls "C:\Program Files\LanMountainDesktop\"
|
||||
|
||||
# 应该看到:
|
||||
# - LanMountainDesktop.Launcher.exe
|
||||
# - app-{version}/
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **检查 app-* 目录是否存在:**
|
||||
```bash
|
||||
ls "C:\Program Files\LanMountainDesktop\app-*"
|
||||
```
|
||||
|
||||
2. **检查主程序是否存在:**
|
||||
```bash
|
||||
ls "C:\Program Files\LanMountainDesktop\app-{version}\LanMountainDesktop.exe"
|
||||
```
|
||||
|
||||
3. **重新安装应用**
|
||||
|
||||
---
|
||||
|
||||
### 问题: OOBE 窗口重复出现
|
||||
|
||||
**症状:** 每次启动都显示欢迎页面
|
||||
|
||||
**原因:** OOBE 完成标记文件丢失或无法创建
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 手动创建标记文件
|
||||
# Windows:
|
||||
New-Item -ItemType File -Path "$env:LOCALAPPDATA\LanMountainDesktop\.launcher\state\first_run_completed"
|
||||
|
||||
# Linux/macOS:
|
||||
mkdir -p ~/.local/share/LanMountainDesktop/.launcher/state
|
||||
touch ~/.local/share/LanMountainDesktop/.launcher/state/first_run_completed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题: Splash 窗口卡住不消失
|
||||
|
||||
**症状:** 启动动画一直显示,主程序无法启动
|
||||
|
||||
**诊断:**
|
||||
```bash
|
||||
# 检查主程序是否启动
|
||||
# Windows:
|
||||
tasklist | findstr LanMountainDesktop
|
||||
|
||||
# Linux/macOS:
|
||||
ps aux | grep LanMountainDesktop
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
1. 强制关闭 Launcher
|
||||
2. 直接运行主程序测试:
|
||||
```bash
|
||||
"C:\Program Files\LanMountainDesktop\app-{version}\LanMountainDesktop.exe"
|
||||
```
|
||||
3. 检查日志文件
|
||||
|
||||
## 更新问题
|
||||
|
||||
### 问题: 更新下载失败
|
||||
|
||||
**症状:**
|
||||
```
|
||||
Failed to download update: The remote server returned an error
|
||||
```
|
||||
|
||||
**可能原因:**
|
||||
- 网络连接问题
|
||||
- GitHub API 限流
|
||||
- 代理设置问题
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **检查网络连接:**
|
||||
```bash
|
||||
# 测试 GitHub 连接
|
||||
curl https://api.github.com/repos/YourOrg/LanMountainDesktop/releases/latest
|
||||
```
|
||||
|
||||
2. **配置代理** (如果需要):
|
||||
```bash
|
||||
# 设置环境变量
|
||||
set HTTP_PROXY=http://proxy.example.com:8080
|
||||
set HTTPS_PROXY=http://proxy.example.com:8080
|
||||
```
|
||||
|
||||
3. **手动下载更新:**
|
||||
- 访问 GitHub Releases 页面
|
||||
- 下载安装包
|
||||
- 重新安装
|
||||
|
||||
---
|
||||
|
||||
### 问题: 更新签名验证失败
|
||||
|
||||
**症状:**
|
||||
```
|
||||
Signature verification failed
|
||||
```
|
||||
|
||||
**原因:**
|
||||
- 文件损坏
|
||||
- 公钥不匹配
|
||||
- 文件被篡改
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **删除损坏的更新文件:**
|
||||
```bash
|
||||
# Windows:
|
||||
Remove-Item "$env:LOCALAPPDATA\LanMountainDesktop\.launcher\update\incoming\*"
|
||||
|
||||
# Linux/macOS:
|
||||
rm -rf ~/.local/share/LanMountainDesktop/.launcher/update/incoming/*
|
||||
```
|
||||
|
||||
2. **重新下载更新**
|
||||
|
||||
3. **如果问题持续,重新安装应用**
|
||||
|
||||
---
|
||||
|
||||
### 问题: 更新后应用无法启动
|
||||
|
||||
**症状:** 更新完成后,应用启动失败或崩溃
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **版本回退:**
|
||||
```bash
|
||||
LanMountainDesktop.Launcher.exe update rollback
|
||||
```
|
||||
|
||||
2. **检查更新快照:**
|
||||
```bash
|
||||
# Windows:
|
||||
ls "$env:LOCALAPPDATA\LanMountainDesktop\.launcher\snapshots\"
|
||||
|
||||
# 查看快照内容
|
||||
cat "$env:LOCALAPPDATA\LanMountainDesktop\.launcher\snapshots\{snapshot-id}.json"
|
||||
```
|
||||
|
||||
3. **手动切换版本:**
|
||||
```bash
|
||||
# 删除新版本的 .current 标记
|
||||
Remove-Item "C:\Program Files\LanMountainDesktop\app-{new}\\.current"
|
||||
|
||||
# 添加 .current 到旧版本
|
||||
New-Item -ItemType File -Path "C:\Program Files\LanMountainDesktop\app-{old}\\.current"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题: 增量更新文件哈希不匹配
|
||||
|
||||
**症状:**
|
||||
```
|
||||
File hash mismatch for 'XXX.dll'
|
||||
```
|
||||
|
||||
**原因:**
|
||||
- 文件下载不完整
|
||||
- 文件损坏
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **删除部分下载的更新:**
|
||||
```bash
|
||||
# 删除标记为 .partial 的目录
|
||||
Remove-Item "C:\Program Files\LanMountainDesktop\app-*" -Recurse -Force -Include *.partial
|
||||
```
|
||||
|
||||
2. **清理更新缓存:**
|
||||
```bash
|
||||
Remove-Item "$env:LOCALAPPDATA\LanMountainDesktop\.launcher\update\incoming\*"
|
||||
```
|
||||
|
||||
3. **重新下载更新**
|
||||
|
||||
## 插件问题
|
||||
|
||||
### 问题: 插件无法加载
|
||||
|
||||
**症状:** 插件列表中看不到已安装的插件
|
||||
|
||||
**诊断:**
|
||||
```bash
|
||||
# 检查插件目录
|
||||
ls "C:\Program Files\LanMountainDesktop\plugins\"
|
||||
|
||||
# 检查插件清单
|
||||
cat "C:\Program Files\LanMountainDesktop\plugins\{plugin-id}\plugin.json"
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **检查插件兼容性:**
|
||||
- 插件 SDK 版本是否匹配
|
||||
- 插件是否支持当前平台
|
||||
|
||||
2. **重新安装插件:**
|
||||
```bash
|
||||
LanMountainDesktop.Launcher.exe plugin install <path-to-plugin.laapp>
|
||||
```
|
||||
|
||||
3. **检查日志文件** 查看插件加载错误
|
||||
|
||||
---
|
||||
|
||||
### 问题: 插件安装失败
|
||||
|
||||
**症状:**
|
||||
```
|
||||
Failed to install plugin: Invalid package format
|
||||
```
|
||||
|
||||
**可能原因:**
|
||||
- `.laapp` 文件损坏
|
||||
- 插件包格式不正确
|
||||
- 权限不足
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **验证插件包:**
|
||||
```bash
|
||||
# .laapp 实际上是 ZIP 文件
|
||||
unzip -t plugin.laapp
|
||||
```
|
||||
|
||||
2. **检查权限:**
|
||||
```bash
|
||||
# 以管理员身份运行 Launcher
|
||||
```
|
||||
|
||||
3. **手动解压安装:**
|
||||
```bash
|
||||
# 解压到插件目录
|
||||
unzip plugin.laapp -d "C:\Program Files\LanMountainDesktop\plugins\{plugin-id}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题: 插件更新失败
|
||||
|
||||
**症状:** 插件升级队列处理失败
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **清理升级队列:**
|
||||
```bash
|
||||
Remove-Item "$env:LOCALAPPDATA\LanMountainDesktop\.launcher\plugin-upgrades\*"
|
||||
```
|
||||
|
||||
2. **手动更新插件:**
|
||||
- 卸载旧版本
|
||||
- 安装新版本
|
||||
|
||||
## 性能问题
|
||||
|
||||
### 问题: CPU 占用过高
|
||||
|
||||
**可能原因:**
|
||||
- 渲染模式不当
|
||||
- 组件更新频率过高
|
||||
- 内存泄漏
|
||||
|
||||
**诊断:**
|
||||
```bash
|
||||
# Windows: 使用任务管理器查看详细信息
|
||||
# Linux: top 或 htop
|
||||
# macOS: Activity Monitor
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **切换渲染模式** (参见"窗口无法显示"部分)
|
||||
|
||||
2. **禁用不必要的组件:**
|
||||
- 减少桌面组件数量
|
||||
- 降低组件更新频率
|
||||
|
||||
3. **检查是否有死循环或资源泄漏**
|
||||
|
||||
---
|
||||
|
||||
### 问题: 内存占用过高
|
||||
|
||||
**诊断:**
|
||||
```bash
|
||||
# 检查内存使用情况
|
||||
# Windows: 任务管理器
|
||||
# Linux: free -h
|
||||
# macOS: Activity Monitor
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **重启应用**
|
||||
|
||||
2. **减少组件数量**
|
||||
|
||||
3. **检查插件是否有内存泄漏**
|
||||
|
||||
4. **更新到最新版本** (可能包含内存优化)
|
||||
|
||||
---
|
||||
|
||||
### 问题: 启动速度慢
|
||||
|
||||
**可能原因:**
|
||||
- 插件过多
|
||||
- 磁盘 I/O 慢
|
||||
- 首次启动需要初始化
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **禁用不必要的插件**
|
||||
|
||||
2. **使用 SSD**
|
||||
|
||||
3. **清理缓存:**
|
||||
```bash
|
||||
Remove-Item "$env:LOCALAPPDATA\LanMountainDesktop\cache\*" -Recurse
|
||||
```
|
||||
|
||||
## 平台特定问题
|
||||
|
||||
### Windows
|
||||
|
||||
#### 问题: WebView2 缺失
|
||||
|
||||
**症状:**
|
||||
```
|
||||
Microsoft Edge WebView2 Runtime is required
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
1. 下载并安装 WebView2 Runtime:
|
||||
https://go.microsoft.com/fwlink/p/?LinkId=2124703
|
||||
|
||||
2. 或使用安装包自动安装
|
||||
|
||||
---
|
||||
|
||||
#### 问题: 与窗口美化工具冲突
|
||||
|
||||
**症状:** 窗口显示异常、崩溃
|
||||
|
||||
**已知冲突工具:**
|
||||
- Mica For Everyone
|
||||
- TranslucentTB
|
||||
- 其他修改窗口材质的工具
|
||||
|
||||
**解决方案:**
|
||||
将 LanMountainDesktop 添加到这些工具的排除列表中。
|
||||
|
||||
---
|
||||
|
||||
### Linux
|
||||
|
||||
#### 问题: 缺少图形库依赖
|
||||
|
||||
**症状:**
|
||||
```
|
||||
error while loading shared libraries: libXXX.so
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
**Debian/Ubuntu:**
|
||||
```bash
|
||||
sudo apt install libx11-6 libice6 libsm6 libfontconfig1
|
||||
```
|
||||
|
||||
**Fedora/RHEL:**
|
||||
```bash
|
||||
sudo dnf install libX11 libICE libSM fontconfig
|
||||
```
|
||||
|
||||
**Arch Linux:**
|
||||
```bash
|
||||
sudo pacman -S libx11 libice libsm fontconfig
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 问题: Wayland 兼容性
|
||||
|
||||
**症状:** 在 Wayland 下运行异常
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 强制使用 X11
|
||||
export GDK_BACKEND=x11
|
||||
./LanMountainDesktop.Launcher
|
||||
```
|
||||
|
||||
或通过 XWayland 运行 (不保证所有功能正常)。
|
||||
|
||||
---
|
||||
|
||||
### macOS
|
||||
|
||||
#### 问题: 应用无法打开 - "来自身份不明的开发者"
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 移除隔离属性
|
||||
xattr -cr /Applications/LanMountainDesktop.app
|
||||
```
|
||||
|
||||
或在"系统偏好设置" > "安全性与隐私"中允许。
|
||||
|
||||
---
|
||||
|
||||
#### 问题: 权限问题
|
||||
|
||||
**症状:** 无法访问某些目录或功能
|
||||
|
||||
**解决方案:**
|
||||
在"系统偏好设置" > "安全性与隐私" > "隐私"中授予必要权限:
|
||||
- 文件和文件夹
|
||||
- 辅助功能 (如果需要)
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果以上方案都无法解决问题:
|
||||
|
||||
1. **查看日志文件** (包含详细错误信息)
|
||||
2. **搜索 GitHub Issues** - 可能已有解决方案
|
||||
3. **提交新 Issue** - 包含:
|
||||
- 操作系统和版本
|
||||
- 应用版本
|
||||
- 详细错误信息
|
||||
- 重现步骤
|
||||
- 日志文件 (如果相关)
|
||||
|
||||
**GitHub Issues:** https://github.com/YourOrg/LanMountainDesktop/issues
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [开发文档](DEVELOPMENT.md)
|
||||
- [Launcher 架构](LAUNCHER.md)
|
||||
- [更新系统](UPDATE_SYSTEM.md)
|
||||
- [构建和部署](BUILD_AND_DEPLOY.md)
|
||||
451
docs/UPDATE_SYSTEM.md
Normal file
451
docs/UPDATE_SYSTEM.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# 更新系统文档
|
||||
|
||||
> LanMountainDesktop 增量更新和版本管理系统
|
||||
|
||||
## 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [更新流程](#更新流程)
|
||||
- [增量更新](#增量更新)
|
||||
- [原子化更新](#原子化更新)
|
||||
- [版本回退](#版本回退)
|
||||
- [CI/CD 集成](#cicd-集成)
|
||||
- [安全机制](#安全机制)
|
||||
|
||||
## 概述
|
||||
|
||||
LanMountainDesktop 使用基于 GitHub Release 的增量更新系统,支持:
|
||||
- ✅ 增量更新 (只下载变更文件)
|
||||
- ✅ 原子化更新 (保证完整性)
|
||||
- ✅ 签名验证 (RSA)
|
||||
- ✅ 版本回退
|
||||
- ✅ 更新频道 (Stable/Preview)
|
||||
- ✅ 静默更新 (后台下载)
|
||||
|
||||
## 更新流程
|
||||
|
||||
### 完整更新流程图
|
||||
|
||||
```
|
||||
Launcher 启动
|
||||
↓
|
||||
UpdateCheckService.CheckForUpdateAsync()
|
||||
├─ 调用 GitHub Release API
|
||||
├─ 根据更新频道过滤版本
|
||||
└─ 对比当前版本和最新版本
|
||||
↓
|
||||
有新版本? ──No→ 继续启动
|
||||
↓ Yes
|
||||
UpdateEngineService.DownloadAsync()
|
||||
├─ 下载 files-{version}.json
|
||||
├─ 下载 files-{version}.json.sig
|
||||
└─ 下载 delta-{old}-to-{new}.zip (或完整包)
|
||||
↓
|
||||
保存到 .launcher/update/incoming/
|
||||
↓
|
||||
下次启动时
|
||||
↓
|
||||
UpdateEngineService.ApplyPendingUpdate()
|
||||
├─ 验证签名
|
||||
├─ 创建 app-{new}/ 目录
|
||||
├─ 标记 .partial
|
||||
├─ 解压增量包
|
||||
├─ 从旧版本复用未变更文件
|
||||
├─ 验证所有文件 SHA256
|
||||
├─ 删除 .partial
|
||||
├─ 添加 .current 到新版本
|
||||
├─ 标记旧版本 .destroy
|
||||
└─ 保存更新快照
|
||||
↓
|
||||
启动新版本
|
||||
↓
|
||||
清理旧版本 (.destroy)
|
||||
```
|
||||
|
||||
### 更新频道
|
||||
|
||||
| 频道 | 说明 | GitHub Release 过滤 |
|
||||
|------|------|---------------------|
|
||||
| **Stable** | 正式版 | `prerelease=false` |
|
||||
| **Preview** | 预览版 | 所有版本 (包括 `prerelease=true`) |
|
||||
|
||||
用户可以在设置中切换更新频道。
|
||||
|
||||
## 增量更新
|
||||
|
||||
### 增量包结构
|
||||
|
||||
**GitHub Release Assets:**
|
||||
```
|
||||
LanMountainDesktop-v1.0.1/
|
||||
├── LanMountainDesktop-Setup-1.0.1-x64.exe # 完整安装包
|
||||
├── app-1.0.1.zip # 完整应用包
|
||||
├── delta-1.0.0-to-1.0.1.zip # 增量包
|
||||
├── files-1.0.1.json # 文件清单
|
||||
└── files-1.0.1.json.sig # RSA 签名
|
||||
```
|
||||
|
||||
### files.json 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"FromVersion": "1.0.0",
|
||||
"ToVersion": "1.0.1",
|
||||
"GeneratedAt": "2025-01-01T00:00:00Z",
|
||||
"Files": [
|
||||
{
|
||||
"Path": "LanMountainDesktop.exe",
|
||||
"Action": "replace",
|
||||
"Sha256": "abc123...",
|
||||
"Size": 1024000,
|
||||
"ArchivePath": "LanMountainDesktop.exe"
|
||||
},
|
||||
{
|
||||
"Path": "LanMountainDesktop.dll",
|
||||
"Action": "reuse",
|
||||
"Sha256": "def456...",
|
||||
"Size": 512000
|
||||
},
|
||||
{
|
||||
"Path": "OldFile.dll",
|
||||
"Action": "delete"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 文件操作类型
|
||||
|
||||
| Action | 说明 | 处理方式 |
|
||||
|--------|------|----------|
|
||||
| `add` | 新增文件 | 从增量包解压 |
|
||||
| `replace` | 替换文件 | 从增量包解压 |
|
||||
| `reuse` | 复用文件 | 从旧版本复制 |
|
||||
| `delete` | 删除文件 | 不操作 (新版本中不存在) |
|
||||
|
||||
### 增量包生成
|
||||
|
||||
使用 `Generate-DeltaPackage.ps1` 脚本:
|
||||
|
||||
```powershell
|
||||
./scripts/Generate-DeltaPackage.ps1 `
|
||||
-PreviousVersion "1.0.0" `
|
||||
-CurrentVersion "1.0.1" `
|
||||
-PreviousDir "./publish/app-1.0.0" `
|
||||
-CurrentDir "./publish/app-1.0.1" `
|
||||
-OutputDir "./delta-output"
|
||||
```
|
||||
|
||||
**生成过程:**
|
||||
1. 扫描两个版本的所有文件
|
||||
2. 计算每个文件的 SHA256
|
||||
3. 对比哈希值,识别变更
|
||||
4. 只打包变更的文件到 `delta.zip`
|
||||
5. 生成 `files.json` 清单
|
||||
|
||||
**优势:**
|
||||
- 大幅减少下载大小 (通常只有 10-30% 的完整包大小)
|
||||
- 加快更新速度
|
||||
- 节省带宽
|
||||
|
||||
## 原子化更新
|
||||
|
||||
### 原子化保证
|
||||
|
||||
更新过程中的任何失败都会触发自动回滚,确保应用始终处于可用状态。
|
||||
|
||||
**关键机制:**
|
||||
1. **`.partial` 标记** - 更新过程中保持此标记
|
||||
2. **旧版本保留** - 直到新版本验证通过
|
||||
3. **SHA256 验证** - 确保所有文件完整性
|
||||
4. **快照记录** - 记录更新前后状态
|
||||
5. **自动回滚** - 失败时恢复到旧版本
|
||||
|
||||
### 更新步骤详解
|
||||
|
||||
```csharp
|
||||
public LauncherResult ApplyPendingUpdate()
|
||||
{
|
||||
// 1. 验证签名
|
||||
var verifyResult = VerifySignature(fileMapPath, signaturePath);
|
||||
if (!verifyResult.Success)
|
||||
return Failed("signature_failed");
|
||||
|
||||
// 2. 创建新版本目录
|
||||
var targetDeployment = _deploymentLocator.BuildNextDeploymentDirectory(targetVersion);
|
||||
Directory.CreateDirectory(targetDeployment);
|
||||
|
||||
// 3. 标记为未完成
|
||||
File.WriteAllText(Path.Combine(targetDeployment, ".partial"), string.Empty);
|
||||
|
||||
// 4. 保存快照
|
||||
var snapshot = new SnapshotMetadata { ... };
|
||||
SaveSnapshot(snapshotPath, snapshot);
|
||||
|
||||
try
|
||||
{
|
||||
// 5. 解压增量包
|
||||
ZipFile.ExtractToDirectory(archivePath, extractRoot);
|
||||
|
||||
// 6. 应用文件操作
|
||||
foreach (var file in fileMap.Files)
|
||||
{
|
||||
ApplyFileEntry(file, currentDeployment, targetDeployment, extractRoot);
|
||||
}
|
||||
|
||||
// 7. 验证所有文件
|
||||
foreach (var file in fileMap.Files)
|
||||
{
|
||||
var actualHash = ComputeSha256Hex(fullPath);
|
||||
if (actualHash != file.Sha256)
|
||||
throw new InvalidOperationException("Hash mismatch");
|
||||
}
|
||||
|
||||
// 8. 激活新版本
|
||||
ActivateDeployment(currentDeployment, targetDeployment);
|
||||
|
||||
// 9. 更新快照状态
|
||||
snapshot.Status = "applied";
|
||||
SaveSnapshot(snapshotPath, snapshot);
|
||||
|
||||
// 10. 清理
|
||||
CleanupIncomingArtifacts();
|
||||
|
||||
return Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 自动回滚
|
||||
TryRollbackOnFailure(snapshot);
|
||||
snapshot.Status = "rolled_back";
|
||||
SaveSnapshot(snapshotPath, snapshot);
|
||||
return Failed("apply_failed", ex.Message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 失败回滚
|
||||
|
||||
```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);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 记录错误但不抛出
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 版本回退
|
||||
|
||||
### 手动回退
|
||||
|
||||
```bash
|
||||
LanMountainDesktop.Launcher.exe update rollback
|
||||
```
|
||||
|
||||
### 回退流程
|
||||
|
||||
```csharp
|
||||
public LauncherResult RollbackLatest()
|
||||
{
|
||||
// 1. 读取最新快照
|
||||
var snapshotPath = Directory
|
||||
.EnumerateFiles(_snapshotsRoot, "*.json")
|
||||
.OrderByDescending(File.GetCreationTimeUtc)
|
||||
.FirstOrDefault();
|
||||
|
||||
var snapshot = JsonSerializer.Deserialize<SnapshotMetadata>(
|
||||
File.ReadAllText(snapshotPath));
|
||||
|
||||
// 2. 获取当前部署
|
||||
var currentDeployment = _deploymentLocator.FindCurrentDeploymentDirectory();
|
||||
|
||||
// 3. 激活旧版本
|
||||
ActivateDeployment(currentDeployment, snapshot.SourceDirectory);
|
||||
|
||||
// 4. 更新快照状态
|
||||
snapshot.Status = "manual_rollback";
|
||||
SaveSnapshot(snapshotPath, snapshot);
|
||||
|
||||
return Success($"Rolled back to {snapshot.SourceVersion}");
|
||||
}
|
||||
```
|
||||
|
||||
### 快照格式
|
||||
|
||||
```json
|
||||
{
|
||||
"SnapshotId": "abc123...",
|
||||
"SourceVersion": "1.0.0",
|
||||
"TargetVersion": "1.0.1",
|
||||
"CreatedAt": "2025-01-01T00:00:00Z",
|
||||
"SourceDirectory": "C:\\...\\app-1.0.0",
|
||||
"TargetDirectory": "C:\\...\\app-1.0.1",
|
||||
"Status": "applied"
|
||||
}
|
||||
```
|
||||
|
||||
## CI/CD 集成
|
||||
|
||||
### GitHub Actions 工作流
|
||||
|
||||
**release.yml 关键步骤:**
|
||||
|
||||
```yaml
|
||||
- name: Restructure for Launcher
|
||||
run: |
|
||||
# 重组为 app-{version} 结构
|
||||
$appDir = "app-${{ needs.prepare.outputs.version }}"
|
||||
New-Item -ItemType Directory -Path "publish-launcher/windows-x64"
|
||||
Move-Item -Path "publish/windows-x64" -Destination "publish-launcher/windows-x64/$appDir"
|
||||
|
||||
# 移动 Launcher 到根目录
|
||||
Move-Item -Path "publish-launcher/windows-x64/$appDir/Launcher/*" -Destination "publish-launcher/windows-x64/"
|
||||
|
||||
# 创建 .current 标记
|
||||
New-Item -ItemType File -Path "publish-launcher/windows-x64/$appDir/.current"
|
||||
|
||||
- name: Generate Delta Package
|
||||
run: |
|
||||
# 生成 files.json
|
||||
$files = Get-ChildItem -Path $currentAppPath -Recurse -File
|
||||
# ... 计算 SHA256 ...
|
||||
|
||||
# 创建完整应用包
|
||||
Compress-Archive -Path "$currentAppPath\*" -DestinationPath "app-$version.zip"
|
||||
|
||||
- name: Upload Delta Package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: delta-package-windows-x64
|
||||
path: delta-output/*
|
||||
```
|
||||
|
||||
### 增量包生成脚本
|
||||
|
||||
**scripts/Generate-DeltaPackage.ps1:**
|
||||
- 对比两个版本目录
|
||||
- 识别新增、修改、删除的文件
|
||||
- 只打包变更文件
|
||||
- 生成 `files.json` 清单
|
||||
|
||||
**scripts/Sign-FileMap.ps1:**
|
||||
- 使用 RSA 私钥签名 `files.json`
|
||||
- 生成 `files.json.sig`
|
||||
|
||||
## 安全机制
|
||||
|
||||
### RSA 签名验证
|
||||
|
||||
**签名生成 (CI):**
|
||||
```powershell
|
||||
# 读取私钥
|
||||
$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
|
||||
)
|
||||
|
||||
# 保存为 Base64
|
||||
$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 (!NeedsVerification(file))
|
||||
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}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Launcher 架构文档](LAUNCHER.md)
|
||||
- [构建和部署指南](BUILD_AND_DEPLOY.md)
|
||||
- [故障排除指南](TROUBLESHOOTING.md)
|
||||
|
||||
## VeloPack Packaging (Current)
|
||||
|
||||
- Release pipeline now produces VeloPack native assets (
|
||||
eleases.win.json, *.nupkg, RELEASES).
|
||||
- Launcher remains the installer and rollback authority; only package generation moved to VeloPack.
|
||||
- Legacy iles.json + update.zip generation remains available only as a disabled fallback path in CI.
|
||||
@@ -16,7 +16,7 @@
|
||||
| `LanMountainDesktop.DesktopHost/` | 桌面宿主流程 | 生命周期、宿主流程支撑 |
|
||||
| `LanMountainDesktop.DesktopComponents.Runtime/` | 组件运行时 | 组件宿主运行时支撑 |
|
||||
| `LanMountainDesktop.Host.Abstractions/` | 宿主抽象 | 宿主接口与抽象层 |
|
||||
| `LanMountainDesktop.PluginsInstallHelper/` | 插件安装辅助 | 发布输出和插件安装辅助程序 |
|
||||
| `LanMountainDesktop.Launcher/` | 启动器 | 发布输出、OOBE、启动页、更新与插件安装/更新 |
|
||||
| `LanMountainDesktop.PluginTemplate/` | 插件模板 | `dotnet new lmd-plugin` 模板内容 |
|
||||
| `LanMountainDesktop.Tests/` | 测试 | 行为回归、契约验证、基础能力校验 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user