diff --git a/website/docs/doc/GeneralUpdate.Bowl.md b/website/docs/doc/GeneralUpdate.Bowl.md index 4ecc369..b15cfd1 100644 --- a/website/docs/doc/GeneralUpdate.Bowl.md +++ b/website/docs/doc/GeneralUpdate.Bowl.md @@ -4,58 +4,145 @@ sidebar_position: 3 # GeneralUpdate.Bowl -## 简介 +**命名空间:** `GeneralUpdate.Bowl` | **主要入口:** `new Bowl().LaunchAsync(BowlContext, CancellationToken)` | **NuGet 包:** `GeneralUpdate.Bowl` -**GeneralUpdate.Bowl** 是升级完成后的启动守护组件。它不负责下载、解压或替换升级包,而是在新版本文件落地、主程序即将启动或已经启动时,监控目标进程是否在启动阶段崩溃。如果捕获到崩溃,它会生成 Dump、写出失败报告、导出诊断信息,并在升级模式下把备份目录恢复回安装目录,避免用户一直停留在不可启动的新版本上。 +## 1. 组件简介 -**命名空间:** `GeneralUpdate.Bowl` +### 1.1 组件概述 -**程序集:** `GeneralUpdate.Bowl.dll` +**GeneralUpdate.Bowl** 是升级完成后的启动守护组件。它不负责下载、解压或替换升级包,而是在新版本文件落地后、主程序即将启动或已启动时,监控目标进程是否在启动阶段崩溃。如果捕获到崩溃,它会生成 Dump 内存快照、写出失败报告 JSON、导出系统诊断信息,并在升级模式下自动将备份目录恢复回安装目录,避免用户一直停留在不可启动的新版本上。 -**当前主要入口:** `new Bowl().LaunchAsync(BowlContext context, CancellationToken ct = default)` +**核心能力:** -## 阅读导航 - -| 主题 | 适合解决的问题 | +| 能力 | 说明 | +| --- | --- | +| 进程崩溃监控 | 通过 ProcDump(Windows/Linux)或 lldb(macOS)附加到目标进程,捕获启动期未处理异常 | +| Dump 内存快照 | 支持 Full / Mini / Heap 三种 Dump 类型,可按需选择文件大小和完整度 | +| 崩溃报告生成 | 自动生成包含监控参数和工具输出的 `{version}_fail.json` 报告文件 | +| 系统诊断导出 | Windows 下自动导出驱动列表、系统信息和最近系统事件日志 | +| 自动回滚恢复 | 升级模式下将备份目录覆盖复制回安装目录,实现一键回退到旧版本 | +| 失败版本标记 | 写入 `UpgradeFail` 标记,Core 后续跳过该失败版本直到服务端提供更高版本 | +| 事件回调通知 | `OnCrash` 回调允许上传诊断包、通知用户或记录审计信息 | +| 独立监控模式 | Normal 模式只做崩溃捕获和报告输出,不自动恢复备份,适合通用进程监控 | + +**解决的业务痛点:** +- 新版本升级后在启动阶段崩溃,用户无法使用应用且无法自行回退 +- 开发者缺少崩溃现场信息(Dump、系统环境)来定位"升级后打不开"的问题 +- 需要自动化回滚机制降低升级风险,避免人工介入 + +**业务使用场景:** +- 桌面应用升级后启动健康检查与自动回滚保护 +- 通用进程启动崩溃监控与诊断信息采集 +- CI/CD 冒烟测试失败后的自动诊断 + +### 1.2 环境与依赖 + +| 项目 | 说明 | | --- | --- | -| [生命周期位置](#生命周期位置) | Bowl 应该在升级流程的哪个阶段运行 | -| [快速接入](#快速接入) | 用当前 `BowlContext` API 完成一次监控 | -| [崩溃检测与恢复流程](#崩溃检测与恢复流程) | 崩溃后组件具体做了什么 | -| [BowlContext 参数](#bowlcontext-参数) | 每个配置项的含义和推荐值 | -| [输出文件](#输出文件) | Dump、失败报告、系统诊断、追踪日志在哪里 | -| [事件回调](#事件回调) | 如何在崩溃时上传报告或通知用户 | -| [日志开关](#日志开关) | 如何为了性能关闭组件追踪日志 | -| [平台差异](#平台差异) | Windows、Linux、macOS 的监控能力差异 | -| [恢复场景](#恢复场景) | 一次真实升级失败回滚过程 | -| [旧 API 迁移](#旧-api-迁移) | 从 `MonitorParameter` 迁移到 `BowlContext` | +| **版本** | `10.5.0-beta.2` | +| **目标框架** | `netstandard2.0`(兼容 .NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 5+) | +| **依赖包** | `System.Collections.Immutable`, `System.Text.Json` | +| **内置工具** | Windows: `procdump.exe` / `procdump64.exe` / `procdump64a.exe`;Linux: `procdump` deb/rpm 包 + `install.sh`;macOS: `/usr/bin/lldb` | +| **兼容性** | Windows(完整支持)/ Linux(deb/rpm 发行版)/ macOS(基础支持,受 SIP 和调试权限限制) | -## 生命周期位置 +--- -在 GeneralUpdate 的完整升级链路中,Bowl 位于**文件替换完成之后、用户正式使用新版本之前**: +## 2. 组件功能列表 + +| 功能名称 | 功能描述 | 类型 | 是否必填 | 备注限制 | +| --- | --- | --- | --- | --- | +| 升级模式监控 | 监控新版本启动崩溃,自动恢复备份、标记失败版本 | 基础 | 推荐 | `WorkModel = "Upgrade"` | +| 独立监控模式 | 仅捕获崩溃、生成报告,不自动恢复 | 基础 | 可选 | `WorkModel = "Normal"` | +| Full Dump | 完整内存快照,信息最完整 | 基础 | 可选 | `DumpType.Full`,文件最大 | +| Mini Dump | 小型内存快照,生成更快 | 基础 | 可选 | `DumpType.Mini`,生产环境推荐 | +| Heap Dump | 带堆信息的小型 Dump | 基础 | 可选 | `DumpType.Heap`,介于 Mini 和 Full 之间 | +| 崩溃报告 JSON | 自动生成结构化崩溃报告文件 | 基础 | 自动 | 输出到 `FailDirectory` | +| 系统诊断导出 | Windows 下导出驱动/系统信息/事件日志 | 拓展 | 自动 | Windows only | +| 自动备份恢复 | 崩溃后将备份目录覆盖回安装目录 | 基础 | 可选 | `AutoRestore = true` | +| 失败版本标记 | 写入升级失败版本,Core 后续跳过 | 基础 | 自动 | 升级模式下生效 | +| 崩溃回调通知 | 检测到崩溃后触发业务回调 | 拓展 | 可选 | `OnCrash` 回调函数 | +| 日志追踪 | `GeneralTracer` 运行时诊断日志 | 拓展 | 可选 | 默认开启,可关闭 | -1. Core 获取更新信息、下载包、校验并应用更新。 -2. Core/Upgrade 进程准备启动主程序。 -3. Bowl 作为守护逻辑启动,附加到目标进程并等待启动期异常。 -4. 主程序正常启动:没有 Dump 产生,Bowl 返回本次监控结果。 -5. 主程序启动崩溃:Bowl 进入故障处理管线,生成诊断文件并按配置恢复备份。 +--- -在当前 Core 代码中,Windows 的 `UpdateStrategy` 会在更新完成后通过 OS 策略启动主程序,并在配置了 Bowl 进程名时一并启动 Bowl 辅助进程。Linux/macOS 侧 Core 策略没有同等的 Bowl helper 自动启动能力,通常需要由你的启动器、服务脚本或独立进程显式调用 `LaunchAsync`。 +## 3. API 配置说明 -:::tip -Bowl 是“升级后健康检查与回滚保护”,不是固件恢复、系统还原或升级包安装器。它处理的是应用启动崩溃后的诊断与应用目录级备份恢复。 -::: +### 3.1 配置字段(属性 Props) -## 快速接入 +**BowlContext:** -### 安装 +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `ProcessNameOrId` | `string` | — | 是 | 进程名或 PID | 要监控的目标进程名称或进程 ID | +| `DumpFileName` | `string` | — | 是 | 有效文件名 | Dump 输出文件名,推荐 `"{version}_fail.dmp"` | +| `FailFileName` | `string` | — | 是 | 有效文件名 | 崩溃报告 JSON 文件名,推荐 `"{version}_fail.json"` | +| `TargetPath` | `string` | — | 是 | 有效目录路径 | 应用安装根目录,恢复备份时覆盖复制到这里 | +| `FailDirectory` | `string` | — | 是 | 有效目录路径 | 故障文件输出目录,推荐 `{TargetPath}/fail/{version}` | +| `BackupDirectory` | `string` | — | 推荐 | 有效目录路径 | 升级前备份目录,`AutoRestore` 打开时必须存在 | +| `WorkModel` | `string` | `"Upgrade"`(`Normalize()` 后) | 可选 | `"Upgrade"` / `"Normal"` | 工作模式:升级回滚 / 独立监控 | +| `ExtendedField` | `string` | `null` | 可选 | — | 扩展字段,通常存储版本号,升级模式下写入 `UpgradeFail` | +| `TimeoutMs` | `int` | `30000`(`Normalize()` 后) | 可选 | 正整数(毫秒) | 监控子进程超时时间,按应用启动耗时调整 | +| `DumpType` | `DumpType` | `DumpType.Full`(`Normalize()` 后) | 可选 | `Full(0)`, `Mini(1)`, `Heap(2)` | Dump 捕获类型 | +| `AutoRestore` | `bool` | `false` | 可选 | `true` / `false` | 是否自动恢复备份,升级模式需显式设为 `true` | +| `OnCrash` | `Func?` | `null` | 可选 | — | 崩溃事件回调,仅在检测到 Dump 后触发 | -```bash -dotnet add package GeneralUpdate.Bowl -``` +**DumpType 枚举:** + +| 枚举值 | 数值 | Windows ProcDump 参数 | 特点 | +| --- | --- | --- | --- | +| `Full` | `0` | `-ma` | 完整内存快照,信息最完整,文件最大 | +| `Mini` | `1` | `-mm` | 小型快照,生成快、文件小,适合生产默认采集 | +| `Heap` | `2` | `-mh` | 带堆信息小型快照,介于 Mini 和 Full 之间 | + +### 3.2 实例方法 + +**Bowl:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `LaunchAsync(BowlContext, CancellationToken)` | `context` — 执行上下文(建议先调 `Normalize()`);`ct` — 取消令牌 | `Task` | 启动崩溃监控守护流程 | 三阶段:准备监控 → 运行监控 → 检测到 Dump 则进入故障处理管线 | + +**BowlContext:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `Normalize()` | 无 | `BowlContext` | 应用默认值(`WorkModel` → `"Upgrade"`,`TimeoutMs` → `30000`,`DumpType` → `Full`) | 返回新实例,不修改原实例 | + +### 3.3 回调事件 + +| 事件名称 | 回调参数 | 触发时机 | 使用说明 | +| --- | --- | --- | --- | +| `OnCrash` | `CrashInfo` — `DumpFilePath`, `CrashReportPath`, `Version`, `ExitCode`;`CancellationToken` | 检测到 Dump 文件后触发 | 适合上传诊断包、通知用户"新版本已回退"、记录业务审计。回调异常被 Bowl 记录到追踪日志,不会阻止 `LaunchAsync` 返回 | + +**GeneralTracer 日志控制:** + +| 方法 | 说明 | +| --- | --- | +| `GeneralTracer.SetTracingEnabled(false)` | 关闭 Bowl 日志输出 | +| `GeneralTracer.SetTracingEnabled(true)` | 重新开启日志输出 | +| `GeneralTracer.IsTracingEnabled()` | 查询当前日志开关状态 | +| `GeneralTracer.Dispose()` | 释放文件监听器并清空 Trace listeners | + +--- + +## 4. 扩展示例(高阶用法) + +### 4.1 组件可扩展能力总览 + +Bowl 的主要扩展点是 `BowlContext.OnCrash` 回调。内部策略接口(`IBowlStrategy`、`ICrashReporter`、`ISystemInfoProvider`)为 internal,如需新增平台支持或自定义报告逻辑,可在 GeneralUpdate 仓库贡献代码。 + +| 扩展点 | 类型 | 说明 | +| --- | --- | --- | +| `OnCrash` 回调 | `Func?` | `BowlContext` 中配置,崩溃时触发 | +| `GeneralTracer` 日志 | 静态类 | 可通过 `SetTracingEnabled` 开关日志 | + +### 4.2 分场景示例 + +#### 场景 1:升级模式监控 + 崩溃告警上传 -### 升级模式监控 +【场景说明】桌面应用升级到新版本后,Bowl 监控启动崩溃;发生崩溃时自动回退,并上传诊断包到内部日志平台。 -升级模式适合放在升级程序或 Bowl helper 中运行。关键点是:`BackupDirectory` 指向升级前保留的备份,`TargetPath` 指向当前安装目录,`ExtendedField` 填本次升级版本号。 +【示例代码】 ```csharp using GeneralUpdate.Bowl; @@ -73,28 +160,52 @@ var context = new BowlContext BackupDirectory = Path.Combine(installPath, version), WorkModel = "Upgrade", ExtendedField = version, - TimeoutMs = 30_000, - DumpType = DumpType.Full, + TimeoutMs = 60_000, // 应用启动较慢,给 60 秒 + DumpType = DumpType.Mini, AutoRestore = true, - OnCrash = (info, ct) => + OnCrash = async (info, ct) => { - Console.WriteLine($"Crash dump: {info.DumpFilePath}"); - Console.WriteLine($"Crash report: {info.CrashReportPath}"); - return Task.CompletedTask; + // 打包诊断文件 + var zipPath = Path.Combine( + Path.GetDirectoryName(info.DumpFilePath)!, + $"crash_{info.Version}_{DateTimeOffset.Now:yyyyMMddHHmmss}.zip"); + + System.IO.Compression.ZipFile.CreateFromDirectory( + Path.GetDirectoryName(info.DumpFilePath)!, zipPath); + + // 上传到日志平台 + using var client = new HttpClient(); + var content = new MultipartFormDataContent(); + content.Add(new StreamContent(File.OpenRead(zipPath)), "file", Path.GetFileName(zipPath)); + content.Add(new StringContent(info.Version), "version"); + content.Add(new StringContent(info.ExitCode.ToString()), "exitCode"); + + await client.PostAsync("https://logs.example.com/api/crash", content, ct); + + // 通知用户 + Console.WriteLine($"Version {info.Version} crashed (exit code {info.ExitCode})."); + Console.WriteLine($"Diagnostics uploaded. Previous version restored."); } }; BowlResult result = await new Bowl().LaunchAsync(context); if (result.DumpCaptured && result.Restored) -{ - Console.WriteLine("The upgraded version crashed and the backup was restored."); -} + Console.WriteLine("Crash detected and backup restored."); +else if (!result.DumpCaptured) + Console.WriteLine("Process started successfully."); ``` -### 独立监控模式 +【效果&注意事项】 +- `TimeoutMs` 需要大于应用正常启动时间 +- `DumpType.Mini` 生成更快,适合生产环境;疑难问题再切换到 `Full` +- 回调异常不会阻断恢复流程 + +#### 场景 2:独立监控模式(非升级场景) -`Normal` 模式只做崩溃捕获、报告输出和回调通知,不会自动恢复备份,也不会写入 `UpgradeFail` 失败版本标记。 +【场景说明】对 Worker 进程做通用启动崩溃监控,只采集诊断信息,不自动回滚。 + +【示例代码】 ```csharp var context = new BowlContext @@ -108,197 +219,202 @@ var context = new BowlContext WorkModel = "Normal", TimeoutMs = 15_000, DumpType = DumpType.Mini, - AutoRestore = false + AutoRestore = false, + OnCrash = (info, ct) => + { + Console.WriteLine($"Worker crashed: {info.DumpFilePath}"); + return Task.CompletedTask; + } }; BowlResult result = await new Bowl().LaunchAsync(context); -``` -## 崩溃检测与恢复流程 +if (result.DumpCaptured) +{ + Console.WriteLine($"Dump captured at: {result.DumpFilePath}"); + Console.WriteLine($"Report at: {result.CrashReportPath}"); +} +``` -`LaunchAsync` 的核心判断非常直接:平台策略先启动监控工具,监控工具输出到 `FailDirectory`;Bowl 再检查 `{FailDirectory}/{DumpFileName}` 是否存在。存在 Dump 就认为启动阶段发生了崩溃。 +【效果&注意事项】 +- `WorkModel = "Normal"` 不会恢复备份也不会标记 `UpgradeFail` +- 适合通用进程监控、CI 测试守护等非升级场景 -| 阶段 | 当前实现 | -| --- | --- | -| 准备监控 | 根据操作系统选择 `WindowsBowlStrategy`、`LinuxBowlStrategy` 或 `MacBowlStrategy` | -| 捕获异常 | Windows 使用 ProcDump;Linux 尝试安装并调用 ProcDump;macOS 使用 `lldb` 基础能力 | -| 判断崩溃 | 检查 `FailDirectory` 中是否生成指定 Dump 文件 | -| 生成报告 | 写出 `{version}_fail.json`,包含监控参数和监控工具输出 | -| 导出诊断 | Windows 调用 `Applications/Windows/export.bat` 导出驱动、系统信息和最近系统日志 | -| 恢复备份 | 仅当 `WorkModel == "Upgrade"` 且 `AutoRestore == true` 时,把 `BackupDirectory` 覆盖复制回 `TargetPath` | -| 标记失败版本 | 升级模式下写入 `UpgradeFail = ExtendedField`,Core 后续会跳过小于等于该失败版本的更新 | -| 通知业务 | 如果配置了 `OnCrash`,传出 Dump 路径、报告路径、版本号和退出码 | +--- -`TimeoutMs` 是监控子进程的等待上限。超时且没有 Dump 时,Bowl 不会执行恢复管线;此时更应该关注 `DumpCaptured` 是否为 `true`,而不是只看 `Success`。 +## 5. 常规使用示例 -## BowlContext 参数 +### 5.1 快速入门示例(最简 demo) -| 参数 | 说明 | 建议 | -| --- | --- | --- | -| `ProcessNameOrId` | 要监控的进程名或 PID | Windows 可使用进程名;Linux 上更建议传 PID | -| `DumpFileName` | Dump 文件名 | 推荐包含版本号,例如 `2.0.0_fail.dmp` | -| `FailFileName` | 崩溃报告 JSON 文件名 | 推荐和 Dump 同版本,例如 `2.0.0_fail.json` | -| `TargetPath` | 当前应用安装根目录 | 恢复备份时会覆盖复制到这里 | -| `FailDirectory` | 故障文件输出目录 | 推荐 `Path.Combine(TargetPath, "fail", version)` | -| `BackupDirectory` | 升级前备份目录 | `AutoRestore` 打开时必须确保目录存在且内容完整 | -| `WorkModel` | `Upgrade` 或 `Normal` | 升级后回滚用 `Upgrade`;普通崩溃采集用 `Normal` | -| `ExtendedField` | 扩展字段,当前主要存版本号 | 升级模式下会写入 `UpgradeFail` | -| `TimeoutMs` | 监控子进程超时时间 | 默认归一化为 30000 ms,按应用启动耗时调大 | -| `DumpType` | `Full`、`Mini`、`Heap` | 生产环境可先用 `Mini` 降低体积;疑难问题用 `Full` | -| `AutoRestore` | 是否自动恢复备份 | 升级模式要显式设置为 `true` | -| `OnCrash` | 单次崩溃回调 | 适合上传报告、通知用户、写入业务日志 | - -### DumpType 选择 - -| 类型 | Windows ProcDump 参数 | 特点 | -| --- | --- | --- | -| `Full` | `-ma` | 信息最完整,文件最大,适合难复现问题 | -| `Mini` | `-mm` | 文件更小,生成更快,适合生产默认采集 | -| `Heap` | `-mh` | 带堆信息的小型 Dump,介于 Mini 和 Full 之间 | - -## 输出文件 +```csharp +using GeneralUpdate.Bowl; -一次升级失败后,推荐按版本存放所有故障文件: +var context = new BowlContext +{ + ProcessNameOrId = "MyApp.exe", + DumpFileName = "fail.dmp", + FailFileName = "fail.json", + TargetPath = AppDomain.CurrentDomain.BaseDirectory, + FailDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "fail"), + BackupDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "backup"), +}.Normalize(); // 应用默认值 -```text -MyApp/ - fail/ - 2.0.0/ - 2.0.0_fail.dmp - 2.0.0_fail.json - driverInfo.txt - systeminfo.txt - systemlog.evtx - Logs/ - generalupdate-trace 2026-01-01.log +BowlResult result = await new Bowl().LaunchAsync(context); +Console.WriteLine($"Success: {result.Success}, Dump captured: {result.DumpCaptured}"); ``` -| 文件 | 来源 | 内容 | -| --- | --- | --- | -| `{version}_fail.dmp` | ProcDump 或 lldb | 崩溃现场内存快照 | -| `{version}_fail.json` | `CrashReporter` | `BowlContext` 映射参数和监控工具输出行 | -| `driverInfo.txt` | Windows `driverquery` | Windows 驱动列表 | -| `systeminfo.txt` | Windows `systeminfo` | OS、硬件、内存等系统信息 | -| `systemlog.evtx` | Windows `wevtutil` | 最近一天 Windows System 事件日志 | -| `Logs/generalupdate-trace yyyy-MM-dd.log` | `GeneralTracer` | Bowl 自身运行追踪日志 | - -非 Windows 平台当前不会导出 `driverInfo.txt`、`systeminfo.txt`、`systemlog.evtx`,但仍会尽量生成 Dump 和失败 JSON。 +### 5.2 基础参数组合示例 -失败 JSON 的结构来自当前 `CrashReporter`: +```csharp +var version = "2.0.0"; +var installPath = @"C:\Program Files\MyApp"; -```json +var context = new BowlContext { - "Parameter": { - "TargetPath": "C:\\Program Files\\MyApp", - "FailDirectory": "C:\\Program Files\\MyApp\\fail\\2.0.0", - "BackupDirectory": "C:\\Program Files\\MyApp\\2.0.0", - "ProcessNameOrId": "MyApp.exe", - "DumpFileName": "2.0.0_fail.dmp", - "FailFileName": "2.0.0_fail.json", - "WorkModel": "Upgrade", - "ExtendedField": "2.0.0" - }, - "ProcdumpOutPutLines": [ - "ProcDump v11.0 - Sysinternals process dump utility", - "[10:00:03] Dump 1 initiated: C:\\Program Files\\MyApp\\fail\\2.0.0\\2.0.0_fail.dmp", - "[10:00:03] Dump count reached." - ] -} + ProcessNameOrId = "MyApp.exe", + DumpFileName = $"{version}_fail.dmp", + FailFileName = $"{version}_fail.json", + TargetPath = installPath, + FailDirectory = Path.Combine(installPath, "fail", version), + BackupDirectory = Path.Combine(installPath, version), + WorkModel = "Upgrade", + ExtendedField = version, + TimeoutMs = 30_000, + DumpType = DumpType.Full, + AutoRestore = true, + OnCrash = (info, ct) => + { + Console.WriteLine($"Crash: {info.DumpFilePath}"); + return Task.CompletedTask; + } +}; + +BowlResult result = await new Bowl().LaunchAsync(context); + +if (result.DumpCaptured && result.Restored) + Console.WriteLine("The upgraded version crashed and the backup was restored."); ``` -## 事件回调 +### 5.3 真实业务落地示例 -`OnCrash` 是单次崩溃事件回调,只在检测到 Dump 后触发。它拿到的是整理后的 `CrashInfo`: +完整升级后 Bowl 守护流程,包含升级程序侧集成: ```csharp -public readonly record struct CrashInfo -{ - public string DumpFilePath { get; init; } - public string CrashReportPath { get; init; } - public string Version { get; init; } - public int ExitCode { get; init; } -} -``` - -常见用途: +using GeneralUpdate.Bowl; -| 场景 | 做法 | -| --- | --- | -| 上传诊断包 | 在回调中打包 Dump、JSON 和 Windows 诊断文件,上传到内部日志平台 | -| 提示用户 | 告知“新版本启动失败,已恢复上一版本”,并附带问题编号 | -| 记录业务审计 | 把 `Version`、`ExitCode`、报告路径写入你的业务日志 | +// --- 升级程序(Update.exe)侧 --- +// 1. 主程序已停止,升级程序完成文件替换 +// 2. 备份旧版本到 BackupDirectory +// 3. 启动 Bowl 监控新版本 -回调异常会被 Bowl 记录到追踪日志中,不会阻止 `LaunchAsync` 返回最终 `BowlResult`。取消操作请通过 `CancellationToken` 传递。 +var version = "2.0.0"; +var installPath = @"C:\Program Files\MyApp"; -## 日志开关 +var bowlContext = new BowlContext +{ + ProcessNameOrId = "MyApp.exe", + DumpFileName = $"{version}_fail.dmp", + FailFileName = $"{version}_fail.json", + TargetPath = installPath, + FailDirectory = Path.Combine(installPath, "fail", version), + BackupDirectory = Path.Combine(installPath, "backups", version), + WorkModel = "Upgrade", + ExtendedField = version, + TimeoutMs = 45_000, // 应用冷启动约 30s,留 15s buffer + DumpType = DumpType.Mini, + AutoRestore = true, + OnCrash = async (info, ct) => + { + try + { + // 上传诊断信息 + using var client = new HttpClient(); + var crashData = new + { + version = info.Version, + exitCode = info.ExitCode, + dumpPath = info.DumpFilePath, + reportPath = info.CrashReportPath, + timestamp = DateTimeOffset.UtcNow + }; + await client.PostAsJsonAsync( + "https://monitor.mycompany.com/api/crash-report", + crashData, ct); + } + catch (Exception ex) + { + // 上报失败不影响恢复流程 + Console.WriteLine($"Failed to upload crash report: {ex.Message}"); + } + } +}; -Bowl 使用公开的 `GeneralTracer` 写运行追踪。默认会输出到控制台,并在运行目录下按日期写入: +var result = await new Bowl().LaunchAsync(bowlContext); -```text -Logs/generalupdate-trace yyyy-MM-dd.log +if (result.Success) +{ + Console.WriteLine("New version started successfully."); +} +else if (result.DumpCaptured) +{ + Console.WriteLine($"New version crashed (exit code: {result.ExitCode})."); + Console.WriteLine($"Backup restored: {result.Restored}"); + Console.WriteLine($"Dump: {result.DumpFilePath}"); + Console.WriteLine($"Report: {result.CrashReportPath}"); +} +else +{ + Console.WriteLine($"Process exited abnormally (exit code: {result.ExitCode}), but no dump was captured."); +} ``` -如果你的场景对启动性能、磁盘写入或控制台输出非常敏感,可以关闭追踪: +--- + +## 6. 全局配置 + +Bowl 不依赖全局配置文件。所有配置通过 `BowlContext` 传入。日志行为通过静态类 `GeneralTracer` 控制。 + +### 日志开关 ```csharp +// 性能敏感场景关闭日志 GeneralTracer.SetTracingEnabled(false); var result = await new Bowl().LaunchAsync(context); +// 排查问题时重新开启 GeneralTracer.SetTracingEnabled(true); ``` -关闭后,Bowl 自身的诊断追踪会减少,但崩溃 Dump 和失败 JSON 的生成逻辑不依赖该开关。排查升级失败时建议保持开启;稳定生产环境可按你的性能策略关闭。 - -## 平台差异 - -| 平台 | 监控工具 | 诊断导出 | 注意事项 | -| --- | --- | --- | --- | -| Windows | 内置 ProcDump:`procdump.exe`、`procdump64.exe`、`procdump64a.exe` | 支持 `driverInfo.txt`、`systeminfo.txt`、`systemlog.evtx` | 监控工具路径来自 `TargetPath/Applications/Windows`;需要足够权限生成 Dump | -| Linux | 内置 deb/rpm 包 + `install.sh` 安装 ProcDump 后调用 `procdump` | 当前为 no-op | 支持 Ubuntu、Debian、RHEL、CentOS、Fedora、ClearOS 映射包;脚本可能需要 `sudo` | -| macOS | `/usr/bin/lldb` | 当前为 no-op | 受 SIP、调试权限、签名策略影响;当前是基础实现 | - -NuGet 包会把 `Applications/**/*` 作为内容输出到构建目录。自部署时请确认这些工具文件没有被裁剪,否则平台策略可能返回“监控工具不可用”或进程启动失败。 - -## 恢复场景 - -假设用户从 `1.0.0` 升级到 `2.0.0`,新版本启动后立即崩溃: - -1. 升级流程先把旧版本备份到 `BackupDirectory`,例如 `C:\Program Files\MyApp\2.0.0`。 -2. 新版本文件被复制到 `TargetPath`。 -3. 主程序启动,同时 Bowl 使用 `ProcessNameOrId = "MyApp.exe"` 监控启动期异常。 -4. ProcDump 捕获到未处理异常,写出 `fail\2.0.0\2.0.0_fail.dmp`。 -5. Bowl 写出 `2.0.0_fail.json`,Windows 下继续导出驱动、系统信息和最近系统日志。 -6. 因为 `WorkModel == "Upgrade"` 且 `AutoRestore == true`,Bowl 将 `BackupDirectory` 覆盖复制回 `TargetPath`。 -7. Bowl 写入 `UpgradeFail = "2.0.0"`;Core 下次检测到服务端仍返回 `2.0.0` 或更低版本时,会跳过这个已知失败版本,直到服务端提供更高版本。 -8. `OnCrash` 回调可以上传诊断包,或提示用户已经回退到可用版本。 - -这个机制的目标是降低“升级成功但新版本打不开”的风险:用户回到可启动版本,开发者拿到 Dump 和上下文继续修复。 +### 输出文件结构 -## 旧 API 迁移 +一次升级失败后,推荐按版本存放所有故障文件: -旧示例中的 `GeneralUpdate.Bowl.Strategys.MonitorParameter` 已标记为过时,推荐迁移到 `BowlContext` 和异步入口: +```text +MyApp/ + fail/ + 2.0.0/ + 2.0.0_fail.dmp # Dump 内存快照 + 2.0.0_fail.json # 崩溃报告 JSON + driverInfo.txt # Windows 驱动列表 + systeminfo.txt # OS/硬件/内存信息 + systemlog.evtx # Windows 系统事件日志 + Logs/ + generalupdate-trace 2026-01-01.log # Bowl 自身追踪日志 +``` -```csharp -var oldParameter = new GeneralUpdate.Bowl.Strategys.MonitorParameter -{ - ProcessNameOrId = "MyApp.exe", - DumpFileName = "2.0.0_fail.dmp", - FailFileName = "2.0.0_fail.json", - TargetPath = installPath, - FailDirectory = Path.Combine(installPath, "fail", "2.0.0"), - BackupDirectory = Path.Combine(installPath, "2.0.0"), - WorkModel = "Upgrade", - ExtendedField = "2.0.0" -}; +### 平台差异 -BowlContext context = Bowl.MapToContext(oldParameter); -BowlResult result = await new Bowl().LaunchAsync(context); -``` +| 平台 | 监控工具 | 诊断导出 | 注意事项 | +| --- | --- | --- | --- | +| Windows | 内置 ProcDump(`procdump.exe`/`procdump64.exe`/`procdump64a.exe`) | 支持 `driverInfo.txt`、`systeminfo.txt`、`systemlog.evtx` | 需要足够权限生成 Dump | +| Linux | 内置 deb/rpm 包 + `install.sh` 安装 ProcDump | 当前为 no-op | 支持 Ubuntu/Debian/RHEL/CentOS/Fedora/ClearOS | +| macOS | `/usr/bin/lldb` | 当前为 no-op | 受 SIP、调试权限、签名策略影响,基础实现 | -如果是新代码,直接创建 `BowlContext`,不要再依赖旧 `MonitorParameter`。 +--- ## 相关资源 -- **示例代码**:[GeneralUpdate-Samples / Bowl](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Bowl) -- **主仓库**:[GeneralUpdate](https://github.com/GeneralLibrary/GeneralUpdate) +- [Bowl 示例代码](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Bowl) +- [GeneralUpdate 仓库](https://github.com/GeneralLibrary/GeneralUpdate) +- [Dump 指南](../guide/Dump.md) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 80580b8..876446d 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -4,184 +4,445 @@ sidebar_position: 5 # GeneralUpdate.Core -`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心,重点提供可编程的启动器、配置模型、事件模型、下载子系统扩展点、生命周期钩子、状态上报、差分管道和平台策略扩展。本页聚焦组件 API、属性和扩展方式;完整端到端上手流程会放到 cookbook 中。 +**命名空间:** `GeneralUpdate.Core` | **主要入口:** `GeneralUpdateBootstrap` | **NuGet 包:** `GeneralUpdate.Core` -**命名空间:** `GeneralUpdate.Core` -**主要入口:** `GeneralUpdateBootstrap` -**NuGet 包:** `GeneralUpdate.Core` +## 1. 组件简介 -```bash -dotnet add package GeneralUpdate.Core -``` +### 1.1 组件概述 -## 文档大纲与知识点导航 {#knowledge-map} +**GeneralUpdate.Core** 是 GeneralUpdate 生态的更新执行核心引擎,负责客户端应用的全生命周期更新管理。它提供可编程的启动器、配置模型、事件通知系统、下载子系统(支持并发、断点续传、重试、校验、后处理管道)、差分补丁管道、版本回写、IPC 进程通信以及平台策略扩展。 -如果你是第一次阅读 Core 文档,可以先看这个导航,再跳到对应知识点。本文按照“入口与配置 -> 执行策略 -> 差分/下载/并发 -> 扩展点 -> 工具链关系”的顺序组织。 +**核心能力:** -| 你想了解什么 | 推荐阅读 | +| 能力 | 说明 | | --- | --- | -| Core 到底负责什么、不负责什么 | [组件能力边界](#组件能力边界) | -| 如何启动一次标准更新 | [入口类:GeneralUpdateBootstrap](#入口类generalupdatebootstrap)、[标准更新策略](#standard-update-strategy) | -| 如何用极简配置接入更新 | [generalupdate.manifest.json](#应用身份清单generalupdatemanifestjson)、[极简配置理念](#极简配置理念)、[配合引导类使用](#配合引导类使用) | -| `Client`、`Upgrade`、`OssClient`、`OssUpgrade` 有什么区别 | [执行策略总览](#execution-strategies) | -| 静默更新到底什么时候下载、什么时候替换 | [静默更新策略](#silent-update-strategy) | -| 差分算法有哪些,如何选择 | [差分算法与补丁管道](#differential-pipeline) | -| 下载和差分如何并发、多线程 | [下载并发与差分并行](#download-diff-concurrency) | -| 如何接收更新过程事件通知 | [事件 API](#事件-api) | -| 如何关闭日志降低性能损耗 | [日志与性能](#logging-performance) | -| 如何扩展下载、校验、认证、Hook 或平台策略 | [扩展点总览](#扩展点总览) | -| Tools 生成的产物在 Core 中怎么消费 | [与 GeneralUpdate.Tools 的关系](#与-generalupdatetools-的关系) | - -## 组件能力边界 - -Core 负责“执行更新”,不负责生成更新包,也不直接管理服务端后台。 - -| 能力 | Core 是否负责 | 说明 | -| --- | --- | --- | -| 读取更新配置 | 是 | 通过 `UpdateRequest`、配置文件、`SetSource` 或 IPC 恢复运行参数。 | -| 检查服务端版本 | 是 | `Client` / `OssClient` 角色会读取版本清单并生成下载计划。 | -| 下载更新包 | 是 | 可替换下载来源、执行器、重试策略、后处理管道或完整编排器。 | -| 校验与应用补丁 | 是 | 支持 Hash 校验、压缩包处理、差分补丁管道。 | -| 文件替换与重启应用 | 是 | `Upgrade` / `OssUpgrade` 角色用于独立升级程序。 | -| 生成差分包 | 否 | 推荐使用 `GeneralUpdate.Tools`。 | +| 多策略更新执行 | 内置标准 Client/Upgrade 更新、OSS 对象存储更新、静默后台轮询更新三种执行策略 | +| 配置驱动 | 通过 `UpdateRequest` 强类型配置或 `SetSource` 轻配置入口,配合 `generalupdate.manifest.json` 实现极简接入 | +| 下载子系统 | 可替换的下载来源、执行器、重试策略、后处理管道和批量编排器,默认支持并发下载、断点续传、SHA256 校验 | +| 差分补丁管道 | 文件级二进制差分(BSDIFF 4.0 / Streaming HDiff),目录级对比与批量补丁分发,支持并行处理 | +| 事件通知 | 7 种事件回调(版本发现、下载进度、完成、错误、异常等),支持批量事件监听器注册 | +| 扩展点体系 | 10 个可替换接口:生命周期钩子、状态上报、SSL 证书策略、HTTP 认证、下载来源/策略/执行器/管道/编排器、平台策略 | +| 版本清单体系 | `generalupdate.manifest.json` 自动发现应用身份,更新后自动回写本地版本,无需业务代码维护版本号 | +| IPC 进程通信 | 主程序与升级程序之间通过加密文件传递更新上下文,保障升级流程的原子性和安全性 | +| SignalR 实时推送 | 基于 SignalR 的版本更新实时推送(`UpgradeHubService`),支持点对点和广播推送、自动重连、多事件订阅 | + +**解决的业务痛点:** +- 桌面应用需要可靠的自动更新能力,但手写更新逻辑涉及版本对比、下载、校验、解压、文件替换、进程重启等多个复杂环节 +- 大型应用的分发包体积大,全量更新带宽成本高,需要差分更新降低下载量 +- 需要灵活的更新策略(静默后台、用户手动触发、OSS/CDN 分发) +- 升级程序版本独立演进,需要主程序和升级程序的版本协调 +- 多产品线需要统一的更新框架,减少重复开发 + +**业务使用场景:** +- WPF / WinForms / Avalonia / WinUI 桌面应用的自动更新 +- 企业内部工具的统一版本管理 +- 通过 CDN / OSS 分发更新包的客户端应用 +- 需要差分更新降低带宽消耗的大型客户端 + +### 1.2 环境与依赖 + +| 项目 | 说明 | +| --- | --- | +| **版本** | `10.5.0-beta.2` | +| **目标框架** | `netstandard2.0`(兼容 .NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 5+) | +| **依赖包** | `GeneralUpdate.Differential`(差分算法)、`System.Text.Json`、`Microsoft.Extensions.Logging.Abstractions` | +| **兼容性** | Windows(主支持)/ Linux / macOS;支持 x86 / x64 / ARM64 | -## 入口类:GeneralUpdateBootstrap +--- -`GeneralUpdateBootstrap` 是 Core 的主要门面类。它继承 `AbstractBootstrap`,因此同时拥有自身方法和基类提供的扩展注册方法。 +## 2. 组件功能列表 -```csharp -using GeneralUpdate.Core; +| 功能名称 | 功能描述 | 类型 | 是否必填 | 备注限制 | +| --- | --- | --- | --- | --- | +| 标准 Client 更新 | 主程序检查版本、下载更新包、启动升级程序替换文件 | 基础 | 必选 | 需要服务端版本检查 API | +| 标准 Upgrade 更新 | 独立升级程序读取 IPC 上下文并执行文件替换、差分补丁、版本回写 | 基础 | 必选 | 由主程序启动,通过加密 IPC 传递上下文 | +| OSS Client 更新 | 从 OSS/CDN 下载版本配置,对比后启动升级程序 | 基础 | 可选 | 版本配置文件托管在对象存储,不依赖服务端 API | +| OSS Upgrade 更新 | OSS 模式下的升级程序,下载并解压资源包 | 基础 | 可选 | 配合 OssClient 使用 | +| 静默后台更新 | 后台轮询版本、静默下载、进程退出时触发升级 | 基础 | 可选 | 需设置 `Option.Silent = true` | +| 差分补丁管道 | 文件级二进制差分生成与应用,目录级批量补丁分发 | 基础 | 可选 | 需要 `Option.PatchEnabled = true` | +| 并发下载 | 多资源包并发下载,支持断点续传和 SHA256 校验 | 基础 | 可选 | 通过 `Option.MaxConcurrency` 控制 | +| 事件通知回调 | 版本发现、下载进度、完成、异常等 7 种事件 | 基础 | 可选 | 通过 `AddListener*` 方法注册 | +| 应用身份清单 | `generalupdate.manifest.json` 自动发现与版本回写 | 拓展 | 推荐 | 由 `GeneralUpdate.Tools` 生成 | +| 下载来源扩展 | 自定义版本清单和下载资源来源 | 拓展 | 可选 | 实现 `IDownloadSource` | +| 下载执行器扩展 | 自定义单文件下载实现(HTTP/FTP/SFTP 等) | 拓展 | 可选 | 实现 `IDownloadExecutor` | +| 下载重试策略扩展 | 自定义重试、超时、熔断策略 | 拓展 | 可选 | 实现 `IDownloadPolicy` | +| 下载后处理管道扩展 | 下载完成后自定义校验、解密、扫描等 | 拓展 | 可选 | 实现 `IDownloadPipeline` | +| 批量下载编排扩展 | 完全替换批量下载并发控制逻辑 | 拓展 | 可选 | 实现 `IDownloadOrchestrator` | +| 生命周期钩子 | 更新前、下载后、更新后、异常、启动前的业务逻辑注入 | 拓展 | 可选 | 实现 `IUpdateHooks` | +| 状态上报扩展 | 更新状态上报到自有服务端 | 拓展 | 可选 | 实现 `IUpdateReporter` | +| HTTP 认证扩展 | 自定义 HTTP 请求认证头 | 拓展 | 可选 | 实现 `IHttpAuthProvider` | +| SSL 证书策略扩展 | 自定义 HTTPS 证书校验逻辑 | 拓展 | 可选 | 实现 `ISslValidationPolicy` | +| 平台策略扩展 | 替换平台级文件操作或启动逻辑 | 拓展 | 可选 | 实现 `IStrategy` | +| SignalR 实时推送 | 服务端主动推送版本更新通知,客户端订阅接收,支持点对点和广播推送 | 拓展 | 可选 | `UpgradeHubService`,命名空间 `GeneralUpdate.Core.Hubs` | +| 推送重连机制 | 断线自动重连(随机退避策略),连接生命周期管理 | 拓展 | 可选 | `RandomRetryPolicy` | +| 推送事件订阅 | 接收消息、在线状态、重连通知、关闭通知四种事件 | 拓展 | 可选 | 通过 `AddListener*` 方法注册 | -var bootstrap = new GeneralUpdateBootstrap(); -``` +--- -### 方法总览 +## 3. API 配置说明 + +### 3.1 配置字段(属性 Props) + +**UpdateRequest 配置属性:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `UpdateUrl` | `string` | — | 是 | 有效绝对 URL | 更新检查 API 地址 | +| `UpdateAppName` | `string` | `"Update.exe"` | 推荐 | 有效文件名 | 升级程序文件名,如与实际不同必须显式设置 | +| `MainAppName` | `string` | — | 推荐 | 有效文件名 | 主程序文件名,用于重启和识别 | +| `ClientVersion` | `string` | — | 推荐 | SemVer 格式 | 当前主程序版本 | +| `AppSecretKey` | `string` | — | 推荐 | — | 应用密钥,用于服务端认证 | +| `InstallPath` | `string` | `AppDomain.CurrentDomain.BaseDirectory` | 可选 | 有效目录路径 | 应用安装根目录 | +| `ReportUrl` | `string` | `null` | 可选 | 有效绝对 URL | 更新状态上报 API | +| `UpdateLogUrl` | `string` | `null` | 可选 | 有效绝对 URL | 更新日志页面地址 | +| `UpgradeClientVersion` | `string` | — | 可选 | SemVer 格式 | 升级程序自身版本 | +| `ProductId` | `string` | — | 可选 | — | 产品标识,多产品时用于区分 | +| `UpdatePath` | `string` | `InstallPath` | 可选 | 有效目录路径 | 升级程序所在目录 | +| `Bowl` | `string` | `null` | 可选 | 有效文件名 | 更新前需关闭的辅助进程名 | +| `Scheme` | `string` | `null` | 可选 | `"Bearer"` 等 | 认证方案 | +| `Token` | `string` | `null` | 可选 | — | 认证令牌 | +| `Files` | `List` | `null` | 可选 | — | 更新时跳过的指定文件列表 | +| `Formats` | `List` | `null` | 可选 | — | 更新时跳过的扩展名列表 | +| `Directories` | `List` | `null` | 可选 | — | 更新时跳过的目录列表 | +| `DriverDirectory` | `string` | `null` | 可选 | 有效目录路径 | 驱动更新目录 | + +**Option 运行时选项:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `Option.AppType` | `AppType` | `Client` | 是 | `Client(1)`, `Upgrade(2)`, `OssClient(3)`, `OssUpgrade(4)` | 当前进程角色 | +| `Option.DiffMode` | `DiffMode` | `Serial` | 可选 | `Serial`, `Parallel` | 下载执行模式 | +| `Option.Encoding` | `Encoding` | `UTF8` | 可选 | `Encoding` 实例 | 压缩包处理编码 | +| `Option.Format` | `Format` | `Zip` | 可选 | `Zip` | 更新包格式 | +| `Option.DownloadTimeout` | `int?` | `30` | 可选 | 正整数(秒) | 下载超时时间 | +| `Option.PatchEnabled` | `bool?` | `true` | 可选 | `true` / `false` | 是否启用差分补丁 | +| `Option.BackupEnabled` | `bool?` | `true` | 可选 | `true` / `false` | 更新前是否备份被替换文件 | +| `Option.Silent` | `bool` | `false` | 可选 | `true` / `false` | 是否启用静默轮询更新 | +| `Option.SilentPollIntervalMinutes` | `int` | `60` | 可选 | 正整数 | 静默模式轮询间隔(分钟) | +| `Option.LaunchClientAfterUpdate` | `bool` | `true` | 可选 | `true` / `false` | 升级后是否启动主程序 | +| `Option.MaxConcurrency` | `int` | `3` | 可选 | `1` ~ `ProcessorCount × 2` | 下载最大并发数 | +| `Option.EnableResume` | `bool` | `true` | 可选 | `true` / `false` | 是否启用断点续传 | +| `Option.RetryCount` | `int` | `3` | 可选 | 非负整数 | 下载重试次数 | +| `Option.VerifyChecksum` | `bool` | `true` | 可选 | `true` / `false` | 是否校验下载文件 Hash | +| `Option.RetryInterval` | `TimeSpan` | `1s` | 可选 | `TimeSpan` 正值 | 下载重试间隔 | + +### 3.2 实例方法 + +**GeneralUpdateBootstrap:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `LaunchAsync()` | 无(读取已配置的 Option 和 Config) | `Task` | 所有 Core 使用场景的最终入口 | 会根据 `Option.AppType` 自动选择执行策略 | +| `Cancel()` | 无 | `void` | UI 中提供"取消更新"按钮 | 触发内部 `CancellationTokenSource` | +| `SetConfig(UpdateRequest)` | `configInfo` — 更新配置对象 | `GeneralUpdateBootstrap` | 主程序内显式配置更新参数 | 会调用 `Validate()` 检查关键字段 | +| `SetConfig(string)` | `filePath` — JSON 配置文件路径 | `GeneralUpdateBootstrap` | 从文件读取更新配置 | 支持相对路径和绝对路径;UTF-8 JSON 格式 | +| `SetSource(...)` | `updateUrl`, `appSecretKey`, `reportUrl?`, `scheme?`, `token?` | `GeneralUpdateBootstrap` | 轻配置入口,配合 manifest 使用 | 只提供服务端入口和密钥,身份信息由 manifest 补齐 | +| `SetOption(Option, T)` | `option` — 选项键, `value` — 选项值 | `GeneralUpdateBootstrap` | 设置运行时选项 | 传入 `null` 给可空选项会移除当前设置 | +| `UseDiffPipeline(Action)` | `configure` — 差分管道配置委托 | `GeneralUpdateBootstrap` | 替换或调整差分补丁管道 | 未调用时使用默认配置 | +| `AddListenerUpdateInfo(...)` | `EventHandler` | `GeneralUpdateBootstrap` | 接收服务端版本信息 | 无更新时也会触发(`Info.Code = 404`) | +| `AddListenerUpdatePrecheck(...)` | `Func` | `GeneralUpdateBootstrap` | 下载前预检查 | 返回 `true` 表示跳过非强制更新 | +| `AddListenerProgress(...)` | `EventHandler` | `GeneralUpdateBootstrap` | 更新进度条、状态文本 | 同时包含下载进度和差分进度 | +| `AddListenerMultiDownloadCompleted(...)` | `EventHandler` | `GeneralUpdateBootstrap` | 标记单个资源下载完成 | 不要当做"全部下载完成" | +| `AddListenerMultiAllDownloadCompleted(...)` | `EventHandler` | `GeneralUpdateBootstrap` | 全部下载完成后的后续处理 | 包含失败汇总 `FailedVersions` | +| `AddListenerMultiDownloadError(...)` | `EventHandler` | `GeneralUpdateBootstrap` | 记录单个资源下载失败 | 整体成功仍以 `MultiAllDownloadCompleted` 为准 | +| `AddListenerMultiDownloadStatistics(...)` | `EventHandler` | `GeneralUpdateBootstrap` | 展示下载速度和剩余时间 | 新代码优先使用 `AddListenerProgress` | +| `AddListenerException(...)` | `EventHandler` | `GeneralUpdateBootstrap` | 上报异常、展示错误信息 | 仅通知,不自动重试 | +| `AddEventListener()` | 泛型参数 — 监听器类型 | `GeneralUpdateBootstrap` | 批量注册事件监听器 | `T` 必须实现 `IUpdateEventListener`,推荐继承 `UpdateEventListenerBase` | +| `Hooks()` | 泛型参数 — Hook 类型 | `GeneralUpdateBootstrap` | 注册生命周期钩子 | `T` 必须实现 `IUpdateHooks` 且有无参构造函数 | +| `UpdateReporter()` | 泛型参数 — Reporter 类型 | `GeneralUpdateBootstrap` | 注册状态上报器 | `T` 必须实现 `IUpdateReporter` | +| `SslPolicy()` | 泛型参数 — SSL 策略类型 | `GeneralUpdateBootstrap` | 自定义 HTTPS 证书校验 | 生产环境不建议无条件返回 `true` | +| `HttpAuth()` | 泛型参数 — 认证提供器类型 | `GeneralUpdateBootstrap` | 自定义 HTTP 请求认证 | `T` 必须实现 `IHttpAuthProvider` | +| `DownloadSource()` | 泛型参数 — 下载来源类型 | `GeneralUpdateBootstrap` | 自定义版本清单来源 | `T` 必须实现 `IDownloadSource` | +| `DownloadPolicy()` | 泛型参数 — 下载策略类型 | `GeneralUpdateBootstrap` | 自定义下载重试/超时策略 | `T` 必须实现 `IDownloadPolicy` | +| `DownloadExecutor()` | 泛型参数 — 下载执行器类型 | `GeneralUpdateBootstrap` | 自定义单文件下载实现 | `T` 必须实现 `IDownloadExecutor` | +| `DownloadPipeline()` | 泛型参数 — 下载管道类型 | `GeneralUpdateBootstrap` | 自定义下载后处理 | `T` 必须实现 `IDownloadPipeline` | +| `DownloadOrchestrator()` | 泛型参数 — 下载编排器类型 | `GeneralUpdateBootstrap` | 完全替换批量下载逻辑 | 只有需要完整替换下载行为时才建议实现 | +| `Strategy()` | 泛型参数 — 策略类型 | `GeneralUpdateBootstrap` | 自定义平台级更新策略 | `T` 必须实现 `IStrategy` | + +**DiffPipelineBuilder:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `UseDiffer(IBinaryDiffer)` | `differ` — 差分算法实例 | `DiffPipelineBuilder` | 替换文件级差分算法 | 可选 `BsdiffDiffer` / `StreamingHdiffDiffer` / 自定义 | +| `UseCleanMatcher(ICleanMatcher)` | `matcher` — Clean 匹配器 | `DiffPipelineBuilder` | 自定义 Clean 阶段的文件匹配逻辑 | 默认 `DefaultCleanMatcher` | +| `UseDirtyMatcher(IDirtyMatcher)` | `matcher` — Dirty 匹配器 | `DiffPipelineBuilder` | 自定义 Dirty 阶段的补丁匹配逻辑 | 默认 `DefaultDirtyMatcher` | +| `WithParallelism(int)` | `degree` — 并行度 | `DiffPipelineBuilder` | 设置差分文件并行处理数 | 默认 2;建议 1-8 | +| `WithStopOnFirstError(bool)` | `stop` — 是否首次错误即停止 | `DiffPipelineBuilder` | 错误策略控制 | 默认 `false` | +| `WithProgress(IProgress)` | `progress` — 进度报告器 | `DiffPipelineBuilder` | 接入差分进度回调 | 可配合 `Progress` 使用 | +| `Build()` | 无 | `DiffPipeline` | 构建差分管道实例 | 一般在 `UseDiffPipeline` 回调内部调用 | -| 方法 | 用途 | 常用场景 | -| --- | --- | --- | -| `LaunchAsync()` | 按当前 `Option.AppType` 启动更新流程。 | 所有 Core 使用场景最终都会调用。 | -| `Cancel()` | 请求取消当前更新操作。 | UI 中提供“取消更新”按钮。 | -| `SetConfig(UpdateRequest)` | 使用强类型对象配置更新。 | 主程序内显式配置更新参数。 | -| `SetConfig(string)` | 从 JSON 文件读取 `UpdateRequest`。 | 将更新参数放到 `update_config.json` 或自定义配置文件。 | -| `SetSource(...)` | 只提供更新地址、密钥、报告地址等基础参数。 | 零配置/轻配置入口。 | -| `SetOption(Option, T)` | 设置运行时选项。 | 设置角色、超时、并发、差分、静默更新等。 | -| `UseDiffPipeline(Action)` | 自定义差分补丁管道。 | 替换 differ、调整并行度、接入补丁进度。 | -| `AddListenerUpdatePrecheck(Func)` | 下载前预检查。 | 检查磁盘空间、网络状态或弹窗确认。 | -| `AddListener...` | 注册单个事件回调。 | 更新 UI、写日志、上报监控。 | -| `AddEventListener()` | 批量注册事件监听器。 | 将事件处理封装成类。 | - -### LaunchAsync +**UpdateRequestBuilder:** -```csharp -public Task LaunchAsync() -``` +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `Create()` | 无 | `UpdateRequestBuilder` | 从 `update_config.json` 读取配置 | 文件不存在会抛出 `FileNotFoundException` | +| `SetUpdateUrl(string)` | `url` | `UpdateRequestBuilder` | 设置更新地址 | 必须为绝对 URL | +| `SetUpgradeAppName(string)` | `name` | `UpdateRequestBuilder` | 设置升级程序文件名 | — | +| `SetMainAppName(string)` | `name` | `UpdateRequestBuilder` | 设置主程序文件名 | — | +| `SetClientVersion(string)` | `version` | `UpdateRequestBuilder` | 设置客户端版本 | SemVer 格式 | +| `SetAppSecretKey(string)` | `key` | `UpdateRequestBuilder` | 设置应用密钥 | — | +| `SetInstallPath(string)` | `path` | `UpdateRequestBuilder` | 设置安装目录 | — | +| `SetProductId(string)` | `id` | `UpdateRequestBuilder` | 设置产品标识 | — | +| `SetReportUrl(string)` | `url` | `UpdateRequestBuilder` | 设置上报地址 | — | +| `SetUpdateLogUrl(string)` | `url` | `UpdateRequestBuilder` | 设置更新日志地址 | — | +| `SetUpgradeClientVersion(string)` | `version` | `UpdateRequestBuilder` | 设置升级程序版本 | — | +| `SetBowl(string)` | `bowl` | `UpdateRequestBuilder` | 设置 Bowl 进程名 | — | +| `SetDriverDirectory(string)` | `path` | `UpdateRequestBuilder` | 设置驱动目录 | — | +| `SetScheme(string)` | `scheme` | `UpdateRequestBuilder` | 设置认证方案 | — | +| `SetToken(string)` | `token` | `UpdateRequestBuilder` | 设置认证令牌 | — | +| `SetFiles(List)` | `files` | `UpdateRequestBuilder` | 设置跳过文件列表 | — | +| `SetFormats(List)` | `formats` | `UpdateRequestBuilder` | 设置跳过扩展名列表 | — | +| `SetDirectories(List)` | `dirs` | `UpdateRequestBuilder` | 设置跳过目录列表 | — | +| `Build()` | 无 | `UpdateRequest` | 构建并校验配置对象 | 会执行 `Validate()` | + +**UpgradeHubService:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `UpgradeHubService(string, string?, string?)` | `url` — SignalR Hub 地址;`token` — 可选 ID4 认证令牌;`appkey` — 可选客户端唯一标识 | —(构造函数) | 创建推送服务实例 | `appkey` 用于服务端定向推送,推荐使用固定 GUID | +| `StartAsync()` | 无 | `Task` | 建立 SignalR 长连接,开始接收推送 | 可重复调用(先 `StopAsync` 后重新 `StartAsync`) | +| `StopAsync()` | 无 | `Task` | 优雅停止连接,保留重连能力 | 适合应用进入后台时调用 | +| `DisposeAsync()` | 无 | `Task` | 彻底释放 Hub 及所有资源 | 释放后不可再复用 | +| `AddListenerReceive(Action)` | `receiveMessageCallback` — 接收消息回调 | `void` | 订阅服务端推送的版本更新消息 | 消息内容为服务端推送的 JSON 字符串 | +| `AddListenerOnline(Action)` | `onlineMessageCallback` — 状态回调 | `void` | 订阅在线/离线状态变化通知 | — | +| `AddListenerReconnected(Func?)` | `reconnectedCallback` — 重连回调 | `void` | 订阅断线重连成功通知 | 参数为新的 connectionId(可能为 null) | +| `AddListenerClosed(Func)` | `closeCallback` — 关闭回调 | `void` | 订阅连接关闭通知 | 正常关闭时异常参数为 null | + +### 3.3 回调事件 + +| 事件名称 | 回调参数 | 触发时机 | 使用说明 | +| --- | --- | --- | --- | +| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` — `Info.Code`, `Info.Body`(`VersionEntry` 列表) | 标准 Client 策略完成版本对比后触发 | 无更新时 `Code = 404`;有更新时 `Body` 包含待下载的 `VersionEntry` 列表 | +| `AddListenerUpdatePrecheck` | `Func` — 返回 `true` 跳过(非强制),`false` 继续 | `UpdateInfo` 事件之后、下载之前 | 用于磁盘空间检查、网络检测、用户确认弹窗;强制更新不进入跳过逻辑 | +| `AddListenerProgress` | `ProgressEventArgs` — `Progress`(下载)或 `DiffProgress`(差分) | 下载进度或差分进度更新时 | 同一参数中 `Progress` 和 `DiffProgress` 只有一个非空 | +| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` — `Version`, `IsCompleted` | 单个资源包下载完成时 | 不要当作"全部资源下载完成"的判断依据 | +| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` — `IsAllDownloadCompleted`, `FailedVersions` | 所有下载任务结束后触发一次 | 失败明细在 `FailedVersions` 中 | +| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` — `Exception`, `Version` | 单个资源下载失败时 | 记录失败项用于展示或监控 | +| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` — `Speed`, `Remaining`, `BytesReceived` | 兼容旧下载统计/自定义下载实现 | 新代码建议使用 `AddListenerProgress` | +| `AddListenerException` | `ExceptionEventArgs` — `Exception`, `Message` | 各策略捕获异常时 | 仅通知,不等同于自动重试 | + +**UpgradeHubService 推送事件:** + +| 事件名称 | 回调参数 | 触发时机 | 使用说明 | +| --- | --- | --- | --- | +| `AddListenerReceive` | `Action` — 消息内容(JSON 字符串) | 服务端推送版本更新消息时 | 消息格式由服务端决定,建议 JSON 格式 | +| `AddListenerOnline` | `Action` — 状态描述 | 在线/离线状态变化时 | 用于 UI 状态展示 | +| `AddListenerReconnected` | `Func?` — 新的 connectionId | 断线重连成功后 | 可用于刷新客户端状态 | +| `AddListenerClosed` | `Func` — 关闭原因(null 为正常关闭) | 连接关闭时 | 用于记录日志和清理资源 | + +--- -`LaunchAsync` 会读取 `Option.AppType` 并选择对应策略: +## 4. 扩展示例(高阶用法) -| `Option.AppType` | 策略 | 说明 | +### 4.1 组件可扩展能力总览 + +Core 通过 `AbstractBootstrap` 基类提供 10 个扩展注册方法,全部返回当前 bootstrap 实例,支持链式调用。所有注册的类型必须具有无参构造函数。 + +| 扩展接口 | 注册方法 | 影响范围 | | --- | --- | --- | -| `AppType.Client` | `ClientStrategy` | 主程序侧:检查版本、下载包、准备升级上下文、启动升级程序。 | -| `AppType.Upgrade` | `UpdateStrategy` | 升级程序侧:读取 IPC 上下文并执行文件替换。 | -| `AppType.OssClient` | `OssStrategy` | OSS 主程序侧更新流程。 | -| `AppType.OssUpgrade` | `OssStrategy` | OSS 升级程序侧更新流程。 | +| `IUpdateHooks` | `Hooks()` | 更新生命周期前后置逻辑 | +| `IUpdateReporter` | `UpdateReporter()` | 更新状态上报 | +| `ISslValidationPolicy` | `SslPolicy()` | HTTPS 证书校验 | +| `IHttpAuthProvider` | `HttpAuth()` | HTTP 请求认证 | +| `IDownloadSource` | `DownloadSource()` | 版本清单和下载资源来源 | +| `IDownloadPolicy` | `DownloadPolicy()` | 下载重试、超时、熔断策略 | +| `IDownloadExecutor` | `DownloadExecutor()` | 单文件下载实现 | +| `IDownloadPipeline` | `DownloadPipeline()` | 下载后处理(校验、解密、扫描) | +| `IDownloadOrchestrator` | `DownloadOrchestrator()` | 批量下载完整编排 | +| `IStrategy` | `Strategy()` | 自定义平台级更新策略 | + +### 4.2 分场景示例 + +#### 场景 1:自定义差分算法与并行度 -示例:独立升级程序入口。 +【场景说明】大型项目希望使用 `StreamingHdiffDiffer` 获得更快的客户端补丁应用速度,并设置并行度为 4。 + +【示例代码】 ```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Differential.Differ; + await new GeneralUpdateBootstrap() - .SetOption(Option.AppType, AppType.Upgrade) - .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .SetConfig(request) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new StreamingHdiffDiffer()) + .UseCleanMatcher(new DefaultCleanMatcher()) + .UseDirtyMatcher(new DefaultDirtyMatcher()) + .WithParallelism(4) + .WithStopOnFirstError(true); + }) + .SetOption(Option.PatchEnabled, true) + .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -> 当升级程序由主程序启动时,Core 会通过加密文件 IPC 自动恢复更新上下文,通常不需要在升级程序里再次调用 `SetConfig`。 +【效果&注意事项】 +- `StreamingHdiffDiffer` 默认使用 Deflate 压缩,客户端应用补丁更快 +- `WithStopOnFirstError(true)` 表示任意补丁失败立即停止所有并行任务 +- 并行度 4 适合多核 CPU + SSD 环境 -## 执行策略总览 {#execution-strategies} +#### 场景 2:自定义生命周期钩子 -Core 内置三类上层执行策略:标准更新策略、OSS 更新策略和静默更新策略。它们不是互相独立的 API,而是由 `LaunchAsync()` 根据 `Option.AppType`、`Option.Silent` 和当前配置自动选择。 +【场景说明】在更新前检查磁盘空间、更新后写日志、Linux/macOS 下启动前赋予执行权限。 -| 策略 | 触发条件 | 主要角色 | 适用场景 | -| --- | --- | --- | --- | -| [标准更新策略](#standard-update-strategy) | `Option.AppType = AppType.Client` 或 `AppType.Upgrade`,且 `Option.Silent = false` | 主程序检查/下载,升级程序替换文件 | 有服务端版本检查 API、需要标准更新和回写版本的桌面应用 | -| [OSS 更新策略](#oss-update-strategy) | `Option.AppType = AppType.OssClient` 或 `AppType.OssUpgrade` | 主程序下载 OSS 版本配置,升级程序下载并解压资源 | 版本配置和包都托管在对象存储/CDN 的应用 | -| [静默更新策略](#silent-update-strategy) | `Option.AppType = AppType.Client` 且 `Option.Silent = true` | 主程序后台轮询,退出时启动升级程序 | 希望用户使用期间无打扰下载,退出或合适时机再替换 | +【示例代码】 -### 标准更新策略 {#standard-update-strategy} +```csharp +using GeneralUpdate.Core.Hooks; -标准更新由 `ClientStrategy` 和 `UpdateStrategy` 配合完成。`ClientStrategy` 运行在主程序中,负责发现本地清单、请求服务端版本、生成下载计划、下载更新包、准备 IPC 上下文并启动升级程序;`UpdateStrategy` 运行在独立升级程序中,负责读取 IPC 上下文、解压、应用差分补丁、替换文件、回写版本并按需启动主程序。 +public sealed class ProductUpdateHooks : IUpdateHooks +{ + public Task OnBeforeUpdateAsync(HookContext ctx) + { + var drive = new DriveInfo(Path.GetPathRoot(ctx.InstallPath)!); + if (drive.AvailableFreeSpace < 500L * 1024 * 1024) + { + Console.WriteLine("Insufficient disk space for update."); + return Task.FromResult(false); // 拒绝更新 + } + return Task.FromResult(true); + } -标准流程的核心顺序如下: + public Task OnDownloadCompletedAsync(DownloadContext ctx) + { + Console.WriteLine($"Downloaded {ctx.AssetName}: {ctx.Success}"); + return Task.CompletedTask; + } -1. 主程序调用 `SetConfig(...)` 或 `SetSource(...)` 后执行 `LaunchAsync()`。 -2. Core 自动读取安装目录下的 `generalupdate.manifest.json`,补齐未显式提供的 `MainAppName`、`UpdateAppName`、`ClientVersion`、`UpgradeClientVersion`、`ProductId`、`InstallPath` 等身份字段。 -3. `ClientStrategy` 使用 `DownloadSource` 获取服务端资源列表,并分别比较主程序版本和升级程序版本。 -4. `DownloadPlanBuilder` 过滤冻结包、按版本排序、检查 `MinClientVersion`,形成下载计划。 -5. 下载阶段通过 `IDownloadOrchestrator` 批量下载资源,默认支持并发、断点续传、重试和 SHA256 校验。 -6. Core 按资源 `AppType` 拆分为升级程序包和主程序包,再根据场景分派。 -7. 升级程序包可以在主程序侧先应用并回写 `UpgradeClientVersion`;主程序包会通过加密 IPC 交给升级程序替换。 -8. 升级程序完成替换后,Core 基于清单体系自动回写 `ClientVersion`,开发者不需要在业务代码中维护本地版本号。 + public Task OnAfterUpdateAsync(HookContext ctx) + { + File.AppendAllText( + Path.Combine(ctx.InstallPath, "update-history.log"), + $"{DateTimeOffset.Now:O} {ctx.CurrentVersion} -> {ctx.TargetVersion}{Environment.NewLine}"); + return Task.CompletedTask; + } -| 场景 | 判断结果 | Core 行为 | -| --- | --- | --- | -| `None` | 主程序和升级程序都无需更新 | 分发“无更新”事件并结束。 | -| `UpgradeOnly` | 只有升级程序需要更新 | 主程序下载升级程序包,直接应用到升级程序目录,回写 `UpgradeClientVersion`,主程序继续运行。 | -| `MainOnly` | 只有主程序需要更新 | 主程序下载主程序包,写入 IPC 上下文,启动升级程序替换主程序文件。 | -| `Both` | 主程序和升级程序都需要更新 | 先更新升级程序并回写 `UpgradeClientVersion`,再把主程序包交给新的升级程序处理。 | + public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) + { + File.AppendAllText( + Path.Combine(ctx.InstallPath, "update-error.log"), + $"{ex}{Environment.NewLine}"); + return Task.CompletedTask; + } + + public Task OnBeforeStartAppAsync(HookContext ctx) + { + // Linux/macOS 下可用 UnixPermissionHooks + return Task.CompletedTask; + } +} -```csharp await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret", - reportUrl: "https://update.example.com/api/upgrade/report") + .SetConfig(request) + .Hooks() .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.PatchEnabled, true) - .SetOption(Option.DiffMode, DiffMode.Parallel) - .SetOption(Option.MaxConcurrency, 4) .LaunchAsync(); ``` -### OSS 更新策略 {#oss-update-strategy} +【效果&注意事项】 +- `OnBeforeUpdateAsync` 返回 `false` 会中断本次更新 +- `OnBeforeStartAppAsync` 适合 Linux/macOS 的 `chmod +x` 操作 +- 内置 `UnixPermissionHooks` 可直接使用 -OSS 更新由同一个 `OssStrategy` 根据角色分成 `OssClient` 和 `OssUpgrade` 两段。它适合把版本配置 JSON 和更新包放在 OSS、S3、MinIO、CDN 或静态文件服务器上,不依赖标准服务端版本检查 API。 +#### 场景 3:自定义下载来源(私有服务/配置中心) -| 角色 | 本地行为 | 关键配置 | -| --- | --- | --- | -| `AppType.OssClient` | 从 `UpdateUrl` 下载 OSS 版本配置到安装目录,比较远端最新版本和本地 `ClientVersion`,需要更新时启动升级程序并退出。 | `UpdateUrl` 指向版本配置文件地址;`MainAppName` / `UpdateAppName` 可由 manifest 提供。 | -| `AppType.OssUpgrade` | 读取本地版本配置或自定义 `DownloadSource`,筛选高于本地版本的资源,下载到安装目录,解压 ZIP,删除压缩包,启动主程序并退出。 | 安装目录可写;资源列表中的版本号必须可比较。 | +【场景说明】从企业内部配置中心获取下载资源列表,而不是调用标准版本检查 API。 -OSS 版本配置文件会保存为 `{MainAppName}_versions.json` 或 `{UpdateAppName}_versions.json`。如果注册了 `DownloadSource()`,OSS 升级侧可以跳过默认文件读取逻辑,改为由你的下载源返回资源列表;如果注册了 `DownloadOrchestrator()`,下载过程也可以完全替换。 +【示例代码】 ```csharp +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; + +public sealed class ConfigCenterDownloadSource : IDownloadSource +{ + public async Task ListAsync(CancellationToken token = default) + { + // 从配置中心拉取资源列表... + var assets = new[] + { + new DownloadAsset( + Name: "MyApp-2.0.0.zip", + Url: "https://cdn.internal.example.com/releases/MyApp-2.0.0.zip", + Size: 50_000_000, + SHA256: "abc123...", + Version: "2.0.0") + }; + + return new DownloadSourceResult + { + Assets = assets, + HasMainUpdate = true, + HasUpgradeUpdate = false + }; + } +} + await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://cdn.example.com/myapp_versions.json", - appSecretKey: "oss-mode-secret") - .SetOption(Option.AppType, AppType.OssClient) + .SetConfig(request) + .DownloadSource() + .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` +【效果&注意事项】 +- 自定义 `IDownloadSource` 会完全替换默认的 HTTP 版本检查逻辑 +- 需要同时注册 `DownloadOrchestrator()` 时,orchestrator 会接管完整下载流程 + +#### 场景 4:自定义 HTTP 认证 + +【场景说明】为 Core 发出的 HTTP 请求追加 JWT Bearer Token 认证头。 + +【示例代码】 + ```csharp +using GeneralUpdate.Core.Security; + +public sealed class JwtAuthProvider : IHttpAuthProvider +{ + private readonly string _token; + + public JwtAuthProvider() + { + // 从配置或环境变量读取 token + _token = Environment.GetEnvironmentVariable("UPDATE_JWT_TOKEN") ?? ""; + } + + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) + { + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token); + return Task.CompletedTask; + } +} + await new GeneralUpdateBootstrap() - .SetOption(Option.AppType, AppType.OssUpgrade) + .SetConfig(request) + .HttpAuth() + .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -### 静默更新策略 {#silent-update-strategy} +【效果&注意事项】 +- 认证提供器在每次 HTTP 请求前被调用 +- 需要在无参构造函数中自行读取配置 -静默更新只在 `AppType.Client` 下生效。启用后,`LaunchAsync()` 会进入静默启动分支,创建和标准更新相同的 `ClientStrategy`,但把 `LaunchAfterPrepare` 设为 `false`,再交给 `SilentPollOrchestrator` 做后台轮询。 +#### 场景 5:静默更新 + 进程退出触发升级 -静默模式不会重新实现更新逻辑;它只是把“检查和下载”放到后台,把“启动升级程序替换文件”延后到进程退出时。这样用户可以继续使用当前进程,更新包先准备好,真正替换发生在应用退出之后。 +【场景说明】主程序启动后后台轮询更新,下载完成后不打扰用户,待进程退出时启动升级程序。 -| 阶段 | 标准更新 | 静默更新 | -| --- | --- | --- | -| 版本检查 | 用户触发后立即执行一次 | 后台按 `Option.SilentPollIntervalMinutes` 周期执行 | -| 下载 | 发现更新后立即下载 | 发现更新后后台下载 | -| 启动升级程序 | 主程序准备完成后立即启动 | 主程序退出时由 `ProcessExit` 处理启动 | -| 用户体验 | 适合显式“检查更新/立即更新” | 适合无打扰准备更新 | +【示例代码】 ```csharp -await new GeneralUpdateBootstrap() +using GeneralUpdate.Core; + +// 主程序启动时 +var bootstrap = new GeneralUpdateBootstrap() .SetSource( updateUrl: "https://update.example.com/api/upgrade/verification", appSecretKey: "your-app-secret") @@ -189,44 +450,160 @@ await new GeneralUpdateBootstrap() .SetOption(Option.Silent, true) .SetOption(Option.SilentPollIntervalMinutes, 30) .SetOption(Option.LaunchClientAfterUpdate, true) - .LaunchAsync(); + .AddListenerException((_, e) => + { + Console.WriteLine($"Update error: {e.Message}"); + }); + +await bootstrap.LaunchAsync(); + +// 应用退出时检查是否有准备好的更新 +AppDomain.CurrentDomain.ProcessExit += (_, _) => +{ + if (bootstrap.SilentOrchestrator?.HasPreparedUpdate == true) + { + bootstrap.SilentOrchestrator.TryLaunchUpgrade(); + } +}; ``` -静默更新仍然会使用你注册的 `IUpdateHooks`、`IUpdateReporter`、下载扩展、证书策略、认证策略和差分管道。需要注意的是,静默模式适合“下载准备无感知”,不等于“文件替换无感知”;主程序文件仍应由独立升级程序在主程序退出后替换。 +【效果&注意事项】 +- 静默模式在 `AppType.Client` 下生效 +- 文件替换发生在主程序退出之后 +- 轮询间隔建议不低于 30 分钟 -### Cancel +#### 场景 6:SignalR 实时推送 + 常规更新联动 + +【场景说明】客户端同时使用 `UpgradeHubService` 接收服务端实时推送和 `GeneralUpdateBootstrap` 执行常规更新。服务端有新版时可立即推送通知,客户端无需等待轮询即可触发更新。 + +【示例代码】 ```csharp -public void Cancel() +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Hubs; + +// 1. 启动 SignalR 推送监听 +var hub = new UpgradeHubService( + "http://localhost:5000/UpgradeHub", + token: null, + appkey: "dfeb5833-975e-4afb-88f1-6278ee9aeff6"); + +hub.AddListenerReceive(async (message) => +{ + Console.WriteLine($"收到实时推送: {message}"); + // 收到推送后可以立即触发更新检查 + // 或在 UI 中提示用户有新版本可用 +}); + +hub.AddListenerOnline((info) => + Console.WriteLine($"在线状态: {info}")); + +hub.AddListenerReconnected((connectionId) => +{ + Console.WriteLine($"已重连,connectionId={connectionId}"); + return Task.CompletedTask; +}); + +hub.AddListenerClosed((exception) => +{ + if (exception is not null) + Console.WriteLine($"连接异常关闭: {exception.Message}"); + else + Console.WriteLine("连接已正常关闭"); + return Task.CompletedTask; +}); + +await hub.StartAsync(); + +// 2. 常规更新流程 +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret") + .SetOption(Option.AppType, AppType.Client) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .LaunchAsync(); + +// 3. 应用退出时清理 +// await hub.StopAsync(); +// await hub.DisposeAsync(); ``` -`Cancel` 会触发内部 `CancellationTokenSource`,更新策略会在安全检查点观察取消请求。适合 UI 应用把 bootstrap 保存为字段后绑定取消按钮。 +【效果&注意事项】 +- `UpgradeHubService` 与 `GeneralUpdateBootstrap` 互补:推送做通知,Bootstrap 做实际更新 +- `appkey` 推荐与 `AppSecretKey` 保持一致,便于服务端定向推送 +- `StopAsync` 保留重连能力,适合应用进入后台时调用 +- `DisposeAsync` 彻底释放,适合应用退出时调用 + +#### 场景 7:DI 容器中注册 UpgradeHubService + +【场景说明】在 Prism / Generic Host / ASP.NET Core 等 DI 容器中注册 `IUpgradeHubService`,管理推送服务生命周期。 + +【示例代码】 ```csharp -private GeneralUpdateBootstrap? _bootstrap; +using GeneralUpdate.Core.Hubs; -async Task StartUpdateAsync(UpdateRequest request) +// Prism 示例 +protected override void RegisterTypes(IContainerRegistry containerRegistry) { - _bootstrap = new GeneralUpdateBootstrap() - .SetConfig(request) - .AddListenerException((_, e) => Console.WriteLine(e.Exception)); - - await _bootstrap.LaunchAsync(); + containerRegistry.Register(); } -void CancelUpdate() +// 在 ViewModel 中使用 +public MainWindowViewModel(IUpgradeHubService hubService) { - _bootstrap?.Cancel(); + hubService.AddListenerReceive((message) => + { + Console.WriteLine($"收到推送: {message}"); + }); + _ = hubService.StartAsync(); } + +// Generic Host / ASP.NET Core 示例 +builder.Services.AddSingleton(sp => +{ + var config = sp.GetRequiredService(); + return new UpgradeHubService( + config["HubUrl"]!, + appkey: config["AppSecretKey"]); +}); +``` + +【效果&注意事项】 +- DI 容器管理生命周期,避免手动 Dispose +- 可将配置从 `appsettings.json` 注入 + +--- + +## 5. 常规使用示例 + +### 5.1 快速入门示例(最简 demo) + +最简配置:使用 manifest 自动发现身份信息,只需要配置服务端入口和密钥。 + +```csharp +using GeneralUpdate.Core; + +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret") + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); ``` -### SetConfig(UpdateRequest) +升级程序入口(`Update.exe`): ```csharp -public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.Upgrade) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .LaunchAsync(); ``` -`SetConfig(UpdateRequest)` 会调用 `UpdateRequest.Validate()`,并把外部配置映射为内部 `UpdateContext`。当角色不是 `AppType.Upgrade` 时,它还会初始化临时目录和黑名单匹配器。 +### 5.2 基础参数组合示例 ```csharp using GeneralUpdate.Core; @@ -250,963 +627,259 @@ var request = new UpdateRequest await new GeneralUpdateBootstrap() .SetConfig(request) .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 4) + .SetOption(Option.PatchEnabled, true) + .AddListenerProgress((_, e) => + { + if (e.Progress != null) + Console.WriteLine($"{e.Progress.AssetName}: {e.Progress.Percentage:F1}%"); + }) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) .LaunchAsync(); ``` -### SetConfig(string) +### 5.2.1 SignalR 实时推送快速入门 ```csharp -public GeneralUpdateBootstrap SetConfig(string filePath) -``` +using GeneralUpdate.Core.Hubs; -`SetConfig(string)` 从 UTF-8 JSON 文件读取 `UpdateRequest`。如果只传文件名,会从当前应用基目录解析;如果传相对或绝对路径,会按路径解析。 +// 创建推送客户端 +var hub = new UpgradeHubService( + "http://localhost:5000/UpgradeHub", + appkey: Guid.NewGuid().ToString()); -```json +// 订阅推送消息 +hub.AddListenerReceive((message) => { - "updateUrl": "https://update.example.com/api/upgrade/verification", - "reportUrl": "https://update.example.com/api/upgrade/report", - "updateAppName": "UpgradeSample.exe", - "mainAppName": "ClientSample.exe", - "installPath": "C:\\Program Files\\MyApp", - "clientVersion": "1.0.0", - "appSecretKey": "your-app-secret", - "productId": "your-product-id" -} -``` - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig("update_config.json") - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` + Console.WriteLine($"收到更新推送: {message}"); +}); -### SetSource +// 建立连接 +await hub.StartAsync(); -```csharp -public GeneralUpdateBootstrap SetSource( - string updateUrl, - string appSecretKey, - string? reportUrl = null, - string? scheme = null, - string? token = null) -``` +Console.WriteLine("已连接,等待服务端推送..."); +Console.ReadLine(); -`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json`,只在代码中指定服务端入口和密钥。 +// 停止连接(保留重连能力) +await hub.StopAsync(); -```csharp -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret", - reportUrl: "https://update.example.com/api/upgrade/report", - scheme: "Bearer", - token: "access-token") - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); +// 释放资源(不可再复用) +await hub.DisposeAsync(); ``` -### UseDiffPipeline - -```csharp -public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) -``` +### 5.3 真实业务落地示例(多参数联动) -`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,引导类会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。关于算法差异、补丁阶段和并发设置,请看 [差分算法与补丁管道](#differential-pipeline)。 +完整 Client 端更新流程,包含事件监听、差分管道、并发控制和状态上报: ```csharp -using GeneralUpdate.Core.Differential; -using GeneralUpdate.Core.Models; +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Event; +using GeneralUpdate.Core.Hooks; using GeneralUpdate.Core.Pipeline; using GeneralUpdate.Differential.Differ; -await new GeneralUpdateBootstrap() - .SetConfig(request) - .UseDiffPipeline(builder => +// 1. 构建配置 +var request = new UpdateRequestBuilder() + .SetUpdateUrl("https://update.mycompany.com/api/upgrade/verification") + .SetReportUrl("https://update.mycompany.com/api/upgrade/report") + .SetUpgradeAppName("MyApp.Upgrade.exe") + .SetMainAppName("MyApp.exe") + .SetClientVersion("1.0.0") + .SetUpgradeClientVersion("1.0.0") + .SetAppSecretKey("prod-secret-key") + .SetProductId("my-product") + .SetInstallPath(AppDomain.CurrentDomain.BaseDirectory) + .SetScheme("Bearer") + .SetToken(Environment.GetEnvironmentVariable("UPDATE_TOKEN") ?? "") + .SetFiles(new List { "appsettings.Development.json" }) + .SetFormats(new List { ".log", ".tmp", ".pdb" }) + .SetDirectories(new List { "logs", "cache", "temp" }) + .Build(); + +// 2. 注入业务钩子 +public sealed class BusinessUpdateHooks : IUpdateHooks +{ + public Task OnBeforeUpdateAsync(HookContext ctx) { - builder - .UseDiffer(new StreamingHdiffDiffer()) - .UseCleanMatcher(new DefaultCleanMatcher()) - .UseDirtyMatcher(new DefaultDirtyMatcher()) - .WithParallelism(4) - .WithStopOnFirstError(true) - .WithProgress(new Progress(p => - { - Console.WriteLine($"{p.Completed}/{p.Total}: {p.FileName}"); - })); - }) - .SetOption(Option.PatchEnabled, true) - .LaunchAsync(); -``` + // 检查磁盘空间 + var drive = new DriveInfo(Path.GetPathRoot(ctx.InstallPath)!); + if (drive.AvailableFreeSpace < 1024L * 1024 * 1024) // < 1GB + return Task.FromResult(false); + return Task.FromResult(true); + } -## 差分算法与补丁管道 {#differential-pipeline} + public Task OnDownloadCompletedAsync(DownloadContext ctx) + => Task.CompletedTask; -Core 的差分能力分两层:`IBinaryDiffer` 负责“单个文件如何生成/应用补丁”,`DiffPipeline` 负责“目录中哪些文件需要补丁、哪些文件是新增/删除、如何并行处理多个文件”。普通使用者只需要打开 `Option.PatchEnabled`;需要调优性能或兼容性时,再通过 `UseDiffPipeline(...)` 调整。 + public Task OnAfterUpdateAsync(HookContext ctx) + { + // 写更新成功日志 + File.AppendAllText( + Path.Combine(ctx.InstallPath, "update.log"), + $"{DateTimeOffset.Now:O} Updated to {ctx.TargetVersion}{Environment.NewLine}"); + return Task.CompletedTask; + } -### 差分算法类型 + public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) + { + // 上报到监控系统 + return Task.CompletedTask; + } -| 算法/实现 | 默认位置 | 特点 | 适合场景 | -| --- | --- | --- | --- | -| `BsdiffDiffer` | `GeneralUpdateBootstrap` 默认使用 | 经典 BSDIFF 4.0 算法,默认使用 BZip2 压缩,补丁格式兼容性强。 | 追求稳定兼容、已有包体系基于 BSDIFF 的项目。 | -| `StreamingHdiffDiffer` | `DiffPipeline` 直接构造时的默认 differ;也可通过 `UseDiffPipeline` 显式选择 | 使用块级 Hash 索引做候选匹配,典型复杂度更低,默认使用 Deflate,并生成可由 Dirty 阶段读取的 BSDIFF 兼容补丁格式。 | 大文件较多、希望降低生成补丁时内存和 CPU 压力的项目。 | -| 自定义 `IBinaryDiffer` | 通过 `UseDiffPipeline(builder => builder.UseDiffer(...))` 接入 | 完全替换单文件差分算法。 | 企业内部已有补丁格式、加密补丁或专用二进制差分算法。 | - -`BsdiffDiffer` 还支持替换压缩提供器:`BZip2CompressionProvider` 是兼容默认值,`DeflateCompressionProvider` 更偏向速度,`.NET 6+` 可使用 `BrotliCompressionProvider` 在压缩率和解压速度之间取得更好平衡。选择压缩提供器时要确保生成补丁和应用补丁的运行时都能识别对应格式版本。 - -```csharp -using GeneralUpdate.Core.Pipeline; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Differ; - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .UseDiffPipeline(builder => - { - builder - .UseDiffer(new BsdiffDiffer(new DeflateCompressionProvider())) - .WithParallelism(4); - }) - .SetOption(Option.PatchEnabled, true) - .LaunchAsync(); -``` - -### Clean 和 Dirty 两个阶段 - -| 阶段 | 方法 | 运行位置 | 作用 | -| --- | --- | --- | --- | -| Clean | `DiffPipeline.CleanAsync(oldDir, newDir, patchDir)` | 发布侧/工具侧 | 对比旧版本和新版本目录,生成 `.patch` 文件,复制新增文件,并写入 `generalupdate.delete.json` 删除清单。 | -| Dirty | `DiffPipeline.DirtyAsync(appDir, patchDir)` | 客户端升级侧 | 读取补丁目录,把 `.patch` 应用到旧文件,复制新增文件,按删除清单移除旧文件。 | - -Core 主要消费 Dirty 阶段;差分包生成建议交给 `GeneralUpdate.Tools` 或发布流水线完成。标准更新中,下载完成后升级程序会先解压包,再在 `PatchEnabled = true` 时通过 `PatchMiddleware` 调用 `DiffPipeline.DirtyAsync(...)` 应用补丁。 - -### 下载并发与差分并行 {#download-diff-concurrency} - -Core 支持两个层面的多线程能力:下载阶段可以并发下载多个更新资源,差分阶段可以并行处理多个文件补丁。内置标准流程是“先完成当前下载计划,再进入解压/差分/替换阶段”;如果你需要把下载和应用做成更细粒度的流水线,可以通过自定义 `IDownloadOrchestrator` 或自定义 `IStrategy` 接管。 - -| 层级 | 控制 API | 默认行为 | 说明 | -| --- | --- | --- | --- | -| 批量下载并发 | `Option.DiffMode` + `Option.MaxConcurrency` | `DiffMode.Serial` 会强制下载并发为 `1`;`DiffMode.Parallel` 使用 `MaxConcurrency`,并被限制在 `1` 到 `Environment.ProcessorCount * 2` 之间。 | 由 `DefaultDownloadOrchestrator` 使用 `SemaphoreSlim` 控制,支持重试、断点续传和校验。 | -| 差分文件并行 | `UseDiffPipeline(...WithParallelism(n))` | 引导类默认 `2`。 | `DiffPipeline` 对多个文件创建任务,并用 `SemaphoreSlim` 限制同时生成/应用补丁的文件数。 | -| 下载后处理 | `DownloadPipeline()` | 默认 SHA256 校验。 | 每个资源下载成功后执行,可替换为解密、扫描、二次校验等。 | - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.DiffMode, DiffMode.Parallel) - .SetOption(Option.MaxConcurrency, 6) - .UseDiffPipeline(builder => - { - builder - .UseDiffer(new StreamingHdiffDiffer()) - .WithParallelism(4); - }) - .LaunchAsync(); -``` - -并发值不是越大越好。网络慢但磁盘快时可以提高 `Option.MaxConcurrency`;补丁文件很多且磁盘是 SSD 时可以提高 `WithParallelism`;机械硬盘、低配终端或后台静默更新建议降低并发,避免影响主程序响应。 - -## 配置模型:UpdateRequest - -`UpdateRequest` 是外部调用者最常用的配置对象。它继承 `UpdateConfiguration`,并在 `Validate()` 中检查关键字段。 - -### 必填或强烈建议配置的属性 - -| 属性 | 说明 | -| --- | --- | -| `UpdateUrl` | 更新检查 API 地址。必须是绝对 URL。 | -| `UpdateAppName` | 升级程序文件名,默认 `Update.exe`。如果你的升级程序叫 `UpgradeSample.exe`,必须显式设置。 | -| `MainAppName` | 主程序文件名。用于升级后重新启动,也用于识别要更新的应用。 | -| `ClientVersion` | 当前主程序版本。 | -| `AppSecretKey` | 应用密钥,用于和服务端约定认证。 | -| `InstallPath` | 应用安装目录,默认当前应用基目录。生产环境建议显式设置。 | - -### 可选属性 - -| 属性 | 说明 | -| --- | --- | -| `ReportUrl` | 更新状态上报 API。 | -| `UpdateLogUrl` | 更新日志页面地址。 | -| `UpgradeClientVersion` | 升级程序自身版本。 | -| `ProductId` | 产品标识,同一服务端管理多个产品时使用。 | -| `UpdatePath` | 升级程序所在目录;为空时使用 `InstallPath`。 | -| `Bowl` | 更新前需要关闭的辅助进程名。 | -| `Scheme` / `Token` | 请求认证信息,可与内置认证提供器配合。 | -| `Files` | 更新时跳过的指定文件。 | -| `Formats` | 更新时跳过的扩展名,例如 `.log`。 | -| `Directories` | 更新时跳过的目录。 | - -### 使用 UpdateRequestBuilder - -`UpdateRequestBuilder` 提供链式构建 API,并在 `Build()` 时执行校验。 - -```csharp -using GeneralUpdate.Core.Configuration; - -var request = new UpdateRequestBuilder() - .SetUpdateUrl("https://update.example.com/api/upgrade/verification") - .SetReportUrl("https://update.example.com/api/upgrade/report") - .SetUpgradeAppName("UpgradeSample.exe") - .SetMainAppName("ClientSample.exe") - .SetClientVersion("1.0.0") - .SetAppSecretKey("your-app-secret") - .SetProductId("your-product-id") - .SetInstallPath(AppDomain.CurrentDomain.BaseDirectory) - .SetFiles(new List { "appsettings.json" }) - .SetFormats(new List { ".log", ".tmp" }) - .SetDirectories(new List { "logs" }) - .Build(); -``` - -`UpdateRequestBuilder.Create()` 会尝试从应用运行目录的 `update_config.json` 读取配置。如果文件不存在,会抛出 `FileNotFoundException`。 - -```csharp -var request = UpdateRequestBuilder.Create().Build(); -``` - -## 应用身份清单:generalupdate.manifest.json - -`generalupdate.manifest.json` 是由 `GeneralUpdate.Tools` 生成、由 Core 消费的应用身份清单。它的核心价值是**帮开发者节约接入和维护时间**:Tools 把“主程序叫什么、当前版本是多少、升级程序叫什么、产品标识是什么、升级程序放在哪个目录”等稳定元数据生成到清单里,Core 在运行时自动消费这些信息,业务代码只需要补充服务端地址、密钥、令牌等运行时或敏感参数。 - -换句话说,使用 manifest 后,接入 GeneralUpdate 不再需要手写一大段完整 `UpdateRequest`。发布时让 Tools 生成 `generalupdate.manifest.json`,运行时再配少量敏感信息,就可以直接启动更新流程。这是 Core 推荐的极简配置方式。 - -推荐把它放在应用安装目录,也就是 `UpdateRequest.InstallPath` 指向的目录。默认情况下 `InstallPath` 是 `AppDomain.CurrentDomain.BaseDirectory`,因此普通桌面应用通常把清单放在主程序输出目录根部。 - -```text -MyProduct/ -├─ ClientSample.exe -├─ generalupdate.manifest.json -└─ update/ - └─ UpgradeSample.exe -``` - -### 清单结构 - -Tools 生成的 JSON 使用小驼峰字段名,Core 中对应类型是 `ManifestInfo`。 - -```json -{ - "mainAppName": "ClientSample.exe", - "clientVersion": "1.0.0", - "appType": "Client", - "updateAppName": "UpgradeSample.exe", - "upgradeClientVersion": "1.0.0", - "productId": "sample-product", - "updatePath": "update/" + public Task OnBeforeStartAppAsync(HookContext ctx) + => Task.CompletedTask; } -``` - -| JSON 字段 | Core 字段 | 说明 | -| --- | --- | --- | -| `mainAppName` | `MainAppName` | 主程序可执行文件名。升级完成后用于重新启动主程序,也用于识别当前产品。 | -| `clientVersion` | `ClientVersion` | 当前主程序版本。Core 用它向服务端询问是否有主程序更新。 | -| `appType` | `AppType` | 当前进程角色字符串,例如 `Client`、`Upgrade`、`OssClient`、`OssUpgrade`。 | -| `updateAppName` | `UpdateAppName` | 升级程序文件名,默认 `Update.exe`。 | -| `upgradeClientVersion` | `UpgradeClientVersion` | 升级程序自身版本。Core 用它判断是否需要先更新升级程序。 | -| `productId` | `ProductId` | 产品标识。一个更新服务管理多个产品时用于区分产品。 | -| `updatePath` | `UpdatePath` | 升级程序所在目录;可以是相对 `InstallPath` 的目录,例如 `update/`。 | - -清单刻意不包含 `UpdateUrl`、`ReportUrl`、`AppSecretKey`、`Scheme`、`Token` 等服务端和认证信息。这样可以让 Tools 负责构建可发布的身份元数据,而密钥仍由应用代码、配置中心或部署环境提供。 - -### 极简配置理念 {#极简配置理念} - -manifest 体系把更新配置拆成两部分: - -| 配置类型 | 由谁提供 | 为什么这样拆 | -| --- | --- | --- | -| 稳定身份信息 | `GeneralUpdate.Tools` 生成到 `generalupdate.manifest.json` | 这些字段来自项目、版本和发布目录,重复手写容易出错,也会增加每个应用接入更新的时间。 | -| 运行时/敏感信息 | 应用代码、配置中心、环境变量或部署系统提供 | 服务端地址、密钥、Token 可能因环境变化,也不应该由 Tools 固化到可发布清单里。 | - -因此最常见的接入路径是: - -1. 用 `GeneralUpdate.Tools` 生成并随应用发布 `generalupdate.manifest.json`。 -2. 在应用启动更新时只配置 `UpdateUrl`、`AppSecretKey`、`ReportUrl`,以及必要的认证信息。 -3. 让 `GeneralUpdateBootstrap` 在内部读取 manifest,自动补齐应用身份、版本、升级程序位置,并在更新成功后回写本地版本。 -这种方式把开发者需要关心的配置压缩到“敏感信息 + 少量运行选项”,既减少样板代码,也避免多个应用重复维护主程序名、升级程序名、本地版本号和产品标识。 - -### Tools 如何生成清单 - -`GeneralUpdate.Tools` 的配置生成流程会解析主程序和升级程序的 `.csproj`,校验版本号,然后写出 `generalupdate.manifest.json`。 - -| Tools 阶段 | 作用 | -| --- | --- | -| `CsprojParseStep` | 解析主程序 `.csproj`;如果提供升级程序 `.csproj`,也会一起解析。 | -| `SemverValidateStep` | 校验 `ClientVersion` 和 `UpgradeClientVersion` 必须符合 semver,例如 `1.0.0`。 | -| `ManifestBuildStep` | 如果 UI 中没有手动填写 `MainAppName` / `UpdateAppName`,使用 `.csproj` 的 `AssemblyName` 补齐。 | -| `FileEmitStep` | 把清单写到输出目录,文件名固定为 `generalupdate.manifest.json`。 | - -配置界面的发布样例流程还会调用 `SamplePublisherService.PublishAsync(...)`,把主程序输出、升级程序输出和清单一起组织到可运行样例目录中。因此新手不需要从零手写完整 `UpdateRequest`,可以先用 Tools 生成清单,再在应用代码中补充服务端入口和密钥。 - -### 配合引导类使用 - -使用清单后,业务代码不需要关心 `MainAppName`、`ClientVersion`、`UpdateAppName`、`UpgradeClientVersion`、`ProductId`、`UpdatePath` 这些身份字段,也不需要手动读取 `generalupdate.manifest.json`。引导类启动更新流程时会在内部读取 `InstallPath/generalupdate.manifest.json`,并把清单中的应用身份信息带入后续的版本检查、下载、启动升级程序和版本回写流程。 - -默认安装目录就是当前应用目录时,只需要把服务端入口和密钥传给 `SetSource`: - -```csharp -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret", - reportUrl: "https://update.example.com/api/upgrade/report") - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` - -如果应用的实际安装目录不是当前进程基目录,只需要在 `UpdateRequest` 中补充 `InstallPath`,仍然不需要把清单中的身份字段重复写进代码: - -```csharp -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; - -var request = new UpdateRequest +// 3. 配置差分管道 +Action configurePipeline = builder => { - UpdateUrl = "https://update.example.com/api/upgrade/verification", - ReportUrl = "https://update.example.com/api/upgrade/report", - AppSecretKey = "your-app-secret", - InstallPath = @"C:\Program Files\MyProduct" + builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4) + .WithStopOnFirstError(true) + .WithProgress(new Progress(p => + { + Console.WriteLine($"Patch: {p.Completed}/{p.Total} {p.CurrentFile} {p.Percentage}%"); + })); }; -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` - -推荐的职责拆分是: - -| 由 manifest 提供 | 由代码或环境提供 | -| --- | --- | -| `MainAppName`、`ClientVersion`、`UpdateAppName`、`UpgradeClientVersion`、`ProductId`、`UpdatePath` | `UpdateUrl`、`ReportUrl`、`AppSecretKey`、`Scheme`、`Token`、事件、扩展点、运行选项 | - -### 版本回写 - -在 `generalupdate.manifest.json` 体系下,清单同时也是本地版本状态文件。开发者只需要在首次发布时通过 Tools 生成清单,不需要在每次更新完成后再写业务代码去修改本地版本号。更新成功后,Core 会把已应用的新版本自动写回安装目录下的同一个 `generalupdate.manifest.json`: - -| 场景 | 回写字段 | -| --- | --- | -| 主程序更新完成 | `ClientVersion` | -| 升级程序自身更新完成 | `UpgradeClientVersion` | - -这样下一次轮询或启动时,引导类会基于清单中的最新本地版本继续向服务端验证,而不是继续使用打包时的旧版本。回写的意义是把“本地版本号维护”收进 Core 的更新流程里,避免开发者在应用代码中额外维护 `ClientVersion` 或 `UpgradeClientVersion`。这个行为依赖安装目录可写;如果应用安装在受限目录,需要确保升级程序拥有写入清单的权限。 - -## 运行选项:Option - -Core 使用强类型 `Option` 注册运行时选项,并通过 `SetOption` 设置值。 - -```csharp -await new GeneralUpdateBootstrap() +// 4. 启动更新 +var bootstrap = new GeneralUpdateBootstrap() .SetConfig(request) .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.DiffMode, DiffMode.Parallel) .SetOption(Option.MaxConcurrency, 4) + .SetOption(Option.DownloadTimeout, 120) + .SetOption(Option.PatchEnabled, true) + .SetOption(Option.BackupEnabled, true) .SetOption(Option.VerifyChecksum, true) - .LaunchAsync(); -``` - -| 选项 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| `Option.AppType` | `AppType` | `Client` | 当前进程角色。 | -| `Option.DiffMode` | `DiffMode` | `Serial` | 执行模式。`Serial` 会让默认下载编排器串行下载;`Parallel` 允许按 `Option.MaxConcurrency` 并发下载。 | -| `Option.Encoding` | `Encoding` | `UTF8` | 压缩包处理编码。 | -| `Option.Format` | `Format` | `Zip` | 更新包格式。 | -| `Option.DownloadTimeout` | `int?` | `30` | 下载超时时间,单位秒。 | -| `Option.PatchEnabled` | `bool?` | `true` | 是否启用差分补丁处理。 | -| `Option.BackupEnabled` | `bool?` | `true` | 更新前是否备份被替换文件。 | -| `Option.Silent` | `bool` | `false` | 是否启用静默轮询更新。 | -| `Option.SilentPollIntervalMinutes` | `int` | `60` | 静默模式轮询间隔。 | -| `Option.LaunchClientAfterUpdate` | `bool` | `true` | 升级后是否启动主程序。 | -| `Option.MaxConcurrency` | `int` | `3` | 默认下载编排器最大并发数,实际值会被限制到合理范围。 | -| `Option.EnableResume` | `bool` | `true` | 是否启用断点续传。 | -| `Option.RetryCount` | `int` | `3` | 下载重试次数。 | -| `Option.VerifyChecksum` | `bool` | `true` | 是否校验下载文件 Hash。 | -| `Option.RetryInterval` | `TimeSpan` | `1s` | 下载重试间隔。 | - -如果传入 `null` 给可空选项,`SetOption` 会移除当前设置,后续读取回到默认值。 - -## 事件 API {#事件-api} - -事件适合观察更新过程,不应该承载复杂业务流程。复杂流程建议封装成 `IUpdateHooks` 或 cookbook 中的完整方案。 - -### 单个事件回调 - -单个事件回调适合在启动器链式配置中直接订阅某一个通知。Core 内部通过全局 `EventManager` 按 `EventArgs` 类型分发事件;同一类型可以注册多个回调,某个回调抛出的异常会被记录到 `GeneralTracer`,不会阻断其他回调。 - -这些回调通常可能在更新流程线程、下载任务线程或差分任务线程中触发,不会自动切回 UI 线程。WPF、Avalonia、WinUI、MAUI 等客户端需要在回调里把 UI 更新切回 Dispatcher / SynchronizationContext;耗时业务也建议投递到队列或后台任务,避免阻塞下载和差分并行度。 - -| 方法 | 参数类型 | 当前代码中的触发时机 | 关键字段 | 推荐用途 | -| --- | --- | --- | --- | --- | -| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` | 标准 `Client` 策略完成版本对比后触发。没有可更新内容时也会触发一次,`Info.Code` 为 `404`、`Info.Body` 为空列表;有更新时 `Info.Body` 是需要下载的 `VersionEntry` 列表。 | `Info.Code`、`Info.Message`、`Info.Body`;`VersionEntry` 包含 `RecordId`、`Name`、`Version`、`Url`、`Hash`、`AppType`、`IsForcibly`、`UpgradeMode`、`FromVersion`、`ToVersion` 等。 | 展示更新说明、版本数量、强制更新提示,或者记录服务端返回的版本元数据。不要在这里做文件替换。 | -| `AddListenerUpdatePrecheck` | `Func` | `UpdateInfo` 事件之后、Hook 和下载之前执行。按当前 `ClientStrategy.CanSkip` 实现:非强制更新下返回 `true` 表示跳过本次更新,返回 `false` 表示继续;强制更新不会进入跳过判断。 | 入参同 `UpdateInfoEventArgs`,可以读取本次更新涉及的所有 `VersionEntry`,包括版本号、更新说明、Hash、包地址、升级模式、跨版本范围等。 | 做下载前的轻量决策,也可以整理版本信息弹窗给用户,让用户阅读更新内容后决定是否继续。需要异步、可取消或有副作用的流程请使用 `IUpdateHooks.OnBeforeUpdateAsync`。 | -| `AddListenerProgress` | `ProgressEventArgs` | 默认下载通道报告 `DownloadProgress` 时触发;差分 Clean / Dirty 管道报告 `DiffProgress` 时也会触发。同一个事件参数中 `Progress` 和 `DiffProgress` 只会有一个非空。 | 下载:`Progress.AssetName`、`BytesDownloaded`、`TotalBytes`、`Percentage`、`Status`。差分:`DiffProgress.Completed`、`Total`、`CurrentFile`、`Percentage`、`IsComplete`、`Error`。 | 更新进度条、状态文本、下载速度/大小展示、差分补丁进度展示。默认下载进度应优先使用这个事件。 | -| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` | `DownloadProgressReporter` 收到 `DownloadStatus.Completed` 时触发。当前默认桥接中 `Version` 实际携带 `AssetName`,自定义下载器也可以放入自己的对象。 | `Version`、`IsCompleted`。 | 标记某个资源包下载完成、追加下载日志。不要把它当作“全部资源下载完成”。 | -| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` | `DefaultDownloadOrchestrator` 等待所有下载任务结束后触发一次。并发下载时它在所有任务都完成、失败结果收集完之后触发。 | `IsAllDownloadCompleted`;`FailedVersions` 是失败明细列表,元素为 `(asset, errorMessage)`。 | 在所有资源下载结束后刷新整体 UI、输出失败汇总、决定是否展示重试入口。 | -| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` | `DownloadProgressReporter` 收到 `DownloadStatus.Failed` 时触发。当前默认桥接中的 `Version` 可能是 `AssetName`。 | `Exception`、`Version`。 | 记录单个资源下载失败、展示失败项、触发外部监控。整体是否成功仍以 `MultiAllDownloadCompleted` 为准。 | -| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` | 兼容旧下载统计或自定义下载实现的事件。当前默认下载编排主要通过 `AddListenerProgress` 分发下载进度,不会额外合成统计事件。 | `Version`、`Remaining`、`Speed`、`TotalBytesToReceive`、`BytesReceived`、`ProgressPercentage`。 | 如果你接入了仍会分发该事件的下载器,可以用它展示剩余时间和速度;新代码建议优先监听 `AddListenerProgress`。 | -| `AddListenerException` | `ExceptionEventArgs` | `GeneralUpdateBootstrap`、各平台策略、标准策略、OSS 策略和更新策略捕获异常时触发。 | `Exception`、`Message`。 | 上报异常、展示错误信息、写入业务日志。这个事件表示异常已经被 Core 捕获并通知,不等同于自动重试。 | - -`UpdateInfoEventArgs.Info.Body` 中的元素是 Core 经过版本对比、应用类型筛选和下载计划构建后需要处理的版本包,不是简单的原始 HTTP 响应透传。需要关注下载 URL、Hash、强制更新、跨版本差分范围时,可以直接读取 `VersionEntry` 上的属性。 - -`AddListenerUpdatePrecheck` 的返回值容易误解:以当前代码为准,返回 `true` 是“可以跳过”,不是“继续下载”。它适合放在“下载前确认”这个场景里:先从 `UpdateInfoEventArgs.Info.Body` 整理本次更新涉及的版本号、更新日志、包大小、升级类型等内容,弹窗给用户阅读;用户确认更新时返回 `false` 继续,用户选择稍后、磁盘不足或当前网络不允许时返回 `true` 跳过非强制更新。如果只是展示服务端版本信息、不需要决定是否跳过,可以只监听 `AddListenerUpdateInfo`。 - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) + .SetOption(Option.RetryCount, 5) + .SetOption(Option.RetryInterval, TimeSpan.FromSeconds(2)) + .Hooks() + .UseDiffPipeline(configurePipeline) .AddListenerUpdateInfo((_, e) => { - Console.WriteLine($"Versions from server: {e.Info?.Body?.Count ?? 0}"); + if (e.Info?.Code == "404") + { + Console.WriteLine("Already up to date."); + return; + } + Console.WriteLine($"Found {e.Info?.Body?.Count ?? 0} version(s) to download."); }) .AddListenerUpdatePrecheck(e => { - var versions = e.Info?.Body; - var hasUpdate = (versions?.Count ?? 0) > 0; - var enoughDisk = DriveInfo.GetDrives() - .Where(d => d.IsReady) - .Any(d => d.AvailableFreeSpace > 1024L * 1024 * 1024); - var userRejected = versions != null && !ShowUpdateDialog(versions); - - // 当前实现中返回 true 表示跳过非强制更新,返回 false 表示继续。 - return !hasUpdate || !enoughDisk || userRejected; - }) - .AddListenerMultiDownloadCompleted((_, e) => - { - Console.WriteLine($"{e.Version}: {(e.IsCompleted ? "completed" : "failed")}"); - }) - .AddListenerMultiAllDownloadCompleted((_, e) => - { - Console.WriteLine(e.IsAllDownloadCompleted - ? "All downloads completed." - : $"Failed downloads: {e.FailedVersions.Count}"); - }) - .AddListenerMultiDownloadError((_, e) => - { - Console.WriteLine($"Download failed: {e.Version}"); - Console.WriteLine(e.Exception); + var hasUpdate = (e.Info?.Body?.Count ?? 0) > 0; + return !hasUpdate; // 有更新则继续(返回 false = 不跳过) }) .AddListenerProgress((_, e) => { if (e.Progress != null) - Console.WriteLine($"Download {e.Progress.AssetName}: {e.Progress.Percentage:F1}% {e.Progress.Status}"); - + { + var pct = e.Progress.Percentage; + var status = e.Progress.Status; + var name = e.Progress.AssetName; + Console.WriteLine($"[Download] {name}: {pct:F1}% ({status})"); + } if (e.DiffProgress != null) - Console.WriteLine($"Patch: {e.DiffProgress.Completed}/{e.DiffProgress.Total} {e.DiffProgress.CurrentFile}"); + { + var d = e.DiffProgress; + Console.WriteLine($"[Patch] {d.CurrentFile}: {d.Completed}/{d.Total} {d.Percentage}%"); + } }) - .AddListenerException((_, e) => + .AddListenerMultiAllDownloadCompleted((_, e) => { - Console.WriteLine(e.Message); - Console.WriteLine(e.Exception); + if (e.IsAllDownloadCompleted) + Console.WriteLine("All downloads completed successfully."); + else + { + Console.WriteLine($"Download completed with {e.FailedVersions.Count} failure(s):"); + foreach (var (asset, error) in e.FailedVersions) + Console.WriteLine($" - {asset.Name}: {error}"); + } }) - .LaunchAsync(); -``` - -### 批量事件监听器 - -实现 `IUpdateEventListener` 可以把事件处理集中到一个类。若只关心部分事件,继承 `UpdateEventListenerBase` 更简单。 - -```csharp -using GeneralUpdate.Core.Download; -using GeneralUpdate.Core.Event; - -public sealed class ConsoleUpdateListener : UpdateEventListenerBase -{ - public override void OnUpdateInfo(UpdateInfoEventArgs args) - { - Console.WriteLine($"Update count: {args.Info?.Body?.Count ?? 0}"); - } - - public override void OnProgress(ProgressEventArgs args) - { - if (args.Progress != null) - Console.WriteLine($"{args.Progress.AssetName}: {args.Progress.Percentage:F1}%"); - } - - public override void OnException(ExceptionEventArgs args) - { - Console.WriteLine(args.Exception); - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .AddEventListener() - .LaunchAsync(); -``` - -## 日志与性能 {#logging-performance} - -Core 内置 `GeneralTracer`,默认开启。它基于 `System.Diagnostics.Trace` 输出日志:Windows 下会写入调试输出窗口,同时写到控制台,并在应用基目录创建 `Logs/generalupdate-trace yyyy-MM-dd.log` 文件。文件监听器使用后台队列写入,但每条日志仍会做开关检查、时间格式化、调用栈定位和入队/输出,因此在性能敏感场景可以关闭。 - -| API | 作用 | -| --- | --- | -| `GeneralTracer.SetTracingEnabled(false)` | 关闭 Core 日志输出。关闭后 `Debug` / `Info` / `Warn` / `Error` / `Fatal` 会快速返回,同时 Trace listener 被过滤。 | -| `GeneralTracer.SetTracingEnabled(true)` | 重新开启日志输出。适合诊断版本、灰度排查或用户反馈问题时启用。 | -| `GeneralTracer.IsTracingEnabled()` | 查询当前日志开关状态。 | -| `GeneralTracer.Dispose()` | 释放文件监听器并清空 Trace listeners,通常只在测试、工具进程退出或你明确接管 Trace listeners 时使用。 | - -建议在应用启动早期设置日志开关,确保更新流程中的下载、校验、差分和替换日志都遵循同一个策略。 - -```csharp -using GeneralUpdate.Core; - -if (performanceMode) -{ - GeneralTracer.SetTracingEnabled(false); -} - -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret") - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` - -关闭日志适合低功耗设备、I/O 较慢的终端、静默后台轮询、批量自动化更新或对启动耗时极敏感的产品形态。排查线上问题时建议临时开启,因为 Core 的日志会记录策略分派、下载编排、校验、差分、Hook 和异常路径。 - -## 扩展点总览 - -扩展点由 `AbstractBootstrap` 提供,所有注册方法都返回当前 bootstrap,可链式调用。 - -| 注册方法 | 接口 | 影响范围 | -| --- | --- | --- | -| `Hooks()` | `IUpdateHooks` | 更新生命周期前后置逻辑。 | -| `UpdateReporter()` | `IUpdateReporter` | 更新状态上报。 | -| `SslPolicy()` | `ISslValidationPolicy` | HTTPS 证书校验。 | -| `HttpAuth()` | `IHttpAuthProvider` | HTTP 请求认证。 | -| `DownloadSource()` | `IDownloadSource` | 版本清单和下载资源来源。 | -| `DownloadPolicy()` | `IDownloadPolicy` | 下载重试、超时、熔断等策略。 | -| `DownloadExecutor()` | `IDownloadExecutor` | 单文件下载实现。 | -| `DownloadPipeline()` | `IDownloadPipeline` | 下载后处理,例如校验、解密、扫描。 | -| `DownloadOrchestrator()` | `IDownloadOrchestrator` | 批量下载完整编排。 | -| `Strategy()` | `IStrategy` | 自定义平台级更新策略。 | - -> 通过这些方法注册的类型必须有无参构造函数,因为 Core 使用 `new()` 或反射创建实例。需要复杂依赖时,建议在自定义类型内部读取配置,或在应用层封装一个无参适配器。 - -## 生命周期钩子:IUpdateHooks - -`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。它也是一个非常灵活的开放点:在 Linux 或 macOS 上,更新后的可执行文件可能需要重新赋予执行权限,或者需要先执行企业内部的授权脚本、签名校验脚本、权限修复脚本,再启动主程序;这些操作都可以放在 `OnBeforeStartAppAsync` 中完成。 - -```csharp -using GeneralUpdate.Core.Hooks; - -public sealed class ProductUpdateHooks : IUpdateHooks -{ - public Task OnBeforeUpdateAsync(HookContext ctx) - { - Console.WriteLine($"Before update: {ctx.CurrentVersion} -> {ctx.TargetVersion}"); - return Task.FromResult(true); - } - - public Task OnDownloadCompletedAsync(DownloadContext ctx) - { - Console.WriteLine($"Downloaded {ctx.AssetName}, success={ctx.Success}, path={ctx.LocalPath}"); - return Task.CompletedTask; - } - - public Task OnAfterUpdateAsync(HookContext ctx) - { - File.WriteAllText(Path.Combine(ctx.InstallPath, "last-update.txt"), DateTimeOffset.Now.ToString("O")); - return Task.CompletedTask; - } - - public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) - { - File.AppendAllText(Path.Combine(ctx.InstallPath, "update-error.log"), ex + Environment.NewLine); - return Task.CompletedTask; - } - - public Task OnBeforeStartAppAsync(HookContext ctx) + .AddListenerException((_, e) => { - Console.WriteLine($"Starting app from {ctx.InstallPath}"); - return Task.CompletedTask; - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .Hooks() - .LaunchAsync(); -``` - -Linux/macOS 场景可以直接注册内置的 `UnixPermissionHooks`,让 Core 在启动应用前执行 `chmod +x`: - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .Hooks() - .LaunchAsync(); -``` - -如果需要执行自己的赋权脚本,可以封装一个无参 hook 适配器,再通过 `Hooks()` 注册: - -```csharp -using GeneralUpdate.Core.Hooks; - -public sealed class ProductPermissionHooks : IUpdateHooks -{ - private readonly CustomPermissionHooks _inner = - new("/opt/my-product/scripts/fix-permissions.sh"); - - public Task OnBeforeStartAppAsync(HookContext ctx) - => _inner.OnBeforeStartAppAsync(ctx); - - public Task OnBeforeUpdateAsync(HookContext ctx) => Task.FromResult(true); - public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; - public Task OnAfterUpdateAsync(HookContext ctx) => Task.CompletedTask; - public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) => Task.CompletedTask; -} + Console.WriteLine($"Update error: {e.Message}"); + Console.WriteLine(e.Exception); + }); -await new GeneralUpdateBootstrap() - .SetConfig(request) - .Hooks() - .LaunchAsync(); +await bootstrap.LaunchAsync(); ``` -内置实现包括: - -| 类型 | 说明 | -| --- | --- | -| `NoOpUpdateHooks` | 默认空实现。 | -| `UnixPermissionHooks` | 在 Unix-like 系统启动前执行 `chmod +x`。 | -| `CustomPermissionHooks` | 执行自定义权限脚本;该类型构造函数需要参数,不适合直接用 `Hooks()` 注册,可自行包装无参适配器。 | - -## 状态上报:IUpdateReporter - -`IUpdateReporter` 用于把更新状态上报到服务端。 - -```csharp -using GeneralUpdate.Core.Download.Reporting; +--- -public sealed class ConsoleUpdateReporter : IUpdateReporter -{ - public Task ReportAsync(UpdateReport report, CancellationToken token = default) - { - Console.WriteLine($"Report: record={report.RecordId}, status={report.Status}, type={report.Type}"); - return Task.CompletedTask; - } -} +## 6. 全局配置 -await new GeneralUpdateBootstrap() - .SetConfig(request) - .UpdateReporter() - .LaunchAsync(); -``` +Core 不依赖全局配置文件,而是通过 `generalupdate.manifest.json` 提供应用身份信息。下面是 manifest 的配置语法和优先级规则。 -内置 `HttpUpdateReporter` 会向 `ReportUrl` 发送 JSON: +### Manifest 配置语法 ```json { - "recordId": 123, - "status": 1, - "type": 1 + "mainAppName": "ClientSample.exe", + "clientVersion": "1.0.0", + "appType": "Client", + "updateAppName": "UpgradeSample.exe", + "upgradeClientVersion": "1.0.0", + "productId": "sample-product", + "updatePath": "update/" } ``` -状态值: +### 配置优先级规则 -| 枚举 | 值 | 说明 | +| 优先级 | 配置来源 | 说明 | | --- | --- | --- | -| `UpdateStatus.Updating` | `1` | 更新中。 | -| `UpdateStatus.Success` | `2` | 更新成功。 | -| `UpdateStatus.Failure` | `3` | 更新失败。 | +| 1(最高) | 代码中 `SetConfig(UpdateRequest)` 或 `SetSource(...)` 显式设置的值 | 覆盖所有其他来源 | +| 2 | `generalupdate.manifest.json` 中的字段 | 自动补齐代码中未显式设置的字段 | +| 3(最低) | 组件内部默认值 | `UpdateAppName = "Update.exe"`, `InstallPath = BaseDirectory` 等 | -## HTTP 认证:IHttpAuthProvider +### Manifest 字段映射 -`IHttpAuthProvider` 可以为 Core 发出的 HTTP 请求追加认证头。 - -```csharp -using GeneralUpdate.Core.Security; - -public sealed class StaticBearerAuthProvider : IHttpAuthProvider -{ - public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) - { - request.Headers.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "access-token"); - - return Task.CompletedTask; - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .HttpAuth() - .LaunchAsync(); -``` - -Core 内置的认证类型包括 `NoOpAuthProvider`、`BearerTokenAuthProvider`、`ApiKeyAuthProvider` 和 `HmacAuthProvider`。这些类型中部分构造函数需要参数,因此如果要通过 `HttpAuth()` 注册,通常需要写一个无参包装类。 - -## HTTPS 证书策略:ISslValidationPolicy - -`ISslValidationPolicy` 用于控制 HTTPS 证书校验。默认 `StrictSslValidationPolicy` 只接受没有 SSL policy errors 的证书。 - -```csharp -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using GeneralUpdate.Core.Security; - -public sealed class DevelopmentSslPolicy : ISslValidationPolicy -{ - public bool ValidateCertificate( - X509Certificate2? certificate, - X509Chain? chain, - SslPolicyErrors sslPolicyErrors) - { - return sslPolicyErrors == SslPolicyErrors.None - || certificate?.Issuer.Contains("CN=Local Dev Root") == true; - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SslPolicy() - .LaunchAsync(); -``` - -生产环境不建议无条件返回 `true`,否则会绕过 HTTPS 的安全保证。 - -## 下载来源:IDownloadSource - -`IDownloadSource` 负责返回待下载资源列表。适合接入私有服务、文件服务器、配置中心或自定义云存储。 - -```csharp -using GeneralUpdate.Core.Download.Abstractions; -using GeneralUpdate.Core.Download.Models; - -public sealed class StaticDownloadSource : IDownloadSource -{ - public Task ListAsync(CancellationToken token = default) - { - var assets = new[] - { - new DownloadAsset( - Name: "app-1.0.1.zip", - Url: "https://cdn.example.com/releases/app-1.0.1.zip", - Size: 25_000_000, - SHA256: "expected-sha256", - Version: "1.0.1") - }; - - return Task.FromResult(new DownloadSourceResult - { - Assets = assets, - HasMainUpdate = true, - HasUpgradeUpdate = false - }); - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .DownloadSource() - .LaunchAsync(); -``` - -## 下载重试策略:IDownloadPolicy - -`IDownloadPolicy` 包装单次下载动作,适合实现重试、超时、熔断或限流。 - -```csharp -using GeneralUpdate.Core.Download.Abstractions; - -public sealed class TwoAttemptDownloadPolicy : IDownloadPolicy -{ - public async Task ExecuteAsync( - Func> action, - CancellationToken token = default) - { - try - { - return await action(token); - } - catch when (!token.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromSeconds(2), token); - return await action(token); - } - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .DownloadPolicy() - .LaunchAsync(); -``` - -如果同时注册 `DownloadOrchestrator()`,自定义 orchestrator 会接管完整下载流程,`DownloadPolicy()` 是否生效取决于 orchestrator 自己是否使用该策略。 - -## 单文件下载:IDownloadExecutor - -`IDownloadExecutor` 负责把一个 `DownloadAsset` 下载到目标路径。适合支持 FTP、SFTP、私有协议或自定义 HTTP 客户端。 - -```csharp -using GeneralUpdate.Core.Download.Abstractions; -using GeneralUpdate.Core.Download.Models; - -public sealed class MirrorDownloadExecutor : IDownloadExecutor -{ - private readonly HttpClient _client = new(); - - public async Task ExecuteAsync( - DownloadAsset asset, - string destPath, - IProgress? progress = null, - CancellationToken token = default) - { - var started = DateTimeOffset.Now; - await using var input = await _client.GetStreamAsync(asset.Url, token); - await using var output = File.Create(destPath); - await input.CopyToAsync(output, token); - - var fileInfo = new FileInfo(destPath); - return new DownloadResult(asset, destPath, fileInfo.Length, DateTimeOffset.Now - started, 0, true, null); - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .DownloadExecutor() - .LaunchAsync(); -``` - -## 下载后处理:IDownloadPipeline - -`IDownloadPipeline` 在文件下载完成后运行。适合做 Hash 校验、解密、病毒扫描、格式转换等。 - -```csharp -using GeneralUpdate.Core.Download.Abstractions; - -public sealed class AntivirusPipeline : IDownloadPipeline -{ - public Task ProcessAsync(string downloadedPath, CancellationToken token = default) - { - if (!File.Exists(downloadedPath)) - throw new FileNotFoundException("Downloaded file not found.", downloadedPath); - - Console.WriteLine($"Scanning {downloadedPath}"); - return Task.FromResult(downloadedPath); - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .DownloadPipeline() - .LaunchAsync(); -``` - -Core 在创建下载管道时会优先尝试使用 `string` 构造函数传入期望 Hash;如果没有该构造函数,则使用无参构造函数。 - -## 批量下载编排:IDownloadOrchestrator - -`IDownloadOrchestrator` 是下载子系统的最高层扩展点。注册后,它会接管批量下载、并发控制、重试、进度和结果汇总。 - -```csharp -using GeneralUpdate.Core.Download.Abstractions; -using GeneralUpdate.Core.Download.Executors; -using GeneralUpdate.Core.Download.Models; - -public sealed class SerialDownloadOrchestrator : IDownloadOrchestrator -{ - private readonly IDownloadExecutor _executor = new HttpDownloadExecutor(new HttpClient()); - - public async Task ExecuteAsync( - DownloadPlan plan, - string destDir, - int maxConcurrency = 3, - IProgress? progress = null, - CancellationToken token = default) - { - var results = new List(); - var started = DateTimeOffset.Now; - - foreach (var asset in plan.Assets) - { - var destPath = Path.Combine(destDir, asset.Name); - results.Add(await _executor.ExecuteAsync(asset, destPath, progress, token)); - } - - return new DownloadReport( - results, - results.Where(r => r.Success).Sum(r => r.DownloadedBytes), - DateTimeOffset.Now - started, - results.Count(r => r.Success), - results.Count(r => !r.Success)); - } -} +| JSON 字段 | Core 字段 | 说明 | +| --- | --- | --- | +| `mainAppName` | `MainAppName` | 主程序可执行文件名 | +| `clientVersion` | `ClientVersion` | 当前主程序版本 | +| `appType` | `AppType` | 当前进程角色(`Client`, `Upgrade`, `OssClient`, `OssUpgrade`) | +| `updateAppName` | `UpdateAppName` | 升级程序文件名 | +| `upgradeClientVersion` | `UpgradeClientVersion` | 升级程序自身版本 | +| `productId` | `ProductId` | 产品标识 | +| `updatePath` | `UpdatePath` | 升级程序所在目录(相对 `InstallPath`) | -await new GeneralUpdateBootstrap() - .SetConfig(request) - .DownloadOrchestrator() - .LaunchAsync(); -``` +### 版本回写机制 -只有当你需要完整替换下载行为时才建议实现 orchestrator。多数情况下替换 `IDownloadExecutor`、`IDownloadPolicy` 或 `IDownloadPipeline` 就够了。 +更新成功后,Core 自动回写客户端版本到 `generalupdate.manifest.json`: -## 平台策略:IStrategy +| 场景 | 回写字段 | +| --- | --- | +| 主程序更新完成 | `ClientVersion` | +| 升级程序自身更新完成 | `UpgradeClientVersion` | -`IStrategy` 是最高级别的更新策略接口。Core 内置 `ClientStrategy`、`UpdateStrategy`、`OssStrategy` 以及 Windows/Linux/macOS 平台策略。只有在你需要替换平台级文件操作或启动逻辑时,才应实现它。 +### 日志配置 ```csharp -using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.Download.Reporting; -using GeneralUpdate.Core.Hooks; -using GeneralUpdate.Core.Strategy; - -public sealed class LoggingStrategy : IStrategy -{ - private UpdateContext? _context; - - public IUpdateHooks Hooks { get; set; } = new NoOpUpdateHooks(); - public IUpdateReporter Reporter { get; set; } = new HttpUpdateReporter(); - - public void Create(UpdateContext parameter) - { - _context = parameter; - } +using GeneralUpdate.Core; - public async Task ExecuteAsync() - { - if (_context == null) - throw new InvalidOperationException("Strategy was not initialized."); - - Console.WriteLine($"Custom strategy executing in {_context.InstallPath}"); - await Hooks.OnBeforeUpdateAsync(new HookContext( - _context.UpdateAppName, - _context.InstallPath, - _context.ClientVersion, - _context.LastVersion, - _context.AppType ?? AppType.Client)); - } +// 关闭日志(性能敏感场景) +GeneralTracer.SetTracingEnabled(false); - public Task StartAppAsync() - { - Console.WriteLine("Custom start app logic."); - return Task.CompletedTask; - } -} +// 重新开启(排查问题) +GeneralTracer.SetTracingEnabled(true); -await new GeneralUpdateBootstrap() - .SetConfig(request) - .Strategy() - .LaunchAsync(); +// 释放日志资源 +GeneralTracer.Dispose(); ``` -> 静默更新不是单独的扩展接口,而是内置执行策略。配置方式和生命周期见 [静默更新策略](#silent-update-strategy)。 - -## 与 GeneralUpdate.Tools 的关系 +--- -Core 消费更新清单和更新包;`GeneralUpdate.Tools` 负责辅助生成和验证这些产物。 +## 相关资源 -| Tools 能力 | Core 中对应消费点 | -| --- | --- | -| Patch Package | `Option.PatchEnabled`、`UseDiffPipeline`、差分补丁处理。 | -| Manifest Generator | `ManifestInfo`、`AppMetadataDiscoverer`、版本回写。 | -| Extension Package | 作为更新包内容或扩展包分发,由下载和部署流程消费。 | -| OSS Config | `OssClient` / `OssUpgrade` 角色读取 OSS 配置并下载。 | -| Hash / Simulation / Report | 对应 `Option.VerifyChecksum`、下载后校验和状态上报。 | - -## 相关示例 - -- [Upgrade sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/blob/main/src/Upgrade/Program.cs) -- [OSS upgrade sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/OSS/OSSUpgradeSample) -- [GeneralUpdate repository](https://github.com/GeneralLibrary/GeneralUpdate) -- [GeneralUpdate.Tools repository](https://github.com/GeneralLibrary/GeneralUpdate.Tools) +- [GeneralUpdate 仓库](https://github.com/GeneralLibrary/GeneralUpdate) +- [Samples 示例代码](https://github.com/GeneralLibrary/GeneralUpdate-Samples) +- [GeneralUpdate.Tools](https://github.com/GeneralLibrary/GeneralUpdate.Tools) +- [快速开始](../quickstart/Quik%20start.md) +- [架构指南](../guide/Architecture.md) diff --git a/website/docs/doc/GeneralUpdate.Differential.md b/website/docs/doc/GeneralUpdate.Differential.md index 04678d2..df623d3 100644 --- a/website/docs/doc/GeneralUpdate.Differential.md +++ b/website/docs/doc/GeneralUpdate.Differential.md @@ -4,447 +4,364 @@ sidebar_position: 6 # GeneralUpdate.Differential -`GeneralUpdate.Differential` 是 GeneralUpdate 的二进制差分组件,专注解决“一个旧文件 + 一个补丁文件 = 一个新文件”的问题。它提供可替换的文件级差分算法、补丁压缩抽象和 BSDIFF 兼容补丁读写能力;目录级对比、批量补丁生成、并行调度、删除文件处理和更新流程编排由 `GeneralUpdate.Core` 的 `DiffPipeline` 或 `GeneralUpdate.Tools` 承担。 +**命名空间:** `GeneralUpdate.Differential` | **主要入口:** `IBinaryDiffer`、`BsdiffDiffer`、`StreamingHdiffDiffer` | **NuGet 包:** `GeneralUpdate.Differential` -**命名空间:** `GeneralUpdate.Differential`、`GeneralUpdate.Differential.Differ`、`GeneralUpdate.Differential.Abstractions` +## 1. 组件简介 -**主要入口:** `IBinaryDiffer`、`BsdiffDiffer`、`StreamingHdiffDiffer` +### 1.1 组件概述 -**NuGet 包:** `GeneralUpdate.Differential` +**GeneralUpdate.Differential** 是 GeneralUpdate 的二进制差分组件,专注解决"一个旧文件 + 一个补丁文件 = 一个新文件"的问题。它提供可替换的文件级差分算法(BSDIFF 4.0 / Streaming HDiff)、补丁压缩抽象(BZip2 / Deflate / Brotli 预留)和 BSDIFF 兼容补丁读写能力。 -```bash -dotnet add package GeneralUpdate.Differential -``` - -## 文档大纲与知识点导航 {#knowledge-map} +目录级对比、批量补丁生成、并行调度、删除文件处理和更新流程编排由 `GeneralUpdate.Core` 的 `DiffPipeline` 或 `GeneralUpdate.Tools` 承担。 -如果你第一次阅读 Differential 文档,可以先看这个导航,再跳到对应知识点。本文按照“能力边界 -> 文件级 API -> 算法选择 -> 压缩格式 -> 与 Core/Tools 集成 -> 性能与扩展”的顺序组织。 +**核心能力:** -| 你想了解什么 | 推荐阅读 | +| 能力 | 说明 | | --- | --- | -| Differential 到底负责什么、不负责什么 | [组件能力边界](#组件能力边界) | -| `Clean` / `Dirty` 是什么含义 | [Clean 与 Dirty 语义](#clean-与-dirty-语义) | -| 如何给单个文件生成并应用补丁 | [单文件快速开始](#单文件快速开始) | -| 使用 Core 时是否还要手动集成 Differential | [与 GeneralUpdate.Core 的关系](#与-generalupdatecore-的关系) | -| 当前有哪些差分算法,如何选择 | [差分算法选择](#差分算法选择) | -| BSDIFF 补丁格式和压缩字节怎么工作 | [补丁格式与压缩 Provider](#补丁格式与压缩-provider) | -| 如何在 Core 更新流程里启用目录级差分 | [与 GeneralUpdate.Core 的关系](#与-generalupdatecore-的关系) | -| Tools 构建差分包时用了什么能力 | [与 GeneralUpdate.Tools 的关系](#与-generalupdatetools-的关系) | -| 下载和差分是否可以多线程并行 | [并发模型与性能建议](#并发模型与性能建议) | -| 大型项目如何提升差分构建效率 | [大型项目并行差分](#大型项目并行差分) | -| 如何接入自定义差分算法或压缩方式 | [扩展点](#扩展点) | - -## 组件能力边界 - -Differential 是底层文件补丁库,不是完整的更新编排器。理解这个边界可以避免把旧文档里的 `DifferentialCore`、黑名单、目录批量处理等概念误认为当前组件 API。 - -| 能力 | Differential 是否负责 | 说明 | -| --- | --- | --- | -| 单文件二进制补丁生成 | 是 | 通过 `IBinaryDiffer.CleanAsync(oldFile, newFile, patchFile)` 完成。 | -| 单文件二进制补丁应用 | 是 | 通过 `IBinaryDiffer.DirtyAsync(oldFile, outputNewFile, patchFile)` 完成。 | -| 差分算法实现 | 是 | 当前主要实现为 `BsdiffDiffer` 和 `StreamingHdiffDiffer`。 | -| 补丁数据压缩/解压 | 是 | 通过 `ICompressionProvider` 抽象,内置 BZip2、Deflate,源码中预留 .NET 6+ Brotli。 | -| 目录级新旧版本对比 | 否 | 由 `GeneralUpdate.Core.Pipeline.DiffPipeline` 的 matcher 负责。 | -| 新增文件复制、删除清单、批量 patch 命名 | 否 | 由 `DiffPipeline` 负责生成 `.patch` 文件、复制新增文件和写入 `generalupdate.delete.json`。 | -| 更新包生成工具 | 否 | 推荐由 `GeneralUpdate.Tools` 调用 Core 差分管道生成发布产物。 | -| 下载、校验、解压、版本回写、重启 | 否 | 这些属于 `GeneralUpdate.Core` 更新流程。 | - -> 当前源码中没有旧文档提到的 `DifferentialCore` 单例。直接使用 Differential 组件时,请面向 `IBinaryDiffer` 和具体 differ 实现编程;需要目录级能力时使用 Core 的 `DiffPipeline`。 +| 文件级差分生成 | `CleanAsync(oldFile, newFile, patchFile)` — 对比新旧文件生成 `.patch` 补丁 | +| 文件级差分应用 | `DirtyAsync(oldFile, newFile, patchFile)` — 旧文件 + 补丁 → 新文件 | +| 可替换差分算法 | `BsdiffDiffer`(BSDIFF 4.0,后缀排序)和 `StreamingHdiffDiffer`(块哈希索引) | +| 可替换压缩格式 | BZip2 (0x00)、Deflate (0x01),源码中预留 .NET 6+ Brotli (0x02) | +| BSDIFF 兼容格式 | 写入 33 字节扩展头(32 字节 BSDIFF40 + 1 字节压缩格式),兼容 32 字节旧头 | +| 线程安全 | 内置 differ 和压缩提供器均支持并发调用 | + +**解决的业务痛点:** +- 全量更新带宽成本高,差分更新可将更新包从 GB 级降低到 MB 甚至 KB 级 +- 不同文件类型和变化模式需要不同的差分策略(细粒度匹配 vs 快速块匹配) +- 压缩算法的选择影响客户端解压速度和补丁体积的平衡 + +**业务使用场景:** +- 大型桌面应用(多 DLL、资源文件)的增量更新 +- 固件/驱动包的二进制差分分发 +- 游戏资源热更新 +- CI/CD 发布流水线中自动生成增量补丁包 + +### 1.2 环境与依赖 + +| 项目 | 说明 | +| --- | --- | +| **版本** | `10.5.0-beta.2` | +| **目标框架** | `netstandard2.0`(兼容 .NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 5+) | +| **依赖包** | 无外部依赖(纯 .NET BCL) | +| **兼容性** | 所有支持 .NET Standard 2.0 的平台 | -## Clean 与 Dirty 语义 {#clean-与-dirty-语义} +--- -Differential 沿用了 GeneralUpdate 差分流程中的两个术语: +## 2. 组件功能列表 -| 术语 | 方法 | 输入 | 输出 | 常用位置 | +| 功能名称 | 功能描述 | 类型 | 是否必填 | 备注限制 | | --- | --- | --- | --- | --- | -| `Clean` | `CleanAsync` | 旧文件、新文件、补丁路径 | `.patch` 补丁文件 | 构建/发布阶段 | -| `Dirty` | `DirtyAsync` | 旧文件、输出新文件路径、补丁路径 | 还原后的新文件 | 客户端升级阶段 | +| BSDIFF 4.0 差分生成 | 基于后缀排序的经典差分算法,补丁体积稳定 | 基础 | 可选 | `BsdiffDiffer`,默认 BZip2 压缩 | +| BSDIFF 4.0 补丁应用 | 将 BSDIFF 格式补丁应用到旧文件 | 基础 | 可选 | 支持 32/33 字节两种头部格式 | +| Streaming HDiff 差分生成 | 基于 FNV-1a 块哈希索引的快速差分 | 基础 | 可选 | `StreamingHdiffDiffer`,默认 Deflate 压缩 | +| BZip2 压缩 | 补丁控制段/差异段/额外段的 BZip2 压缩 | 基础 | 可选 | 格式字节 `0x00`,`BsdiffDiffer` 默认 | +| Deflate 压缩 | 补丁段的 Deflate 压缩,解压更快 | 基础 | 可选 | 格式字节 `0x01`,`StreamingHdiffDiffer` 默认 | +| 自定义差分算法 | 实现 `IBinaryDiffer` 接入自研算法 | 拓展 | 可选 | 需保证 Clean/Dirty 一致性 | +| 自定义压缩提供器 | 实现 `ICompressionProvider` 替换压缩方式 | 拓展 | 可选 | 新格式字节需配合扩展补丁读取逻辑 | -文件级补丁应用不会直接覆盖旧文件,而是把还原结果写到你传入的 `newFilePath`。Core 的 `DiffPipeline` 在目录级更新时会先写临时文件,成功后再替换原文件,从而避免补丁应用失败时破坏原文件。 +--- -## 单文件快速开始 +## 3. API 配置说明 -下面示例只演示 Differential 的底层单文件能力。如果你已经在使用 `GeneralUpdate.Core`,Core 默认已经集成 Differential,不需要为了正常更新流程再手动集成或直接调用本组件。如果你要比较两个目录、生成一批 `.patch`、复制新增文件或处理删除文件,请直接看 [与 GeneralUpdate.Core 的关系](#与-generalupdatecore-的关系)。 +### 3.1 配置字段(属性 Props) -```csharp -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Differ; +Differential 本身是底层库,不提供配置类。所有参数通过构造函数传入。 -IBinaryDiffer differ = new BsdiffDiffer(); +**BsdiffDiffer 构造参数:** -var oldFile = @"D:\releases\1.0.0\app.dll"; -var newFile = @"D:\releases\1.0.1\app.dll"; -var patchFile = @"D:\patches\app.dll.patch"; -var outputFile = @"D:\restore\app.dll"; +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `compressionProvider` | `ICompressionProvider` | `BZip2CompressionProvider` | 可选 | `BZip2CompressionProvider` / `DeflateCompressionProvider` | 补丁压缩提供器 | -// 生成补丁:oldFile + newFile -> patchFile -await differ.CleanAsync(oldFile, newFile, patchFile); +**StreamingHdiffDiffer 构造参数:** -// 应用补丁:oldFile + patchFile -> outputFile -await differ.DirtyAsync(oldFile, outputFile, patchFile); -``` +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `compressionProvider` | `ICompressionProvider` | `DeflateCompressionProvider` | 可选 | `BZip2CompressionProvider` / `DeflateCompressionProvider` | 补丁压缩提供器 | +| `blockSize` | `int` | `65536`(64 KB) | 可选 | 正整数字节数 | 块大小,用于旧文件哈希索引 | +| `maxWindowSize` | `int` | `134217728`(128 MB) | 可选 | 正整数字节数 | 参与计算的最大内存窗口 | + +**DeflateCompressionProvider 构造参数:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `optimalLevel` | `bool` | `true` | 可选 | `true` / `false` | `true` = `CompressionLevel.Optimal`,`false` = `CompressionLevel.Fastest` | + +**ICompressionProvider 格式标识:** + +| Provider | 格式字节 | 可用性 | 说明 | +| --- | --- | --- | --- | +| `BZip2CompressionProvider` | `0x00` | 完全可用 | BSDIFF 旧补丁兼容,解压成本较高 | +| `DeflateCompressionProvider` | `0x01` | 完全可用 | 解压速度更友好,适合客户端批量应用 | +| `BrotliCompressionProvider` | `0x02` | 仅 .NET 6+ 编译(源码预留) | 生产不建议使用 | -`CleanAsync` 和 `DirtyAsync` 都支持 `CancellationToken`。当前实现会在任务开始和 Core 管道调度点观察取消请求;单个算法内部不是每一个字节循环都检查取消,因此大文件取消可能会等到当前文件处理结束后才完全停下。 +### 3.2 实例方法 -## 核心 API +**IBinaryDiffer:** -### IBinaryDiffer +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `CleanAsync(string, string, string, CancellationToken)` | `oldFilePath` — 旧文件路径;`newFilePath` — 新文件路径;`patchFilePath` — 补丁输出路径;`cancellationToken` | `Task` | 发布/构建阶段生成补丁 | 大文件取消不会立即响应,需等待当前文件处理完成 | +| `DirtyAsync(string, string, string, CancellationToken)` | `oldFilePath` — 旧文件路径;`newFilePath` — 补丁还原后文件输出路径;`patchFilePath` — 补丁文件路径;`cancellationToken` | `Task` | 客户端升级阶段应用补丁 | 不会直接覆盖旧文件,结果写入 `newFilePath` | + +### 3.3 回调事件 + +Differential 不发布事件。进度报告和事件通知由 Core 的 `DiffPipeline` 通过 `DiffProgress` 和 `EventManager` 实现。 + +--- + +## 4. 扩展示例(高阶用法) + +### 4.1 组件可扩展能力总览 + +| 扩展接口 | 说明 | +| --- | --- | +| `IBinaryDiffer` | 自定义文件级差分算法,可接入原生库或自研算法 | +| `ICompressionProvider` | 自定义补丁段压缩方式 | + +### 4.2 分场景示例 + +#### 场景 1:自定义差分算法 + +【场景说明】接入企业内部自研的高压缩率差分算法。 -`IBinaryDiffer` 是所有文件级差分算法的统一抽象,也是 Core 差分管道接入自定义算法的关键接口。 +【示例代码】 ```csharp -public interface IBinaryDiffer +using GeneralUpdate.Differential.Abstractions; + +public sealed class HighRatioDiffer : IBinaryDiffer { - Task DirtyAsync( + public Task CleanAsync( string oldFilePath, string newFilePath, string patchFilePath, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken = default) + { + // 调用自研算法生成补丁 + // NativeMethods.GeneratePatch(oldFilePath, newFilePath, patchFilePath); + return Task.CompletedTask; + } - Task CleanAsync( + public Task DirtyAsync( string oldFilePath, string newFilePath, string patchFilePath, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken = default) + { + // 调用自研算法应用补丁 + // NativeMethods.ApplyPatch(oldFilePath, patchFilePath, newFilePath); + return Task.CompletedTask; + } } -``` - -| 参数 | 含义 | -| --- | --- | -| `oldFilePath` | 旧版本文件路径。生成补丁和应用补丁时都需要。 | -| `newFilePath` | `CleanAsync` 中表示新版本源文件;`DirtyAsync` 中表示还原后的输出文件。 | -| `patchFilePath` | 补丁文件路径。`CleanAsync` 写入它,`DirtyAsync` 读取它。 | - -### BsdiffDiffer -`BsdiffDiffer` 实现 BSDIFF 4.0 文件级二进制差分算法。它会把旧文件和新文件读入内存,通过后缀排序寻找匹配块,再输出控制段、差异段和额外段。 +// 在 Core DiffPipeline 中使用 +using GeneralUpdate.Core.Pipeline; -```csharp -using GeneralUpdate.Differential.Differ; +var pipeline = new DiffPipelineBuilder() + .UseDiffer(new HighRatioDiffer()) + .WithParallelism(4) + .Build(); -var differ = new BsdiffDiffer(); -await differ.CleanAsync(oldFile, newFile, patchFile); -await differ.DirtyAsync(oldFile, outputFile, patchFile); +await pipeline.CleanAsync(oldDir, newDir, patchDir); ``` -| 特性 | 说明 | -| --- | --- | -| 默认压缩 | `BZip2CompressionProvider`,兼容历史 BSDIFF 补丁。 | -| 可替换压缩 | 构造函数接受 `ICompressionProvider`。 | -| 补丁兼容 | 支持 32 字节旧 BSDIFF 头,也支持 33 字节扩展头。 | -| 适用场景 | 追求兼容性、补丁体积稳定、单文件体积可控的场景。 | -| 资源特征 | 生成补丁时会读入旧文件和新文件,单文件很大时需要关注内存占用。 | +【效果&注意事项】 +- 必须保证 `CleanAsync` 产出的补丁能被同一算法的 `DirtyAsync` 正确应用 +- 发布侧和客户端必须使用同一套 differ 实现 -`BsdiffDiffer` 也保留了 `Clean(...)` 和 `Dirty(...)` 方法;新代码建议优先面向 `IBinaryDiffer.CleanAsync` / `DirtyAsync`,便于切换算法。 +#### 场景 2:自定义压缩提供器 + BsdiffDiffer -### StreamingHdiffDiffer +【场景说明】使用 BsdiffDiffer 算法 + Deflate 压缩,获得更快的客户端补丁应用速度。 -`StreamingHdiffDiffer` 是当前源码中的另一种 differ 实现。它使用块级 FNV-1a 哈希索引预筛候选位置,再进行字节级扩展匹配,输出 BSDIFF 兼容的补丁结构。 +【示例代码】 ```csharp using GeneralUpdate.Differential.Abstractions; using GeneralUpdate.Differential.Differ; -var differ = new StreamingHdiffDiffer( - compressionProvider: new DeflateCompressionProvider(optimalLevel: true), - blockSize: 64 * 1024, - maxWindowSize: 128 * 1024 * 1024); +// BsdiffDiffer 的精确匹配 + Deflate 的快速解压 +var differ = new BsdiffDiffer( + new DeflateCompressionProvider(optimalLevel: false)); await differ.CleanAsync(oldFile, newFile, patchFile); await differ.DirtyAsync(oldFile, outputFile, patchFile); ``` -| 特性 | 说明 | -| --- | --- | -| 默认压缩 | `DeflateCompressionProvider`。 | -| 块大小 | `BlockSize` 默认 64 KB,用于建立旧文件块哈希索引。 | -| 窗口预算 | `MaxWindowSize` 默认 128 MB,影响生成补丁时参与计算的内存窗口。 | -| 应用补丁 | `DirtyAsync` 委托给 `BsdiffDiffer` 的补丁应用逻辑。 | -| 适用场景 | 需要更快候选匹配、希望与 Core `DiffPipeline` 默认算法保持一致的目录级差分构建。 | - -需要注意的是,当前实现不是完整外存流式差分:当单个文件超过 `MaxWindowSize` 时,算法只会读取预算窗口参与计算。对超大单文件,请在业务侧验证补丁还原结果,或调大 `MaxWindowSize`,或改用 `BsdiffDiffer` 等更适合当前文件规模的实现。 - -## 差分算法选择 - -当前 Differential 内置两种文件级差分算法。它们都输出 BSDIFF 兼容补丁结构,但生成补丁时的匹配方式、默认压缩、性能侧重点不同。 - -| 对比项 | `BsdiffDiffer` | `StreamingHdiffDiffer` | -| --- | --- | --- | -| 核心思路 | 经典 BSDIFF 4.0,基于后缀排序寻找旧文件和新文件之间的最长匹配。 | 使用块级 FNV-1a 哈希建立旧文件索引,先用哈希快速筛选候选块,再做字节级扩展匹配。 | -| 默认压缩 | BZip2 (`0x00`)。 | Deflate (`0x01`)。 | -| 补丁应用 | 自己实现 BSDIFF Dirty 逻辑。 | `DirtyAsync` 委托给 `BsdiffDiffer`,因此应用阶段和 BSDIFF 补丁兼容。 | -| 生成效率 | 匹配更精细,局部或分散变化下补丁生成表现稳定;但后缀排序和全量读入会带来 CPU/内存开销。 | 块命中效果好时生成更快;如果变化分散、块哈希命中少,生成可能变慢。 | -| 客户端应用性能 | 默认 BZip2 解压成本更高,客户端应用大量补丁时耗时可能更明显。 | 默认 Deflate 解压更快,更适合客户端批量应用补丁。 | -| 补丁体积倾向 | 通常更追求细粒度匹配,补丁体积明显更稳定。 | 速度优先,补丁体积与文件变化分布、块大小、窗口预算强相关;块命中差时可能接近完整文件。 | -| 内存特征 | 生成阶段读取旧文件和新文件,单个大文件需要关注内存峰值。 | 通过 `BlockSize` 和 `MaxWindowSize` 控制匹配窗口,超大单文件需要额外验证或调参。 | -| 兼容性 | 最适合需要兼容旧 BSDIFF/BZip2 补丁的场景。 | 适合新项目、目录级批量差分和 Core `DiffPipeline` 默认构建。 | - -可以简单理解为:`BsdiffDiffer` 更偏“兼容和补丁体积稳定”,`StreamingHdiffDiffer` 更偏“客户端应用速度和可调参数”。如果项目非常在意补丁体积或文件变化较分散,优先考虑 `BsdiffDiffer`;如果项目更在意客户端应用速度,并且经过压测确认补丁体积可接受,可以考虑 `StreamingHdiffDiffer`。 +【效果&注意事项】 +- `optimalLevel: false` 生成更快,适合开发/CI 环境 +- `optimalLevel: true` 补丁体积更小,适合生产环境 +- 生成和消费两侧都需要能识别 Deflate 格式(`0x01`) -### 参考基准数据 {#benchmark-reference} +#### 场景 3:StreamingHdiffDiffer 参数调优 -下面数据来自当前源码的一组本地微基准,用于给开发者判断量级,不是跨所有项目的性能承诺。测试环境为 Windows x64、.NET Release 构建,使用 2-4 MB 合成文件;真实结果会受 CPU、磁盘、文件类型、变化比例、压缩级别和并行度影响。 +【场景说明】大型单文件(200MB+)的差分,调整窗口预算避免内存溢出。 -| 场景 | `BsdiffDiffer` 生成 | `StreamingHdiffDiffer` 生成 | `BsdiffDiffer` 应用 | `StreamingHdiffDiffer` 应用 | `BsdiffDiffer` 补丁体积 | `StreamingHdiffDiffer` 补丁体积 | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | -| 2 MB 文本,少量行变更/插入 | 484 ms | 2059 ms | 55 ms | 36 ms | 0.05% | 3.50% | -| 4 MB 二进制,连续局部块变更 | 1030 ms | 318 ms | 55 ms | 11 ms | 2.58% | 100.04% | -| 4 MB 二进制,随机分散字节变更 | 757 ms | 4176 ms | 70 ms | 30 ms | 2.18% | 100.27% | +【示例代码】 -从这组数据可以得到几个实用预估: - -| 指标 | 参考结论 | -| --- | --- | -| 补丁体积 | `BsdiffDiffer` 在测试场景中约为新文件的 0.05%-2.58%;`StreamingHdiffDiffer` 约为 3.50%-100%。如果补丁包大小是第一优先级,优先测试 `BsdiffDiffer`。 | -| 客户端应用速度 | `StreamingHdiffDiffer` 默认 Deflate,在测试中应用补丁约快 1.5-5 倍。大量文件批量应用时,这个差距会更明显。 | -| 生成速度 | 没有绝对赢家:连续局部二进制变更中 `StreamingHdiffDiffer` 约快 3.2 倍;文本和随机分散变更中 `BsdiffDiffer` 约快 4.3-5.5 倍。 | -| 大型项目选择 | 大型项目建议同时看“补丁总体积 + 构建耗时 + 客户端应用耗时”。如果大量文件可以并行,`WithParallelism(...)` 往往比单个 differ 的微小差距更影响总体耗时。 | +```csharp +using GeneralUpdate.Differential.Abstractions; +using GeneralUpdate.Differential.Differ; -> 这组数据的重点是帮助判断方向:`BsdiffDiffer` 通常更容易得到小补丁,`StreamingHdiffDiffer` 的应用阶段更快,但补丁体积和生成速度对文件变化形态非常敏感。正式发布前建议用自己项目的真实产物做一次压测。 +// 大文件场景:增大窗口,减小块大小以获得更精细匹配 +var differ = new StreamingHdiffDiffer( + compressionProvider: new DeflateCompressionProvider(optimalLevel: true), + blockSize: 32 * 1024, // 32 KB 块,更密集的哈希索引 + maxWindowSize: 256 * 1024 * 1024); // 256 MB,允许读入更大文件 -推荐选择: +await differ.CleanAsync(oldLargeFile, newLargeFile, patchFile); +``` -| 场景 | 建议 | -| --- | --- | -| 只需要低层单文件补丁,并希望最大兼容 | 使用 `new BsdiffDiffer()`。 | -| 通过 Core `DiffPipeline` 批量生成目录级补丁 | 先用默认配置跑基准;若补丁体积偏大,可显式切换到 `BsdiffDiffer`;再结合 `WithParallelism(...)` 提升吞吐。 | -| 客户端解压性能更敏感 | 优先选择 Deflate 补丁,即 `StreamingHdiffDiffer` 默认配置,或 `new BsdiffDiffer(new DeflateCompressionProvider())`。 | -| 历史补丁仍是旧 BSDIFF/BZip2 | 使用 `BsdiffDiffer` 应用;32 字节头会按 BZip2 处理。 | -| 大型项目包含大量 DLL、资源文件、插件文件 | 使用 Core `DiffPipeline` 做文件级并行,避免自己逐个文件串行调用 Differential。 | +【效果&注意事项】 +- `blockSize` 越小,哈希索引越密集,匹配更精确但内存消耗更大 +- `maxWindowSize` 决定能参与计算的最大数据量,超出部分不参与匹配 +- 超大文件建议先在业务侧压测补丁体积和应用还原结果 -## 补丁格式与压缩 Provider {#补丁格式与压缩-provider} +--- -Differential 生成的是 BSDIFF 风格补丁。当前实现写入 33 字节扩展头: +## 5. 常规使用示例 -| 偏移 | 长度 | 含义 | -| --- | --- | --- | -| `0` | 8 | 魔数 `"BSDIFF40"`。 | -| `8` | 8 | 压缩后控制段长度。 | -| `16` | 8 | 压缩后差异段长度。 | -| `24` | 8 | 新文件长度。 | -| `32` | 1 | 压缩格式版本。 | +### 5.1 快速入门示例(最简 demo) -应用补丁时也兼容 32 字节旧头:如果没有第 33 个格式字节,就按 BZip2 旧补丁处理。 +```csharp +using GeneralUpdate.Differential.Abstractions; +using GeneralUpdate.Differential.Differ; -### ICompressionProvider +IBinaryDiffer differ = new BsdiffDiffer(); -`ICompressionProvider` 负责把控制段、差异段和额外段包装成压缩流。 +var oldFile = @"D:\releases\1.0.0\app.dll"; +var newFile = @"D:\releases\1.0.1\app.dll"; +var patchFile = @"D:\patches\app.dll.patch"; +var outputFile = @"D:\restore\app.dll"; -```csharp -public interface ICompressionProvider -{ - byte FormatVersion { get; } +// 生成补丁:oldFile + newFile → patchFile +await differ.CleanAsync(oldFile, newFile, patchFile); - Stream CreateCompressStream( - Stream output, - CancellationToken cancellationToken = default); +// 应用补丁:oldFile + patchFile → outputFile +await differ.DirtyAsync(oldFile, outputFile, patchFile); - Stream CreateDecompressStream( - Stream input, - CancellationToken cancellationToken = default); -} +// 验证还原结果 +var newHash = ComputeSha256(newFile); +var outputHash = ComputeSha256(outputFile); +Console.WriteLine(newHash == outputHash ? "Patch verified." : "MISMATCH!"); ``` -| Provider | 格式字节 | 当前可用性 | 说明 | -| --- | --- | --- | --- | -| `BZip2CompressionProvider` | `0x00` | 可用 | `BsdiffDiffer` 默认值,兼容旧 BSDIFF 补丁。 | -| `DeflateCompressionProvider` | `0x01` | 可用 | BCL `DeflateStream`,解压速度更适合客户端更新。 | -| `BrotliCompressionProvider` | `0x02` | 源码中以 `NET6_0_OR_GREATER` 条件编译预留 | 当前 `GeneralUpdate.Differential` 项目目标为 `netstandard2.0`,并且补丁读取逻辑当前只识别 `0x00` / `0x01`,不要把 Brotli 作为生产更新包格式。 | - -自定义压缩时,生成补丁和应用补丁必须使用能被补丁读取逻辑识别的格式字节。当前生产建议只使用 BZip2 或 Deflate。 +### 5.2 基础参数组合示例 ```csharp using GeneralUpdate.Differential.Abstractions; using GeneralUpdate.Differential.Differ; -var differ = new BsdiffDiffer( - new DeflateCompressionProvider(optimalLevel: false)); +// 方案 A:经典 BSDIFF + BZip2 → 补丁体积最小 +var differA = new BsdiffDiffer(); -await differ.CleanAsync(oldFile, newFile, patchFile); -await differ.DirtyAsync(oldFile, outputFile, patchFile); -``` +// 方案 B:经典 BSDIFF + Deflate → 补丁体积小 + 应用更快 +var differB = new BsdiffDiffer(new DeflateCompressionProvider(optimalLevel: true)); -## 与 GeneralUpdate.Core 的关系 {#与-generalupdatecore-的关系} +// 方案 C:Streaming HDiff + Deflate → 生成快 + 应用最快 +var differC = new StreamingHdiffDiffer( + new DeflateCompressionProvider(optimalLevel: true), + blockSize: 64 * 1024, + maxWindowSize: 128 * 1024 * 1024); -`GeneralUpdate.Core` 在 Differential 之上提供目录级差分管道 `DiffPipeline`。它会负责: +// 对同一组文件测试三种方案,选择最优 +foreach (var differ in new IBinaryDiffer[] { differA, differB, differC }) +{ + var sw = Stopwatch.StartNew(); + await differ.CleanAsync(oldFile, newFile, patchFile); + sw.Stop(); -1. 对比旧目录和新目录。 -2. 找出发生变化的文件并调用 `IBinaryDiffer.CleanAsync` 生成 `.patch`。 -3. 复制新增文件到补丁目录。 -4. 生成 `generalupdate.delete.json` 记录删除文件。 -5. 客户端应用补丁时并行调用 `IBinaryDiffer.DirtyAsync`,先写临时文件,成功后替换原文件。 + var patchSize = new FileInfo(patchFile).Length; + Console.WriteLine($"{differ.GetType().Name}: {sw.ElapsedMilliseconds}ms, {patchSize} bytes"); +} +``` -如果你在应用更新流程中使用 `GeneralUpdate.Core`,Core 默认已经集成 Differential 并内置差分管道。也就是说,常规更新接入时不需要额外安装、初始化或手动调用 `GeneralUpdate.Differential`;只要使用 Core 的更新流程,并按业务需要启用补丁更新能力,Core 会在内部完成 differ 创建、补丁应用和目录级编排。 +### 5.3 真实业务落地示例(通过 Core DiffPipeline 使用) -只有在你想替换默认差分算法、调整并行度、改变错误策略或接入自定义 matcher 时,才需要通过 `UseDiffPipeline` 做高级配置: +大多数情况下不直接使用 Differential,而是通过 Core 的 `DiffPipeline` 做目录级差分: ```csharp using GeneralUpdate.Core; +using GeneralUpdate.Core.Pipeline; using GeneralUpdate.Core.Models; using GeneralUpdate.Differential.Differ; +// 构建端:对比新旧版本目录,生成补丁 +var pipeline = new DiffPipelineBuilder() + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(8) // CI 构建机,高并行度 + .WithStopOnFirstError(true) + .WithProgress(new Progress(p => + { + Console.WriteLine($"[Build] {p.Completed}/{p.Total} {p.CurrentFile}"); + })) + .Build(); + +await pipeline.CleanAsync( + @"D:\builds\v1.0.0", + @"D:\builds\v1.0.1", + @"D:\patches\v1.0.0-to-v1.0.1"); + +// 客户端:通过 GeneralUpdateBootstrap 使用 await new GeneralUpdateBootstrap() .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret") + updateUrl: "https://update.mycompany.com/api/upgrade/verification", + appSecretKey: "prod-key") .SetOption(Option.AppType, AppType.Client) .SetOption(Option.PatchEnabled, true) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 4) .UseDiffPipeline(builder => builder .UseDiffer(new StreamingHdiffDiffer()) - .WithParallelism(4) - .WithStopOnFirstError(true)) + .WithParallelism(4)) .LaunchAsync(); ``` -当前源码里有两个默认层级需要区分: - -| 使用方式 | 默认 differ | -| --- | --- | -| 直接 `new DiffPipeline()` 或 `new DiffPipelineBuilder().Build()` | `StreamingHdiffDiffer` | -| `GeneralUpdateBootstrap` 未显式调用 `UseDiffPipeline(...)` 时内部构建 | `BsdiffDiffer`,并行度 2,带 `DiffProgressReporter` | - -因此,普通用户可以把 Differential 看作 Core 已经带好的底层能力,不需要特地集成;只有希望 Core 更新流程明确使用某个算法或自定义差分行为时,才建议显式调用 `UseDiffPipeline(...)`。 - -## 与 GeneralUpdate.Tools 的关系 {#与-generalupdatetools-的关系} - -`GeneralUpdate.Tools` 面向发布侧,帮助开发者构建更新产物。当前 `DiffService` 会创建 `new DiffPipeline()`,再调用: - -```csharp -await pipeline.CleanAsync(oldDir, newDir, patchDir); -``` - -也就是说,Tools 生成目录级差分包时,本质上使用的是 Core 的 `DiffPipeline`,而 `DiffPipeline` 再调用 Differential 的 `IBinaryDiffer` 生成每个变更文件的补丁。对大多数开发者来说,推荐路径是: - -1. 用 Tools 对比旧版本目录和新版本目录,生成补丁目录和清单产物。 -2. 用 Core 在客户端检查版本、下载补丁包、应用补丁。 -3. 只有在需要自定义差分算法、压缩格式或单文件补丁实验时,才直接使用 Differential。 - -这种分层可以让业务代码保持简单:Tools 负责构建,Core 负责更新,Differential 负责底层文件差分。 - -## 并发模型与性能建议 - -Differential 的单个 differ 实例没有保存某次补丁任务的可变共享状态。只要传入的 `ICompressionProvider` 是线程安全的,内置 differ 可以被 Core 管道并发调用;内置 BZip2、Deflate provider 都会为每次调用创建新的压缩流,适合并发使用。 - -真正的“多线程差分”通常发生在 Core `DiffPipeline` 层: +--- -```csharp -var pipeline = new DiffPipelineBuilder() - .UseDiffer(new StreamingHdiffDiffer()) - .WithParallelism(4) - .Build(); +## 6. 算法选择指南 -await pipeline.CleanAsync(oldDir, newDir, patchDir); -``` +### Clean 与 Dirty 语义 -### 大型项目并行差分 {#大型项目并行差分} +| 术语 | 方法 | 输入 | 输出 | 常用位置 | +| --- | --- | --- | --- | --- | +| Clean | `CleanAsync` | 旧文件、新文件、补丁输出路径 | `.patch` 补丁文件 | 构建/发布阶段 | +| Dirty | `DirtyAsync` | 旧文件、输出新文件路径、补丁路径 | 还原后的新文件 | 客户端升级阶段 | -大型桌面项目通常不是“一个超大文件”,而是由主程序、多个 DLL、插件、资源文件、运行时文件和配置文件组成。Core `DiffPipeline` 会把目录对比结果拆成文件级任务,每个变更文件独立调用 `IBinaryDiffer.CleanAsync` 生成补丁,因此可以通过 `WithParallelism(...)` 同时处理多个文件。 +### 算法对比 -这种并行模型对大型项目很重要: +| 对比维度 | `BsdiffDiffer` | `StreamingHdiffDiffer` | +| --- | --- | --- | +| 核心思路 | 经典 BSDIFF 4.0,后缀排序 + 最长匹配 | 块级 FNV-1a 哈希索引 + 字节级扩展匹配 | +| 默认压缩 | BZip2 (0x00) | Deflate (0x01) | +| 补丁应用 | 自实现 BSDIFF Dirty | 委托给 `BsdiffDiffer`(BSDIF 兼容) | +| 补丁体积 | 更稳定,通常更小 | 受文件变化分布影响大,块命中差时可能接近原文件大小 | +| 客户端应用速度 | BZip2 解压较慢 | Deflate 解压更快(约 1.5-5x) | +| 生成内存 | 全量读入新旧文件 | 按 `maxWindowSize` 预算读入 | +| 兼容性 | 兼容旧 BSDIFF/BZip2 补丁 | 适合新项目 | -1. 构建侧可以同时为多个变更文件生成 `.patch`,缩短发布包构建时间。 -2. 客户端应用补丁时也可以并行处理多个文件,减少升级窗口。 -3. 新增文件复制、删除清单处理和差分补丁生成由 Core 管道统一编排,开发者不需要手写多线程调度。 -4. 并行度可以按机器能力调整,构建机可以设置更高,低配置客户端可以保持较低。 +### 场景推荐 -| 参数/策略 | 建议 | +| 场景 | 推荐 | | --- | --- | -| `WithParallelism(1)` | 资源敏感、机械硬盘、低内存环境。 | -| `WithParallelism(2)` | 默认平衡值,适合多数桌面应用。 | -| `WithParallelism(4-8)` | 多核 CPU、SSD、构建机或发布服务器。 | -| BZip2 | 补丁兼容性好,但客户端解压成本更高。 | -| Deflate | 解压速度更友好,适合客户端大批量应用补丁。 | -| 大文件 | 先压测补丁生成耗时、内存峰值和还原结果,不要只看补丁体积。 | - -并行差分适合“文件数量多、每个文件可独立处理”的大型项目。需要注意的是,单个超大文件内部仍由具体 differ 算法处理,不会因为 `WithParallelism(8)` 就把一个文件拆成 8 份并行计算;并行度提升的是多个文件之间的吞吐。 - -下载与差分可以在上层更新流程中并行:Core 下载阶段可以并发拉取多个资源,差分应用阶段也可以按文件并行处理补丁。Differential 只负责单个文件的补丁计算,不直接管理网络下载线程。 - -## 扩展点 - -### 自定义差分算法 +| 补丁体积优先 | `BsdiffDiffer` + BZip2 | +| 客户端应用速度优先 | `StreamingHdiffDiffer`(默认 Deflate) | +| 兼容旧补丁格式 | `BsdiffDiffer`(32 字节旧头自动按 BZip2 处理) | +| 大文件(>500MB) | 先压测,可能需要 `StreamingHdiffDiffer` + 调大 `maxWindowSize` | +| 目录级批量差分 | 通过 Core `DiffPipeline`,配合 `WithParallelism` 提升吞吐 | +| 新项目 | 先用默认配置跑基准,再根据体积和速度需求调整 | -实现 `IBinaryDiffer` 后即可接入 Core 管道。适合接入其他算法、调用原生库,或对特定文件类型做特殊优化。 +### 并发模型 -```csharp -using GeneralUpdate.Differential.Abstractions; - -public sealed class MyBinaryDiffer : IBinaryDiffer -{ - public Task CleanAsync( - string oldFilePath, - string newFilePath, - string patchFilePath, - CancellationToken cancellationToken = default) - { - // Generate patchFilePath from oldFilePath and newFilePath. - throw new NotImplementedException(); - } - - public Task DirtyAsync( - string oldFilePath, - string newFilePath, - string patchFilePath, - CancellationToken cancellationToken = default) - { - // Restore newFilePath from oldFilePath and patchFilePath. - throw new NotImplementedException(); - } -} -``` - -```csharp -var pipeline = new DiffPipelineBuilder() - .UseDiffer(new MyBinaryDiffer()) - .WithParallelism(4) - .Build(); -``` - -自定义算法需要保证 `CleanAsync` 产出的补丁能被同一算法的 `DirtyAsync` 正确应用;如果补丁要交给 Core 客户端使用,发布侧和客户端必须使用同一套 differ 实现。 - -### 自定义压缩 Provider - -如果仍使用 BSDIFF 兼容补丁结构,只想替换控制段、差异段和额外段的压缩方式,可以实现 `ICompressionProvider`。 - -```csharp -using GeneralUpdate.Differential.Abstractions; - -public sealed class MyCompressionProvider : ICompressionProvider -{ - public byte FormatVersion => 0x01; - - public Stream CreateCompressStream( - Stream output, - CancellationToken cancellationToken = default) - { - return new DeflateStream( - output, - CompressionLevel.Optimal, - leaveOpen: true); - } - - public Stream CreateDecompressStream( - Stream input, - CancellationToken cancellationToken = default) - { - return new DeflateStream( - input, - CompressionMode.Decompress, - leaveOpen: true); - } -} -``` +- 单个 `IBinaryDiffer` 实例线程安全(当 `ICompressionProvider` 线程安全时) +- 真正的多线程差分在 Core `DiffPipeline` 层,通过 `WithParallelism(n)` 控制 +- BZip2 / Deflate 内置 provider 均为每次调用创建新流,支持并发 -不要随意分配新的 `FormatVersion`。当前 `BsdiffDiffer.DirtyAsync` 只识别 BZip2 (`0x00`) 和 Deflate (`0x01`);如果你引入新格式,也需要同步扩展补丁读取逻辑,否则客户端无法应用补丁。 - -## 实战建议 +--- -| 场景 | 推荐做法 | -| --- | --- | -| 普通应用发布差分更新 | 使用 `GeneralUpdate.Tools` 生成产物,客户端使用 Core。 | -| 需要控制目录级并行、错误策略和进度 | 使用 Core `DiffPipelineBuilder`。 | -| 只验证某个文件的补丁效果 | 直接使用 `IBinaryDiffer`。 | -| 对补丁体积和应用速度都敏感 | 对同一组文件分别测试 BZip2、Deflate 和不同算法后再定默认策略。 | -| 更新包需要长期兼容旧客户端 | 保守使用 `BsdiffDiffer` + BZip2,或确保客户端已支持 Deflate 扩展头。 | +## 相关资源 -Differential 的价值在于把复杂的二进制差分能力收敛成稳定的文件级抽象。上层开发者可以把重点放在“什么时候更新、下载什么、如何提示用户”上,把具体补丁生成和应用交给 Core/Tools/Differential 的组合完成。 +- [GeneralUpdate 仓库](https://github.com/GeneralLibrary/GeneralUpdate) +- [Samples 差分示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Hub/Samples/DifferentialSample.cs) +- [Core DiffPipeline 文档](GeneralUpdate.Core.md) +- [Tools 打包指南](../guide/Packaging.md) diff --git a/website/docs/doc/GeneralUpdate.Drivelution.md b/website/docs/doc/GeneralUpdate.Drivelution.md index 9a7940b..fa6dcc4 100644 --- a/website/docs/doc/GeneralUpdate.Drivelution.md +++ b/website/docs/doc/GeneralUpdate.Drivelution.md @@ -2,92 +2,186 @@ sidebar_position: 12 --- -### 定义 +# GeneralUpdate.Drivelution -命名空间:`GeneralUpdate.Drivelution` +**命名空间:** `GeneralUpdate.Drivelution` | **主要入口:** `GeneralDrivelution`(静态类) | **NuGet 包:** `GeneralUpdate.Drivelution` -程序集:`GeneralUpdate.Drivelution.dll` +## 1. 组件简介 -```c# -public static class GeneralDrivelution -``` +### 1.1 组件概述 -`GeneralUpdate.Drivelution` 是面向驱动更新场景的跨平台组件。它把驱动更新中容易出错的步骤拆成统一流水线:平台识别、权限检查、文件验证、备份、安装、结果验证、失败后的回滚入口,并在 Windows、Linux、macOS 上分别调用系统原生工具完成驱动安装。 +**GeneralUpdate.Drivelution** 是面向驱动更新场景的跨平台组件。它把驱动更新中容易出错的步骤拆成统一流水线:平台识别 → 权限检查 → 文件验证(哈希/签名/兼容性) → 备份 → 安装 → 结果验证 → 失败后回滚,并在 Windows、Linux、macOS 上分别调用系统原生工具完成驱动安装。 -驱动更新不是普通应用文件替换。应用文件通常只需要下载、解压、覆盖并重启进程;驱动更新会影响内核、设备节点、系统扩展或驱动仓库,因此必须额外关注管理员权限、签名可信度、目标系统和 CPU 架构、安装命令返回值、是否需要系统重启以及失败后的恢复路径。Drivelution 只负责操作系统驱动更新,不处理设备内部写入流程。 +驱动更新不是普通应用文件替换。应用文件通常只需要下载、解压、覆盖并重启进程;驱动更新会影响内核、设备节点、系统扩展或驱动仓库,因此必须额外关注管理员权限、签名可信度、目标系统和 CPU 架构、安装命令返回值、是否需要系统重启以及失败后的恢复路径。 -### 核心能力速览 +**核心能力:** -| 能力 | 当前实现 | +| 能力 | 说明 | | --- | --- | -| 平台适配 | `GeneralDrivelution.Create()` 自动选择 Windows、Linux 或 macOS 实现。 | -| 标准流水线 | Windows/Linux 会先做权限检查,然后执行 `Validate -> Backup -> Install -> Verify`。macOS 当前包含 `CheckSudo` 步骤并继续执行系统命令,实际安装仍取决于系统权限。 | -| 验证 | 文件存在检查、可选哈希校验、可选签名校验、目标 OS/架构兼容性检查。 | -| 备份 | `UpdateStrategy.RequireBackup` 默认为 `true`;备份路径来自 `UpdateStrategy.BackupPath`。 | -| 安装 | Windows 使用 `pnputil.exe`;Linux 使用 `insmod`/`modprobe`、`dpkg`、`rpm`/`dnf`;macOS 使用 `kextload`、`installer` 等系统工具。 | -| 回滚 | 暴露 `RollbackAsync(backupPath)`;Windows 会尝试重新安装备份中的 `.inf`,Linux 会尝试恢复 `.ko`,macOS 会尝试恢复 `.kext`。 | -| 批量/并行 | `BatchUpdateAsync` 支持 `BatchMode.Sequential` 和 `BatchMode.Parallel`,适合大型项目按驱动清单处理。 | -| 日志 | `GeneralTracer` 默认写入控制台和 `Logs\generalupdate-trace yyyy-MM-dd.log`,可通过 `SetTracingEnabled(false)` 关闭。 | - -### 何时使用 Drivelution - -适合使用 Drivelution 的场景: - -- 硬件厂商客户端需要随应用一起交付网卡、采集卡、USB、虚拟设备等驱动。 -- 企业或工业现场需要批量扫描驱动包并按清单更新。 -- 安装器、维护工具、设备管理服务需要统一处理 Windows/Linux/macOS 驱动差异。 -- 需要在更新前进行哈希、签名、系统架构校验,并在失败后保留可恢复的备份。 +| 跨平台抽象 | `GeneralDrivelution.Create()` 自动检测平台(Windows/Linux/macOS)创建对应实现 | +| 标准更新流水线 | 权限检查 → 文件验证(存在/哈希/签名/兼容性) → 备份 → 安装 → 验证 → 失败回滚 | +| 文件验证 | 文件存在检查、SHA256/MD5 哈希校验、Authenticode/GPG/codesign 签名校验、OS/架构兼容性检查 | +| 备份与回滚 | `RequireBackup` 默认为 `true`,失败后自动回滚并保留备份路径供业务显式调用 `RollbackAsync` | +| Windows 安装 | 通过 `pnputil.exe /add-driver /install` 安装 INF 驱动包 | +| Linux 安装 | 支持 `.ko`(`insmod`/`modprobe`)、`.deb`(`dpkg -i`)、`.rpm`(`rpm -ivh`/`dnf install`) | +| macOS 安装 | 支持 `.kext`(`kextload`)、`.dext`(SystemExtensions)、`.pkg`(`installer`) | +| 批量更新 | `BatchUpdateAsync` 支持 `BatchMode.Sequential` 和 `BatchMode.Parallel` | +| 进度报告 | 通过 `IProgress` 上报各步骤的进度、状态和消息 | +| 日志追踪 | `GeneralTracer` 默认控制台 + 按日期轮转文件输出 | + +**解决的业务痛点:** +- 硬件厂商客户端需要随应用交付驱动更新,但不同 OS 驱动安装方式差异大 +- 驱动安装需要管理员权限、签名校验、架构兼容性检查等多重保障 +- 驱动安装失败后需要可靠的回滚机制,避免设备不可用 + +**业务使用场景:** +- 硬件厂商客户端:网卡、采集卡、USB 设备、虚拟设备驱动随应用一起交付 +- 企业/工业现场:批量扫描驱动包并按清单更新 +- 安装器/维护工具:统一处理 Windows/Linux/macOS 驱动差异 + +### 1.2 环境与依赖 + +| 项目 | 说明 | +| --- | --- | +| **版本** | `10.5.0-beta.2` | +| **目标框架** | `net8.0` / `net10.0`(多目标) | +| **依赖包** | `Microsoft.Extensions.DependencyInjection`、`Microsoft.Extensions.Logging.Abstractions`、`Microsoft.Extensions.Options` | +| **兼容性** | Windows(完整支持,需管理员权限)/ Linux(需 root/sudo)/ macOS(受 SIP 和系统扩展策略影响) | -不适合把 Drivelution 当作普通文件更新器使用。如果只是更新应用自身的 exe、dll、资源文件或插件,请优先使用 `GeneralUpdate.Core` 的应用更新流程。 +--- -### 安装 +## 2. 组件功能列表 + +| 功能名称 | 功能描述 | 类型 | 是否必填 | 备注限制 | +| --- | --- | --- | --- | --- | +| 单驱动快速更新 | `QuickUpdateAsync` 使用默认策略快速更新单个驱动 | 基础 | 可选 | 推荐生产环境使用自定义策略 | +| 自定义策略更新 | `UpdateAsync` 配合 `UpdateStrategy` 和 `DrivelutionOptions` | 基础 | 推荐 | 可控制备份、重试、超时、重启策略 | +| 驱动验证 | 文件存在、SHA256 哈希、签名、OS/架构兼容性检查 | 基础 | 可选 | `ValidateAsync`,可通过策略跳过部分校验 | +| 驱动备份 | 更新前备份驱动文件到指定路径 | 基础 | 自动 | `RequireBackup` 默认为 `true` | +| 驱动回滚 | 从备份路径恢复驱动 | 基础 | 可选 | `RollbackAsync(backupPath)` | +| 目录扫描 | 从目录扫描并解析驱动信息(`.inf`/`.ko`/`.kext` 等) | 基础 | 可选 | `GetDriversFromDirectoryAsync` | +| 批量更新 | 按清单批量更新,支持顺序/并行模式 | 拓展 | 可选 | `BatchUpdateAsync` | +| 平台信息查询 | 获取当前 OS/架构/版本/是否支持 | 基础 | 可选 | `GetPlatformInfo()` | +| 重启行为控制 | `UpdateStrategy.RestartMode` 设定重启意图 | 拓展 | 可选 | 更新流水线不会自动重启,需业务层调用 `RestartHelper` | +| DI 注册 | `AddDrivelution` 扩展方法注册平台服务 | 拓展 | 可选 | 支持 Generic Host / ASP.NET Core | +| 日志追踪 | `GeneralTracer` 运行时诊断日志 | 拓展 | 可选 | 默认开启,可关闭 | -```bash -dotnet add package GeneralUpdate.Drivelution -``` +--- -或在项目文件中添加: +## 3. API 配置说明 + +### 3.1 配置字段(属性 Props) + +**DriverInfo:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `Name` | `string` | `""` | 是 | — | 驱动名称 | +| `Version` | `string` | `""` | 推荐 | SemVer 格式 | 驱动版本,扫描目录时会尽量从元数据读取 | +| `FilePath` | `string` | `""` | 是 | 有效文件路径 | Windows: `.inf`;Linux: `.ko`/`.deb`/`.rpm`;macOS: `.kext`/`.dext`/`.pkg` | +| `TargetOS` | `string` | `""` | 可选 | `"Windows"`, `"Linux"`, `"MacOS"` | 为空时不限制 OS | +| `Architecture` | `string` | `""` | 可选 | `"x64"`/`"amd64"`/`"x86"`/`"arm64"`/`"arm"` | 支持常见别名归一化;为空不限制 | +| `HardwareId` | `string` | `""` | 可选 | — | 硬件 ID 或模块别名 | +| `Hash` | `string` | `""` | 可选 | SHA256/MD5 哈希值 | 非空且 `SkipHashValidation = false` 时执行哈希校验 | +| `HashAlgorithm` | `string` | `"SHA256"` | 可选 | `"SHA256"` / `"MD5"` | 哈希算法 | +| `TrustedPublishers` | `List` | `new()` | 可选 | — | 可信发布者列表,非空且未跳过签名校验时验证签名 | +| `Description` | `string` | `""` | 可选 | — | 驱动描述 | +| `ReleaseDate` | `DateTime` | — | 可选 | — | 发布时间 | +| `Metadata` | `Dictionary` | `new()` | 可选 | — | 扩展元数据 | + +**UpdateStrategy:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `RequireBackup` | `bool` | `true` | 可选 | `true` / `false` | 是否执行备份步骤 | +| `BackupPath` | `string` | `""` | 推荐 | 有效目录路径 | 备份根路径,流水线在该路径下生成 `backup_{Name}_{yyyyMMddHHmmss}` | +| `RestartMode` | `RestartMode` | `Prompt` | 可选 | `None`, `Prompt`, `Delayed`, `Immediate` | 重启意图,流水线不会自动重启系统 | +| `SkipHashValidation` | `bool` | `false` | 可选 | `true` / `false` | 跳过哈希校验(仅调试/受控环境) | +| `SkipSignatureValidation` | `bool` | `false` | 可选 | `true` / `false` | 跳过签名校验(仅调试/受控环境) | +| `TimeoutSeconds` | `int` | `300` | 可选 | 正整数 | 单次更新超时,≤0 时使用 `DrivelutionOptions.DefaultTimeoutSeconds` | +| `RetryCount` | `int` | `3` | 可选 | 正整数 | 重试次数(策略字段,实际重试来自 `DrivelutionOptions`) | +| `RetryIntervalSeconds` | `int` | `5` | 可选 | 正整数 | 重试间隔秒数 | +| `Mode` | `UpdateMode` | `Full` | 可选 | `Full`, `Incremental` | 更新模式 | +| `ForceUpdate` | `bool` | `false` | 可选 | `true` / `false` | 是否强制更新 | +| `Priority` | `int` | `0` | 可选 | — | 优先级 | + +**DrivelutionOptions:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `DefaultBackupPath` | `string` | `"./DriverBackups"` | 可选 | 默认备份路径 | +| `DefaultRetryCount` | `int` | `3` | 可选 | 默认重试次数 | +| `DefaultRetryIntervalSeconds` | `int` | `5` | 可选 | 默认重试间隔 | +| `DefaultTimeoutSeconds` | `int` | `300` | 可选 | 默认超时时间 | +| `DebugModeSkipSignature` | `bool` | `false` | 可选 | 调试模式跳过签名 | +| `DebugModeSkipHash` | `bool` | `false` | 可选 | 调试模式跳过哈希 | +| `ForceTerminateOnPermissionFailure` | `bool` | `true` | 可选 | 权限失败时立即终止 | +| `AutoCleanupBackups` | `bool` | `true` | 可选 | 自动清理旧备份 | +| `BackupsToKeep` | `int` | `5` | 可选 | 保留备份数量 | +| `UseExponentialBackoff` | `bool` | `false` | 可选 | 是否使用指数退避 | + +### 3.2 实例方法 + +**GeneralDrivelution(静态类):** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `Create(DrivelutionOptions?)` | `options` — 全局选项 | `IGeneralDrivelution` | 创建当前平台的驱动更新器 | 自动检测平台 | +| `Create(IServiceProvider)` | `serviceProvider` — DI 容器 | `IGeneralDrivelution` | 从 DI 容器解析 | 未注册时回退到自动平台创建 | +| `QuickUpdateAsync(DriverInfo, UpdateStrategy?, IProgress?, CancellationToken)` | `driverInfo` — 驱动信息;`strategy` — 可选策略(null 时使用默认);`progress` — 进度报告;`ct` — 取消令牌 | `Task` | 快速单驱动更新 | 使用安全默认策略 | +| `ValidateAsync(DriverInfo, CancellationToken)` | `driverInfo` — 驱动信息;`ct` — 取消令牌 | `Task` | 单独验证驱动文件 | — | +| `GetPlatformInfo()` | 无 | `PlatformInfo` | 查询当前平台信息 | 返回 OS/架构/版本/是否支持 | +| `GetDriversFromDirectoryAsync(string, string?, CancellationToken)` | `directoryPath` — 目录路径;`searchPattern` — 搜索模式;`ct` — 取消令牌 | `Task>` | 扫描目录解析驱动信息 | 默认搜索模式与平台相关 | +| `BatchUpdateAsync(IEnumerable, UpdateStrategy, BatchMode, IProgress?, CancellationToken)` | `drivers` — 驱动列表;`strategy` — 更新策略;`mode` — 顺序/并行;`progress` — 进度;`ct` — 取消令牌 | `Task` | 批量更新多个驱动 | 并行模式下底层系统工具可能竞争资源 | + +**IGeneralDrivelution:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `UpdateAsync(...)` | 同 `QuickUpdateAsync` | `Task` | 执行完整更新流水线 | — | +| `ValidateAsync(...)` | 同 `GeneralDrivelution.ValidateAsync` | `Task` | 单独验证 | — | +| `BackupAsync(DriverInfo, string, CancellationToken)` | `driverInfo`, `backupPath`, `ct` | `Task` | 单独备份驱动文件 | — | +| `RollbackAsync(string, CancellationToken)` | `backupPath` — 备份路径;`ct` — 取消令牌 | `Task` | 从备份恢复驱动 | 不同平台恢复逻辑不同 | +| `GetDriversFromDirectoryAsync(...)` | 同 `GeneralDrivelution` | `Task>` | 扫描目录 | — | +| `BatchUpdateAsync(...)` | 同 `GeneralDrivelution` | `Task` | 批量更新 | — | + +### 3.3 回调事件 + +Drivelution 通过 `IProgress` 报告进度,不提供独立的事件系统。 + +| 进度字段 | 类型 | 说明 | +| --- | --- | --- | +| `CurrentStatus` | `UpdateStatus` | 当前状态(`Validating`/`BackingUp`/`Updating`/`Verifying`/`Succeeded`/`Failed`/`RolledBack`) | +| `StepName` | `string` | 当前步骤名称 | +| `Percentage` | `int` | 进度百分比 (0-100) | +| `Message` | `string` | 进度消息 | +| `StepIndex` | `int` | 当前步骤索引 | +| `TotalSteps` | `int` | 总步骤数 | -```xml - -``` +--- -### 快速开始:更新单个驱动 +## 4. 扩展示例(高阶用法) -```c# -using GeneralUpdate.Drivelution; -using GeneralUpdate.Drivelution.Abstractions.Models; +### 4.1 组件可扩展能力总览 -var driver = new DriverInfo -{ - Name = "MyDevice Driver", - Version = "1.2.0", - FilePath = @"C:\Drivers\mydevice.inf", - TargetOS = "Windows", - Architecture = "x64", - Hash = "driver-file-sha256", - HashAlgorithm = "SHA256", - TrustedPublishers = { "Contoso Hardware" } -}; +| 扩展接口 | 说明 | +| --- | --- | +| `IGeneralDrivelution` | 完整替换驱动更新器的所有行为 | +| `IDriverValidator` | 自定义文件验证逻辑 | +| `IDriverBackup` | 自定义备份/恢复策略 | +| `ICommandRunner` | 自定义系统命令执行器 | +| `INetworkDownloader` | 预留接口(网络下载) | +| `BaseDriverUpdater` | 抽象基类,可继承创建新平台实现 | -var result = await GeneralDrivelution.QuickUpdateAsync(driver); +### 4.2 分场景示例 -if (result.Success) -{ - Console.WriteLine($"Driver updated. Duration={result.DurationMs}ms"); -} -else -{ - Console.WriteLine($"Driver update failed: {result.Error?.Message}"); - Console.WriteLine(string.Join(Environment.NewLine, result.StepLogs)); -} -``` +#### 场景 1:自定义策略 + 回滚处理 -`QuickUpdateAsync` 会创建当前平台的更新器,并使用安全默认策略:需要备份、失败可重试 3 次、重试间隔 5 秒。生产环境建议显式传入 `UpdateStrategy`,尤其是备份路径、超时时间和重启策略。 +【场景说明】生产环境驱动更新,启用所有安全检查,失败时显式回滚。 -### 使用自定义策略 +【示例代码】 -```c# +```csharp using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions.Configuration; using GeneralUpdate.Drivelution.Abstractions.Models; @@ -108,266 +202,356 @@ var strategy = new UpdateStrategy { RequireBackup = true, BackupPath = @"C:\DriverBackups\graphics", - RetryCount = 3, - RetryIntervalSeconds = 5, TimeoutSeconds = 600, RestartMode = RestartMode.Prompt, SkipHashValidation = false, SkipSignatureValidation = false }; +var driver = new DriverInfo +{ + Name = "Graphics Driver", + Version = "2.1.0", + FilePath = @"C:\Drivers\graphics.inf", + TargetOS = "Windows", + Architecture = "x64", + Hash = "expected-sha256...", + HashAlgorithm = "SHA256", + TrustedPublishers = { "Contoso Hardware Inc." } +}; + var progress = new Progress(p => { - Console.WriteLine($"{p.Percentage}% {p.StepName}: {p.Message}"); + Console.WriteLine($"[{p.StepName}] {p.Percentage}%: {p.Message}"); }); var result = await updater.UpdateAsync(driver, strategy, progress); -if (!result.Success && result.BackupPath is not null) +if (!result.Success) { - await updater.RollbackAsync(result.BackupPath); + Console.WriteLine($"Update failed: {result.Error?.Message}"); + + if (result.BackupPath is not null) + { + Console.WriteLine("Rolling back..."); + await updater.RollbackAsync(result.BackupPath); + } +} +else if (RestartHelper.IsRestartRequired(strategy.RestartMode)) +{ + await RestartHelper.HandleRestartAsync( + strategy.RestartMode, + delaySeconds: 60, + message: "Driver updated. Restart now?"); } ``` -> 注意:`UpdateStrategy.RetryCount` 和 `RetryIntervalSeconds` 是策略模型字段;当前流水线实际重试策略来自 `DrivelutionOptions.DefaultRetryCount`、`DefaultRetryIntervalSeconds` 和 `UseExponentialBackoff`。如果需要统一控制重试行为,请在创建更新器时配置 `DrivelutionOptions`。 +【效果&注意事项】 +- 生产环境不要跳过哈希和签名校验 +- 回滚后驱动恢复到安装前状态,但仍建议提示用户可能需要重启 -### DI 注册 +#### 场景 2:DI 容器集成 -在 Generic Host、ASP.NET Core 或自己的服务容器中,可以通过扩展方法注册当前平台实现: +【场景说明】在 ASP.NET Core / Generic Host 应用中通过 DI 注册驱动更新服务。 -```c# +【示例代码】 + +```csharp using GeneralUpdate.Drivelution.Core; +var builder = WebApplication.CreateBuilder(args); + builder.Services.AddDrivelution(options => { options.DefaultBackupPath = "./DriverBackups"; options.DefaultTimeoutSeconds = 600; + options.DefaultRetryCount = 3; + options.AutoCleanupBackups = true; + options.BackupsToKeep = 5; }); -var updater = GeneralDrivelution.Create(builder.Services.BuildServiceProvider()); +var app = builder.Build(); + +// 在 Controller 或 Service 中使用 +app.MapPost("/drivers/update", async (DriverInfo driver, IGeneralDrivelution updater) => +{ + var result = await updater.UpdateAsync(driver, new UpdateStrategy()); + return result.Success ? Results.Ok(result) : Results.BadRequest(result.Error); +}); ``` -`AddDrivelution` 会注册 `ICommandRunner`、平台对应的 `IDriverValidator`、`IDriverBackup` 和 `IGeneralDrivelution`。 +【效果&注意事项】 +- `AddDrivelution` 自动注册平台对应的所有服务 +- 支持通过 `IServiceProvider` 创建:`GeneralDrivelution.Create(serviceProvider)` -### API 概览 +#### 场景 3:批量并行扫描 + 顺序安装 -#### `GeneralDrivelution` +【场景说明】大型项目先并行扫描验证所有驱动,再按风险分组顺序安装。 -| 方法 | 说明 | -| --- | --- | -| `Create(DrivelutionOptions? options = null)` | 自动检测当前系统并创建平台驱动更新器。 | -| `Create(IServiceProvider serviceProvider)` | 从 DI 容器解析 `IGeneralDrivelution`;未注册时回退到自动平台创建。 | -| `QuickUpdateAsync(driverInfo, strategy?, progress?, token?)` | 使用默认或自定义策略快速更新单个驱动。 | -| `ValidateAsync(driverInfo, token?)` | 使用当前平台验证器检查驱动文件。 | -| `GetPlatformInfo()` | 返回平台、系统、架构、系统版本和是否支持。 | -| `GetDriversFromDirectoryAsync(path, pattern?, token?)` | 从目录扫描并解析驱动信息。 | -| `BatchUpdateAsync(drivers, strategy, mode, progress?, token?)` | 批量更新驱动,可选择顺序或并行。 | +【示例代码】 -#### `IGeneralDrivelution` +```csharp +using GeneralUpdate.Drivelution; +using GeneralUpdate.Drivelution.Abstractions.Models; -| 方法 | 说明 | -| --- | --- | -| `UpdateAsync(driverInfo, strategy, progress?, token?)` | 执行完整更新流水线。 | -| `ValidateAsync(driverInfo, token?)` | 单独验证驱动。 | -| `BackupAsync(driverInfo, backupPath, token?)` | 单独备份驱动文件。 | -| `RollbackAsync(backupPath, token?)` | 按平台实现尝试从备份恢复。 | -| `GetDriversFromDirectoryAsync(path, pattern?, token?)` | 扫描目录。 | -| `BatchUpdateAsync(drivers, strategy, mode, progress?, token?)` | 批量处理多个驱动。 | +// 1. 并行扫描和验证所有驱动 +var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync(@"C:\DriverPackages"); -### 数据模型 +var compatibleDrivers = new List(); +foreach (var driver in drivers) +{ + var valid = await GeneralDrivelution.ValidateAsync(driver); + if (valid) + { + Console.WriteLine($"{driver.Name} v{driver.Version}: valid"); + compatibleDrivers.Add(driver); + } + else + { + Console.WriteLine($"{driver.Name} v{driver.Version}: INVALID, skipping"); + } +} -#### `DriverInfo` +// 2. 按风险分组 +var coreDrivers = compatibleDrivers + .Where(d => d.Metadata.ContainsKey("RiskLevel") && d.Metadata["RiskLevel"] == "Core") + .ToList(); -| 属性 | 说明 | -| --- | --- | -| `Name` | 驱动名称。 | -| `Version` | 驱动版本。扫描目录时会尽量从 INF、modinfo、包元数据或 plist 中读取,读取不到时使用 `1.0.0`。 | -| `FilePath` | 驱动文件路径。Windows 通常为 `.inf`,Linux 为 `.ko`/`.deb`/`.rpm`,macOS 为 `.kext`/`.dext`/`.pkg`。 | -| `TargetOS` | 目标系统。为空时兼容性检查视为通过;不为空时需要包含当前系统名,例如 `Windows`、`Linux`、`MacOS`。 | -| `Architecture` | 目标架构。支持常见别名归一化:`x64/amd64/x86_64`、`x86/i386/i686`、`arm64/aarch64`、`arm/armv7`。 | -| `HardwareId` | 硬件 ID 或模块别名。Windows 解析 INF,Linux 可从 `modinfo alias` 读取。 | -| `Hash` / `HashAlgorithm` | 完整性校验。当前支持 `SHA256` 和兼容用 `MD5`。 | -| `TrustedPublishers` | 可信发布者列表。只有该列表非空且未跳过签名校验时才执行签名验证。 | -| `Description`、`ReleaseDate`、`Metadata` | 展示和扩展信息。 | - -#### `UpdateStrategy` - -| 属性 | 说明 | -| --- | --- | -| `RequireBackup` | 是否执行备份步骤,默认 `true`。 | -| `BackupPath` | 备份根路径。流水线会在该路径下生成 `backup_{Name}_{yyyyMMddHHmmss}`。 | -| `RestartMode` | 重启意图:`None`、`Prompt`、`Delayed`、`Immediate`。当前更新流水线不会自动重启系统,应用可在成功后调用 `RestartHelper.HandleRestartAsync(...)`。 | -| `SkipHashValidation` | 跳过哈希校验。仅建议调试或受控环境使用。 | -| `SkipSignatureValidation` | 跳过签名校验。仅建议调试或受控环境使用。 | -| `TimeoutSeconds` | 单次更新超时;小于等于 0 时使用 `DrivelutionOptions.DefaultTimeoutSeconds`。 | -| `Mode`、`ForceUpdate`、`Priority` | 策略模型保留字段,可供上层调度或 UI 使用。 | +var optionalDrivers = compatibleDrivers + .Except(coreDrivers) + .ToList(); -#### `UpdateResult` +// 3. 核心驱动顺序安装 +if (coreDrivers.Any()) +{ + var coreResult = await GeneralDrivelution.BatchUpdateAsync( + coreDrivers, + new UpdateStrategy { RequireBackup = true }, + BatchMode.Sequential); // 顺序安装,降低风险 -| 属性 | 说明 | -| --- | --- | -| `Success` / `Status` | 是否成功以及当前状态:`NotStarted`、`Validating`、`BackingUp`、`Updating`、`Verifying`、`Succeeded`、`Failed`、`RolledBack`。 | -| `Error` | 失败时的错误类型、错误码、消息、详情和堆栈。 | -| `BackupPath` | 本次备份路径。 | -| `RolledBack` | 流水线失败后是否进入回滚路径。若需要强制执行平台恢复,建议显式调用 `RollbackAsync(BackupPath)`。 | -| `StepLogs` | 每个步骤的文本日志,适合展示在安装结果页或上传诊断。 | -| `DurationMs` | 总耗时。 | + Console.WriteLine($"Core drivers: {coreResult.SucceededCount}/{coreResult.SucceededCount + coreResult.FailedCount}"); +} -### 更新流水线 +// 4. 可选驱动并行安装 +if (optionalDrivers.Any()) +{ + var optResult = await GeneralDrivelution.BatchUpdateAsync( + optionalDrivers, + new UpdateStrategy { RequireBackup = true }, + BatchMode.Parallel); // 互不依赖,并行安装 -`BaseDriverUpdater.UpdateAsync` 会按顺序执行当前平台步骤: + Console.WriteLine($"Optional drivers: {optResult.SucceededCount}/{optResult.SucceededCount + optResult.FailedCount}"); +} +``` -1. 平台权限步骤:Windows 为 `CheckPermissions`,Linux 为 `CheckSudo`,macOS 为 `CheckSudo`。 -2. `Validate`:检查文件存在、哈希、签名和兼容性。 -3. `Backup`:当 `RequireBackup == true` 时执行。 -4. `Install`:调用平台安装命令。 -5. `Verify`:平台验证安装结果。Windows 会执行 `pnputil.exe /enum-drivers`,验证不确定时记录警告但不让整个更新失败。 +【效果&注意事项】 +- `BatchMode.Parallel` 不一定更快,底层系统工具可能竞争驱动仓库锁 +- 核心驱动建议顺序安装,降低系统风险 -每个步骤会通过 `IProgress` 上报 `StepName`、`Percentage`、`Message`、`StepIndex` 和 `TotalSteps`。发生异常或步骤失败时,`UpdateResult.Error` 会映射为可展示的错误信息;如果有备份路径,流水线会进入回滚路径并在 `StepLogs` 中记录。 +--- -### 验证策略 +## 5. 常规使用示例 -Drivelution 的验证逻辑是条件触发的: +### 5.1 快速入门示例(最简 demo) -- 文件存在是必做项。 -- `DriverInfo.Hash` 不为空且 `SkipHashValidation == false` 时,计算文件哈希并与期望值比较。 -- `DriverInfo.TrustedPublishers.Count > 0` 且 `SkipSignatureValidation == false` 时,执行签名校验。 -- 兼容性校验始终执行;`TargetOS` 或 `Architecture` 为空表示不限制该项。 +```csharp +using GeneralUpdate.Drivelution; +using GeneralUpdate.Drivelution.Abstractions.Models; -平台签名行为: +var driver = new DriverInfo +{ + Name = "MyDevice Driver", + Version = "1.2.0", + FilePath = @"C:\Drivers\mydevice.inf", + TargetOS = "Windows", + Architecture = "x64", + Hash = "driver-file-sha256", + HashAlgorithm = "SHA256", + TrustedPublishers = { "Contoso Hardware" } +}; -| 平台 | 签名验证 | -| --- | --- | -| Windows | 使用 Authenticode 相关逻辑验证文件签名,并检查可信发布者。 | -| Linux | 查找同名 `.sig` 或 `.asc` 文件并执行 GPG 签名验证;未提供可信发布者时允许无签名通过。 | -| macOS | 使用 `codesign -v`,失败后尝试 `codesign -v --deep`;指定可信发布者时通过 `codesign -dvv` 输出匹配。 | +var result = await GeneralDrivelution.QuickUpdateAsync(driver); -### 平台差异 +if (result.Success) + Console.WriteLine($"Driver updated successfully in {result.DurationMs}ms."); +else +{ + Console.WriteLine($"Update failed: {result.Error?.Message}"); + foreach (var log in result.StepLogs) + Console.WriteLine($" {log}"); +} +``` + +### 5.2 基础参数组合示例 + +```csharp +using GeneralUpdate.Drivelution; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Abstractions.Models; -#### Windows +// 查询平台信息 +var platform = GeneralDrivelution.GetPlatformInfo(); +Console.WriteLine($"OS: {platform.OperatingSystem}, Arch: {platform.Architecture}"); -Windows 实现面向 INF 驱动包: +// 扫描驱动目录 +var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync(@"C:\Drivers"); +Console.WriteLine($"Found {drivers.Count} driver(s)."); -- 扫描默认模式:`*.inf`。 -- 权限:必须以管理员身份运行,否则 `CheckPermissions` 会失败。 -- 安装:`pnputil.exe /add-driver /install`。 -- 验证:`pnputil.exe /enum-drivers`,验证不确定时记录警告但不阻断更新。 -- 元数据:解析 `DriverVer`、`DriverDesc`、`HardwareId`,并计算 SHA256。 -- 回滚:`RollbackAsync` 会扫描备份目录中的 `.inf` 并重新调用 PnPUtil 安装。 +foreach (var d in drivers) + Console.WriteLine($" {d.Name} v{d.Version} ({d.FilePath})"); -#### Linux +// 创建更新器 +var updater = GeneralDrivelution.Create(new DrivelutionOptions +{ + DefaultBackupPath = @"C:\DriverBackups", + DefaultRetryCount = 3, + DefaultTimeoutSeconds = 600 +}); -Linux 实现支持内核模块和发行版包: +// 更新,带进度 +var progress = new Progress(p => + Console.WriteLine($"{p.StepName}: {p.Percentage}%")); -- 扫描默认包括 `.ko`,未指定搜索模式时还会扫描 `.deb` 和 `.rpm`。 -- 权限:通过 sudo/root 检查,驱动安装通常需要 root。 -- `.ko` 安装:先 `insmod `,失败后回退 `modprobe `。 -- `.deb` 安装:`dpkg -i `。 -- `.rpm` 安装:先 `rpm -ivh `,失败后回退 `dnf install -y `。 -- 元数据:`.ko` 通过 `modinfo` 读取版本、描述和 alias;`.deb` 通过 `dpkg-deb -I`;`.rpm` 通过 `rpm -qip`。 -- 回滚:当前主要恢复 `.ko`,先尝试 `modprobe -r ` 卸载当前模块,再 `insmod ` 加载备份模块。 +var strategy = new UpdateStrategy +{ + RequireBackup = true, + BackupPath = @"C:\DriverBackups\mydevice", + TimeoutSeconds = 300 +}; -#### macOS +var result = await updater.UpdateAsync(drivers[0], strategy, progress); +Console.WriteLine($"Result: {(result.Success ? "Success" : "Failed")}"); -macOS 实现面向内核扩展、DriverKit 扩展和安装包: +if (result.Success && RestartHelper.IsRestartRequired(strategy.RestartMode)) + await RestartHelper.HandleRestartAsync(strategy.RestartMode, 60); +``` -- 扫描默认包括 `.kext`、`.dext`、`.pkg`。 -- `.kext` 安装:复制到 `/Library/Extensions/`,设置 `root:wheel` 和 `755`,执行 `kextload`,再执行 `kextcache -i /`。 -- `.dext` 安装:复制到 `/Library/SystemExtensions/`;DriverKit 扩展通常还需要用户在系统设置的安全隐私区域批准。 -- `.pkg` 安装:`/usr/sbin/installer -pkg -target /`。 -- 签名:使用 `codesign` 验证。 -- 限制:新版 macOS 对 kext、dext 有 SIP、用户批准和系统扩展策略限制;命令成功不代表用户批准流程已完成。 -- 回滚:当前主要恢复 `.kext`,复制回 `/Library/Extensions/` 并尝试 `kextload`。 +### 5.3 真实业务落地示例 -### 批量与并行更新 +完整驱动更新工作流,覆盖扫描、验证、安装、回滚、重启: -批量更新适合大型项目把驱动包拆成清单后统一处理: +```csharp +using GeneralUpdate.Drivelution; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Abstractions.Models; -```c# -var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync(@"C:\Drivers"); +// 1. 全局配置 +var options = new DrivelutionOptions +{ + DefaultBackupPath = @"C:\ProgramData\MyProduct\DriverBackups", + DefaultRetryCount = 2, + DefaultRetryIntervalSeconds = 10, + DefaultTimeoutSeconds = 900, + UseExponentialBackoff = true, + ForceTerminateOnPermissionFailure = true, + AutoCleanupBackups = true, + BackupsToKeep = 3 +}; -var batch = await GeneralDrivelution.BatchUpdateAsync( - drivers, - strategy, - BatchMode.Parallel, - progress); +// 2. 创建更新器 +var updater = GeneralDrivelution.Create(options); -Console.WriteLine(batch); -``` +// 3. 扫描驱动目录 +var platform = GeneralDrivelution.GetPlatformInfo(); +Console.WriteLine($"Running on {platform.OperatingSystem} {platform.Architecture}"); -`BatchMode.Sequential` 会按顺序逐个更新,适合核心驱动、互相依赖的驱动或需要降低系统风险的场景。`BatchMode.Parallel` 使用 `Task.WhenAll` 并行处理多个驱动,适合互不依赖的驱动包扫描、验证和安装任务,但底层系统工具可能仍会竞争驱动仓库、包管理器锁或内核模块资源。大型项目建议先并行验证和扫描,再对高风险安装阶段做分组或顺序控制。 +var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync( + @"C:\ProgramData\MyProduct\DriverPackages"); -### 重启行为 +var compatible = drivers + .Where(d => string.IsNullOrEmpty(d.TargetOS) || d.TargetOS == platform.OperatingSystem) + .ToList(); -`UpdateStrategy.RestartMode` 表示本次驱动更新完成后的重启意图: +Console.WriteLine($"Found {drivers.Count} driver(s), {compatible.Count} compatible."); -| 值 | 含义 | -| --- | --- | -| `None` | 不需要重启。 | -| `Prompt` | 应用提示用户重启。当前 `RestartHelper.PromptUserForRestart` 只输出提示并返回 `false`,适合由 GUI 自行接管。 | -| `Delayed` | 延迟后调用系统重启命令。 | -| `Immediate` | 立即调用系统重启命令。 | +// 4. 逐驱动验证并更新 +var results = new List<(DriverInfo Driver, UpdateResult Result)>(); +foreach (var driver in compatible) +{ + // 先验证 + var valid = await GeneralDrivelution.ValidateAsync(driver); + if (!valid) + { + Console.WriteLine($"[SKIP] {driver.Name}: validation failed"); + continue; + } + + // 再更新 + var strategy = new UpdateStrategy + { + RequireBackup = true, + BackupPath = Path.Combine(options.DefaultBackupPath, driver.Name), + TimeoutSeconds = 600, + RestartMode = RestartMode.Prompt, + SkipHashValidation = false, + SkipSignatureValidation = false + }; + + var progress = new Progress(p => + { + if (p.Percentage % 25 == 0 || p.CurrentStatus == UpdateStatus.Succeeded) + Console.WriteLine($"[{driver.Name}] {p.StepName} {p.Percentage}%: {p.Message}"); + }); + + var result = await updater.UpdateAsync(driver, strategy, progress); + results.Add((driver, result)); + + if (!result.Success) + { + Console.WriteLine($"[FAIL] {driver.Name}: {result.Error?.Message}"); + if (result.BackupPath != null) + { + Console.WriteLine($" Rolling back {driver.Name}..."); + await updater.RollbackAsync(result.BackupPath); + } + } + else + { + Console.WriteLine($"[OK] {driver.Name} v{driver.Version} ({result.DurationMs}ms)"); + } +} -当前 `UpdateAsync` 不会自动调用 `RestartHelper`,因此不会在驱动安装后直接重启系统。推荐在业务层根据驱动类型和安装结果决定是否调用: +// 5. 汇总并处理重启 +var succeeded = results.Count(r => r.Result.Success); +var failed = results.Count(r => !r.Result.Success); +Console.WriteLine($"\nSummary: {succeeded} succeeded, {failed} failed."); -```c# -if (result.Success && RestartHelper.IsRestartRequired(strategy.RestartMode)) +if (succeeded > 0 && results.Any(r => RestartHelper.IsRestartRequired(r.Result.Status == UpdateStatus.Succeeded + ? RestartMode.Prompt : RestartMode.None))) { - await RestartHelper.HandleRestartAsync( - strategy.RestartMode, - delaySeconds: 60, - message: "Driver update completed. Restart now?"); + Console.WriteLine("Some drivers may require a restart."); + var userAccepted = RestartHelper.PromptUserForRestart("Driver update completed. Restart now?"); + if (userAccepted) + await RestartHelper.RestartSystemAsync(); } ``` -### 日志与性能开关 +--- -Drivelution 使用 `GeneralTracer` 输出内部诊断信息: +## 6. 全局配置 -- 默认启用。 -- 控制台输出:通过 `TextWriterTraceListener(Console.Out)`。 -- 文件输出:应用基目录下的 `Logs\generalupdate-trace yyyy-MM-dd.log`,按日期切换。 -- Windows 调试输出:Windows 下会额外添加 `WindowsOutputDebugListener`。 -- 调试器附加时会添加 `DefaultTraceListener`。 +### 平台差异速查 -驱动更新通常涉及外部命令和系统权限,日志对排查失败很重要。但 `GeneralTracer` 会生成时间戳、调用栈位置并写入 Trace Listener;在性能敏感、批量验证或大量并行处理场景中,可以关闭它降低额外开销: +| 平台 | 驱动格式 | 安装命令 | 签名验证 | 权限要求 | +| --- | --- | --- | --- | --- | +| Windows | `.inf` | `pnputil.exe /add-driver /install` | Authenticode | 管理员 | +| Linux | `.ko` / `.deb` / `.rpm` | `insmod`/`modprobe` / `dpkg -i` / `rpm -ivh` | GPG(`.sig`/`.asc`) | root/sudo | +| macOS | `.kext` / `.dext` / `.pkg` | `kextload` / SystemExtensions / `installer` | `codesign -v` | root(SIP 和用户批准策略影响) | -```c# -GeneralTracer.SetTracingEnabled(false); +### 日志配置 -// 执行性能敏感的扫描或批量验证 +```csharp +// 性能敏感场景关闭日志 +GeneralTracer.SetTracingEnabled(false); +// 排查问题时重新开启 GeneralTracer.SetTracingEnabled(true); ``` -如果需要把日志桥接到自己的 UI 或日志系统,可以使用 `DrivelutionLogger` 的 `LogMessage` 事件自行包装;当前主更新流水线主要使用 `GeneralTracer`。 - -### 推荐实践 - -| 场景 | 建议 | -| --- | --- | -| 生产更新 | 保持 `RequireBackup = true`,设置明确的 `BackupPath`,不要跳过哈希和签名。 | -| 首次集成 | 先调用 `ValidateAsync` 和 `GetPlatformInfo()`,在 UI 中展示目标 OS、架构、版本和发布者。 | -| Windows | 以管理员启动进程,并优先使用厂商签名的 INF 包。 | -| Linux | 确认 root/sudo 权限、内核版本和包管理器锁;核心模块建议顺序更新。 | -| macOS | 提前告知用户可能需要批准系统扩展;kext 受 SIP 和系统策略影响较大。 | -| 大批量驱动 | 扫描和验证可并行,安装阶段按驱动风险分组;失败时保留 `StepLogs` 和 `BackupPath`。 | -| 高性能场景 | 批量扫描时可临时关闭 `GeneralTracer`,结束后再恢复。 | - -### 常见问题 - -#### 为什么有时签名校验没有执行? - -签名校验只在 `DriverInfo.TrustedPublishers` 非空且 `SkipSignatureValidation == false` 时执行。如果你希望强制校验签名,请提供可信发布者列表,并确保平台对应的签名文件或系统签名信息可用。 - -#### 为什么设置了 `RestartMode` 但系统没有重启? - -`RestartMode` 当前是策略字段,更新流水线不会自动重启系统。应用需要在 `UpdateAsync` 成功后调用 `RestartHelper.HandleRestartAsync(...)`,或用自己的 GUI/服务逻辑接管重启。 - -#### `BatchMode.Parallel` 是否一定更快? - -不一定。并行可以提升扫描、验证和互不依赖任务的吞吐,但驱动安装会调用系统工具,可能遇到驱动仓库锁、包管理器锁、模块依赖或重启要求。大型项目建议先并行验证,再对安装阶段分组控制并发。 +--- -#### 回滚应该如何设计? +## 相关资源 -更新前保留备份路径,失败时读取 `UpdateResult.BackupPath` 和 `StepLogs`。如果业务要求强恢复,显式调用 `RollbackAsync(backupPath)`,并在 UI 中提示用户可能仍需重启或重新插拔设备。 +- [驱动更新示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Hub/Samples/ImDiskQuickInstallSample.cs) +- [GeneralUpdate 仓库](https://github.com/GeneralLibrary/GeneralUpdate) +- [驱动指南](../guide/Driver.md) diff --git a/website/docs/doc/GeneralUpdate.Extension.md b/website/docs/doc/GeneralUpdate.Extension.md index d5aba2e..62be37a 100644 --- a/website/docs/doc/GeneralUpdate.Extension.md +++ b/website/docs/doc/GeneralUpdate.Extension.md @@ -4,515 +4,626 @@ sidebar_position: 12 # GeneralUpdate.Extension -## 组件概览 +**命名空间:** `GeneralUpdate.Extension` | **主要入口:** `GeneralExtensionHost`(实现 `IExtensionHost`) | **NuGet 包:** `GeneralUpdate.Extension` + +## 1. 组件简介 + +### 1.1 组件概述 **GeneralUpdate.Extension** 是面向 .NET 应用的扩展管理组件,设计目标是让宿主程序具备类似 VS Code 的扩展生态能力:从远程服务查询扩展、下载扩展包、安装或更新到本地目录,并在这个过程中处理版本兼容、平台匹配、依赖扩展、SHA256 校验、失败回滚和事件通知。 -它适合用于把主程序和可选能力拆开发布的场景,例如报表、认证、行业插件、客户定制模块、脚本执行器等。主程序只需要集成 `GeneralExtensionHost`,扩展包可以独立发布、独立更新,也可以通过 Tools 侧的打包流程生成标准 ZIP 包后交给 Extension 组件安装和管理。 +它适合把主程序和可选能力拆开发布的场景,例如报表、认证、行业插件、客户定制模块、脚本执行器等。主程序只需要集成 `GeneralExtensionHost`,扩展包可以独立发布、独立更新。 -**命名空间:** `GeneralUpdate.Extension` +**核心能力:** -**程序集:** `GeneralUpdate.Extension.dll` -**NuGet 包:** `GeneralUpdate.Extension` +| 能力 | 说明 | +| --- | --- | +| 扩展查询 | 通过服务端 API 分页查询可用扩展,支持名称、发布者、分类、平台等筛选条件 | +| 一键更新 | `UpdateExtensionAsync` 串起查询→兼容性检查→平台检查→依赖递归安装→下载→SHA256 校验→安装→catalog 更新 | +| 安全安装 | Zip Slip 路径穿越防护、安装前备份、失败自动回滚到旧版本 | +| 批量更新 | `UpdateExtensionsAsync` 按顺序批量处理多个扩展,返回每个扩展的成功/失败结果 | +| 版本兼容性 | `MinHostVersion` ≤ `HostVersion` ≤ `MaxHostVersion` 范围内才允许安装 | +| 平台匹配 | `[Flags] TargetPlatform` 位运算判断扩展是否支持当前 OS | +| 依赖解析 | 拓扑排序依赖树,检测循环依赖,递归安装未安装的依赖扩展 | +| 断点续传 | 下载支持 HTTP Range,已存在部分文件时从断点继续 | +| 本地 Catalog | 每个扩展独立 `manifest.json`,原子写入(`.tmp` → 重命名),支持持久化和加载 | +| 生命周期钩子 | 安装前后、激活/停用前后、卸载前后的业务逻辑注入 | +| 自动更新策略 | `SetGlobalAutoUpdate` / `SetAutoUpdate` 控制全局或单扩展的自动更新开关 | +| DI 集成 | `ExtensionHostBuilder` 注册默认服务,所有服务均可通过 DI 替换 | + +**解决的业务痛点:** +- 主程序体积膨胀,需要把非核心功能拆成可独立更新的扩展 +- 不同客户需要不同功能组合,扩展生态可以实现按需安装 +- 扩展之间有依赖关系,需要自动管理依赖的安装和版本兼容性 +- 需要统一的扩展管理框架减少重复开发 + +**业务使用场景:** +- IDE 类应用的插件市场 +- 企业 ERP/CRM 的行业模块(报表模板、认证方式、数据导出等) +- 客户定制功能独立分发 +- 脚本执行器/工具集的组件化发布 + +### 1.2 环境与依赖 + +| 项目 | 说明 | +| --- | --- | +| **版本** | `10.5.0-beta.2` | +| **目标框架** | `netstandard2.0`(兼容 .NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 5+) | +| **依赖包** | `Microsoft.Extensions.DependencyInjection`、`Microsoft.Extensions.Logging.Abstractions`、`Microsoft.Extensions.Options`、`Newtonsoft.Json`、`System.Net.Http`、`System.IO.Compression`、`System.IO.Compression.ZipFile` | +| **兼容性** | 所有支持 .NET Standard 2.0 的平台 | -```csharp -public interface IExtensionHost -{ - IExtensionCatalog ExtensionCatalog { get; } - event EventHandler? ExtensionUpdateStatusChanged; - - Task>> QueryExtensionsAsync(ExtensionQueryDTO query); - Task DownloadExtensionAsync(string extensionId, string savePath); - Task UpdateExtensionAsync(string extensionId); - Task InstallExtensionAsync(string extensionPath, bool rollbackOnFailure = true); - Task> UpdateExtensionsAsync(IEnumerable extensionIds, CancellationToken cancellationToken = default); - bool IsExtensionCompatible(ExtensionMetadata extension); - void SetAutoUpdate(string extensionId, bool autoUpdate); - void SetGlobalAutoUpdate(bool enabled); -} -``` +--- + +## 2. 组件功能列表 + +| 功能名称 | 功能描述 | 类型 | 是否必填 | 备注限制 | +| --- | --- | --- | --- | --- | +| 扩展查询 | 从服务端 API 分页查询扩展列表 | 基础 | 推荐 | 支持多条件筛选 | +| 扩展下载 | 从服务端下载扩展 ZIP 包,支持断点续传 | 基础 | 自动 | 通过 `DownloadExtensionAsync` 或一键更新自动触发 | +| 扩展安装 | 安全解压 ZIP 到本地目录,支持 Zip Slip 防护和回滚 | 基础 | 自动 | 仅接受 `.zip` 格式 | +| 一键更新 | 自动串起查询→兼容性→依赖→下载→校验→安装全流程 | 基础 | 推荐 | `UpdateExtensionAsync` | +| 批量更新 | 按顺序批量更新多个扩展 | 拓展 | 可选 | `UpdateExtensionsAsync` | +| 扩展卸载 | 从本地 catalog 移除并删除扩展目录 | 基础 | 可选 | `UninstallExtensionAsync` | +| 版本兼容性检查 | 宿主版本必须在扩展的 Min/Max 范围内 | 基础 | 自动 | 更新流程中自动检查 | +| 平台匹配 | 自动识别当前 OS,匹配扩展支持的平台 | 基础 | 自动 | `PlatformMatcher` 通过 `RuntimeInformation` 检测 | +| 依赖递归安装 | 发现未安装依赖时递归调用更新 | 基础 | 自动 | 依赖必须能被同一服务端查询和下载 | +| 循环依赖检测 | 拓扑排序时检测依赖环 | 基础 | 自动 | `DependencyResolver` | +| SHA256 校验 | 下载后校验文件完整性 | 基础 | 自动 | 服务端 `Hash` 非空时校验 | +| 本地 Catalog 管理 | 每个扩展独立 `manifest.json`,原子写入 | 基础 | 自动 | 存储在扩展目录下 | +| 自动更新策略 | 全局/单扩展自动更新开关 | 拓展 | 可选 | 仅在内存中保存状态,不自动轮询 | +| 生命周期钩子 | 安装/激活/停用/卸载前后业务逻辑 | 拓展 | 可选 | 实现 `IExtensionLifecycleHooks` 或继承 `DefaultExtensionLifecycleHooks` | +| DI Builder | `ExtensionHostBuilder` 注册并替换所有服务 | 拓展 | 可选 | 支持自定义 `IExtensionServiceFactory` | +| 下载队列管理 | 并发下载控制(默认 3) | 拓展 | 可选 | `DownloadQueueManager` | --- -## 阅读导航 +## 3. API 配置说明 + +### 3.1 配置字段(属性 Props) + +**ExtensionHostOptions:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 枚举/取值范围 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `ServerUrl` | `string` | — | 是 | 有效绝对 URL | 扩展服务根地址,客户端调用 `{ServerUrl}/Query` 和 `{ServerUrl}/Download/{extensionId}` | +| `Scheme` | `string` | `""` | 可选 | `"Bearer"` 等 | Authorization 认证方案,为空不设置认证头 | +| `Token` | `string` | `""` | 可选 | — | Authorization token,需和 `Scheme` 同时非空才生效 | +| `HostVersion` | `string` | — | 推荐 | SemVer 格式 | 宿主应用版本,用于兼容性判断 | +| `ExtensionsDirectory` | `string` | — | 是 | 有效目录路径 | 扩展包下载、安装和 `.backup` 目录所在位置 | +| `CatalogPath` | `string` | `null` | 可选 | 有效目录路径 | 本地扩展目录扫描路径,为空时使用 `ExtensionsDirectory` | + +**ExtensionMetadata(本地模型):** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `Id` | `string` | — | 是 | 扩展唯一 ID,依赖、查询、更新、卸载都以它为关键标识 | +| `Name` | `string` | `null` | 推荐 | 扩展目录名和包名的稳定名称 | +| `DisplayName` | `string` | `null` | 可选 | 展示名称 | +| `Version` | `string` | `null` | 推荐 | 扩展版本,建议 `1.2.3` 格式 | +| `FileSize` | `long?` | `null` | 可选 | 包大小(字节) | +| `Format` | `string` | `null` | 推荐 | 包格式,当前安装要求 `.zip` | +| `Hash` | `string` | `null` | 推荐 | SHA256,非空时更新流程校验下载文件 | +| `Publisher` | `string` | `null` | 可选 | 发布者 | +| `Categories` | `string` | `null` | 可选 | 逗号分隔分类 | +| `SupportedPlatforms` | `TargetPlatform` | `All` | 推荐 | `[Flags]` 位标志:`Windows(1)`, `Linux(2)`, `MacOS(4)`, `All(7)` | +| `MinHostVersion` | `string` | `null` | 可选 | 最低宿主版本 | +| `MaxHostVersion` | `string` | `null` | 可选 | 最高宿主版本 | +| `Dependencies` | `string` | `null` | 可选 | 逗号分隔的依赖扩展 ID | +| `IsPreRelease` | `bool` | `false` | 可选 | 是否预发布 | +| `CustomProperties` | `string` | `null` | 可选 | JSON 字符串形式的自定义属性 | + +**ExtensionQueryDTO(查询筛选):** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `Id` | `string?` | `null` | 可选 | 按 ID 精确查询 | +| `Name` | `string?` | `null` | 可选 | 按名称模糊匹配 | +| `Publisher` | `string?` | `null` | 可选 | 按发布者模糊匹配 | +| `Category` | `string?` | `null` | 可选 | 按分类筛选 | +| `Platform` | `TargetPlatform?` | `null` | 可选 | 按目标平台筛选 | +| `HostVersion` | `string?` | `null` | 可选 | 用于服务端兼容性判断 | +| `IsPreRelease` | `bool?` | `null` | 可选 | 是否包含预发布 | +| `Status` | `bool?` | `null` | 可选 | 按启用状态筛选 | +| `PageNumber` | `int` | `1` | 可选 | 页码(从 1 开始) | +| `PageSize` | `int` | `10` | 可选 | 每页大小 | + +### 3.2 实例方法 + +**IExtensionHost:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `QueryExtensionsAsync(ExtensionQueryDTO)` | `query` — 查询条件 | `Task>>` | 搜索/浏览可用扩展 | 响应数据在 `Body.Items` 中 | +| `DownloadExtensionAsync(string, string)` | `extensionId` — 扩展 ID;`savePath` — 保存路径 | `Task` | 单独下载扩展包 | 支持 HTTP Range 断点续传 | +| `UpdateExtensionAsync(string)` | `extensionId` — 扩展 ID | `Task` | 一键更新单个扩展(推荐入口) | 串起查询→兼容性→依赖→下载→校验→安装全流程 | +| `InstallExtensionAsync(string, bool)` | `extensionPath` — ZIP 包路径;`rollbackOnFailure` — 是否失败回滚 | `Task` | 手动安装本地扩展包 | 仅接受 `.zip` 格式 | +| `UpdateExtensionsAsync(IEnumerable, CancellationToken)` | `extensionIds` — 扩展 ID 列表;`ct` — 取消令牌 | `Task>` | 批量更新 | 按传入顺序逐个处理 | +| `UninstallExtensionAsync(string, CancellationToken)` | `extensionId` — 扩展 ID;`ct` — 取消令牌 | `Task` | 卸载扩展 | 移除 catalog 记录并删除扩展目录 | +| `ActivateExtensionAsync(string, CancellationToken)` | `extensionId`;`ct` | `Task` | 激活扩展 | 调用生命周期钩子 | +| `DeactivateExtensionAsync(string, CancellationToken)` | `extensionId`;`ct` | `Task` | 停用扩展 | 调用生命周期钩子 | +| `IsExtensionCompatible(ExtensionMetadata)` | `extension` — 扩展元数据 | `bool` | 检查扩展兼容性 | 基于 `HostVersion` 与 `MinHostVersion`/`MaxHostVersion` 比较 | +| `SetAutoUpdate(string, bool)` | `extensionId` — 扩展 ID;`autoUpdate` — 是否自动更新 | `void` | 设置单扩展自动更新开关 | 仅内存状态,不自动后台轮询 | +| `SetGlobalAutoUpdate(bool)` | `enabled` — 是否启用 | `void` | 设置全局自动更新默认值 | 仅内存状态 | + +**GeneralExtensionHost 附加方法:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `IsAutoUpdateEnabled(string)` | `extensionId` — 扩展 ID | `bool` | 查询指定扩展的自动更新开关 | 单扩展设置优先于全局设置 | + +**ExtensionHostBuilder:** + +| 方法名 | 入参明细 | 返回值 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | --- | +| `ConfigureOptions(Action)` | `configure` — 配置委托 | `ExtensionHostBuilder` | 通过 Lambda 配置选项 | — | +| `WithOptions(ExtensionHostOptions)` | `options` — 选项对象 | `ExtensionHostBuilder` | 直接设置选项 | — | +| `ConfigureServices(Action)` | `configure` — DI 注册委托 | `ExtensionHostBuilder` | 替换或添加服务 | 在 `Build()` 前调用 | +| `Build()` | 无 | `IExtensionHost` | 构建宿主实例 | 自动注册未覆盖的默认服务 | + +### 3.3 回调事件 + +| 事件名称 | 回调参数 | 触发时机 | 使用说明 | +| --- | --- | --- | --- | +| `ExtensionUpdateStatusChanged` | `ExtensionUpdateEventArgs` — `ExtensionId`, `ExtensionName`, `Status`, `Progress`(0-100), `ErrorMessage` | 扩展更新流程各阶段 | `Status`: `Queued`→`Updating`(下载进度)→`UpdateSuccessful`/`UpdateFailed` | + +**ExtensionUpdateStatus 枚举:** -| 主题 | 说明 | +| 值 | 说明 | | --- | --- | -| [快速开始](#快速开始) | 最小配置、查询扩展、更新扩展 | -| [核心流程](#核心流程) | 查询、下载、安装、更新、回滚、卸载分别做什么 | -| [扩展元数据与 manifest](#扩展元数据与-manifest) | `ExtensionMetadata`、服务端 DTO、本地 `manifest.json` | -| [扩展包结构与 Tools 打包关系](#扩展包结构与-tools-打包关系) | ZIP 命名、包内文件、发布侧与消费侧的分工 | -| [兼容性、平台与依赖](#兼容性平台与依赖) | Host 版本范围、`TargetPlatform`、依赖递归安装 | -| [事件通知与自动更新开关](#事件通知与自动更新开关) | 状态事件、全局/单扩展自动更新配置 | -| [服务器 API 契约](#服务器-api-契约) | `/Query` 与 `/Download/{extensionId}` 的真实调用方式 | -| [高级扩展点](#高级扩展点) | DI Builder、自定义 HttpClient、生命周期钩子 | -| [最佳实践](#最佳实践) | 生产环境接入建议 | +| `Queued` (0) | 已加入更新队列 | +| `Updating` (1) | 正在下载/更新中 | +| `UpdateSuccessful` (2) | 更新成功 | +| `UpdateFailed` (3) | 更新失败 | --- -## 快速开始 +## 4. 扩展示例(高阶用法) -### 安装 +### 4.1 组件可扩展能力总览 -```bash -dotnet add package GeneralUpdate.Extension -``` +所有服务均可通过 `ExtensionHostBuilder.ConfigureServices()` 替换: + +| 服务接口 | 默认实现 | 说明 | +| --- | --- | --- | +| `IExtensionHttpClient` | `ExtensionHttpClient` | HTTP 通信(查询/下载) | +| `IVersionCompatibilityChecker` | `VersionCompatibilityChecker` | 版本兼容性检查 | +| `IDownloadQueueManager` | `DownloadQueueManager` | 下载队列管理 | +| `IPlatformMatcher` | `PlatformMatcher` | 平台检测 | +| `IPlatformServices` | `RuntimePlatformServices` | 运行时平台信息 | +| `IExtensionMetadataMapper` | `DefaultExtensionMetadataMapper` | DTO→模型映射 | +| `IExtensionCatalog` | `ExtensionCatalog` | 本地扩展目录管理 | +| `IDependencyResolver` | `DependencyResolver` | 依赖解析 | +| `IExtensionLifecycleHooks` | `DefaultExtensionLifecycleHooks` | 生命周期钩子(所有方法 virtual) | +| `IExtensionServiceFactory` | `ExtensionServiceFactory` | 服务工厂 | -### 初始化扩展宿主 +### 4.2 分场景示例 + +#### 场景 1:自定义生命周期钩子 + +【场景说明】在扩展安装前后执行自定义逻辑:安装前检查许可证、安装后初始化扩展数据库。 + +【示例代码】 ```csharp -using GeneralUpdate.Extension.Common.DTOs; -using GeneralUpdate.Extension.Common.Enums; -using GeneralUpdate.Extension.Common.Models; using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.Models; -var options = new ExtensionHostOptions +public sealed class LicensedLifecycleHooks : DefaultExtensionLifecycleHooks { - ServerUrl = "https://extensions.example.com/Extension", - Scheme = "Bearer", - Token = "your-token", - HostVersion = "1.0.0", - ExtensionsDirectory = "./extensions" -}; + public override async Task OnBeforeInstallAsync( + ExtensionMetadata extension, + string? packagePath, + CancellationToken cancellationToken = default) + { + // 检查许可证 + if (!LicenseManager.IsLicensed(extension.Id)) + { + Console.WriteLine($"Extension '{extension.Id}' is not licensed."); + return false; // 阻止安装 + } + return true; + } -var host = new GeneralExtensionHost(options); + public override async Task OnAfterInstallAsync( + ExtensionMetadata extension, + CancellationToken cancellationToken = default) + { + // 初始化扩展数据库 + if (extension.CustomProperties != null) + { + var props = Newtonsoft.Json.JsonConvert + .DeserializeObject>(extension.CustomProperties); + if (props?.ContainsKey("DbInitScript") == true) + { + await DatabaseInitializer.RunAsync(props["DbInitScript"], cancellationToken); + } + } + Console.WriteLine($"Extension '{extension.DisplayName}' installed successfully."); + } -host.ExtensionUpdateStatusChanged += (sender, e) => -{ - Console.WriteLine($"{e.ExtensionId} {e.Status} {e.Progress}% {e.ErrorMessage}"); -}; + public override async Task OnBeforeUninstallAsync( + ExtensionMetadata extension, + CancellationToken cancellationToken = default) + { + // 检查是否有关联数据 + var hasData = await DataService.HasExtensionDataAsync(extension.Id, cancellationToken); + if (hasData) + { + Console.WriteLine($"Extension '{extension.Id}' has associated data. Clean up first."); + return false; // 阻止卸载 + } + return true; + } +} + +// 使用 Builder 注册 +var host = new ExtensionHostBuilder() + .WithOptions(options) + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .Build(); ``` -`ExtensionHostOptions` 当前可配置项如下: +【效果&注意事项】 +- 返回 `false` 会阻止操作继续执行 +- 所有钩子方法都是 `virtual`,只需覆写需要的部分 -| 属性 | 说明 | -| --- | --- | -| `ServerUrl` | 扩展服务根地址。客户端会调用 `{ServerUrl}/Query` 和 `{ServerUrl}/Download/{extensionId}` | -| `Scheme` | Authorization 认证方案,例如 `Bearer`。为空时不设置认证头 | -| `Token` | Authorization token。需要和 `Scheme` 同时非空才会生效 | -| `HostVersion` | 宿主应用版本,用于 `MinHostVersion` / `MaxHostVersion` 兼容性判断 | -| `ExtensionsDirectory` | 扩展包下载、安装和 `.backup` 目录所在位置 | -| `CatalogPath` | 可选,本地扩展目录扫描路径;为空时使用 `ExtensionsDirectory` | +#### 场景 2:自定义 HTTP 客户端 + 共享连接池 + +【场景说明】与主应用共享 `HttpClient` 连接池,避免 socket 耗尽;同时切换为 POST 查询。 -### 查询和更新扩展 +【示例代码】 ```csharp -var query = new ExtensionQueryDTO +using GeneralUpdate.Extension.Communication; + +// 共享主应用的 HttpClient +var sharedClient = new HttpClient(); // 或从 IHttpClientFactory 获取 + +var httpClient = new ExtensionHttpClient( + serverUrl: "https://extensions.mycompany.com/Extension", + scheme: "Bearer", + token: "jwt-token", + httpClient: sharedClient, + ownsHttpClient: false) // 不拥有,不 Dispose { - Platform = TargetPlatform.Windows, - HostVersion = options.HostVersion, - Status = true, - PageNumber = 1, - PageSize = 20 + UsePostForQuery = true // 服务端要求 POST 查询 }; -var response = await host.QueryExtensionsAsync(query); -if (response.Body != null) -{ - foreach (var extension in response.Body.Items) +var host = new ExtensionHostBuilder() + .WithOptions(options) + .ConfigureServices(services => { - Console.WriteLine($"{extension.DisplayName} v{extension.Version}, compatible: {extension.IsCompatible}"); - } -} -else -{ - Console.WriteLine(response.Message); -} - -var success = await host.UpdateExtensionAsync("extension-id"); + services.AddSingleton(httpClient); + }) + .Build(); ``` ---- +【效果&注意事项】 +- `ownsHttpClient: false` 确保 Dispose 时不关闭共享连接 +- `UsePostForQuery = true` 将默认 GET+JSON Body 改为 POST+JSON Body -## 核心流程 +#### 场景 3:依赖解析 + 条件批量更新 -### 1. 查询远程扩展 +【场景说明】用户选择安装一个扩展时,自动解析依赖并一起安装。 -`QueryExtensionsAsync` 直接把 `ExtensionQueryDTO` 交给 `ExtensionHttpClient`,返回 `HttpResponseDTO>`。当前响应数据在 `Body` 属性中,不是 `Data`。 +【示例代码】 ```csharp +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Enums; + +var host = new GeneralExtensionHost(options); + +// 查询目标扩展 var response = await host.QueryExtensionsAsync(new ExtensionQueryDTO { - Name = "report", - Publisher = "general", - Category = "Tools", - Platform = TargetPlatform.Windows | TargetPlatform.Linux, - HostVersion = "1.2.0", - IsPreRelease = false, - PageNumber = 1, - PageSize = 10 + Id = "report-extension", + PageSize = 1 }); -if (response.Body == null) +if (response.Body?.Items.Any() != true) { - Console.WriteLine($"Query failed: {response.Code} {response.Message}"); + Console.WriteLine("Extension not found."); return; } -Console.WriteLine($"Total: {response.Body.TotalCount}"); -``` +var ext = response.Body.Items.First(); -### 2. 下载扩展包 +// 解析依赖 +var catalog = host.ExtensionCatalog; +catalog.LoadInstalledExtensions(); -`DownloadExtensionAsync(extensionId, savePath)` 会调用远程下载接口并写入 `savePath`。底层下载器支持: +var resolver = new GeneralUpdate.Extension.Dependencies.DependencyResolver(catalog); +var deps = resolver.ResolveDependencies( + new ExtensionMetadata { Id = ext.Id, Dependencies = string.Join(",", ext.Dependencies ?? []) }); -- 已存在部分文件时使用 HTTP Range 续传; -- 下载过程中通过 `ExtensionUpdateStatusChanged` 报告 `Updating` 和进度; -- `DownloadExtensionWithResultAsync` 在底层提供更细的错误分类,例如网络错误、4xx、5xx、取消、I/O 错误。 +var missingDeps = resolver.GetMissingDependencies( + new ExtensionMetadata { Id = ext.Id, Dependencies = string.Join(",", ext.Dependencies ?? []) }); -```csharp -var downloaded = await host.DownloadExtensionAsync( - extensionId: "report-extension", - savePath: "./extensions/report-extension_1.0.0.zip"); -``` +Console.WriteLine($"Dependencies for {ext.DisplayName}: {deps.Count} total, {missingDeps.Count} missing."); -### 3. 安装扩展包 +// 先安装缺失依赖 +var updateOrder = new List(); +updateOrder.AddRange(missingDeps); +updateOrder.Add(ext.Id); -`InstallExtensionAsync` 只接受 `.zip` 包。安装时会根据文件名推导目录名:`name_version.zip` 会安装到 `{ExtensionsDirectory}/name`。 +var results = await host.UpdateExtensionsAsync(updateOrder); -```csharp -var installed = await host.InstallExtensionAsync( - extensionPath: "./extensions/report-extension_1.0.0.zip", - rollbackOnFailure: true); +foreach (var (id, success) in results) + Console.WriteLine($" {id}: {(success ? "OK" : "FAILED")}"); ``` -安装过程: +【效果&注意事项】 +- `DependencyResolver.ResolveDependencies` 返回拓扑排序后的完整依赖列表 +- `GetMissingDependencies` 过滤出未安装在本地 catalog 中的依赖 +- 循环依赖会被检测并抛出异常 -1. 检查文件是否存在,并确认扩展包是 `.zip`。 -2. 调用 `IExtensionLifecycleHooks.OnBeforeInstallAsync`,返回 `false` 时取消安装。 -3. 如果本地已有同名扩展且开启回滚,复制旧目录到 `{ExtensionsDirectory}/.backup`。 -4. 删除旧目录,创建目标目录。 -5. 安全解压 ZIP。解压时会校验目标路径,跳过 Zip Slip 路径穿越条目。 -6. 安装成功后删除备份,并调用 `OnAfterInstallAsync`。 -7. 安装失败时尝试从备份目录恢复。 +--- -### 4. 一键更新扩展 +## 5. 常规使用示例 -`UpdateExtensionAsync(extensionId)` 是推荐入口。它会串起查询、兼容性检查、平台检查、依赖递归安装、下载、SHA256 校验、安装和 catalog 更新。 +### 5.1 快速入门示例(最简 demo) ```csharp -var success = await host.UpdateExtensionAsync("report-extension"); -if (!success) -{ - Console.WriteLine("Update failed. Read ExtensionUpdateStatusChanged for details."); -} -``` - -完整流程: +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Models; -1. 触发 `Queued` 事件。 -2. 用 `Id = extensionId` 查询服务端扩展信息。 -3. 将 `ExtensionDTO` 映射为 `ExtensionMetadata`。 -4. 检查宿主版本是否落在 `MinHostVersion` / `MaxHostVersion` 范围内。 -5. 检查当前 OS 是否包含在 `SupportedPlatforms`。 -6. 遇到未安装依赖时递归调用 `UpdateExtensionAsync(depId)`。 -7. 下载 `{Name}_{Version}{Format}` 到 `ExtensionsDirectory`。 -8. 如果 `Hash` 非空,计算下载文件 SHA256 并对比。 -9. 调用 `InstallExtensionAsync(..., rollbackOnFailure: true)`。 -10. 写入或更新本地 catalog 的 `manifest.json`。 -11. 成功触发 `UpdateSuccessful`,失败触发 `UpdateFailed`。 +var options = new ExtensionHostOptions +{ + ServerUrl = "https://extensions.example.com/Extension", + Scheme = "Bearer", + Token = "your-token", + HostVersion = "1.0.0", + ExtensionsDirectory = "./extensions" +}; -### 5. 批量更新 +var host = new GeneralExtensionHost(options); -`UpdateExtensionsAsync` 会按传入顺序逐个更新扩展,并返回每个扩展的成功/失败结果。当前实现是顺序处理;如需并发策略,应在业务层控制并发数量后分别调用 `UpdateExtensionAsync`。 +host.ExtensionUpdateStatusChanged += (sender, e) => +{ + Console.WriteLine($"[{e.Status}] {e.ExtensionId}: {e.Progress}% {e.ErrorMessage}"); +}; -```csharp -var result = await host.UpdateExtensionsAsync(new[] +// 查询可用扩展 +var response = await host.QueryExtensionsAsync(new ExtensionQueryDTO { - "report-extension", - "auth-extension", - "theme-extension" -}, cancellationToken); + Platform = TargetPlatform.Windows, + PageNumber = 1, + PageSize = 20 +}); -foreach (var item in result) +if (response.Body != null) { - Console.WriteLine($"{item.Key}: {item.Value}"); + foreach (var ext in response.Body.Items) + Console.WriteLine($"{ext.DisplayName} v{ext.Version} [{ext.Id}]"); } -``` - -### 6. 回滚 - -回滚由 `InstallExtensionAsync` 负责,核心是备份旧目录、失败时恢复旧目录。它适合覆盖安装或更新失败场景;首次安装失败时因为没有旧目录,通常没有可恢复内容。 - -备份目录位于: -```text -{ExtensionsDirectory}/.backup/{extensionName}_{yyyyMMddHHmmss} +// 更新指定扩展 +var success = await host.UpdateExtensionAsync("report-extension"); +Console.WriteLine(success ? "Extension updated." : "Update failed."); ``` -### 7. 卸载 - -`IExtensionHost` 当前没有暴露 `UninstallExtensionAsync`。卸载能力在 `IExtensionCatalog.RemoveInstalledExtension(extensionId)` 中,调用后会移除内存记录,并尝试删除对应扩展目录。 +### 5.2 基础参数组合示例 ```csharp -host.ExtensionCatalog.RemoveInstalledExtension("report-extension"); -``` - -如果业务需要审批、停用、卸载前检查或卸载后清理,可以在应用层封装卸载服务,并复用 `IExtensionLifecycleHooks.OnBeforeUninstallAsync` / `OnAfterUninstallAsync` 的语义保持一致。 - ---- - -## 扩展元数据与 manifest - -### ExtensionMetadata - -`ExtensionMetadata` 是 Extension 组件本地安装、catalog 持久化和兼容性判断使用的核心模型。 - -| 属性 | 说明 | -| --- | --- | -| `Id` | 扩展唯一 ID。依赖、查询、更新、卸载都以它为关键标识 | -| `Name` | 扩展目录名和包名建议使用的稳定名称,例如 `report-extension` | -| `DisplayName` | 展示名称 | -| `Version` | 扩展版本。兼容性比较使用 .NET `Version.TryParse`,建议使用 `1.2.3` 或 `1.2.3.0` | -| `FileSize` | 扩展包大小,单位字节 | -| `UploadTime` | 上传时间 | -| `Status` | 是否启用 | -| `Description` | 描述 | -| `Format` | 包格式。当前安装实现要求 `.zip` | -| `Hash` | 可选 SHA256。非空时更新流程会校验下载文件 | -| `Publisher` | 发布者 | -| `License` | 许可证 | -| `Categories` | 逗号分隔分类 | -| `SupportedPlatforms` | `TargetPlatform` 位标志 | -| `MinHostVersion` | 最低宿主版本 | -| `MaxHostVersion` | 最高宿主版本 | -| `ReleaseDate` | 发布时间 | -| `Dependencies` | 逗号分隔的依赖扩展 ID | -| `IsPreRelease` | 是否预发布 | -| `DownloadUrl` | 下载地址元数据;当前默认下载调用仍使用 `{ServerUrl}/Download/{extensionId}` | -| `CustomProperties` | JSON 字符串形式的自定义属性 | - -### 服务端 DTO 与本地 manifest - -服务端查询返回 `ExtensionDTO`,其中 `Categories` 和 `Dependencies` 是 `List`;客户端会把它们映射为本地 `ExtensionMetadata` 的逗号分隔字符串。 - -本地安装 catalog 不再是单个 `catalog.json`。当前实现会扫描 `CatalogPath` 下的子目录,并读取每个扩展目录中的: - -```text -manifest.json -``` - -`AddOrUpdateInstalledExtension` 会把每个扩展写入独立目录: +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Enums; -```text -{CatalogPath}/{safe-extension-name}/manifest.json -``` +var host = new GeneralExtensionHost(new ExtensionHostOptions +{ + ServerUrl = "https://extensions.mycompany.com/Extension", + Scheme = "Bearer", + Token = Environment.GetEnvironmentVariable("EXTENSION_TOKEN") ?? "", + HostVersion = "2.0.0", + ExtensionsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "extensions") +}); -写入时使用 `manifest.json.tmp -> manifest.json` 的方式尽量保证原子替换;`LoadInstalledExtensions` 会清理遗留的 `.tmp` 文件,并跳过包含 `.backup` 的目录。 +// 事件监听 +host.ExtensionUpdateStatusChanged += (_, e) => +{ + switch (e.Status) + { + case ExtensionUpdateStatus.Queued: + Console.WriteLine($"{e.ExtensionId}: queued"); + break; + case ExtensionUpdateStatus.Updating: + Console.WriteLine($"{e.ExtensionId}: downloading... {e.Progress}%"); + break; + case ExtensionUpdateStatus.UpdateSuccessful: + Console.WriteLine($"{e.ExtensionName ?? e.ExtensionId}: updated successfully"); + break; + case ExtensionUpdateStatus.UpdateFailed: + Console.WriteLine($"{e.ExtensionId}: failed — {e.ErrorMessage}"); + break; + } +}; -示例 manifest: +// 安装本地扩展包 +var installed = await host.InstallExtensionAsync( + "./downloads/report-extension_1.0.0.zip", + rollbackOnFailure: true); +Console.WriteLine(installed ? "Installed." : "Installation failed."); -```json +// 查询已安装扩展 +host.ExtensionCatalog.LoadInstalledExtensions(); +var installedExts = host.ExtensionCatalog.GetInstalledExtensions(); +foreach (var ext in installedExts) { - "Id": "report-extension", - "Name": "report-extension", - "DisplayName": "Report Extension", - "Version": "1.0.0", - "Status": true, - "Description": "Adds PDF and Excel reports.", - "Format": ".zip", - "Hash": "6f5902ac237024bdd0c176cb93063dc4...", - "Publisher": "GeneralLibrary", - "License": "MIT", - "Categories": "Reports,Tools", - "SupportedPlatforms": 7, - "MinHostVersion": "1.0.0", - "MaxHostVersion": "2.0.0", - "Dependencies": "base-extension", - "IsPreRelease": false + var compat = host.IsExtensionCompatible(ext); + Console.WriteLine($"{ext.DisplayName} v{ext.Version} — compatible: {compat}"); } -``` - -`SupportedPlatforms` 是 `[Flags]` 枚举,`All = Windows | Linux | MacOS = 7`。 - ---- - -## 扩展包结构与 Tools 打包关系 -Extension 组件负责“消费”扩展包:下载、校验、解压、安装、回滚和登记 manifest。Tools 或 CI/CD 负责“生产”扩展包:编译扩展、生成元数据、计算 SHA256、压缩为 ZIP、上传到扩展服务。 +// 配置自动更新策略 +host.SetGlobalAutoUpdate(true); +host.SetAutoUpdate("large-extension", false); // 大型扩展关闭自动更新 -推荐包名: - -```text -{Name}_{Version}.zip +var checkResult = host.IsAutoUpdateEnabled("large-extension"); +Console.WriteLine($"Auto-update for large-extension: {checkResult}"); ``` -例如: - -```text -report-extension_1.0.0.zip -``` +### 5.3 真实业务落地示例 -推荐 ZIP 内容: +包含异常处理、依赖管理、兼容性检查的完整工作流: -```text -report-extension_1.0.0.zip -├─ manifest.json # 推荐放入包内,供业务和 catalog 复用 -├─ extension.dll # 扩展主体程序集 -├─ extension.deps.json # .NET 依赖描述 -├─ README.md -├─ CHANGELOG.md -└─ LICENSE.txt -``` - -当前 `InstallExtensionAsync` 本身不强制读取包内 `manifest.json`,它主要负责安全解压和回滚;`UpdateExtensionAsync` 会使用服务端返回的 `ExtensionDTO` 更新本地 catalog。因此生产侧必须保证服务端元数据与 ZIP 包内容一致。 - -发布侧建议流程: - -1. 编译扩展项目。 -2. 准备 `manifest.json`,字段对齐 `ExtensionMetadata`。 -3. 生成 `{Name}_{Version}.zip`。 -4. 计算 ZIP 的 SHA256,写入服务端 `Hash`。 -5. 上传 ZIP 和 `ExtensionDTO` 元数据。 -6. 宿主应用通过 `QueryExtensionsAsync` 查询,通过 `UpdateExtensionAsync` 消费。 - -打包基础可参考 [Packaging](../guide/Packaging.md)。高级 Cookbook 的扩展发布流水线会在任务 [#54](https://github.com/GeneralLibrary/GeneralUpdate-Samples/issues/54) 中展开,建议在那里把 Tools 打包、清单生成、哈希计算、上传和宿主灰度消费串成完整流水线。 - ---- - -## 兼容性、平台与依赖 +```csharp +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Enums; +using GeneralUpdate.Extension.Common.Models; -### 版本兼容性 +// 1. 初始化 +var options = new ExtensionHostOptions +{ + ServerUrl = "https://extensions.mycompany.com/Extension", + Scheme = "Bearer", + Token = Configuration.GetExtensionToken(), + HostVersion = AppInfo.CurrentVersion.ToString(), + ExtensionsDirectory = Path.Combine(AppInfo.DataDirectory, "extensions") +}; -`VersionCompatibilityChecker.IsCompatible` 使用 `HostVersion` 与扩展的 `MinHostVersion`、`MaxHostVersion` 比较: +// 2. 使用 Builder 注册自定义服务 +var host = new ExtensionHostBuilder() + .WithOptions(options) + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .Build(); -- `HostVersion` 为空:视为不限制,返回兼容; -- `HostVersion` 无法被 `Version.TryParse` 解析:不兼容; -- `MinHostVersion` 非空且无法解析:不兼容; -- `MaxHostVersion` 非空且无法解析:不兼容; -- 宿主版本必须满足 `MinHostVersion <= HostVersion <= MaxHostVersion`。 +host.ExtensionUpdateStatusChanged += OnExtensionStatusChanged; -| HostVersion | MinHostVersion | MaxHostVersion | 结果 | -| --- | --- | --- | --- | -| `1.5.0` | `1.0.0` | `2.0.0` | 兼容 | -| `1.5.0` | `1.6.0` | `2.0.0` | 不兼容 | -| `1.5.0` | `1.0.0` | `1.4.0` | 不兼容 | -| `1.5.0` | 空 | 空 | 兼容 | +// 3. 加载本地已安装扩展 +host.ExtensionCatalog.LoadInstalledExtensions(); +var installed = host.ExtensionCatalog.GetInstalledExtensions(); +Console.WriteLine($"Loaded {installed.Count} installed extension(s)."); -```csharp -var extension = host.ExtensionCatalog.GetInstalledExtensionById("report-extension"); -if (extension != null && host.IsExtensionCompatible(extension)) +// 4. 查询服务端可用扩展 +HttpResponseDTO>? response = null; +try { - Console.WriteLine("Compatible"); + response = await host.QueryExtensionsAsync(new ExtensionQueryDTO + { + Platform = TargetPlatform.Windows | TargetPlatform.Linux, + HostVersion = options.HostVersion, + Status = true, + PageNumber = 1, + PageSize = 100 + }); } -``` - -### 平台匹配 - -```csharp -[Flags] -public enum TargetPlatform +catch (HttpRequestException ex) { - None = 0, - Windows = 1, - Linux = 2, - MacOS = 4, - All = Windows | Linux | MacOS + Console.WriteLine($"Failed to query extensions: {ex.Message}"); + return; } -``` - -`PlatformMatcher` 通过 `RuntimeInformation` 自动识别当前系统,并用位运算判断扩展是否支持当前平台。 -```csharp -var metadata = new ExtensionMetadata +if (response?.Body == null) { - Id = "report-extension", - Name = "report-extension", - SupportedPlatforms = TargetPlatform.Windows | TargetPlatform.Linux -}; -``` - -### 依赖处理 - -`ExtensionMetadata.Dependencies` 是逗号分隔的扩展 ID。`DependencyList` 会把它解析为列表。`UpdateExtensionAsync` 发现未安装依赖时,会先递归更新依赖,再安装当前扩展。 + Console.WriteLine($"Server returned: {response?.Code} {response?.Message}"); + return; +} -```csharp -var metadata = new ExtensionMetadata +// 5. 筛选可更新的扩展 +var toUpdate = new List(); +foreach (var ext in response.Body.Items) { - Id = "report-extension", - Dependencies = "base-extension,chart-extension" -}; -``` - -`DependencyResolver` 还提供依赖解析能力,可以基于本地 catalog 识别缺失依赖并检测循环依赖。需要注意:`UpdateExtensionAsync` 当前依赖安装是根据服务端返回的当前扩展元数据逐项递归处理;生产侧要确保依赖扩展也能通过同一个扩展服务查询和下载。 + var installedExt = host.ExtensionCatalog.GetInstalledExtensionById(ext.Id); + if (installedExt == null) + { + Console.WriteLine($"[NEW] {ext.DisplayName} v{ext.Version}"); + continue; // 新扩展,不自动安装 + } ---- + if (!host.IsExtensionCompatible(new ExtensionMetadata + { + MinHostVersion = ext.MinHostVersion, + MaxHostVersion = ext.MaxHostVersion + })) + { + Console.WriteLine($"[INCOMPATIBLE] {ext.DisplayName}: requires host {ext.MinHostVersion}-{ext.MaxHostVersion}"); + continue; + } -## 事件通知与自动更新开关 + if (Version.TryParse(ext.Version, out var remoteVer) && + Version.TryParse(installedExt.Version, out var localVer) && + remoteVer > localVer) + { + if (host.IsAutoUpdateEnabled(ext.Id)) + { + Console.WriteLine($"[UPDATE] {ext.DisplayName}: {installedExt.Version} → {ext.Version}"); + toUpdate.Add(ext.Id); + } + else + { + Console.WriteLine($"[SKIP] {ext.DisplayName}: auto-update disabled"); + } + } +} -### ExtensionUpdateStatusChanged +// 6. 执行批量更新 +if (toUpdate.Any()) +{ + Console.WriteLine($"\nUpdating {toUpdate.Count} extension(s)..."); + var results = await host.UpdateExtensionsAsync(toUpdate); -扩展更新事件提供单个扩展的状态变化通知: + var succeeded = results.Count(r => r.Value); + var failed = results.Count(r => !r.Value); -| 字段 | 说明 | -| --- | --- | -| `ExtensionId` | 扩展 ID | -| `ExtensionName` | 扩展名称,部分阶段可能为空 | -| `Status` | `Queued`、`Updating`、`UpdateSuccessful`、`UpdateFailed` | -| `Progress` | 0-100,下载中会更新 | -| `ErrorMessage` | 失败原因 | + Console.WriteLine($"\nDone: {succeeded} succeeded, {failed} failed."); + foreach (var (id, success) in results.Where(r => !r.Value)) + Console.WriteLine($" Failed: {id}"); +} +else +{ + Console.WriteLine("All extensions up to date."); +} -```csharp -host.ExtensionUpdateStatusChanged += (sender, e) => +// 事件处理 +void OnExtensionStatusChanged(object? sender, ExtensionUpdateEventArgs e) { switch (e.Status) { case ExtensionUpdateStatus.Queued: - Console.WriteLine($"{e.ExtensionId} queued"); break; case ExtensionUpdateStatus.Updating: - Console.WriteLine($"{e.ExtensionId} downloading {e.Progress}%"); + UpdateProgressUI(e.ExtensionId, e.Progress); break; case ExtensionUpdateStatus.UpdateSuccessful: - Console.WriteLine($"{e.ExtensionName ?? e.ExtensionId} updated"); + Log.Info($"Extension '{e.ExtensionName ?? e.ExtensionId}' updated."); + RefreshUI(); break; case ExtensionUpdateStatus.UpdateFailed: - Console.WriteLine($"{e.ExtensionId} failed: {e.ErrorMessage}"); + Log.Error($"Extension '{e.ExtensionId}' update failed: {e.ErrorMessage}"); + NotifyUser($"Failed to update {e.ExtensionName ?? e.ExtensionId}"); break; } -}; -``` - -### 自动更新开关 - -`SetGlobalAutoUpdate` 设置全局默认值,`SetAutoUpdate` 设置单个扩展的覆盖值。`IExtensionHost` 暴露了设置方法;`GeneralExtensionHost` 还提供 `IsAutoUpdateEnabled(extensionId)` 用于读取当前结果。 - -```csharp -var concreteHost = new GeneralExtensionHost(options); - -concreteHost.SetGlobalAutoUpdate(true); -concreteHost.SetAutoUpdate("large-extension", false); - -var enabled = concreteHost.IsAutoUpdateEnabled("large-extension"); +} ``` -这些开关只保存于当前 `GeneralExtensionHost` 实例内存中,组件不会自动启动后台轮询。应用层应自行决定何时扫描需要更新的扩展,并根据开关调用 `UpdateExtensionAsync`。 - --- -## 服务器 API 契约 +## 6. 全局配置 -当前 `ExtensionHttpClient` 使用两个端点。 +### 服务器 API 契约 -### 查询 +**查询接口:** ```http GET {ServerUrl}/Query Content-Type: application/json Authorization: {Scheme} {Token} -ExtensionQueryDTO JSON body +Body: ExtensionQueryDTO (JSON) ``` -这里是 **GET + JSON Body**。这不是常见 HTTP 风格,但当前客户端源码明确按这个服务端契约实现。如果经过代理、网关或 API 平台时出现兼容性问题,需要服务端和客户端一起改为 POST 或 query string。 - -响应: +> 注意:当前实现使用 GET + JSON Body,非标准 HTTP 风格。经过代理/网关时可能需要调整为 POST 或 query string。 -```csharp -HttpResponseDTO> -``` - -### 下载 +**下载接口:** ```http GET {ServerUrl}/Download/{extensionId} @@ -520,72 +631,49 @@ Authorization: {Scheme} {Token} Range: bytes={existingLength}- ``` -服务端应支持普通文件流下载,最好同时支持 HTTP Range,便于客户端断点续传。客户端遇到 `416 RequestedRangeNotSatisfiable` 会视为文件已经完整下载。 +> 服务端应支持 HTTP Range 以启用断点续传。 ---- +### 扩展包结构与 Tools 关系 -## 高级扩展点 +| 角色 | 说明 | +| --- | --- | +| Extension 组件 | **消费侧**:下载、校验、解压、安装、回滚、登记 manifest | +| Tools / CI/CD | **生产侧**:编译扩展、生成元数据、计算 SHA256、压缩为 ZIP、上传服务端 | -### 使用 ExtensionHostBuilder 和 DI +推荐包名格式:`{Name}_{Version}.zip` -`ExtensionHostBuilder` 会注册默认服务,同时允许业务替换任意服务: +推荐 ZIP 内容: -```csharp -var host = new ExtensionHostBuilder() - .WithOptions(options) - .ConfigureServices(services => - { - services.AddSingleton(); - services.AddSingleton(sp => - new ExtensionHttpClient(options.ServerUrl, options.Scheme, options.Token, sharedHttpClient)); - }) - .Build(); +```text +report-extension_1.0.0.zip +├── manifest.json # 推荐放入包内 +├── extension.dll # 扩展主体程序集 +├── extension.deps.json # .NET 依赖描述 +├── README.md +├── CHANGELOG.md +└── LICENSE.txt ``` -默认注册包括: +### 自动更新策略优先级 -- `IExtensionHttpClient -> ExtensionHttpClient` -- `IVersionCompatibilityChecker -> VersionCompatibilityChecker` -- `IDownloadQueueManager -> DownloadQueueManager` -- `IPlatformMatcher -> PlatformMatcher` -- `IPlatformServices -> RuntimePlatformServices` -- `IExtensionMetadataMapper -> DefaultExtensionMetadataMapper` -- `IExtensionCatalog -> ExtensionCatalog` -- `IDependencyResolver -> DependencyResolver` -- `IExtensionLifecycleHooks -> DefaultExtensionLifecycleHooks` -- `IExtensionHost -> GeneralExtensionHost` - -### 生命周期钩子 - -`IExtensionLifecycleHooks` 用于在安装、激活、停用、卸载前后接入业务逻辑。当前 `GeneralExtensionHost` 已在安装前后调用 `OnBeforeInstallAsync` 和 `OnAfterInstallAsync`;激活、停用、卸载钩子可供业务层封装对应流程时复用。 - -```csharp -public sealed class MyLifecycleHooks : DefaultExtensionLifecycleHooks -{ - public override Task OnBeforeInstallAsync( - ExtensionMetadata extension, - string? packagePath, - CancellationToken cancellationToken = default) - { - Console.WriteLine($"Installing {packagePath}"); - return Task.FromResult(true); - } -} +``` +单扩展设置 > 全局设置 > 默认值 (false) ``` -### 下载队列 +### 平台兼容性速查 -`DownloadQueueManager` 提供独立队列类型,默认最大并发数为 3,支持 `Enqueue`、`GetTask`、`CancelTask`、`GetActiveTasks` 和 `DownloadStatusChanged` 事件。当前队列管理器本身只负责队列状态和并发槽位,真实下载逻辑由宿主更新流程中的 `ExtensionHttpClient` 完成;如果要做应用级并发下载,可以在业务层组合队列、HTTP 客户端和安装流程。 +| 枚举值 | 数值 | 说明 | +| --- | --- | --- | +| `TargetPlatform.None` | 0 | 不匹配任何平台 | +| `TargetPlatform.Windows` | 1 | Windows | +| `TargetPlatform.Linux` | 2 | Linux | +| `TargetPlatform.MacOS` | 4 | macOS | +| `TargetPlatform.All` | 7 | 所有平台 (Windows \| Linux \| MacOS) | --- -## 最佳实践 +## 相关资源 -1. 生产环境始终使用 `.zip` 扩展包,并采用 `{Name}_{Version}.zip` 命名。 -2. 服务端 `Hash` 建议填写 ZIP 的 SHA256,让 `UpdateExtensionAsync` 自动校验完整性。 -3. `HostVersion`、`MinHostVersion`、`MaxHostVersion` 使用标准可解析版本号。 -4. 本地 catalog 使用每个扩展独立 `manifest.json`,不要再按旧文档维护单个 `catalog.json`。 -5. `SupportedPlatforms` 按实际 OS 能力填写,不要为了省事全部写 `All`。 -6. 依赖扩展必须能被同一个服务端通过 ID 查询和下载,否则递归安装会失败。 -7. 大型扩展建议服务端支持 HTTP Range,并在 UI 中展示 `ExtensionUpdateStatusChanged` 的进度。 -8. 自动更新开关只是策略状态,不是后台任务调度器;扫描、定时、灰度和审批应由应用层控制。 +- [扩展管理示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Hub/Samples/ExtensionSample.cs) +- [GeneralUpdate 仓库](https://github.com/GeneralLibrary/GeneralUpdate) +- [打包指南](../guide/Packaging.md) diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Bowl.md b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Bowl.md index bd21765..fda1f9f 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Bowl.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Bowl.md @@ -4,58 +4,134 @@ sidebar_position: 3 # GeneralUpdate.Bowl -## Overview +**Namespace:** `GeneralUpdate.Bowl` | **Main Entry Point:** `new Bowl().LaunchAsync(BowlContext, CancellationToken)` | **NuGet Package:** `GeneralUpdate.Bowl` -**GeneralUpdate.Bowl** is the startup guard that runs after an application update. It does not download, unpack, or replace update packages. Instead, it watches the target process when the new files have been installed and the main application is starting. If startup crashes, Bowl captures a dump, writes a failure report, exports diagnostics, and, in upgrade mode, restores the backup directory to the installation directory so users are not left on a broken version. +## 1. Component Overview -**Namespace:** `GeneralUpdate.Bowl` +### 1.1 Introduction -**Assembly:** `GeneralUpdate.Bowl.dll` +**GeneralUpdate.Bowl** is a startup watchdog component that runs after an upgrade completes. It does not download, extract, or replace update packages. Instead, after new version files are deployed and the main process is about to start or has started, it monitors the target process for startup crashes. If a crash is detected, it generates a memory dump, writes a failure report JSON, exports system diagnostics, and in Upgrade mode automatically restores the backup directory to the install directory, preventing users from being stuck on a non-bootable new version. -**Current main entry:** `new Bowl().LaunchAsync(BowlContext context, CancellationToken ct = default)` +**Core Capabilities:** -## Navigation - -| Topic | Use it for | +| Capability | Description | +| --- | --- | +| Process Crash Monitoring | Attaches to target process via ProcDump (Windows/Linux) or lldb (macOS) to catch unhandled startup exceptions | +| Memory Dump Snapshots | Supports Full / Mini / Heap dump types, selectable by file size and completeness | +| Crash Report Generation | Auto-generates `{version}_fail.json` report with monitoring parameters and tool output | +| System Diagnostics Export | On Windows, exports driver list, system info, and recent system event logs | +| Automatic Rollback | In Upgrade mode, copies backup directory back to install directory for one-click rollback | +| Failed Version Marking | Writes `UpgradeFail` marker; Core skips this version until server provides a higher one | +| Event Callback | `OnCrash` callback for uploading diagnostics, notifying users, or recording audit info | +| Standalone Monitoring | Normal mode only captures crashes and generates reports without auto-restore | + +**Business Problems Solved:** +- New version crashes during startup after upgrade, leaving users unable to use the app and unable to roll back +- Developers lack crash site information (dump, system environment) to diagnose "won't start after upgrade" issues +- Need automated rollback mechanism to reduce upgrade risk and avoid manual intervention + +**Use Cases:** +- Desktop app post-upgrade startup health check with automatic rollback protection +- General process startup crash monitoring and diagnostic collection +- Automatic diagnostics after CI/CD smoke test failures + +### 1.2 Environment & Dependencies + +| Item | Description | | --- | --- | -| [Lifecycle placement](#lifecycle-placement) | Where Bowl belongs in the update flow | -| [Quick start](#quick-start) | Start monitoring with the current `BowlContext` API | -| [Crash detection and recovery flow](#crash-detection-and-recovery-flow) | What Bowl does after a crash | -| [BowlContext options](#bowlcontext-options) | Configuration fields and recommended values | -| [Output files](#output-files) | Where dumps, reports, diagnostics, and trace logs are written | -| [Crash callback](#crash-callback) | Upload reports or notify users on crash | -| [Trace logging switch](#trace-logging-switch) | Disable tracing for performance-sensitive scenarios | -| [Platform differences](#platform-differences) | Windows, Linux, and macOS behavior | -| [Recovery scenario](#recovery-scenario) | A practical failed-update rollback example | -| [Migrating from the old API](#migrating-from-the-old-api) | Move from `MonitorParameter` to `BowlContext` | +| **Version** | `10.5.0-beta.2` | +| **Target Framework** | `netstandard2.0` (.NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 5+) | +| **Dependencies** | `System.Collections.Immutable`, `System.Text.Json` | +| **Bundled Tools** | Windows: `procdump.exe` / `procdump64.exe` / `procdump64a.exe`; Linux: `procdump` deb/rpm + `install.sh`; macOS: `/usr/bin/lldb` | +| **Compatibility** | Windows (full) / Linux (deb/rpm distros) / macOS (basic, SIP and debug permissions apply) | -## Lifecycle placement +--- -In the full GeneralUpdate flow, Bowl belongs **after file replacement and before users rely on the newly installed version**: +## 2. Feature List + +| Feature | Description | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| Upgrade Mode Monitoring | Monitor new version startup crash, auto-restore backup, mark failed version | Core | Recommended | `WorkModel = "Upgrade"` | +| Standalone Monitoring | Capture crash and generate report only, no auto-restore | Core | Optional | `WorkModel = "Normal"` | +| Full Dump | Complete memory snapshot, most detail | Core | Optional | `DumpType.Full`, largest file | +| Mini Dump | Small memory snapshot, faster generation | Core | Optional | `DumpType.Mini`, recommended for production | +| Heap Dump | Mini dump with heap information | Core | Optional | `DumpType.Heap`, between Mini and Full | +| Crash Report JSON | Auto-generate structured crash report file | Core | Automatic | Output to `FailDirectory` | +| System Diagnostics Export | Export driver list, system info, event logs (Windows) | Extended | Automatic | Windows only | +| Auto Backup Restore | Copy backup directory back to install directory on crash | Core | Optional | `AutoRestore = true` | +| Failed Version Marking | Write upgrade failure version; Core skips subsequently | Core | Automatic | Upgrade mode only | +| Crash Callback | Business callback triggered on crash detection | Extended | Optional | `OnCrash` callback function | +| Trace Logging | `GeneralTracer` runtime diagnostic logs | Extended | Optional | Enabled by default, can be disabled | -1. Core obtains update information, downloads packages, validates them, and applies updates. -2. The Core/Upgrade process prepares to start the main application. -3. Bowl starts as guard logic, attaches to the target process, and waits for startup exceptions. -4. If the main application starts normally, no dump is produced and Bowl returns the monitoring result. -5. If the main application crashes during startup, Bowl runs the failure pipeline and restores the backup when configured. +--- -In the current Core code, the Windows `UpdateStrategy` starts the main application after the update and also starts the configured Bowl helper process. The Linux/macOS Core strategies do not provide the same automatic Bowl helper launch, so use your launcher, service script, or a separate process to call `LaunchAsync` explicitly on those platforms. +## 3. API Configuration Reference -:::tip -Bowl is a post-update health check and rollback guard. It is not firmware recovery, OS restore, or an update package installer. Its scope is application startup crash diagnostics and application-directory backup restoration. -::: +### 3.1 Configuration Properties (Props) -## Quick start +**BowlContext:** -### Install +| Field | Type | Default | Required | Values | Description | +| --- | --- | --- | --- | --- | --- | +| `ProcessNameOrId` | `string` | — | Yes | Process name or PID | Target process to monitor | +| `DumpFileName` | `string` | — | Yes | Valid filename | Dump output filename, e.g., `"{version}_fail.dmp"` | +| `FailFileName` | `string` | — | Yes | Valid filename | Crash report JSON filename, e.g., `"{version}_fail.json"` | +| `TargetPath` | `string` | — | Yes | Valid directory path | App install root; backup restored here on crash | +| `FailDirectory` | `string` | — | Yes | Valid directory path | Failure artifact output, e.g., `{TargetPath}/fail/{version}` | +| `BackupDirectory` | `string` | — | Recommended | Valid directory path | Pre-upgrade backup; must exist when `AutoRestore = true` | +| `WorkModel` | `string` | `"Upgrade"` (after `Normalize()`) | Optional | `"Upgrade"` / `"Normal"` | Work mode: upgrade rollback / standalone monitoring | +| `ExtendedField` | `string` | `null` | Optional | — | Extended field, typically the version number | +| `TimeoutMs` | `int` | `30000` (after `Normalize()`) | Optional | Positive integer (ms) | Monitoring subprocess timeout | +| `DumpType` | `DumpType` | `DumpType.Full` (after `Normalize()`) | Optional | `Full(0)`, `Mini(1)`, `Heap(2)` | Dump capture type | +| `AutoRestore` | `bool` | `false` | Optional | `true` / `false` | Auto-restore backup; set `true` in upgrade mode | +| `OnCrash` | `Func?` | `null` | Optional | — | Crash event callback, fires only when dump detected | -```bash -dotnet add package GeneralUpdate.Bowl -``` +**DumpType Enum:** + +| Value | Code | Windows ProcDump Flag | Characteristics | +| --- | --- | --- | --- | +| `Full` | `0` | `-ma` | Complete memory snapshot, most detail, largest file | +| `Mini` | `1` | `-mm` | Small snapshot, fast generation, recommended for production | +| `Heap` | `2` | `-mh` | Small dump with heap info, between Mini and Full | + +### 3.2 Instance Methods + +**Bowl:** + +| Method | Parameters | Returns | Use Case | Notes | +| --- | --- | --- | --- | --- | +| `LaunchAsync(BowlContext, CancellationToken)` | `context` — execution context (call `Normalize()` first); `ct` — cancellation token | `Task` | Start crash monitoring daemon | Three phases: prepare → run → handle crash if dump found | + +**BowlContext:** + +| Method | Parameters | Returns | Use Case | Notes | +| --- | --- | --- | --- | --- | +| `Normalize()` | None | `BowlContext` | Apply defaults (`WorkModel` → `"Upgrade"`, `TimeoutMs` → `30000`, `DumpType` → `Full`) | Returns new instance; does not modify original | + +### 3.3 Callback Events + +| Event | Callback Parameters | Trigger Timing | Usage Notes | +| --- | --- | --- | --- | +| `OnCrash` | `CrashInfo` — `DumpFilePath`, `CrashReportPath`, `Version`, `ExitCode`; `CancellationToken` | After dump file is detected | Use for uploading diagnostics, notifying users "new version rolled back", recording audit. Callback exceptions are logged to trace but don't block `LaunchAsync` return | + +--- -### Upgrade-mode monitoring +## 4. Advanced Examples -Upgrade mode is intended for an upgrader or Bowl helper process. `BackupDirectory` points to the pre-update backup, `TargetPath` points to the current installation directory, and `ExtendedField` usually stores the version being monitored. +### 4.1 Extension Points Overview + +Bowl's primary extension point is the `BowlContext.OnCrash` callback. Internal strategy interfaces (`IBowlStrategy`, `ICrashReporter`, `ISystemInfoProvider`) are internal; contribute to the GeneralUpdate repository for new platform support or custom report logic. + +| Extension Point | Type | Description | +| --- | --- | --- | +| `OnCrash` Callback | `Func?` | Configured in `BowlContext`, triggered on crash | +| `GeneralTracer` Logging | Static class | Toggle via `SetTracingEnabled` | + +### 4.2 Examples by Scenario + +#### Scenario 1: Upgrade Mode Monitoring + Crash Alert Upload + +**Description:** Desktop app upgrades to new version; Bowl monitors for startup crash. On crash, auto-rollback and upload diagnostics to internal log platform. ```csharp using GeneralUpdate.Bowl; @@ -73,28 +149,38 @@ var context = new BowlContext BackupDirectory = Path.Combine(installPath, version), WorkModel = "Upgrade", ExtendedField = version, - TimeoutMs = 30_000, - DumpType = DumpType.Full, + TimeoutMs = 60_000, + DumpType = DumpType.Mini, AutoRestore = true, - OnCrash = (info, ct) => + OnCrash = async (info, ct) => { - Console.WriteLine($"Crash dump: {info.DumpFilePath}"); - Console.WriteLine($"Crash report: {info.CrashReportPath}"); - return Task.CompletedTask; + var zipPath = Path.Combine( + Path.GetDirectoryName(info.DumpFilePath)!, + $"crash_{info.Version}_{DateTimeOffset.Now:yyyyMMddHHmmss}.zip"); + System.IO.Compression.ZipFile.CreateFromDirectory( + Path.GetDirectoryName(info.DumpFilePath)!, zipPath); + + using var client = new HttpClient(); + var content = new MultipartFormDataContent(); + content.Add(new StreamContent(File.OpenRead(zipPath)), "file", Path.GetFileName(zipPath)); + content.Add(new StringContent(info.Version), "version"); + content.Add(new StringContent(info.ExitCode.ToString()), "exitCode"); + await client.PostAsync("https://logs.example.com/api/crash", content, ct); + + Console.WriteLine($"Version {info.Version} crashed (exit code {info.ExitCode})."); + Console.WriteLine("Diagnostics uploaded. Previous version restored."); } }; BowlResult result = await new Bowl().LaunchAsync(context); if (result.DumpCaptured && result.Restored) -{ - Console.WriteLine("The upgraded version crashed and the backup was restored."); -} + Console.WriteLine("Crash detected and backup restored."); ``` -### Standalone monitoring +#### Scenario 2: Standalone Monitoring (Non-Upgrade) -`Normal` mode only captures crash artifacts and invokes callbacks. It does not restore backups and does not write the `UpgradeFail` failed-version marker. +**Description:** General-purpose worker process startup crash monitoring; collect diagnostics only, no auto-rollback. ```csharp var context = new BowlContext @@ -112,193 +198,168 @@ var context = new BowlContext }; BowlResult result = await new Bowl().LaunchAsync(context); + +if (result.DumpCaptured) +{ + Console.WriteLine($"Dump captured at: {result.DumpFilePath}"); + Console.WriteLine($"Report at: {result.CrashReportPath}"); +} ``` -## Crash detection and recovery flow +--- -`LaunchAsync` uses a simple signal: the platform strategy starts the monitoring tool and writes to `FailDirectory`; Bowl then checks whether `{FailDirectory}/{DumpFileName}` exists. If the dump file exists, startup is treated as failed. +## 5. Basic Usage Examples -| Stage | Current implementation | -| --- | --- | -| Prepare monitoring | Selects `WindowsBowlStrategy`, `LinuxBowlStrategy`, or `MacBowlStrategy` based on the OS | -| Capture exception | Windows uses ProcDump; Linux tries to install and call ProcDump; macOS uses basic `lldb` support | -| Detect crash | Checks for the configured dump file in `FailDirectory` | -| Generate report | Writes `{version}_fail.json` with monitoring parameters and tool output | -| Export diagnostics | On Windows, runs `Applications/Windows/export.bat` for driver info, system info, and recent system logs | -| Restore backup | Only when `WorkModel == "Upgrade"` and `AutoRestore == true`, copies `BackupDirectory` back over `TargetPath` | -| Mark failed version | In upgrade mode, writes `UpgradeFail = ExtendedField`; Core later skips updates at or below that failed version | -| Notify application | If `OnCrash` is configured, passes dump path, report path, version, and exit code | +### 5.1 Quick Start (Minimal Demo) -`TimeoutMs` is the monitoring child process timeout. If the timeout expires and no dump exists, Bowl does not run the recovery pipeline. In integrations, treat `DumpCaptured` as the primary crash signal instead of relying only on `Success`. +```csharp +using GeneralUpdate.Bowl; -## BowlContext options +var context = new BowlContext +{ + ProcessNameOrId = "MyApp.exe", + DumpFileName = "fail.dmp", + FailFileName = "fail.json", + TargetPath = AppDomain.CurrentDomain.BaseDirectory, + FailDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "fail"), + BackupDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "backup"), +}.Normalize(); -| Option | Meaning | Recommendation | -| --- | --- | --- | -| `ProcessNameOrId` | Target process name or PID | Process name works on Windows; PID is preferred on Linux | -| `DumpFileName` | Dump file name | Include the version, for example `2.0.0_fail.dmp` | -| `FailFileName` | Crash report JSON file name | Match the dump version, for example `2.0.0_fail.json` | -| `TargetPath` | Current application installation root | Backup restoration copies files back here | -| `FailDirectory` | Failure artifact output directory | Use `Path.Combine(TargetPath, "fail", version)` | -| `BackupDirectory` | Pre-update backup directory | Must exist and be complete when `AutoRestore` is enabled | -| `WorkModel` | `Upgrade` or `Normal` | Use `Upgrade` for post-update rollback; use `Normal` for standalone crash capture | -| `ExtendedField` | Extension field, currently used mainly as version | Written to `UpgradeFail` in upgrade mode | -| `TimeoutMs` | Monitoring child process timeout | Normalizes to 30000 ms by default; increase for slow-starting apps | -| `DumpType` | `Full`, `Mini`, or `Heap` | Use `Mini` for smaller production artifacts; use `Full` for hard issues | -| `AutoRestore` | Whether to restore backups automatically | Set explicitly to `true` for upgrade rollback | -| `OnCrash` | Single crash callback | Use it to upload reports, notify users, or write business logs | - -### Choosing DumpType - -| Type | Windows ProcDump flag | Characteristics | -| --- | --- | --- | -| `Full` | `-ma` | Most complete data, largest file, best for hard-to-reproduce issues | -| `Mini` | `-mm` | Smaller and faster, a good production default | -| `Heap` | `-mh` | Mini dump with heap information; between Mini and Full | +BowlResult result = await new Bowl().LaunchAsync(context); +Console.WriteLine($"Success: {result.Success}, Dump captured: {result.DumpCaptured}"); +``` -## Output files +### 5.2 Basic Parameter Combination -For failed upgrades, store artifacts by version: +```csharp +var version = "2.0.0"; +var installPath = @"C:\Program Files\MyApp"; -```text -MyApp/ - fail/ - 2.0.0/ - 2.0.0_fail.dmp - 2.0.0_fail.json - driverInfo.txt - systeminfo.txt - systemlog.evtx - Logs/ - generalupdate-trace 2026-01-01.log +var context = new BowlContext +{ + ProcessNameOrId = "MyApp.exe", + DumpFileName = $"{version}_fail.dmp", + FailFileName = $"{version}_fail.json", + TargetPath = installPath, + FailDirectory = Path.Combine(installPath, "fail", version), + BackupDirectory = Path.Combine(installPath, version), + WorkModel = "Upgrade", + ExtendedField = version, + TimeoutMs = 30_000, + DumpType = DumpType.Full, + AutoRestore = true, + OnCrash = (info, ct) => + { + Console.WriteLine($"Crash: {info.DumpFilePath}"); + return Task.CompletedTask; + } +}; + +BowlResult result = await new Bowl().LaunchAsync(context); + +if (result.DumpCaptured && result.Restored) + Console.WriteLine("The upgraded version crashed and the backup was restored."); ``` -| File | Source | Contents | -| --- | --- | --- | -| `{version}_fail.dmp` | ProcDump or lldb | Memory snapshot from the crash | -| `{version}_fail.json` | `CrashReporter` | Mapped `BowlContext` parameters and monitoring tool output lines | -| `driverInfo.txt` | Windows `driverquery` | Windows driver list | -| `systeminfo.txt` | Windows `systeminfo` | OS, hardware, memory, and related system information | -| `systemlog.evtx` | Windows `wevtutil` | Windows System event log for the last day | -| `Logs/generalupdate-trace yyyy-MM-dd.log` | `GeneralTracer` | Bowl runtime trace log | +### 5.3 Production-Ready Example -Non-Windows platforms currently do not export `driverInfo.txt`, `systeminfo.txt`, or `systemlog.evtx`, but Bowl still attempts to produce the dump and failure JSON. +Full post-upgrade Bowl daemon workflow integrated into the upgrade process: -The failure JSON is generated by the current `CrashReporter`: +```csharp +using GeneralUpdate.Bowl; -```json -{ - "Parameter": { - "TargetPath": "C:\\Program Files\\MyApp", - "FailDirectory": "C:\\Program Files\\MyApp\\fail\\2.0.0", - "BackupDirectory": "C:\\Program Files\\MyApp\\2.0.0", - "ProcessNameOrId": "MyApp.exe", - "DumpFileName": "2.0.0_fail.dmp", - "FailFileName": "2.0.0_fail.json", - "WorkModel": "Upgrade", - "ExtendedField": "2.0.0" - }, - "ProcdumpOutPutLines": [ - "ProcDump v11.0 - Sysinternals process dump utility", - "[10:00:03] Dump 1 initiated: C:\\Program Files\\MyApp\\fail\\2.0.0\\2.0.0_fail.dmp", - "[10:00:03] Dump count reached." - ] -} -``` +var version = "2.0.0"; +var installPath = @"C:\Program Files\MyApp"; -## Crash callback +var bowlContext = new BowlContext +{ + ProcessNameOrId = "MyApp.exe", + DumpFileName = $"{version}_fail.dmp", + FailFileName = $"{version}_fail.json", + TargetPath = installPath, + FailDirectory = Path.Combine(installPath, "fail", version), + BackupDirectory = Path.Combine(installPath, "backups", version), + WorkModel = "Upgrade", + ExtendedField = version, + TimeoutMs = 45_000, + DumpType = DumpType.Mini, + AutoRestore = true, + OnCrash = async (info, ct) => + { + try + { + using var client = new HttpClient(); + var crashData = new + { + version = info.Version, + exitCode = info.ExitCode, + dumpPath = info.DumpFilePath, + reportPath = info.CrashReportPath, + timestamp = DateTimeOffset.UtcNow + }; + await client.PostAsJsonAsync( + "https://monitor.mycompany.com/api/crash-report", crashData, ct); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to upload crash report: {ex.Message}"); + } + } +}; -`OnCrash` is a single crash callback. It fires only after Bowl detects a dump and receives a `CrashInfo` payload: +var result = await new Bowl().LaunchAsync(bowlContext); -```csharp -public readonly record struct CrashInfo +if (result.Success) + Console.WriteLine("New version started successfully."); +else if (result.DumpCaptured) { - public string DumpFilePath { get; init; } - public string CrashReportPath { get; init; } - public string Version { get; init; } - public int ExitCode { get; init; } + Console.WriteLine($"New version crashed (exit code: {result.ExitCode})."); + Console.WriteLine($"Backup restored: {result.Restored}"); + Console.WriteLine($"Dump: {result.DumpFilePath}"); } ``` -Common uses: - -| Scenario | Approach | -| --- | --- | -| Upload diagnostics | Package the dump, JSON report, and Windows diagnostics, then upload them to your internal log platform | -| Notify users | Tell the user the new version failed to start and the previous version was restored | -| Audit business events | Record `Version`, `ExitCode`, and report paths in your own log system | - -Callback exceptions are written to the trace log and do not stop `LaunchAsync` from returning its final `BowlResult`. Use the `CancellationToken` for cancellation. +--- -## Trace logging switch +## 6. Global Configuration -Bowl uses the public `GeneralTracer` for runtime tracing. By default it writes to the console and creates a daily file under the runtime directory: +Bowl does not rely on global configuration files. All configuration is passed via `BowlContext`. Logging behavior is controlled through the static `GeneralTracer` class. -```text -Logs/generalupdate-trace yyyy-MM-dd.log -``` - -For startup-performance, disk-write, or console-output sensitive scenarios, disable tracing: +### Logging Toggle ```csharp GeneralTracer.SetTracingEnabled(false); - var result = await new Bowl().LaunchAsync(context); - GeneralTracer.SetTracingEnabled(true); ``` -Disabling tracing reduces Bowl's own diagnostic logs, but dump and failure JSON generation do not depend on this switch. Keep tracing enabled while investigating update failures; disable it in stable production paths according to your performance policy. - -## Platform differences - -| Platform | Monitoring tool | Diagnostic export | Notes | -| --- | --- | --- | --- | -| Windows | Bundled ProcDump: `procdump.exe`, `procdump64.exe`, `procdump64a.exe` | Supports `driverInfo.txt`, `systeminfo.txt`, `systemlog.evtx` | Tool path comes from `TargetPath/Applications/Windows`; sufficient dump permissions are required | -| Linux | Bundled deb/rpm packages + `install.sh`, then `procdump` | Currently no-op | Package mapping covers Ubuntu, Debian, RHEL, CentOS, Fedora, and ClearOS; the script may require `sudo` | -| macOS | `/usr/bin/lldb` | Currently no-op | Affected by SIP, debugging permission, and signing policy; current support is basic | - -The NuGet package outputs `Applications/**/*` as content. If you self-deploy, make sure these files are not trimmed, otherwise the platform strategy may report that monitoring tooling is unavailable or fail to start the tool process. - -## Recovery scenario - -Suppose a user upgrades from `1.0.0` to `2.0.0` and the new version crashes immediately: - -1. The update flow first stores the previous version in `BackupDirectory`, for example `C:\Program Files\MyApp\2.0.0`. -2. The new version is copied into `TargetPath`. -3. The main application starts, while Bowl monitors startup with `ProcessNameOrId = "MyApp.exe"`. -4. ProcDump captures the unhandled exception and writes `fail\2.0.0\2.0.0_fail.dmp`. -5. Bowl writes `2.0.0_fail.json`; on Windows it also exports driver info, system info, and recent system logs. -6. Because `WorkModel == "Upgrade"` and `AutoRestore == true`, Bowl copies `BackupDirectory` back over `TargetPath`. -7. Bowl writes `UpgradeFail = "2.0.0"`; the next Core check skips this known-failed version while the server still returns `2.0.0` or lower, until a higher version is available. -8. `OnCrash` can upload the diagnostic package or tell the user that the app has been restored to a working version. - -The goal is to reduce the risk of "the update succeeded but the new app cannot start": users return to a runnable version, and developers get the dump plus context needed to fix the issue. - -## Migrating from the old API +### Output File Structure -The old `GeneralUpdate.Bowl.Strategys.MonitorParameter` type is obsolete. Prefer `BowlContext` and the async entry point: +```text +MyApp/ + fail/ + 2.0.0/ + 2.0.0_fail.dmp # Memory dump + 2.0.0_fail.json # Crash report JSON + driverInfo.txt # Windows driver list + systeminfo.txt # OS/hardware/memory info + systemlog.evtx # Windows system event log + Logs/ + generalupdate-trace 2026-01-01.log # Bowl trace logs +``` -```csharp -var oldParameter = new GeneralUpdate.Bowl.Strategys.MonitorParameter -{ - ProcessNameOrId = "MyApp.exe", - DumpFileName = "2.0.0_fail.dmp", - FailFileName = "2.0.0_fail.json", - TargetPath = installPath, - FailDirectory = Path.Combine(installPath, "fail", "2.0.0"), - BackupDirectory = Path.Combine(installPath, "2.0.0"), - WorkModel = "Upgrade", - ExtendedField = "2.0.0" -}; +### Platform Differences -BowlContext context = Bowl.MapToContext(oldParameter); -BowlResult result = await new Bowl().LaunchAsync(context); -``` +| Platform | Monitoring Tool | Diagnostics Export | Notes | +| --- | --- | --- | --- | +| Windows | Built-in ProcDump (`procdump.exe`/`procdump64.exe`/`procdump64a.exe`) | Supports `driverInfo.txt`, `systeminfo.txt`, `systemlog.evtx` | Requires sufficient permissions for dumps | +| Linux | Built-in deb/rpm + `install.sh` ProcDump install | Currently no-op | Supports Ubuntu/Debian/RHEL/CentOS/Fedora/ClearOS | +| macOS | `/usr/bin/lldb` | Currently no-op | Affected by SIP, debug permissions, signing policy | -For new code, create `BowlContext` directly instead of depending on `MonitorParameter`. +--- -## Related resources +## Related Resources -- **Samples:** [GeneralUpdate-Samples / Bowl](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Bowl) -- **Main repository:** [GeneralUpdate](https://github.com/GeneralLibrary/GeneralUpdate) +- [Bowl Sample Code](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Bowl) +- [GeneralUpdate Repository](https://github.com/GeneralLibrary/GeneralUpdate) +- [Dump Guide](../guide/Dump.md) diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md index d430e57..226fd93 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md @@ -4,785 +4,245 @@ sidebar_position: 5 # GeneralUpdate.Core -`GeneralUpdate.Core` is the update execution core of GeneralUpdate. It provides the programmable bootstrapper, configuration model, event model, download extension points, lifecycle hooks, reporting, differential pipeline, and platform strategy extension model. This page focuses on component APIs and extension examples; end-to-end workflows belong in the cookbook. +**Namespace:** `GeneralUpdate.Core` | **Main Entry Point:** `GeneralUpdateBootstrap` | **NuGet Package:** `GeneralUpdate.Core` -**Namespace:** `GeneralUpdate.Core` -**Primary entry point:** `GeneralUpdateBootstrap` -**NuGet package:** `GeneralUpdate.Core` +## 1. Component Overview -```bash -dotnet add package GeneralUpdate.Core -``` +### 1.1 Introduction -## Outline and topic map {#knowledge-map} +**GeneralUpdate.Core** is the update execution engine of the GeneralUpdate ecosystem, responsible for full lifecycle update management of client applications. It provides a programmable launcher, configuration models, event notification system, download subsystem (supporting concurrency, resume, retry, verification, and post-processing pipelines), differential patch pipeline, version write-back, IPC process communication, and platform strategy extensions. -If this is your first time reading the Core documentation, start from this map and jump to the topic you need. The page is organized as "entry and configuration -> execution strategies -> differential/download/concurrency -> extension points -> tooling relationship". +**Core Capabilities:** -| What you want to learn | Read | +| Capability | Description | | --- | --- | -| What Core does and does not own | [Responsibility boundary](#responsibility-boundary) | -| How to start a standard update | [GeneralUpdateBootstrap](#entry-point-generalupdatebootstrap), [Standard update strategy](#standard-update-strategy) | -| How to use minimal update configuration | [generalupdate.manifest.json](#application-identity-manifest-generalupdatemanifestjson), [Minimal configuration model](#minimal-configuration-model), [Using the manifest with the bootstrap](#using-the-manifest-with-the-bootstrap) | -| How `Client`, `Upgrade`, `OssClient`, and `OssUpgrade` differ | [Execution strategy overview](#execution-strategies) | -| When silent update downloads and when it replaces files | [Silent update strategy](#silent-update-strategy) | -| Which differential algorithms exist and how to choose one | [Differential algorithms and patch pipeline](#differential-pipeline) | -| How download and patch work use concurrency and threads | [Download concurrency and differential parallelism](#download-diff-concurrency) | -| How to receive update notifications | [Event API](#event-api) | -| How to disable logs to reduce runtime overhead | [Logging and performance](#logging-performance) | -| How to extend downloads, validation, auth, hooks, or platform strategy | [Extension points](#extension-points) | -| How Core consumes artifacts generated by Tools | [Relationship with GeneralUpdate.Tools](#relationship-with-generalupdatetools) | - -## Responsibility boundary - -Core executes updates. It does not generate update packages or manage the server backend. - -| Capability | Owned by Core | Notes | -| --- | --- | --- | -| Load update configuration | Yes | Via `UpdateRequest`, JSON config, `SetSource`, or IPC. | -| Query server version metadata | Yes | `Client` / `OssClient` builds the download plan. | -| Download update packages | Yes | Source, executor, policy, pipeline, and orchestrator are replaceable. | -| Verify and apply patches | Yes | Hash verification, archive handling, and differential patching are supported. | -| Replace files and restart app | Yes | `Upgrade` / `OssUpgrade` is used by the updater process. | -| Generate differential packages | No | Use `GeneralUpdate.Tools`. | - -## Entry point: GeneralUpdateBootstrap - -`GeneralUpdateBootstrap` is Core's main facade. It inherits `AbstractBootstrap`, so it includes both its own methods and the extension registration methods from the base class. - -```csharp -using GeneralUpdate.Core; - -var bootstrap = new GeneralUpdateBootstrap(); -``` - -### Method overview - -| Method | Purpose | Typical use | -| --- | --- | --- | -| `LaunchAsync()` | Starts the update workflow using the current `Option.AppType`. | Final call in every Core scenario. | -| `Cancel()` | Requests cancellation of the current update operation. | A Cancel button in a desktop UI. | -| `SetConfig(UpdateRequest)` | Configures updates with a strongly typed object. | Explicit main-app configuration. | -| `SetConfig(string)` | Loads `UpdateRequest` from a JSON file. | `update_config.json` or another config file. | -| `SetSource(...)` | Supplies only URL, secret, report URL, scheme, and token. | Lightweight setup. | -| `SetOption(Option, T)` | Sets runtime options. | Role, timeout, concurrency, patching, silent update. | -| `UseDiffPipeline(Action)` | Customizes the differential pipeline. | Replace differ, set parallelism, collect patch progress. | -| `AddListenerUpdatePrecheck(Func)` | Runs a pre-download decision callback. | Disk-space check, network check, user confirmation. | -| `AddListener...` | Registers a single event callback. | UI, logs, telemetry. | -| `AddEventListener()` | Registers all event callbacks through a listener class. | Encapsulated event handling. | - -### LaunchAsync - -```csharp -public Task LaunchAsync() -``` - -`LaunchAsync` reads `Option.AppType` and selects a role strategy: - -| `Option.AppType` | Strategy | Description | -| --- | --- | --- | -| `AppType.Client` | `ClientStrategy` | Main-app side: check version, download packages, prepare context, launch updater. | -| `AppType.Upgrade` | `UpdateStrategy` | Updater side: read IPC context and replace files. | -| `AppType.OssClient` | `OssStrategy` | OSS main-app update role. | -| `AppType.OssUpgrade` | `OssStrategy` | OSS updater role. | - -```csharp -await new GeneralUpdateBootstrap() - .SetOption(Option.AppType, AppType.Upgrade) - .AddListenerException((_, e) => Console.WriteLine(e.Exception)) - .LaunchAsync(); -``` - -When the updater is launched by the main app, Core restores the update context through encrypted file IPC. In that case the updater normally does not call `SetConfig` again. - -## Execution strategy overview {#execution-strategies} - -Core includes three upper-level execution strategies: standard update, OSS update, and silent update. They are not separate public APIs. `LaunchAsync()` selects them from `Option.AppType`, `Option.Silent`, and the current configuration. - -| Strategy | Trigger | Main roles | Best for | -| --- | --- | --- | --- | -| [Standard update strategy](#standard-update-strategy) | `Option.AppType = AppType.Client` or `AppType.Upgrade`, with `Option.Silent = false` | Main app checks/downloads; updater replaces files | Desktop apps with a version-check API, standard package update, and manifest version write-back | -| [OSS update strategy](#oss-update-strategy) | `Option.AppType = AppType.OssClient` or `AppType.OssUpgrade` | Main app downloads OSS version config; updater downloads and extracts assets | Apps whose version config and packages are hosted on object storage/CDN | -| [Silent update strategy](#silent-update-strategy) | `Option.AppType = AppType.Client` and `Option.Silent = true` | Main app polls in the background; updater starts on process exit | Products that want no-interruption download preparation and deferred replacement | - -### Standard update strategy {#standard-update-strategy} - -Standard update is implemented by `ClientStrategy` and `UpdateStrategy`. `ClientStrategy` runs in the main application and discovers the local manifest, requests server versions, builds a download plan, downloads packages, prepares encrypted IPC context, and launches the updater. `UpdateStrategy` runs in the updater process and reads IPC context, extracts packages, applies differential patches, replaces files, writes versions back, and optionally starts the main app. - -The core standard workflow is: - -1. The main app calls `SetConfig(...)` or `SetSource(...)`, then `LaunchAsync()`. -2. Core automatically reads `generalupdate.manifest.json` from the install directory and fills identity fields that were not explicitly provided, including `MainAppName`, `UpdateAppName`, `ClientVersion`, `UpgradeClientVersion`, `ProductId`, and `InstallPath`. -3. `ClientStrategy` uses `DownloadSource` to get server assets and compares main-app and updater versions independently. -4. `DownloadPlanBuilder` filters frozen packages, sorts by version, checks `MinClientVersion`, and builds the download plan. -5. Download uses `IDownloadOrchestrator` to download assets in batch. The default orchestrator supports concurrency, resume, retry, and SHA256 verification. -6. Core splits assets by `AppType` into updater packages and main-app packages, then dispatches by scenario. -7. Updater packages can be applied from the main-app side first and write back `UpgradeClientVersion`; main-app packages are passed to the updater through encrypted IPC. -8. After the updater replaces files, Core automatically writes `ClientVersion` back through the manifest system. Application code does not need to maintain the local version manually. - -| Scenario | Condition | Core behavior | -| --- | --- | --- | -| `None` | Neither main app nor updater needs update | Dispatches the no-update event and exits. | -| `UpgradeOnly` | Only the updater needs update | The main app downloads updater packages, applies them to the updater directory, writes back `UpgradeClientVersion`, and keeps running. | -| `MainOnly` | Only the main app needs update | The main app downloads main-app packages, writes IPC context, and launches the updater to replace main-app files. | -| `Both` | Both main app and updater need update | Core updates the updater first and writes back `UpgradeClientVersion`, then hands main-app packages to the new updater. | - -```csharp -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret", - reportUrl: "https://update.example.com/api/upgrade/report") - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.PatchEnabled, true) - .SetOption(Option.DiffMode, DiffMode.Parallel) - .SetOption(Option.MaxConcurrency, 4) - .LaunchAsync(); -``` - -### OSS update strategy {#oss-update-strategy} - -OSS update uses the same `OssStrategy` with two roles: `OssClient` and `OssUpgrade`. It is intended for hosting the version JSON and update packages on OSS, S3, MinIO, CDN, or a static file server, without depending on the standard version-check API. - -| Role | Local behavior | Key configuration | -| --- | --- | --- | -| `AppType.OssClient` | Downloads the OSS version config from `UpdateUrl` into the install directory, compares the remote latest version with local `ClientVersion`, starts the updater if needed, and exits. | `UpdateUrl` points to the version config file. `MainAppName` / `UpdateAppName` can come from the manifest. | -| `AppType.OssUpgrade` | Reads the local version config or a custom `DownloadSource`, filters assets newer than the local version, downloads them to the install directory, extracts ZIP files, deletes archives, starts the main app, and exits. | The install directory must be writable. Versions in the asset list must be comparable. | - -The OSS version config is saved as `{MainAppName}_versions.json` or `{UpdateAppName}_versions.json`. Registering `DownloadSource()` lets the OSS updater skip the default file-reading path and return assets from your own source. Registering `DownloadOrchestrator()` can fully replace the download process. - -```csharp -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://cdn.example.com/myapp_versions.json", - appSecretKey: "oss-mode-secret") - .SetOption(Option.AppType, AppType.OssClient) - .LaunchAsync(); -``` - -```csharp -await new GeneralUpdateBootstrap() - .SetOption(Option.AppType, AppType.OssUpgrade) - .LaunchAsync(); -``` - -### Silent update strategy {#silent-update-strategy} - -Silent update only applies to `AppType.Client`. When enabled, `LaunchAsync()` enters the silent branch, creates the same fully configured `ClientStrategy` as standard update, sets `LaunchAfterPrepare` to `false`, then hands it to `SilentPollOrchestrator` for background polling. - -Silent mode does not reimplement update logic. It moves "check and download" into the background and defers "start updater and replace files" until process exit. The user can keep using the current process while packages are prepared, and actual replacement happens after the app exits. - -| Stage | Standard update | Silent update | -| --- | --- | --- | -| Version check | Runs once after user/code triggers update | Runs in the background every `Option.SilentPollIntervalMinutes` | -| Download | Downloads immediately after update is found | Downloads in the background after update is found | -| Start updater | Starts after the main app prepares update context | Starts from the `ProcessExit` handler | -| User experience | Explicit "check/update now" flow | No-interruption update preparation | - -```csharp -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret") - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.Silent, true) - .SetOption(Option.SilentPollIntervalMinutes, 30) - .SetOption(Option.LaunchClientAfterUpdate, true) - .LaunchAsync(); -``` - -Silent update still uses registered `IUpdateHooks`, `IUpdateReporter`, download extensions, SSL policy, auth policy, and differential pipeline. It is "silent download preparation", not "silent in-place file replacement"; main-app files should still be replaced by the updater after the main app exits. - -### Cancel - -```csharp -public void Cancel() -``` - -`Cancel` signals the internal `CancellationTokenSource`. Strategies observe the token at safe checkpoints. - -```csharp -private GeneralUpdateBootstrap? _bootstrap; - -async Task StartUpdateAsync(UpdateRequest request) -{ - _bootstrap = new GeneralUpdateBootstrap() - .SetConfig(request) - .AddListenerException((_, e) => Console.WriteLine(e.Exception)); - - await _bootstrap.LaunchAsync(); -} - -void CancelUpdate() -{ - _bootstrap?.Cancel(); -} -``` - -### SetConfig(UpdateRequest) - -```csharp -public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) -``` - -`SetConfig(UpdateRequest)` validates the request and maps it into the internal `UpdateContext`. For non-`Upgrade` roles it also initializes the temporary update directory and blacklist matcher. - -```csharp -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; - -var request = new UpdateRequest -{ - UpdateUrl = "https://update.example.com/api/upgrade/verification", - ReportUrl = "https://update.example.com/api/upgrade/report", - UpdateAppName = "UpgradeSample.exe", - MainAppName = "ClientSample.exe", - InstallPath = AppDomain.CurrentDomain.BaseDirectory, - ClientVersion = "1.0.0", - AppSecretKey = "your-app-secret", - ProductId = "your-product-id", - Files = new List { "appsettings.json" }, - Formats = new List { ".log", ".tmp" }, - Directories = new List { "logs", "cache" } -}; - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` - -### SetConfig(string) - -```csharp -public GeneralUpdateBootstrap SetConfig(string filePath) -``` - -`SetConfig(string)` reads a UTF-8 JSON `UpdateRequest`. A bare filename is resolved from the app base directory; relative and absolute paths are used as provided. - -```json -{ - "updateUrl": "https://update.example.com/api/upgrade/verification", - "reportUrl": "https://update.example.com/api/upgrade/report", - "updateAppName": "UpgradeSample.exe", - "mainAppName": "ClientSample.exe", - "installPath": "C:\\Program Files\\MyApp", - "clientVersion": "1.0.0", - "appSecretKey": "your-app-secret", - "productId": "your-product-id" -} -``` - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig("update_config.json") - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` - -### SetSource - -```csharp -public GeneralUpdateBootstrap SetSource( - string updateUrl, - string appSecretKey, - string? reportUrl = null, - string? scheme = null, - string? token = null) -``` - -`SetSource` is a lightweight entry point when identity metadata is provided by `generalupdate.manifest.json`, leaving only the server endpoint and secret in application code. - -```csharp -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret", - reportUrl: "https://update.example.com/api/upgrade/report", - scheme: "Bearer", - token: "access-token") - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` - -### UseDiffPipeline - -```csharp -public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) -``` - -`UseDiffPipeline` customizes differential patch processing. Without it, the bootstrap builds a default pipeline using `BsdiffDiffer`, `DefaultCleanMatcher`, `DefaultDirtyMatcher`, parallelism `2`, and the Core progress reporter. For algorithm differences, patch phases, and concurrency settings, see [Differential algorithms and patch pipeline](#differential-pipeline). - -```csharp -using GeneralUpdate.Core.Differential; -using GeneralUpdate.Core.Models; -using GeneralUpdate.Core.Pipeline; -using GeneralUpdate.Differential.Differ; - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .UseDiffPipeline(builder => - { - builder - .UseDiffer(new StreamingHdiffDiffer()) - .UseCleanMatcher(new DefaultCleanMatcher()) - .UseDirtyMatcher(new DefaultDirtyMatcher()) - .WithParallelism(4) - .WithStopOnFirstError(true) - .WithProgress(new Progress(p => - { - Console.WriteLine($"{p.Completed}/{p.Total}: {p.FileName}"); - })); - }) - .SetOption(Option.PatchEnabled, true) - .LaunchAsync(); -``` - -## Differential algorithms and patch pipeline {#differential-pipeline} - -Core's differential support has two layers: `IBinaryDiffer` defines how a single file generates/applies a patch, and `DiffPipeline` decides which files in a directory need patches, which files are new/deleted, and how multiple files are processed in parallel. Most users only need `Option.PatchEnabled`; tune `UseDiffPipeline(...)` only when you need performance or compatibility control. - -### Differential algorithm types - -| Algorithm / implementation | Default position | Characteristics | Best for | -| --- | --- | --- | --- | -| `BsdiffDiffer` | Default used by `GeneralUpdateBootstrap` | Classic BSDIFF 4.0 algorithm, BZip2 by default, strong patch-format compatibility. | Projects that prioritize compatibility or already use BSDIFF-based packages. | -| `StreamingHdiffDiffer` | Default differ when constructing `DiffPipeline` directly; can also be selected through `UseDiffPipeline` | Uses block-hash indexing for candidate matching, lower typical complexity, Deflate by default, and emits a BSDIFF-compatible patch format readable by Dirty. | Projects with large files that want to reduce patch-generation memory/CPU pressure. | -| Custom `IBinaryDiffer` | Registered with `UseDiffPipeline(builder => builder.UseDiffer(...))` | Fully replaces the single-file binary differ. | Internal patch formats, encrypted patches, or domain-specific binary diff algorithms. | - -`BsdiffDiffer` can also use a custom compression provider. `BZip2CompressionProvider` is the compatible default, `DeflateCompressionProvider` favors speed, and `.NET 6+` can use `BrotliCompressionProvider` for a better compression/decompression balance. Make sure the runtime that applies patches can recognize the generated format version. - -```csharp -using GeneralUpdate.Core.Pipeline; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Differ; - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .UseDiffPipeline(builder => - { - builder - .UseDiffer(new BsdiffDiffer(new DeflateCompressionProvider())) - .WithParallelism(4); - }) - .SetOption(Option.PatchEnabled, true) - .LaunchAsync(); -``` - -### Clean and Dirty phases - -| Phase | Method | Runs on | Purpose | -| --- | --- | --- | --- | -| Clean | `DiffPipeline.CleanAsync(oldDir, newDir, patchDir)` | Publishing/tooling side | Compares old and new directories, creates `.patch` files, copies new files, and writes `generalupdate.delete.json`. | -| Dirty | `DiffPipeline.DirtyAsync(appDir, patchDir)` | Client updater side | Applies `.patch` files to old files, copies new files, and removes old files from the delete manifest. | - -Core mainly consumes the Dirty phase; patch generation should normally be handled by `GeneralUpdate.Tools` or your release pipeline. In standard update, after downloads finish, the updater extracts packages and calls `DiffPipeline.DirtyAsync(...)` through `PatchMiddleware` when `PatchEnabled = true`. - -### Download concurrency and differential parallelism {#download-diff-concurrency} - -Core supports two layers of multithreaded work: the download phase can download multiple assets concurrently, and the differential phase can process multiple file patches in parallel. The built-in standard flow completes the current download plan before entering extract/diff/replace. If you need a finer-grained download/apply pipeline, take over with a custom `IDownloadOrchestrator` or custom `IStrategy`. - -| Layer | Control API | Default behavior | Notes | -| --- | --- | --- | --- | -| Batch download concurrency | `Option.DiffMode` + `Option.MaxConcurrency` | `DiffMode.Serial` forces download concurrency to `1`; `DiffMode.Parallel` uses `MaxConcurrency`, clamped to `1` through `Environment.ProcessorCount * 2`. | `DefaultDownloadOrchestrator` uses `SemaphoreSlim` and supports retry, resume, and verification. | -| Differential file parallelism | `UseDiffPipeline(...WithParallelism(n))` | Bootstrap default is `2`. | `DiffPipeline` creates tasks per file and limits simultaneous patch generation/application with `SemaphoreSlim`. | -| Post-download processing | `DownloadPipeline()` | Default SHA256 verification. | Runs after each successful asset download; can be replaced with decryption, scanning, or extra validation. | - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.DiffMode, DiffMode.Parallel) - .SetOption(Option.MaxConcurrency, 6) - .UseDiffPipeline(builder => - { - builder - .UseDiffer(new StreamingHdiffDiffer()) - .WithParallelism(4); - }) - .LaunchAsync(); -``` - -Higher concurrency is not always better. Increase `Option.MaxConcurrency` when the network is slow but disk is fast; increase `WithParallelism` when there are many patch files and the device has SSD storage; lower both for HDDs, low-end machines, or silent background updates to avoid hurting main-app responsiveness. - -## Configuration model: UpdateRequest - -`UpdateRequest` is the main external configuration object. It inherits `UpdateConfiguration` and validates required fields in `Validate()`. - -### Required or strongly recommended properties - -| Property | Description | -| --- | --- | -| `UpdateUrl` | Version-check API URL. Must be an absolute URL. | -| `UpdateAppName` | Updater executable name. Defaults to `Update.exe`. | -| `MainAppName` | Main application executable name. | -| `ClientVersion` | Current main application version. | -| `AppSecretKey` | Secret shared with the update server. | -| `InstallPath` | Application install directory. Defaults to the current app base directory. | - -### Optional properties - -| Property | Description | -| --- | --- | -| `ReportUrl` | Update status report API. | -| `UpdateLogUrl` | Update log page URL. | -| `UpgradeClientVersion` | Updater application's own version. | -| `ProductId` | Product identifier when one server manages multiple products. | -| `UpdatePath` | Directory containing the updater; falls back to `InstallPath`. | -| `Bowl` | Helper process name to close before update. | -| `Scheme` / `Token` | Request authentication metadata. | -| `Files` | Specific files to skip during update. | -| `Formats` | File extensions to skip, such as `.log`. | -| `Directories` | Directories to skip. | - -### UpdateRequestBuilder - -`UpdateRequestBuilder` provides a fluent builder and validates on `Build()`. - -```csharp -using GeneralUpdate.Core.Configuration; - -var request = new UpdateRequestBuilder() - .SetUpdateUrl("https://update.example.com/api/upgrade/verification") - .SetReportUrl("https://update.example.com/api/upgrade/report") - .SetUpgradeAppName("UpgradeSample.exe") - .SetMainAppName("ClientSample.exe") - .SetClientVersion("1.0.0") - .SetAppSecretKey("your-app-secret") - .SetProductId("your-product-id") - .SetInstallPath(AppDomain.CurrentDomain.BaseDirectory) - .SetFiles(new List { "appsettings.json" }) - .SetFormats(new List { ".log", ".tmp" }) - .SetDirectories(new List { "logs" }) - .Build(); -``` - -`UpdateRequestBuilder.Create()` attempts to load `update_config.json` from the app runtime directory and throws `FileNotFoundException` if it is missing. - -```csharp -var request = UpdateRequestBuilder.Create().Build(); -``` - -## Application identity manifest: generalupdate.manifest.json - -`generalupdate.manifest.json` is the application identity manifest generated by `GeneralUpdate.Tools` and consumed by Core. Its main value is **saving developer setup and maintenance time**: Tools writes stable metadata such as the main executable name, current version, updater executable name, product ID, and updater directory into the manifest, and Core consumes that information at runtime. Application code only needs to provide runtime or sensitive values such as server URLs, secrets, and tokens. - -In other words, with the manifest model, integrating GeneralUpdate no longer requires hand-writing a large complete `UpdateRequest`. Generate `generalupdate.manifest.json` with Tools during publishing, provide a small set of sensitive values at runtime, and the update workflow can start directly. This is the recommended minimal configuration path for Core. - -Place the file in the application install directory, the same directory referenced by `UpdateRequest.InstallPath`. By default, `InstallPath` is `AppDomain.CurrentDomain.BaseDirectory`, so desktop applications normally place the manifest in the main app output root. - -```text -MyProduct/ -├─ ClientSample.exe -├─ generalupdate.manifest.json -└─ update/ - └─ UpgradeSample.exe -``` - -### Manifest structure - -The JSON generated by Tools uses camelCase property names. The Core-side type is `ManifestInfo`. - -```json -{ - "mainAppName": "ClientSample.exe", - "clientVersion": "1.0.0", - "appType": "Client", - "updateAppName": "UpgradeSample.exe", - "upgradeClientVersion": "1.0.0", - "productId": "sample-product", - "updatePath": "update/" -} -``` - -| JSON field | Core field | Description | -| --- | --- | --- | -| `mainAppName` | `MainAppName` | Main application executable name. Used to restart the app after update and identify the current product. | -| `clientVersion` | `ClientVersion` | Current main application version. Core sends it to the server to check whether a main-app update exists. | -| `appType` | `AppType` | Current process role, such as `Client`, `Upgrade`, `OssClient`, or `OssUpgrade`. | -| `updateAppName` | `UpdateAppName` | Updater executable name. Defaults to `Update.exe`. | -| `upgradeClientVersion` | `UpgradeClientVersion` | Updater application's own version. Core uses it to decide whether the updater must be updated first. | -| `productId` | `ProductId` | Product identifier for servers that manage multiple products. | -| `updatePath` | `UpdatePath` | Directory containing the updater. It can be relative to `InstallPath`, for example `update/`. | - -The manifest intentionally does not include `UpdateUrl`, `ReportUrl`, `AppSecretKey`, `Scheme`, or `Token`. This keeps Tools responsible for build-time identity metadata while secrets still come from application code, a configuration service, or the deployment environment. - -### Minimal configuration model {#minimal-configuration-model} - -The manifest model splits update configuration into two parts: - -| Configuration type | Provided by | Why split it this way | -| --- | --- | --- | -| Stable identity metadata | `GeneralUpdate.Tools` generates it into `generalupdate.manifest.json` | These values come from projects, versions, and publish directories. Repeating them in code is error-prone and increases the time needed to onboard every application. | -| Runtime / sensitive values | Application code, configuration service, environment variables, or deployment system | Server endpoints, secrets, and tokens may differ by environment and should not be fixed into the publishable manifest. | - -The most common integration path is: - -1. Generate `generalupdate.manifest.json` with `GeneralUpdate.Tools` and ship it with the application. -2. When starting updates, configure only `UpdateUrl`, `AppSecretKey`, `ReportUrl`, and any required authentication values. -3. Let `GeneralUpdateBootstrap` read the manifest internally, fill application identity, versions, and updater location, then write the local version back after a successful update. - -This keeps the developer-facing configuration down to "sensitive values + a few runtime options". It reduces boilerplate and avoids repeatedly maintaining main-app names, updater names, local versions, and product IDs across applications. - -### How Tools generates the manifest - -The `GeneralUpdate.Tools` configuration flow parses the main-app and updater `.csproj` files, validates version values, and emits `generalupdate.manifest.json`. - -| Tools step | Responsibility | +| Multi-Strategy Update Execution | Built-in standard Client/Upgrade, OSS object storage, and silent background polling strategies | +| Configuration-Driven | Strongly-typed `UpdateRequest` or lightweight `SetSource` entry, with `generalupdate.manifest.json` for minimal configuration | +| Download Subsystem | Pluggable download sources, executors, retry policies, post-processing pipelines, and orchestrators; defaults include concurrent downloads, resume, and SHA256 verification | +| Differential Patch Pipeline | File-level binary diff (BSDIFF 4.0 / Streaming HDiff), directory-level comparison and batch patch distribution with parallel processing | +| Event Notifications | 7 event callbacks (version discovery, download progress, completion, error, exception, etc.) with batch listener registration | +| Extension Points | 10 pluggable interfaces: lifecycle hooks, status reporting, SSL certificate policy, HTTP authentication, download source/policy/executor/pipeline/orchestrator, platform strategy | +| Manifest System | `generalupdate.manifest.json` for auto-discovery of app identity and automatic version write-back | +| IPC Communication | Encrypted file-based context passing between main app and upgrade process | +| SignalR Real-Time Push | Version update push via SignalR (`UpgradeHubService`), supporting peer-to-peer and broadcast, auto-reconnect, multi-event subscription | + +**Business Problems Solved:** +- Desktop apps need reliable auto-update, but hand-writing update logic involves version comparison, download, verification, extraction, file replacement, and process restart +- Full updates for large apps have high bandwidth costs; differential updates reduce download size +- Flexible update strategies needed (silent background, user-triggered, OSS/CDN distribution) +- Upgrade process versioning needs coordination between main app and upgrade process + +**Use Cases:** +- Auto-update for WPF / WinForms / Avalonia / WinUI desktop applications +- Unified version management for enterprise internal tools +- Client apps distributing update packages via CDN / OSS +- Large clients needing differential updates to reduce bandwidth + +### 1.2 Environment & Dependencies + +| Item | Description | | --- | --- | -| `CsprojParseStep` | Parses the main application `.csproj`; if an updater `.csproj` is provided, parses it as well. | -| `SemverValidateStep` | Validates that `ClientVersion` and `UpgradeClientVersion` use semver, for example `1.0.0`. | -| `ManifestBuildStep` | Fills missing `MainAppName` / `UpdateAppName` from `.csproj` `AssemblyName` values. | -| `FileEmitStep` | Writes the manifest to the output directory with the fixed name `generalupdate.manifest.json`. | - -The sample publishing flow in the configuration UI also calls `SamplePublisherService.PublishAsync(...)` to place the main-app output, updater output, and manifest into one runnable sample directory. New users therefore do not need to hand-write a complete `UpdateRequest`: they can generate the manifest with Tools and only add server endpoints and secrets in application code. +| **Version** | `10.5.0-beta.2` | +| **Target Framework** | `netstandard2.0` (.NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 5+) | +| **Dependencies** | `GeneralUpdate.Differential`, `System.Text.Json`, `Microsoft.Extensions.Logging.Abstractions` | +| **Compatibility** | Windows (primary) / Linux / macOS; x86 / x64 / ARM64 | -### Using the manifest with the bootstrap - -With the manifest in place, application code does not need to care about identity fields such as `MainAppName`, `ClientVersion`, `UpdateAppName`, `UpgradeClientVersion`, `ProductId`, or `UpdatePath`, and it does not need to load `generalupdate.manifest.json` manually. The bootstrap reads `InstallPath/generalupdate.manifest.json` internally during the update workflow and carries that identity metadata into version checking, downloading, updater launch, and version write-back. - -When the install directory is the current application directory, pass only the server endpoint and secret to `SetSource`: - -```csharp -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret", - reportUrl: "https://update.example.com/api/upgrade/report") - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` - -If the actual install directory is not the current process base directory, provide `InstallPath` through `UpdateRequest` while still keeping manifest identity fields out of code: - -```csharp -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; - -var request = new UpdateRequest -{ - UpdateUrl = "https://update.example.com/api/upgrade/verification", - ReportUrl = "https://update.example.com/api/upgrade/report", - AppSecretKey = "your-app-secret", - InstallPath = @"C:\Program Files\MyProduct" -}; - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` - -The recommended responsibility split is: - -| Provided by manifest | Provided by code or environment | -| --- | --- | -| `MainAppName`, `ClientVersion`, `UpdateAppName`, `UpgradeClientVersion`, `ProductId`, `UpdatePath` | `UpdateUrl`, `ReportUrl`, `AppSecretKey`, `Scheme`, `Token`, events, extension points, runtime options | - -### Version write-back - -In the `generalupdate.manifest.json` model, the manifest is also the local version state file. Developers generate it with Tools for the first release, but they do not need to write application code that updates the local version after every successful update. After an update succeeds, Core automatically writes the applied version back to the same `generalupdate.manifest.json` under the install directory: - -| Scenario | Field written back | -| --- | --- | -| Main application update completed | `ClientVersion` | -| Updater self-update completed | `UpgradeClientVersion` | - -On the next polling cycle or process start, the bootstrap validates from the latest local version stored in the manifest instead of the build-time version. The purpose of write-back is to move local version maintenance into Core's update workflow, so application code does not need to maintain `ClientVersion` or `UpgradeClientVersion` separately. This requires the install directory to be writable; if the application is installed under a restricted directory, ensure the updater has permission to update the manifest file. - -## Runtime options: Option - -Core uses strongly typed `Option` values and sets them with `SetOption`. - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.MaxConcurrency, 4) - .SetOption(Option.VerifyChecksum, true) - .LaunchAsync(); -``` - -| Option | Type | Default | Description | -| --- | --- | --- | --- | -| `Option.AppType` | `AppType` | `Client` | Current process role. | -| `Option.DiffMode` | `DiffMode` | `Serial` | Execution mode. `Serial` makes the default download orchestrator download serially; `Parallel` allows concurrency based on `Option.MaxConcurrency`. | -| `Option.Encoding` | `Encoding` | `UTF8` | Package processing encoding. | -| `Option.Format` | `Format` | `Zip` | Package archive format. | -| `Option.DownloadTimeout` | `int?` | `30` | Download timeout in seconds. | -| `Option.PatchEnabled` | `bool?` | `true` | Enables differential patch handling. | -| `Option.BackupEnabled` | `bool?` | `true` | Backs up replaced files. | -| `Option.Silent` | `bool` | `false` | Enables silent polling updates. | -| `Option.SilentPollIntervalMinutes` | `int` | `60` | Silent polling interval. | -| `Option.LaunchClientAfterUpdate` | `bool` | `true` | Starts the main app after update. | -| `Option.MaxConcurrency` | `int` | `3` | Max concurrency for the default download orchestrator; the actual value is clamped to a safe range. | -| `Option.EnableResume` | `bool` | `true` | Enables resumable downloads. | -| `Option.RetryCount` | `int` | `3` | Download retry count. | -| `Option.VerifyChecksum` | `bool` | `true` | Verifies downloaded file hashes. | -| `Option.RetryInterval` | `TimeSpan` | `1s` | Delay between retries. | - -Passing `null` to a nullable option removes the custom value and falls back to the default. - -## Events {#event-api} - -Events are for observing update state. Complex business flow should be implemented with `IUpdateHooks` or documented in cookbook workflows. - -### Individual callbacks - -Individual callbacks are useful when you want to subscribe to one notification directly in the bootstrap chain. Core dispatches events through the global `EventManager` by `EventArgs` type. Multiple callbacks can be registered for the same type. If one callback throws, the exception is written through `GeneralTracer` and other callbacks are still invoked. +--- -Callbacks may be raised from the update workflow thread, download task threads, or differential task threads. They are not marshalled back to the UI thread automatically. WPF, Avalonia, WinUI, MAUI, and similar clients should switch to the Dispatcher / SynchronizationContext before touching UI state. Expensive business work should also be queued to background workers so it does not block download or differential parallelism. +## 2. Feature List -| Method | Argument type | Trigger in the current code | Important fields | Recommended use | +| Feature | Description | Type | Required | Notes | | --- | --- | --- | --- | --- | -| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` | Fired by the standard `Client` strategy after version comparison. When no update is needed, it still fires once with `Info.Code = 404` and an empty `Info.Body`; when updates are needed, `Info.Body` contains the `VersionEntry` list to download. | `Info.Code`, `Info.Message`, `Info.Body`; `VersionEntry` includes `RecordId`, `Name`, `Version`, `Url`, `Hash`, `AppType`, `IsForcibly`, `UpgradeMode`, `FromVersion`, `ToVersion`, and more. | Show release notes, update count, forced-update hints, or log version metadata. Do not replace files here. | -| `AddListenerUpdatePrecheck` | `Func` | Runs after `UpdateInfo` and before hooks/download. In the current `ClientStrategy.CanSkip` implementation, returning `true` skips this non-forced update; returning `false` continues. Forced updates do not enter the skip check. | Same input as `UpdateInfoEventArgs`; it can read all `VersionEntry` items involved in this update, including version numbers, release notes, hashes, package URLs, upgrade modes, and cross-version ranges. | Make lightweight pre-download decisions. You can also format the version information into a dialog so the user can read the update contents and decide whether to continue. Use `IUpdateHooks.OnBeforeUpdateAsync` for asynchronous, cancelable, or side-effecting workflows. | -| `AddListenerProgress` | `ProgressEventArgs` | Fired when the default download path reports `DownloadProgress`; also fired when the differential Clean / Dirty pipeline reports `DiffProgress`. Exactly one of `Progress` or `DiffProgress` is non-null in a single event. | Download: `Progress.AssetName`, `BytesDownloaded`, `TotalBytes`, `Percentage`, `Status`. Differential: `DiffProgress.Completed`, `Total`, `CurrentFile`, `Percentage`, `IsComplete`, `Error`. | Update progress bars, status text, downloaded size, and differential patch progress. Prefer this event for default download progress. | -| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` | Fired when `DownloadProgressReporter` receives `DownloadStatus.Completed`. In the default bridge, `Version` carries `AssetName`; custom downloaders may pass their own object. | `Version`, `IsCompleted`. | Mark one asset/reporting item as completed or append a download log entry. Do not treat it as "all downloads completed". | -| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` | Fired once after `DefaultDownloadOrchestrator` waits for all download tasks. With parallel downloads, this happens after every task has completed and failed results have been collected. | `IsAllDownloadCompleted`; `FailedVersions` is the failure detail list, with entries shaped as `(asset, errorMessage)`. | Refresh overall UI, write a failure summary, or decide whether to show a retry entry after all downloads finish. | -| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` | Fired when `DownloadProgressReporter` receives `DownloadStatus.Failed`. In the default bridge, `Version` may be the `AssetName`. | `Exception`, `Version`. | Log one failed asset, show the failed item, or send external monitoring data. Use `MultiAllDownloadCompleted` for final overall success/failure. | -| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` | Compatibility event for legacy download statistics or custom download implementations. The current default download orchestration mainly reports through `AddListenerProgress` and does not synthesize this statistics event. | `Version`, `Remaining`, `Speed`, `TotalBytesToReceive`, `BytesReceived`, `ProgressPercentage`. | Use it only if your downloader still dispatches this event and you need remaining-time/speed UI. New code should prefer `AddListenerProgress`. | -| `AddListenerException` | `ExceptionEventArgs` | Fired when `GeneralUpdateBootstrap`, platform strategies, standard strategy, OSS strategy, or update strategy catches an exception. | `Exception`, `Message`. | Report exceptions, show error text, or write business logs. This notification means Core caught and surfaced the exception; it does not imply automatic retry. | - -Items in `UpdateInfoEventArgs.Info.Body` are packages that Core needs to process after version comparison, app-type filtering, and download-plan construction. They are not merely a raw HTTP response passthrough. Read `VersionEntry` properties directly when you need download URLs, hashes, forced-update flags, or cross-version differential ranges. - -`AddListenerUpdatePrecheck` is easy to misread. In the current code, returning `true` means "skip this non-forced update", not "continue downloading". It fits the "confirm before download" scenario: collect version numbers, release notes, package sizes, and upgrade types from `UpdateInfoEventArgs.Info.Body`, show them in a dialog, then return `false` when the user accepts the update. Return `true` when the user chooses later, disk space is low, or the current network is not allowed. If you only need to display server version metadata and do not need skip/continue control, subscribe to `AddListenerUpdateInfo` instead. +| Standard Client Update | Main app checks version, downloads packages, launches upgrade process | Core | Required | Requires server version check API | +| Standard Upgrade Update | Standalone upgrade process reads IPC context and executes file replacement, diff patches, version write-back | Core | Required | Launched by main app via encrypted IPC | +| OSS Client Update | Download version config from OSS/CDN, compare, launch upgrade process | Core | Optional | Version config hosted on object storage | +| OSS Upgrade Update | OSS-mode upgrade process downloads and extracts resource packages | Core | Optional | Paired with OssClient | +| Silent Background Update | Background polling, silent download, upgrade on process exit | Core | Optional | Set `Option.Silent = true` | +| Differential Patch Pipeline | File-level binary diff generation & application, directory-level batch distribution | Core | Optional | Requires `Option.PatchEnabled = true` | +| Concurrent Downloads | Multi-asset concurrent download with resume & SHA256 verification | Core | Optional | Controlled via `Option.MaxConcurrency` | +| Event Callbacks | 7 event types: version info, progress, completion, errors, exceptions | Core | Optional | Registered via `AddListener*` methods | +| App Identity Manifest | `generalupdate.manifest.json` auto-discovery & version write-back | Extended | Recommended | Generated by `GeneralUpdate.Tools` | +| Custom Download Source | Custom version list and download resource source | Extended | Optional | Implement `IDownloadSource` | +| Custom Download Executor | Custom single-file download (HTTP/FTP/SFTP etc.) | Extended | Optional | Implement `IDownloadExecutor` | +| Custom Retry Policy | Custom retry, timeout, circuit-breaking strategy | Extended | Optional | Implement `IDownloadPolicy` | +| Custom Download Pipeline | Post-download processing (verification, decryption, scanning) | Extended | Optional | Implement `IDownloadPipeline` | +| Custom Download Orchestrator | Fully replace batch download concurrency control | Extended | Optional | Implement `IDownloadOrchestrator` | +| Lifecycle Hooks | Business logic injection: before/after update, download complete, error, before start | Extended | Optional | Implement `IUpdateHooks` | +| Status Reporting | Report update status to your own server | Extended | Optional | Implement `IUpdateReporter` | +| HTTP Authentication | Custom HTTP request authentication headers | Extended | Optional | Implement `IHttpAuthProvider` | +| SSL Certificate Policy | Custom HTTPS certificate validation logic | Extended | Optional | Implement `ISslValidationPolicy` | +| Platform Strategy | Replace platform-level file operations or launch logic | Extended | Optional | Implement `IStrategy` | +| SignalR Real-Time Push | Server proactively pushes version update notifications to connected clients | Extended | Optional | `UpgradeHubService`, namespace `GeneralUpdate.Core.Hubs` | +| Push Reconnect Mechanism | Auto-reconnect on disconnect (random backoff strategy), connection lifecycle management | Extended | Optional | `RandomRetryPolicy` | +| Push Event Subscription | Four events: receive message, online status, reconnect notification, close notification | Extended | Optional | Registered via `AddListener*` methods | -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .AddListenerUpdateInfo((_, e) => - { - Console.WriteLine($"Versions from server: {e.Info?.Body?.Count ?? 0}"); - }) - .AddListenerUpdatePrecheck(e => - { - var versions = e.Info?.Body; - var hasUpdate = (versions?.Count ?? 0) > 0; - var enoughDisk = DriveInfo.GetDrives() - .Where(d => d.IsReady) - .Any(d => d.AvailableFreeSpace > 1024L * 1024 * 1024); - var userRejected = versions != null && !ShowUpdateDialog(versions); - - // In the current implementation, true means "skip non-forced update"; - // false means "continue". - return !hasUpdate || !enoughDisk || userRejected; - }) - .AddListenerMultiDownloadCompleted((_, e) => - { - Console.WriteLine($"{e.Version}: {(e.IsCompleted ? "completed" : "failed")}"); - }) - .AddListenerMultiAllDownloadCompleted((_, e) => - { - Console.WriteLine(e.IsAllDownloadCompleted - ? "All downloads completed." - : $"Failed downloads: {e.FailedVersions.Count}"); - }) - .AddListenerMultiDownloadError((_, e) => - { - Console.WriteLine($"Download failed: {e.Version}"); - Console.WriteLine(e.Exception); - }) - .AddListenerProgress((_, e) => - { - if (e.Progress != null) - Console.WriteLine($"Download {e.Progress.AssetName}: {e.Progress.Percentage:F1}% {e.Progress.Status}"); - - if (e.DiffProgress != null) - Console.WriteLine($"Patch: {e.DiffProgress.Completed}/{e.DiffProgress.Total} {e.DiffProgress.CurrentFile}"); - }) - .AddListenerException((_, e) => - { - Console.WriteLine(e.Message); - Console.WriteLine(e.Exception); - }) - .LaunchAsync(); -``` - -### Listener class - -Implement `IUpdateEventListener` to centralize event handling. Inherit `UpdateEventListenerBase` if you only need some events. - -```csharp -using GeneralUpdate.Core.Download; -using GeneralUpdate.Core.Event; - -public sealed class ConsoleUpdateListener : UpdateEventListenerBase -{ - public override void OnUpdateInfo(UpdateInfoEventArgs args) - { - Console.WriteLine($"Update count: {args.Info?.Body?.Count ?? 0}"); - } - - public override void OnProgress(ProgressEventArgs args) - { - if (args.Progress != null) - Console.WriteLine($"{args.Progress.AssetName}: {args.Progress.Percentage:F1}%"); - } - - public override void OnException(ExceptionEventArgs args) - { - Console.WriteLine(args.Exception); - } -} - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .AddEventListener() - .LaunchAsync(); -``` - -## Logging and performance {#logging-performance} +--- -Core includes `GeneralTracer`, enabled by default. It writes through `System.Diagnostics.Trace`: on Windows it also writes to the debug output window, it writes to the console, and it creates `Logs/generalupdate-trace yyyy-MM-dd.log` under the application base directory. The file listener writes through a background queue, but every log still performs an enable check, timestamp formatting, stack-frame lookup, and queue/output work. Disable tracing in performance-sensitive scenarios. +## 3. API Configuration Reference + +### 3.1 Configuration Properties (Props) + +**UpdateRequest Properties:** + +| Field | Type | Default | Required | Values | Description | +| --- | --- | --- | --- | --- | --- | +| `UpdateUrl` | `string` | — | Yes | Valid absolute URL | Update check API endpoint | +| `UpdateAppName` | `string` | `"Update.exe"` | Recommended | Valid filename | Upgrade process filename | +| `MainAppName` | `string` | — | Recommended | Valid filename | Main app filename for restart & identification | +| `ClientVersion` | `string` | — | Recommended | SemVer format | Current main app version | +| `AppSecretKey` | `string` | — | Recommended | — | App key for server authentication | +| `InstallPath` | `string` | `BaseDirectory` | Optional | Valid directory path | Application install root | +| `ReportUrl` | `string` | `null` | Optional | Valid absolute URL | Status report API | +| `UpdateLogUrl` | `string` | `null` | Optional | Valid absolute URL | Changelog page URL | +| `UpgradeClientVersion` | `string` | — | Optional | SemVer format | Upgrade process version | +| `ProductId` | `string` | — | Optional | — | Product identifier for multi-product servers | +| `UpdatePath` | `string` | `InstallPath` | Optional | Valid directory path | Upgrade process location | +| `Bowl` | `string` | `null` | Optional | Valid filename | Auxiliary process name to close before update | +| `Scheme` | `string` | `null` | Optional | `"Bearer"` etc. | Auth scheme | +| `Token` | `string` | `null` | Optional | — | Auth token | +| `Files` | `List` | `null` | Optional | — | Files to skip during update | +| `Formats` | `List` | `null` | Optional | — | Extensions to skip during update | +| `Directories` | `List` | `null` | Optional | — | Directories to skip during update | +| `DriverDirectory` | `string` | `null` | Optional | Valid directory path | Driver update directory | + +**Option Runtime Options:** + +| Field | Type | Default | Required | Values | Description | +| --- | --- | --- | --- | --- | --- | +| `Option.AppType` | `AppType` | `Client` | Yes | `Client(1)`, `Upgrade(2)`, `OssClient(3)`, `OssUpgrade(4)` | Current process role | +| `Option.DiffMode` | `DiffMode` | `Serial` | Optional | `Serial`, `Parallel` | Download execution mode | +| `Option.Encoding` | `Encoding` | `UTF8` | Optional | `Encoding` instance | Archive processing encoding | +| `Option.Format` | `Format` | `Zip` | Optional | `Zip` | Package format | +| `Option.DownloadTimeout` | `int?` | `30` | Optional | Positive integer (sec) | Download timeout | +| `Option.PatchEnabled` | `bool?` | `true` | Optional | `true` / `false` | Enable differential patching | +| `Option.BackupEnabled` | `bool?` | `true` | Optional | `true` / `false` | Backup files before update | +| `Option.Silent` | `bool` | `false` | Optional | `true` / `false` | Enable silent polling | +| `Option.SilentPollIntervalMinutes` | `int` | `60` | Optional | Positive integer | Polling interval (minutes) | +| `Option.LaunchClientAfterUpdate` | `bool` | `true` | Optional | `true` / `false` | Launch main app after upgrade | +| `Option.MaxConcurrency` | `int` | `3` | Optional | `1` ~ `ProcessorCount × 2` | Max download concurrency | +| `Option.EnableResume` | `bool` | `true` | Optional | `true` / `false` | Enable HTTP Range resume | +| `Option.RetryCount` | `int` | `3` | Optional | Non-negative integer | Download retry count | +| `Option.VerifyChecksum` | `bool` | `true` | Optional | `true` / `false` | Verify download file hash | +| `Option.RetryInterval` | `TimeSpan` | `1s` | Optional | Positive `TimeSpan` | Download retry interval | + +### 3.2 Instance Methods + +**GeneralUpdateBootstrap:** + +| Method | Parameters | Returns | Use Case | Notes | +| --- | --- | --- | --- | --- | +| `LaunchAsync()` | None | `Task` | Final entry for all Core scenarios | Auto-selects strategy based on `Option.AppType` | +| `Cancel()` | None | `void` | UI "Cancel Update" button | Triggers internal `CancellationTokenSource` | +| `SetConfig(UpdateRequest)` | `configInfo` | `GeneralUpdateBootstrap` | Explicit update configuration | Calls `Validate()` on key fields | +| `SetConfig(string)` | `filePath` — JSON config file path | `GeneralUpdateBootstrap` | Read config from file | Supports relative/absolute paths; UTF-8 JSON | +| `SetSource(...)` | `updateUrl`, `appSecretKey`, `reportUrl?`, `scheme?`, `token?` | `GeneralUpdateBootstrap` | Lightweight entry with manifest | Identity info filled by manifest | +| `SetOption(Option, T)` | `option` — key, `value` — value | `GeneralUpdateBootstrap` | Set runtime options | Pass `null` to reset nullable options | +| `UseDiffPipeline(Action)` | `configure` — delegate | `GeneralUpdateBootstrap` | Replace or tune diff pipeline | Default used if not called | +| `AddListenerUpdateInfo(...)` | `EventHandler` | `GeneralUpdateBootstrap` | Receive server version info | Also fires when no update available | +| `AddListenerUpdatePrecheck(...)` | `Func` | `GeneralUpdateBootstrap` | Pre-download check | Return `true` to skip non-forced update | +| `AddListenerProgress(...)` | `EventHandler` | `GeneralUpdateBootstrap` | Progress bar, status text | Contains both download & diff progress | +| `AddListenerMultiDownloadCompleted(...)` | `EventHandler` | `GeneralUpdateBootstrap` | Mark single asset download completion | Not "all downloads complete" | +| `AddListenerMultiAllDownloadCompleted(...)` | `EventHandler` | `GeneralUpdateBootstrap` | Post-all-downloads processing | Includes `FailedVersions` summary | +| `AddListenerMultiDownloadError(...)` | `EventHandler` | `GeneralUpdateBootstrap` | Log single download failure | Overall success still determined by `MultiAllDownloadCompleted` | +| `AddListenerMultiDownloadStatistics(...)` | `EventHandler` | `GeneralUpdateBootstrap` | Display speed & ETA | Prefer `AddListenerProgress` for new code | +| `AddListenerException(...)` | `EventHandler` | `GeneralUpdateBootstrap` | Report exceptions, show errors | Notification only; no automatic retry | +| `AddEventListener()` | Generic — listener type | `GeneralUpdateBootstrap` | Batch register event listeners | `T` must implement `IUpdateEventListener` | +| `Hooks()` | Generic — hook type | `GeneralUpdateBootstrap` | Register lifecycle hooks | `T` needs parameterless constructor | +| `UpdateReporter()` | Generic — reporter type | `GeneralUpdateBootstrap` | Register status reporter | — | +| `SslPolicy()` | Generic — SSL policy type | `GeneralUpdateBootstrap` | Custom HTTPS certificate validation | Don't unconditionally return `true` in production | +| `HttpAuth()` | Generic — auth provider type | `GeneralUpdateBootstrap` | Custom HTTP auth | — | +| `DownloadSource()` | Generic — download source type | `GeneralUpdateBootstrap` | Custom version list source | — | +| `DownloadPolicy()` | Generic — download policy type | `GeneralUpdateBootstrap` | Custom retry/timeout policy | — | +| `DownloadExecutor()` | Generic — executor type | `GeneralUpdateBootstrap` | Custom single-file download | — | +| `DownloadPipeline()` | Generic — pipeline type | `GeneralUpdateBootstrap` | Custom post-download processing | — | +| `DownloadOrchestrator()` | Generic — orchestrator type | `GeneralUpdateBootstrap` | Fully replace batch download | Only when complete replacement needed | +| `Strategy()` | Generic — strategy type | `GeneralUpdateBootstrap` | Custom platform strategy | — | + +**UpgradeHubService:** + +| Method | Parameters | Returns | Use Case | Notes | +| --- | --- | --- | --- | --- | +| `UpgradeHubService(string, string?, string?)` | `url` — SignalR Hub URL; `token` — optional ID4 auth token; `appkey` — optional client unique ID | — (constructor) | Create push service instance | `appkey` used for server-side targeted push; recommended to use a fixed GUID | +| `StartAsync()` | None | `Task` | Establish SignalR long-lived connection | Can re-call after `StopAsync` | +| `StopAsync()` | None | `Task` | Gracefully stop connection, retain reconnect ability | Suitable when app goes to background | +| `DisposeAsync()` | None | `Task` | Fully release Hub and all resources | Cannot be reused after disposal | +| `AddListenerReceive(Action)` | `receiveMessageCallback` | `void` | Subscribe to server push messages | Message content is JSON string from server | +| `AddListenerOnline(Action)` | `onlineMessageCallback` | `void` | Subscribe to online/offline status changes | — | +| `AddListenerReconnected(Func?)` | `reconnectedCallback` | `void` | Subscribe to reconnect success notification | Parameter is new connectionId (may be null) | +| `AddListenerClosed(Func)` | `closeCallback` | `void` | Subscribe to connection close notification | Exception is null for normal close | + +### 3.3 Callback Events + +| Event | Callback Parameters | Trigger Timing | Usage Notes | +| --- | --- | --- | --- | +| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` — `Info.Code`, `Info.Body` (`VersionEntry` list) | After version comparison in standard Client strategy | No update → `Code = 404`; has update → `Body` contains `VersionEntry` list | +| `AddListenerUpdatePrecheck` | `Func` — return `true` to skip (non-forced), `false` to continue | After `UpdateInfo`, before download | For disk space check, network check, user confirmation dialog | +| `AddListenerProgress` | `ProgressEventArgs` — `Progress` (download) or `DiffProgress` (diff) | Download progress or diff progress updates | Only one of `Progress` / `DiffProgress` is non-null per event | +| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` — `Version`, `IsCompleted` | Single asset download completion | Not "all downloads complete" | +| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` — `IsAllDownloadCompleted`, `FailedVersions` | After all download tasks complete | Failure details in `FailedVersions` | +| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` — `Exception`, `Version` | Single download failure | Record failures for display/monitoring | +| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` — `Speed`, `Remaining`, `BytesReceived` | Legacy/compat download statistics | New code should use `AddListenerProgress` | +| `AddListenerException` | `ExceptionEventArgs` — `Exception`, `Message` | When strategies catch exceptions | Notification only; no automatic retry | + +**UpgradeHubService Push Events:** + +| Event | Callback Parameters | Trigger Timing | Usage Notes | +| --- | --- | --- | --- | +| `AddListenerReceive` | `Action` — message content (JSON string) | When server pushes version update | Message format determined by server | +| `AddListenerOnline` | `Action` — status description | When online/offline status changes | Use for UI status display | +| `AddListenerReconnected` | `Func?` — new connectionId | After successful reconnect | Can refresh client state | +| `AddListenerClosed` | `Func` — close reason (null = normal) | When connection closes | Use for logging and cleanup | -| API | Purpose | -| --- | --- | -| `GeneralTracer.SetTracingEnabled(false)` | Disables Core log output. `Debug` / `Info` / `Warn` / `Error` / `Fatal` return quickly, and Trace listeners are filtered. | -| `GeneralTracer.SetTracingEnabled(true)` | Re-enables log output for diagnostics, canary builds, or user issue investigation. | -| `GeneralTracer.IsTracingEnabled()` | Reads the current tracing switch. | -| `GeneralTracer.Dispose()` | Disposes the file listener and clears Trace listeners. Usually only use it in tests, short-lived tooling processes, or when you intentionally take over Trace listeners. | +--- -Set the logging switch early in application startup so download, verification, differential, and replacement logs use the same policy. +## 4. Advanced Examples -```csharp -using GeneralUpdate.Core; +### 4.1 Extension Points Overview -if (performanceMode) -{ - GeneralTracer.SetTracingEnabled(false); -} +Core provides 10 extension registration methods via `AbstractBootstrap`, all returning the bootstrap instance for fluent chaining. All registered types must have parameterless constructors. -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret") - .SetOption(Option.AppType, AppType.Client) - .LaunchAsync(); -``` +| Extension Interface | Registration Method | Scope | +| --- | --- | --- | +| `IUpdateHooks` | `Hooks()` | Update lifecycle hooks | +| `IUpdateReporter` | `UpdateReporter()` | Status reporting | +| `ISslValidationPolicy` | `SslPolicy()` | HTTPS certificate validation | +| `IHttpAuthProvider` | `HttpAuth()` | HTTP request authentication | +| `IDownloadSource` | `DownloadSource()` | Version list & download source | +| `IDownloadPolicy` | `DownloadPolicy()` | Download retry/timeout/circuit-breaker | +| `IDownloadExecutor` | `DownloadExecutor()` | Single file download | +| `IDownloadPipeline` | `DownloadPipeline()` | Post-download processing | +| `IDownloadOrchestrator` | `DownloadOrchestrator()` | Batch download orchestration | +| `IStrategy` | `Strategy()` | Platform-level update strategy | -Disabling logs is useful for low-power devices, slow I/O terminals, silent background polling, large-scale automated updates, or products that are highly sensitive to startup time. Keep tracing available for diagnostics because Core logs strategy dispatch, download orchestration, verification, differential work, hooks, and exception paths. +### 4.2 Examples by Scenario -## Extension points +#### Scenario 1: Custom Diff Algorithm with Parallelism -All extension registration methods are provided by `AbstractBootstrap` and can be chained. +**Description:** Large projects wanting faster client-side patch application with `StreamingHdiffDiffer` and parallelism 4. -| Registration method | Interface | Scope | -| --- | --- | --- | -| `Hooks()` | `IUpdateHooks` | Lifecycle callbacks. | -| `UpdateReporter()` | `IUpdateReporter` | Update status reporting. | -| `SslPolicy()` | `ISslValidationPolicy` | HTTPS certificate validation. | -| `HttpAuth()` | `IHttpAuthProvider` | HTTP request authentication. | -| `DownloadSource()` | `IDownloadSource` | Version manifest and asset source. | -| `DownloadPolicy()` | `IDownloadPolicy` | Retry, timeout, circuit breaker. | -| `DownloadExecutor()` | `IDownloadExecutor` | Single-file download implementation. | -| `DownloadPipeline()` | `IDownloadPipeline` | Post-download verification or transformation. | -| `DownloadOrchestrator()` | `IDownloadOrchestrator` | Full batch download orchestration. | -| `Strategy()` | `IStrategy` | Platform-level update strategy. | +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Differential.Differ; -Registered types must have a parameterless constructor because Core creates them with `new()` or reflection. If dependencies are required, wrap them in a parameterless adapter. +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4) + .WithStopOnFirstError(true); + }) + .SetOption(Option.PatchEnabled, true) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` -## Lifecycle hooks: IUpdateHooks +#### Scenario 2: Custom Lifecycle Hooks -`IUpdateHooks` is best for business logic before update, after download, after update, before app start, and on errors. It is also a flexible open extension point: on Linux or macOS, updated executables may need execute permission restored, or an organization may need to run an internal authorization, signature-check, or permission-repair script before the main app starts. Put that work in `OnBeforeStartAppAsync`. +**Description:** Check disk space before update, write logs after update, grant execute permissions on Linux/macOS. ```csharp using GeneralUpdate.Core.Hooks; @@ -791,419 +251,418 @@ public sealed class ProductUpdateHooks : IUpdateHooks { public Task OnBeforeUpdateAsync(HookContext ctx) { - Console.WriteLine($"Before update: {ctx.CurrentVersion} -> {ctx.TargetVersion}"); + var drive = new DriveInfo(Path.GetPathRoot(ctx.InstallPath)!); + if (drive.AvailableFreeSpace < 500L * 1024 * 1024) + return Task.FromResult(false); // Reject update return Task.FromResult(true); } - public Task OnDownloadCompletedAsync(DownloadContext ctx) - { - Console.WriteLine($"Downloaded {ctx.AssetName}, success={ctx.Success}, path={ctx.LocalPath}"); - return Task.CompletedTask; - } + public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; public Task OnAfterUpdateAsync(HookContext ctx) { - File.WriteAllText(Path.Combine(ctx.InstallPath, "last-update.txt"), DateTimeOffset.Now.ToString("O")); + File.AppendAllText( + Path.Combine(ctx.InstallPath, "update-history.log"), + $"{DateTimeOffset.Now:O} {ctx.CurrentVersion} -> {ctx.TargetVersion}{Environment.NewLine}"); return Task.CompletedTask; } public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) { - File.AppendAllText(Path.Combine(ctx.InstallPath, "update-error.log"), ex + Environment.NewLine); + File.AppendAllText(Path.Combine(ctx.InstallPath, "update-error.log"), $"{ex}{Environment.NewLine}"); return Task.CompletedTask; } - public Task OnBeforeStartAppAsync(HookContext ctx) - { - Console.WriteLine($"Starting app from {ctx.InstallPath}"); - return Task.CompletedTask; - } + public Task OnBeforeStartAppAsync(HookContext ctx) => Task.CompletedTask; } await new GeneralUpdateBootstrap() .SetConfig(request) .Hooks() + .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -On Linux/macOS, you can register the built-in `UnixPermissionHooks` to let Core run `chmod +x` before starting the app: - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .Hooks() - .LaunchAsync(); -``` +#### Scenario 3: Custom Download Source (Private Service/Config Center) -If you need to run your own permission script, wrap it in a parameterless hook adapter and register that adapter with `Hooks()`: +**Description:** Pull download asset lists from an internal config center instead of the standard version check API. ```csharp -using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; -public sealed class ProductPermissionHooks : IUpdateHooks +public sealed class ConfigCenterDownloadSource : IDownloadSource { - private readonly CustomPermissionHooks _inner = - new("/opt/my-product/scripts/fix-permissions.sh"); - - public Task OnBeforeStartAppAsync(HookContext ctx) - => _inner.OnBeforeStartAppAsync(ctx); - - public Task OnBeforeUpdateAsync(HookContext ctx) => Task.FromResult(true); - public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; - public Task OnAfterUpdateAsync(HookContext ctx) => Task.CompletedTask; - public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) => Task.CompletedTask; + public async Task ListAsync(CancellationToken token = default) + { + var assets = new[] + { + new DownloadAsset( + Name: "MyApp-2.0.0.zip", + Url: "https://cdn.internal.example.com/releases/MyApp-2.0.0.zip", + Size: 50_000_000, + SHA256: "abc123...", + Version: "2.0.0") + }; + return new DownloadSourceResult + { + Assets = assets, + HasMainUpdate = true, + HasUpgradeUpdate = false + }; + } } await new GeneralUpdateBootstrap() .SetConfig(request) - .Hooks() + .DownloadSource() + .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -Built-in hook types: - -| Type | Description | -| --- | --- | -| `NoOpUpdateHooks` | Default no-op implementation. | -| `UnixPermissionHooks` | Runs `chmod +x` before starting the app on Unix-like systems. | -| `CustomPermissionHooks` | Runs a custom permission script; wrap it before using `Hooks()` because its constructor requires arguments. | - -## Status reporting: IUpdateReporter +#### Scenario 4: Custom HTTP Authentication -`IUpdateReporter` reports update status to a server or local telemetry. +**Description:** Append JWT Bearer Token to all HTTP requests from Core. ```csharp -using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Security; -public sealed class ConsoleUpdateReporter : IUpdateReporter +public sealed class JwtAuthProvider : IHttpAuthProvider { - public Task ReportAsync(UpdateReport report, CancellationToken token = default) + private readonly string _token = Environment.GetEnvironmentVariable("UPDATE_JWT_TOKEN") ?? ""; + + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) { - Console.WriteLine($"Report: record={report.RecordId}, status={report.Status}, type={report.Type}"); + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token); return Task.CompletedTask; } } await new GeneralUpdateBootstrap() .SetConfig(request) - .UpdateReporter() + .HttpAuth() + .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -The built-in `HttpUpdateReporter` posts JSON to `ReportUrl`: +#### Scenario 5: Silent Update + Process Exit Trigger -```json +**Description:** Main app polls for updates in background, triggers upgrade on process exit. + +```csharp +using GeneralUpdate.Core; + +var bootstrap = new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret") + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 30) + .SetOption(Option.LaunchClientAfterUpdate, true) + .AddListenerException((_, e) => Console.WriteLine($"Update error: {e.Message}")); + +await bootstrap.LaunchAsync(); + +// On app exit: launch upgrade if prepared +AppDomain.CurrentDomain.ProcessExit += (_, _) => { - "recordId": 123, - "status": 1, - "type": 1 -} + if (bootstrap.SilentOrchestrator?.HasPreparedUpdate == true) + bootstrap.SilentOrchestrator.TryLaunchUpgrade(); +}; ``` -| Enum | Value | Description | -| --- | --- | --- | -| `UpdateStatus.Updating` | `1` | Updating. | -| `UpdateStatus.Success` | `2` | Update succeeded. | -| `UpdateStatus.Failure` | `3` | Update failed. | - -## HTTP authentication: IHttpAuthProvider +#### Scenario 6: SignalR Real-Time Push + Standard Update -`IHttpAuthProvider` adds authentication to outgoing Core HTTP requests. +**Description:** Use `UpgradeHubService` for server push notifications alongside `GeneralUpdateBootstrap` for standard updates. Server can push notifications immediately when new versions are available. ```csharp -using GeneralUpdate.Core.Security; +using GeneralUpdate.Core; +using GeneralUpdate.Core.Hubs; + +// 1. Start SignalR push listener +var hub = new UpgradeHubService( + "http://localhost:5000/UpgradeHub", + appkey: "dfeb5833-975e-4afb-88f1-6278ee9aeff6"); -public sealed class StaticBearerAuthProvider : IHttpAuthProvider +hub.AddListenerReceive(async (message) => { - public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) - { - request.Headers.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "access-token"); + Console.WriteLine($"Push notification: {message}"); + // Trigger update check or notify user in UI +}); - return Task.CompletedTask; - } -} +hub.AddListenerOnline((info) => + Console.WriteLine($"Online status: {info}")); + +hub.AddListenerReconnected((connectionId) => +{ + Console.WriteLine($"Reconnected, connectionId={connectionId}"); + return Task.CompletedTask; +}); +hub.AddListenerClosed((exception) => +{ + Console.WriteLine(exception != null + ? $"Connection closed abnormally: {exception.Message}" + : "Connection closed normally"); + return Task.CompletedTask; +}); + +await hub.StartAsync(); + +// 2. Standard update flow await new GeneralUpdateBootstrap() - .SetConfig(request) - .HttpAuth() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret") + .SetOption(Option.AppType, AppType.Client) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) .LaunchAsync(); -``` -Core includes `NoOpAuthProvider`, `BearerTokenAuthProvider`, `ApiKeyAuthProvider`, and `HmacAuthProvider`. Some require constructor parameters, so create a parameterless wrapper when registering through `HttpAuth()`. +// 3. Cleanup on exit +// await hub.StopAsync(); +// await hub.DisposeAsync(); +``` -## HTTPS certificate policy: ISslValidationPolicy +#### Scenario 7: DI Container Registration for UpgradeHubService -`ISslValidationPolicy` controls HTTPS certificate validation. The default `StrictSslValidationPolicy` accepts only certificates without SSL policy errors. +**Description:** Register `IUpgradeHubService` in Prism / Generic Host / ASP.NET Core DI containers. ```csharp -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using GeneralUpdate.Core.Security; +using GeneralUpdate.Core.Hubs; -public sealed class DevelopmentSslPolicy : ISslValidationPolicy +// Prism example +protected override void RegisterTypes(IContainerRegistry containerRegistry) { - public bool ValidateCertificate( - X509Certificate2? certificate, - X509Chain? chain, - SslPolicyErrors sslPolicyErrors) - { - return sslPolicyErrors == SslPolicyErrors.None - || certificate?.Issuer.Contains("CN=Local Dev Root") == true; - } + containerRegistry.Register(); } -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SslPolicy() - .LaunchAsync(); +public MainWindowViewModel(IUpgradeHubService hubService) +{ + hubService.AddListenerReceive((message) => + Console.WriteLine($"Push: {message}")); + _ = hubService.StartAsync(); +} + +// Generic Host / ASP.NET Core example +builder.Services.AddSingleton(sp => +{ + var config = sp.GetRequiredService(); + return new UpgradeHubService( + config["HubUrl"]!, + appkey: config["AppSecretKey"]); +}); ``` -Do not unconditionally return `true` in production. +--- + +## 5. Basic Usage Examples -## Download source: IDownloadSource +### 5.1 Quick Start (Minimal Demo) -`IDownloadSource` returns assets to download. Use it for private services, file servers, config centers, or custom cloud storage. +Minimal config using manifest for identity auto-discovery: ```csharp -using GeneralUpdate.Core.Download.Abstractions; -using GeneralUpdate.Core.Download.Models; +using GeneralUpdate.Core; -public sealed class StaticDownloadSource : IDownloadSource -{ - public Task ListAsync(CancellationToken token = default) - { - var assets = new[] - { - new DownloadAsset( - Name: "app-1.0.1.zip", - Url: "https://cdn.example.com/releases/app-1.0.1.zip", - Size: 25_000_000, - SHA256: "expected-sha256", - Version: "1.0.1") - }; +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret") + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` - return Task.FromResult(new DownloadSourceResult - { - Assets = assets, - HasMainUpdate = true, - HasUpgradeUpdate = false - }); - } -} +Upgrade process entry point (`Update.exe`): +```csharp await new GeneralUpdateBootstrap() - .SetConfig(request) - .DownloadSource() + .SetOption(Option.AppType, AppType.Upgrade) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) .LaunchAsync(); ``` -## Retry policy: IDownloadPolicy - -`IDownloadPolicy` wraps download actions and can implement retry, timeout, circuit breaker, or throttling. +### 5.2 Basic Parameter Combination ```csharp -using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; -public sealed class TwoAttemptDownloadPolicy : IDownloadPolicy +var request = new UpdateRequest { - public async Task ExecuteAsync( - Func> action, - CancellationToken token = default) - { - try - { - return await action(token); - } - catch when (!token.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromSeconds(2), token); - return await action(token); - } - } -} + UpdateUrl = "https://update.example.com/api/upgrade/verification", + ReportUrl = "https://update.example.com/api/upgrade/report", + UpdateAppName = "UpgradeSample.exe", + MainAppName = "ClientSample.exe", + InstallPath = AppDomain.CurrentDomain.BaseDirectory, + ClientVersion = "1.0.0", + AppSecretKey = "your-app-secret", + ProductId = "your-product-id" +}; await new GeneralUpdateBootstrap() .SetConfig(request) - .DownloadPolicy() + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 4) + .SetOption(Option.PatchEnabled, true) + .AddListenerProgress((_, e) => + { + if (e.Progress != null) + Console.WriteLine($"{e.Progress.AssetName}: {e.Progress.Percentage:F1}%"); + }) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) .LaunchAsync(); ``` -When `DownloadOrchestrator()` is also registered, the custom orchestrator owns whether and how policy is used. - -## Single-file download: IDownloadExecutor - -`IDownloadExecutor` downloads one `DownloadAsset` to a destination path. Use it for FTP, SFTP, private protocols, or custom HTTP clients. +### 5.2.1 SignalR Real-Time Push Quick Start ```csharp -using GeneralUpdate.Core.Download.Abstractions; -using GeneralUpdate.Core.Download.Models; +using GeneralUpdate.Core.Hubs; -public sealed class MirrorDownloadExecutor : IDownloadExecutor +// Create push client +var hub = new UpgradeHubService( + "http://localhost:5000/UpgradeHub", + appkey: Guid.NewGuid().ToString()); + +// Subscribe to push messages +hub.AddListenerReceive((message) => { - private readonly HttpClient _client = new(); + Console.WriteLine($"Push notification: {message}"); +}); - public async Task ExecuteAsync( - DownloadAsset asset, - string destPath, - IProgress? progress = null, - CancellationToken token = default) - { - var started = DateTimeOffset.Now; - await using var input = await _client.GetStreamAsync(asset.Url, token); - await using var output = File.Create(destPath); - await input.CopyToAsync(output, token); +// Establish connection +await hub.StartAsync(); - var fileInfo = new FileInfo(destPath); - return new DownloadResult(asset, destPath, fileInfo.Length, DateTimeOffset.Now - started, 0, true, null); - } -} +Console.WriteLine("Connected, waiting for server push..."); +Console.ReadLine(); -await new GeneralUpdateBootstrap() - .SetConfig(request) - .DownloadExecutor() - .LaunchAsync(); +// Stop connection (retain reconnect ability) +await hub.StopAsync(); + +// Release resources (cannot be reused) +await hub.DisposeAsync(); ``` -## Post-download pipeline: IDownloadPipeline +### 5.3 Production-Ready Example -`IDownloadPipeline` runs after a file is downloaded. Use it for hash verification, decryption, antivirus scanning, or format conversion. +Full Client-side update with events, diff pipeline, concurrency control, and status reporting: ```csharp -using GeneralUpdate.Core.Download.Abstractions; - -public sealed class AntivirusPipeline : IDownloadPipeline -{ - public Task ProcessAsync(string downloadedPath, CancellationToken token = default) - { - if (!File.Exists(downloadedPath)) - throw new FileNotFoundException("Downloaded file not found.", downloadedPath); +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Core.Download; +using GeneralUpdate.Differential.Differ; - Console.WriteLine($"Scanning {downloadedPath}"); - return Task.FromResult(downloadedPath); - } -} +var request = new UpdateRequestBuilder() + .SetUpdateUrl("https://update.mycompany.com/api/upgrade/verification") + .SetReportUrl("https://update.mycompany.com/api/upgrade/report") + .SetUpgradeAppName("MyApp.Upgrade.exe") + .SetMainAppName("MyApp.exe") + .SetClientVersion("1.0.0") + .SetUpgradeClientVersion("1.0.0") + .SetAppSecretKey("prod-secret-key") + .SetProductId("my-product") + .SetInstallPath(AppDomain.CurrentDomain.BaseDirectory) + .Build(); -await new GeneralUpdateBootstrap() +var bootstrap = new GeneralUpdateBootstrap() .SetConfig(request) - .DownloadPipeline() - .LaunchAsync(); -``` + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 4) + .SetOption(Option.DownloadTimeout, 120) + .SetOption(Option.PatchEnabled, true) + .SetOption(Option.BackupEnabled, true) + .SetOption(Option.VerifyChecksum, true) + .SetOption(Option.RetryCount, 5) + .SetOption(Option.RetryInterval, TimeSpan.FromSeconds(2)) + .UseDiffPipeline(builder => builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4)) + .AddListenerUpdateInfo((_, e) => + { + Console.WriteLine(e.Info?.Code == "404" + ? "Already up to date." + : $"Found {e.Info?.Body?.Count ?? 0} version(s)."); + }) + .AddListenerProgress((_, e) => + { + if (e.Progress != null) + Console.WriteLine($"[Download] {e.Progress.AssetName}: {e.Progress.Percentage:F1}%"); + if (e.DiffProgress != null) + Console.WriteLine($"[Patch] {e.DiffProgress.CurrentFile}: {e.DiffProgress.Completed}/{e.DiffProgress.Total}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + Console.WriteLine(e.IsAllDownloadCompleted + ? "All downloads completed." + : $"Failed: {e.FailedVersions.Count}"); + }) + .AddListenerException((_, e) => Console.WriteLine($"Error: {e.Message}")); -Core first tries to construct a pipeline with a `string` constructor for the expected hash. If not available, it uses the parameterless constructor. +await bootstrap.LaunchAsync(); +``` -## Batch download orchestration: IDownloadOrchestrator +--- -`IDownloadOrchestrator` is the highest-level download extension point. It owns batch download, concurrency, retry, progress, and result aggregation. +## 6. Global Configuration -```csharp -using GeneralUpdate.Core.Download.Abstractions; -using GeneralUpdate.Core.Download.Executors; -using GeneralUpdate.Core.Download.Models; +### Manifest Configuration -public sealed class SerialDownloadOrchestrator : IDownloadOrchestrator +```json { - private readonly IDownloadExecutor _executor = new HttpDownloadExecutor(new HttpClient()); - - public async Task ExecuteAsync( - DownloadPlan plan, - string destDir, - int maxConcurrency = 3, - IProgress? progress = null, - CancellationToken token = default) - { - var results = new List(); - var started = DateTimeOffset.Now; - - foreach (var asset in plan.Assets) - { - var destPath = Path.Combine(destDir, asset.Name); - results.Add(await _executor.ExecuteAsync(asset, destPath, progress, token)); - } - - return new DownloadReport( - results, - results.Where(r => r.Success).Sum(r => r.DownloadedBytes), - DateTimeOffset.Now - started, - results.Count(r => r.Success), - results.Count(r => !r.Success)); - } + "mainAppName": "ClientSample.exe", + "clientVersion": "1.0.0", + "appType": "Client", + "updateAppName": "UpgradeSample.exe", + "upgradeClientVersion": "1.0.0", + "productId": "sample-product", + "updatePath": "update/" } - -await new GeneralUpdateBootstrap() - .SetConfig(request) - .DownloadOrchestrator() - .LaunchAsync(); ``` -Implement an orchestrator only when you need to replace the entire download behavior. Most cases only need `IDownloadExecutor`, `IDownloadPolicy`, or `IDownloadPipeline`. +### Configuration Priority -## Platform strategy: IStrategy +| Priority | Source | Description | +| --- | --- | --- | +| 1 (Highest) | Code: `SetConfig(UpdateRequest)` or `SetSource(...)` | Overrides all other sources | +| 2 | `generalupdate.manifest.json` fields | Auto-fills fields not explicitly set in code | +| 3 (Lowest) | Component internal defaults | `UpdateAppName = "Update.exe"`, `InstallPath = BaseDirectory`, etc. | -`IStrategy` is the highest-level update strategy interface. Core includes `ClientStrategy`, `UpdateStrategy`, `OssStrategy`, and Windows/Linux/macOS platform strategies. Implement it only when you need to replace platform-level file operations or app startup logic. +### Version Write-Back -```csharp -using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.Download.Reporting; -using GeneralUpdate.Core.Hooks; -using GeneralUpdate.Core.Strategy; +After a successful update, Core automatically writes back the version to `generalupdate.manifest.json`: -public sealed class LoggingStrategy : IStrategy -{ - private UpdateContext? _context; +| Scenario | Write-Back Field | +| --- | --- | +| Main app update completes | `ClientVersion` | +| Upgrade process update completes | `UpgradeClientVersion` | - public IUpdateHooks Hooks { get; set; } = new NoOpUpdateHooks(); - public IUpdateReporter Reporter { get; set; } = new HttpUpdateReporter(); +### Logging Configuration - public void Create(UpdateContext parameter) - { - _context = parameter; - } +```csharp +using GeneralUpdate.Core; - public async Task ExecuteAsync() - { - if (_context == null) - throw new InvalidOperationException("Strategy was not initialized."); - - Console.WriteLine($"Custom strategy executing in {_context.InstallPath}"); - await Hooks.OnBeforeUpdateAsync(new HookContext( - _context.UpdateAppName, - _context.InstallPath, - _context.ClientVersion, - _context.LastVersion, - _context.AppType ?? AppType.Client)); - } +// Disable logging (performance-sensitive scenarios) +GeneralTracer.SetTracingEnabled(false); - public Task StartAppAsync() - { - Console.WriteLine("Custom start app logic."); - return Task.CompletedTask; - } -} +// Re-enable (troubleshooting) +GeneralTracer.SetTracingEnabled(true); -await new GeneralUpdateBootstrap() - .SetConfig(request) - .Strategy() - .LaunchAsync(); +// Release logging resources +GeneralTracer.Dispose(); ``` -> Silent update is not a separate extension interface. It is a built-in execution strategy. See [Silent update strategy](#silent-update-strategy) for configuration and lifecycle details. - -## Relationship with GeneralUpdate.Tools +--- -Core consumes manifests and packages. `GeneralUpdate.Tools` helps generate and validate those artifacts. +## Related Resources -| Tools capability | Core consumption point | -| --- | --- | -| Patch Package | `Option.PatchEnabled`, `UseDiffPipeline`, differential patch processing. | -| Manifest Generator | `ManifestInfo`, `AppMetadataDiscoverer`, version write-back. | -| Extension Package | Distributed as package content and consumed by download/deploy flow. | -| OSS Config | `OssClient` / `OssUpgrade` roles. | -| Hash / Simulation / Report | `Option.VerifyChecksum`, post-download verification, and status reporting. | - -## Related samples - -- [Upgrade sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/blob/main/src/Upgrade/Program.cs) -- [OSS upgrade sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/OSS/OSSUpgradeSample) -- [GeneralUpdate repository](https://github.com/GeneralLibrary/GeneralUpdate) -- [GeneralUpdate.Tools repository](https://github.com/GeneralLibrary/GeneralUpdate.Tools) +- [GeneralUpdate Repository](https://github.com/GeneralLibrary/GeneralUpdate) +- [Samples Code](https://github.com/GeneralLibrary/GeneralUpdate-Samples) +- [GeneralUpdate.Tools](https://github.com/GeneralLibrary/GeneralUpdate.Tools) +- [Quick Start](../quickstart/Quik%20start.md) +- [Architecture Guide](../guide/Architecture.md) diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Differential.md b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Differential.md index c13391f..ae1d454 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Differential.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Differential.md @@ -4,254 +4,153 @@ sidebar_position: 6 # GeneralUpdate.Differential -`GeneralUpdate.Differential` is the binary differential component in GeneralUpdate. It focuses on one low-level problem: old file + patch file = new file. It provides replaceable file-level differ algorithms, patch compression abstractions, and BSDIFF-compatible patch read/write support. Directory comparison, batch patch generation, parallel scheduling, deleted-file handling, and update orchestration are handled by `GeneralUpdate.Core` through `DiffPipeline`, or by `GeneralUpdate.Tools` on the publishing side. +**Namespace:** `GeneralUpdate.Differential` | **Main Entry Points:** `IBinaryDiffer`, `BsdiffDiffer`, `StreamingHdiffDiffer` | **NuGet Package:** `GeneralUpdate.Differential` -**Namespaces:** `GeneralUpdate.Differential`, `GeneralUpdate.Differential.Differ`, `GeneralUpdate.Differential.Abstractions` +## 1. Component Overview -**Main entry points:** `IBinaryDiffer`, `BsdiffDiffer`, `StreamingHdiffDiffer` +### 1.1 Introduction -**NuGet package:** `GeneralUpdate.Differential` +**GeneralUpdate.Differential** is the binary differential component of GeneralUpdate, focused on solving "one old file + one patch file = one new file". It provides pluggable file-level diff algorithms (BSDIFF 4.0 / Streaming HDiff), patch compression abstractions (BZip2 / Deflate / Brotli reserved), and BSDIFF-compatible patch read/write capabilities. -```bash -dotnet add package GeneralUpdate.Differential -``` - -## Documentation outline and topic navigation {#knowledge-map} +Directory-level comparison, batch patch generation, parallel scheduling, deleted file handling, and update workflow orchestration are handled by `GeneralUpdate.Core`'s `DiffPipeline` or `GeneralUpdate.Tools`. -If this is your first time reading the Differential documentation, start with this map and jump to the topic you need. The page is organized as "boundaries -> file-level API -> algorithm selection -> compression format -> Core/Tools integration -> performance and extension points". +**Core Capabilities:** -| What you want to learn | Recommended section | +| Capability | Description | | --- | --- | -| What Differential is responsible for | [Component boundaries](#component-boundaries) | -| What `Clean` and `Dirty` mean | [Clean and Dirty semantics](#clean-and-dirty-semantics) | -| How to generate and apply a patch for one file | [Single-file quick start](#single-file-quick-start) | -| Whether Core users need to integrate Differential manually | [Relationship with GeneralUpdate.Core](#relationship-with-generalupdatecore) | -| Which algorithms exist and how to choose one | [Differ algorithm selection](#differ-algorithm-selection) | -| How the BSDIFF header and compression byte work | [Patch format and compression providers](#patch-format-and-compression-providers) | -| How directory-level differential updates are enabled in Core | [Relationship with GeneralUpdate.Core](#relationship-with-generalupdatecore) | -| What Tools uses when building differential packages | [Relationship with GeneralUpdate.Tools](#relationship-with-generalupdatetools) | -| Whether downloads and diff work can run in parallel | [Concurrency model and performance guidance](#concurrency-model-and-performance-guidance) | -| How large projects can improve differential build throughput | [Parallel differential work for large projects](#parallel-differential-work-for-large-projects) | -| How to plug in a custom differ or compression provider | [Extension points](#extension-points) | - -## Component boundaries - -Differential is a low-level file patching library, not a complete update orchestrator. This boundary is important because older documentation mentioned concepts such as `DifferentialCore`, blacklists, and directory batch processing, but those are not the current public API of this component. - -| Capability | Owned by Differential | Notes | -| --- | --- | --- | -| Generate a binary patch for one file | Yes | Done through `IBinaryDiffer.CleanAsync(oldFile, newFile, patchFile)`. | -| Apply a binary patch for one file | Yes | Done through `IBinaryDiffer.DirtyAsync(oldFile, outputNewFile, patchFile)`. | -| Differ algorithm implementations | Yes | Current main implementations are `BsdiffDiffer` and `StreamingHdiffDiffer`. | -| Compress and decompress patch data | Yes | Abstracted by `ICompressionProvider`; BZip2 and Deflate are available, with .NET 6+ Brotli reserved in source. | -| Compare old and new directories | No | Handled by matchers in `GeneralUpdate.Core.Pipeline.DiffPipeline`. | -| Copy new files, write delete manifests, name batch patches | No | Handled by `DiffPipeline`, which writes `.patch` files, copies new files, and writes `generalupdate.delete.json`. | -| Build update packages | No | Prefer `GeneralUpdate.Tools`, which calls the Core differential pipeline. | -| Download, verify, unzip, write back versions, restart apps | No | These belong to the `GeneralUpdate.Core` update flow. | - -> The current source does not contain the old `DifferentialCore` singleton mentioned by previous docs. When using Differential directly, program against `IBinaryDiffer` and concrete differ implementations. For directory-level behavior, use Core's `DiffPipeline`. +| File-Level Diff Generation | `CleanAsync(oldFile, newFile, patchFile)` — compare old & new files to generate `.patch` | +| File-Level Diff Application | `DirtyAsync(oldFile, newFile, patchFile)` — old file + patch → new file | +| Pluggable Diff Algorithms | `BsdiffDiffer` (BSDIFF 4.0, suffix sort) and `StreamingHdiffDiffer` (block hash indexing) | +| Pluggable Compression | BZip2 (0x00), Deflate (0x01), .NET 6+ Brotli (0x02) reserved in source | +| BSDIFF Compatible Format | 33-byte extended header (32-byte BSDIFF40 + 1-byte compression format), 32-byte legacy compatible | +| Thread Safety | Built-in differ and compression providers support concurrent calls | + +**Business Problems Solved:** +- Full updates have high bandwidth costs; differential updates can reduce packages from GB to MB or KB +- Different file types and change patterns need different diff strategies +- Compression algorithm choice affects client decompression speed vs patch size trade-off + +**Use Cases:** +- Incremental updates for large desktop apps (multiple DLLs, resource files) +- Binary differential distribution for firmware/driver packages +- Game resource hot updates +- Automated incremental patch generation in CI/CD pipelines + +### 1.2 Environment & Dependencies + +| Item | Description | +| --- | --- | +| **Version** | `10.5.0-beta.2` | +| **Target Framework** | `netstandard2.0` (.NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 5+) | +| **Dependencies** | None (pure .NET BCL) | +| **Compatibility** | All .NET Standard 2.0 platforms | -## Clean and Dirty semantics {#clean-and-dirty-semantics} +--- -Differential follows the two terms used by the GeneralUpdate differential flow: +## 2. Feature List -| Term | Method | Input | Output | Common location | +| Feature | Description | Type | Required | Notes | | --- | --- | --- | --- | --- | -| `Clean` | `CleanAsync` | Old file, new file, patch path | `.patch` file | Build/publishing side | -| `Dirty` | `DirtyAsync` | Old file, output new-file path, patch path | Restored new file | Client upgrade side | +| BSDIFF 4.0 Diff Generation | Classic suffix-sort diff algorithm with stable patch sizes | Core | Optional | `BsdiffDiffer`, default BZip2 | +| BSDIFF 4.0 Patch Application | Apply BSDIFF format patches to old files | Core | Optional | Supports both 32/33 byte headers | +| Streaming HDiff Generation | FNV-1a block hash-based fast diff | Core | Optional | `StreamingHdiffDiffer`, default Deflate | +| BZip2 Compression | BZip2 compression for patch segments | Core | Optional | Format byte `0x00`, `BsdiffDiffer` default | +| Deflate Compression | Deflate compression for patch segments, faster decompression | Core | Optional | Format byte `0x01`, `StreamingHdiffDiffer` default | +| Custom Diff Algorithm | Implement `IBinaryDiffer` for proprietary algorithms | Extended | Optional | Must ensure Clean/Dirty consistency | +| Custom Compression Provider | Implement `ICompressionProvider` to swap compression | Extended | Optional | New format bytes require patch reader extension | -File-level patch application does not overwrite the old file directly. It writes the restored result to the `newFilePath` you pass in. At the directory level, Core's `DiffPipeline` writes to a temporary file first and replaces the original only after the patch is applied successfully, avoiding corruption if patch application fails. +--- -## Single-file quick start +## 3. API Configuration Reference -This example only demonstrates the low-level single-file capability. If you already use `GeneralUpdate.Core`, Core integrates Differential by default, so you do not need to manually integrate or directly call this component for the normal update flow. If you need to compare two directories, generate many `.patch` files, copy added files, or handle deleted files, go to [Relationship with GeneralUpdate.Core](#relationship-with-generalupdatecore). +### 3.1 Configuration Properties (Props) -```csharp -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Differ; +Differential is a low-level library with no configuration classes. All parameters are passed via constructors. -IBinaryDiffer differ = new BsdiffDiffer(); +**BsdiffDiffer Constructor Parameters:** -var oldFile = @"D:\releases\1.0.0\app.dll"; -var newFile = @"D:\releases\1.0.1\app.dll"; -var patchFile = @"D:\patches\app.dll.patch"; -var outputFile = @"D:\restore\app.dll"; +| Parameter | Type | Default | Required | Values | Description | +| --- | --- | --- | --- | --- | --- | +| `compressionProvider` | `ICompressionProvider` | `BZip2CompressionProvider` | Optional | `BZip2CompressionProvider` / `DeflateCompressionProvider` | Patch compression provider | -// Generate patch: oldFile + newFile -> patchFile -await differ.CleanAsync(oldFile, newFile, patchFile); +**StreamingHdiffDiffer Constructor Parameters:** -// Apply patch: oldFile + patchFile -> outputFile -await differ.DirtyAsync(oldFile, outputFile, patchFile); -``` +| Parameter | Type | Default | Required | Values | Description | +| --- | --- | --- | --- | --- | --- | +| `compressionProvider` | `ICompressionProvider` | `DeflateCompressionProvider` | Optional | `BZip2CompressionProvider` / `DeflateCompressionProvider` | Patch compression provider | +| `blockSize` | `int` | `65536` (64 KB) | Optional | Positive integer bytes | Block size for old file hash indexing | +| `maxWindowSize` | `int` | `134217728` (128 MB) | Optional | Positive integer bytes | Max memory window for computation | -Both `CleanAsync` and `DirtyAsync` accept a `CancellationToken`. The current implementations observe cancellation when work starts and at Core pipeline scheduling points; the inner algorithm loops do not check cancellation for every byte, so canceling a large file may complete only after the current file finishes processing. +**ICompressionProvider Format Identifiers:** -## Core API +| Provider | Format Byte | Availability | Description | +| --- | --- | --- | --- | +| `BZip2CompressionProvider` | `0x00` | Fully available | Legacy BSDIFF compatible, higher decompression cost | +| `DeflateCompressionProvider` | `0x01` | Fully available | Faster decompression, better for client batch apply | +| `BrotliCompressionProvider` | `0x02` | .NET 6+ only (source reserved) | Not recommended for production | -### IBinaryDiffer +### 3.2 Instance Methods -`IBinaryDiffer` is the shared abstraction for all file-level differ algorithms, and it is the key interface used by Core's differential pipeline. +**IBinaryDiffer:** -```csharp -public interface IBinaryDiffer -{ - Task DirtyAsync( - string oldFilePath, - string newFilePath, - string patchFilePath, - CancellationToken cancellationToken = default); - - Task CleanAsync( - string oldFilePath, - string newFilePath, - string patchFilePath, - CancellationToken cancellationToken = default); -} -``` +| Method | Parameters | Returns | Use Case | Notes | +| --- | --- | --- | --- | --- | +| `CleanAsync(string, string, string, CancellationToken)` | `oldFilePath`, `newFilePath`, `patchFilePath`, `cancellationToken` | `Task` | Generate patch during build/release | Large file cancellation not immediate | +| `DirtyAsync(string, string, string, CancellationToken)` | `oldFilePath`, `newFilePath` (restored output), `patchFilePath`, `cancellationToken` | `Task` | Apply patch during client upgrade | Result written to `newFilePath`, not overwriting old file | -| Parameter | Meaning | -| --- | --- | -| `oldFilePath` | Path to the old-version file. Required when generating and applying patches. | -| `newFilePath` | In `CleanAsync`, the source new-version file; in `DirtyAsync`, the restored output file. | -| `patchFilePath` | Path to the patch file. `CleanAsync` writes it; `DirtyAsync` reads it. | +### 3.3 Callback Events -### BsdiffDiffer +Differential does not publish events. Progress reporting and event notifications are implemented by Core's `DiffPipeline` via `DiffProgress` and `EventManager`. -`BsdiffDiffer` implements the BSDIFF 4.0 file-level binary diff algorithm. It reads the old and new files into memory, uses suffix sorting to find matching blocks, then writes control, diff, and extra sections. +--- -```csharp -using GeneralUpdate.Differential.Differ; +## 4. Advanced Examples -var differ = new BsdiffDiffer(); -await differ.CleanAsync(oldFile, newFile, patchFile); -await differ.DirtyAsync(oldFile, outputFile, patchFile); -``` +### 4.1 Extension Points Overview -| Feature | Notes | +| Extension Interface | Description | | --- | --- | -| Default compression | `BZip2CompressionProvider`, for compatibility with legacy BSDIFF patches. | -| Replaceable compression | The constructor accepts an `ICompressionProvider`. | -| Patch compatibility | Supports legacy 32-byte BSDIFF headers and the current 33-byte extended header. | -| Best fit | Compatibility-focused scenarios where file size is manageable and stable patch size matters. | -| Resource profile | Patch generation reads both old and new files, so large single files require memory planning. | +| `IBinaryDiffer` | Custom file-level diff algorithm; can integrate native libraries or proprietary algorithms | +| `ICompressionProvider` | Custom patch segment compression | -`BsdiffDiffer` also keeps `Clean(...)` and `Dirty(...)` methods. New code should prefer `IBinaryDiffer.CleanAsync` and `DirtyAsync` so the algorithm can be swapped later. +### 4.2 Examples by Scenario -### StreamingHdiffDiffer +#### Scenario 1: Custom Diff Algorithm -`StreamingHdiffDiffer` is the other differ implementation in the current source. It builds a block-level FNV-1a hash index to pre-filter candidate positions, extends matches at byte level, and writes a BSDIFF-compatible patch structure. +**Description:** Integrate an in-house high-compression-ratio diff algorithm. ```csharp using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Differ; - -var differ = new StreamingHdiffDiffer( - compressionProvider: new DeflateCompressionProvider(optimalLevel: true), - blockSize: 64 * 1024, - maxWindowSize: 128 * 1024 * 1024); - -await differ.CleanAsync(oldFile, newFile, patchFile); -await differ.DirtyAsync(oldFile, outputFile, patchFile); -``` - -| Feature | Notes | -| --- | --- | -| Default compression | `DeflateCompressionProvider`. | -| Block size | `BlockSize` defaults to 64 KB and is used to build the old-file block hash index. | -| Window budget | `MaxWindowSize` defaults to 128 MB and affects the memory window used during patch generation. | -| Patch application | `DirtyAsync` delegates to `BsdiffDiffer` patch application logic. | -| Best fit | Faster candidate matching and directory-level builds that should align with `DiffPipeline` defaults. | - -The current implementation is not a full external-memory streaming differ. If a single file exceeds `MaxWindowSize`, only the budgeted window participates in the calculation. For very large files, validate restoration results in your publishing pipeline, increase `MaxWindowSize`, or choose `BsdiffDiffer` or another implementation that better fits the file size. -## Differ algorithm selection - -Differential currently includes two file-level differ algorithms. Both write a BSDIFF-compatible patch structure, but their match strategy, default compression, and performance priorities are different. - -| Comparison | `BsdiffDiffer` | `StreamingHdiffDiffer` | -| --- | --- | --- | -| Core idea | Classic BSDIFF 4.0; uses suffix sorting to find long matches between old and new files. | Builds an old-file index with block-level FNV-1a hashes, filters candidate blocks quickly, then extends matches at byte level. | -| Default compression | BZip2 (`0x00`). | Deflate (`0x01`). | -| Patch application | Implements BSDIFF Dirty logic directly. | `DirtyAsync` delegates to `BsdiffDiffer`, so the apply phase remains BSDIFF-compatible. | -| Generation efficiency | More fine-grained matching and stable behavior for localized or dispersed changes, but suffix sorting and full file loading cost CPU and memory. | Faster when block matches are effective; can become slower when changes are dispersed and block-hash hits are rare. | -| Client-side apply performance | Default BZip2 decompression is more expensive when many patches are applied. | Default Deflate decompression is faster and friendlier for applying many client patches. | -| Patch-size tendency | Usually aims for fine-grained matches and much more stable patch size. | Speed-first; patch size strongly depends on change distribution, block size, and window budget. When block hits are poor, the patch can approach the full file size. | -| Memory profile | Reads the old and new files during generation, so large single files need memory planning. | Uses `BlockSize` and `MaxWindowSize` to control the matching window; very large single files need validation or tuning. | -| Compatibility | Best when legacy BSDIFF/BZip2 compatibility matters. | Best for new projects, directory-level batch diffs, and Core `DiffPipeline` default builds. | - -As a rule of thumb, `BsdiffDiffer` leans toward compatibility and stable patch size, while `StreamingHdiffDiffer` leans toward faster client-side apply and tunable parameters. If patch size matters most or changes are dispersed, start with `BsdiffDiffer`. If client-side apply speed matters more and your own benchmarks show acceptable patch size, consider `StreamingHdiffDiffer`. - -### Benchmark reference {#benchmark-reference} - -The following numbers come from a local microbenchmark against the current source. They are meant to give developers an order-of-magnitude reference, not a performance guarantee across all projects. The test used Windows x64, a .NET Release build, and synthetic 2-4 MB files. Real results depend on CPU, disk, file type, change ratio, compression level, and parallelism. - -| Scenario | `BsdiffDiffer` clean | `StreamingHdiffDiffer` clean | `BsdiffDiffer` dirty | `StreamingHdiffDiffer` dirty | `BsdiffDiffer` patch size | `StreamingHdiffDiffer` patch size | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | -| 2 MB text, small line changes/inserts | 484 ms | 2059 ms | 55 ms | 36 ms | 0.05% | 3.50% | -| 4 MB binary, contiguous local block change | 1030 ms | 318 ms | 55 ms | 11 ms | 2.58% | 100.04% | -| 4 MB binary, dispersed random byte changes | 757 ms | 4176 ms | 70 ms | 30 ms | 2.18% | 100.27% | - -Practical estimates from this benchmark: - -| Metric | Reference conclusion | -| --- | --- | -| Patch size | `BsdiffDiffer` produced patches around 0.05%-2.58% of the new file in these scenarios; `StreamingHdiffDiffer` produced around 3.50%-100%. If package size is the top priority, test `BsdiffDiffer` first. | -| Client-side apply speed | `StreamingHdiffDiffer` defaults to Deflate and applied patches about 1.5-5x faster in this benchmark. The difference matters more when many files are applied. | -| Generation speed | There is no absolute winner: `StreamingHdiffDiffer` was about 3.2x faster for the contiguous binary block change, while `BsdiffDiffer` was about 4.3-5.5x faster for text and dispersed random changes. | -| Large project choice | For large projects, evaluate total patch size, build time, and client apply time together. If many files can be processed independently, `WithParallelism(...)` often affects total time more than small differences in a single differ. | - -> The key takeaway is directional: `BsdiffDiffer` more often produces small patches, while `StreamingHdiffDiffer` applies patches faster, but patch size and generation speed are very sensitive to the shape of file changes. Before production release, benchmark with your own application artifacts. - -Recommended choices: - -| Scenario | Recommendation | -| --- | --- | -| Low-level single-file patching with maximum compatibility | Use `new BsdiffDiffer()`. | -| Directory-level batch patches through Core `DiffPipeline` | Benchmark the default configuration first; if patch size is too large, explicitly switch to `BsdiffDiffer`; then combine it with `WithParallelism(...)` to improve throughput. | -| Client-side decompression performance is more important | Prefer Deflate patches: `StreamingHdiffDiffer` defaults, or `new BsdiffDiffer(new DeflateCompressionProvider())`. | -| Existing patches are legacy BSDIFF/BZip2 | Apply them with `BsdiffDiffer`; 32-byte headers are treated as BZip2. | -| Large projects with many DLLs, resources, or plugin files | Use Core `DiffPipeline` for file-level parallelism instead of calling Differential file by file in a serial loop. | - -## Patch format and compression providers {#patch-format-and-compression-providers} - -Differential writes BSDIFF-style patches. The current implementation writes a 33-byte extended header: - -| Offset | Length | Meaning | -| --- | --- | --- | -| `0` | 8 | Magic string `"BSDIFF40"`. | -| `8` | 8 | Compressed control-section length. | -| `16` | 8 | Compressed diff-section length. | -| `24` | 8 | New-file length. | -| `32` | 1 | Compression format version. | - -Patch application is also compatible with legacy 32-byte headers. If the 33rd format byte is absent, the patch is treated as a BZip2 legacy patch. - -### ICompressionProvider - -`ICompressionProvider` wraps the control, diff, and extra sections in compression streams. - -```csharp -public interface ICompressionProvider +public sealed class HighRatioDiffer : IBinaryDiffer { - byte FormatVersion { get; } - - Stream CreateCompressStream( - Stream output, - CancellationToken cancellationToken = default); + public Task CleanAsync( + string oldFilePath, string newFilePath, string patchFilePath, + CancellationToken cancellationToken = default) + { + // Call proprietary algorithm to generate patch + return Task.CompletedTask; + } - Stream CreateDecompressStream( - Stream input, - CancellationToken cancellationToken = default); + public Task DirtyAsync( + string oldFilePath, string newFilePath, string patchFilePath, + CancellationToken cancellationToken = default) + { + // Call proprietary algorithm to apply patch + return Task.CompletedTask; + } } + +// Use in Core DiffPipeline +var pipeline = new DiffPipelineBuilder() + .UseDiffer(new HighRatioDiffer()) + .WithParallelism(4) + .Build(); ``` -| Provider | Format byte | Current availability | Notes | -| --- | --- | --- | --- | -| `BZip2CompressionProvider` | `0x00` | Available | Default for `BsdiffDiffer`; compatible with legacy BSDIFF patches. | -| `DeflateCompressionProvider` | `0x01` | Available | BCL `DeflateStream`; friendlier decompression speed for client updates. | -| `BrotliCompressionProvider` | `0x02` | Reserved in source behind `NET6_0_OR_GREATER` | The current `GeneralUpdate.Differential` project targets `netstandard2.0`, and the patch reader currently recognizes only `0x00` and `0x01`, so do not use Brotli for production update packages. | +#### Scenario 2: Custom Compression Provider + BsdiffDiffer -When customizing compression, generated patches must use a format byte that the patch reader can recognize. For production use today, prefer BZip2 or Deflate. +**Description:** Use BsdiffDiffer's precise matching with Deflate's fast decompression for better client-side patch application speed. ```csharp using GeneralUpdate.Differential.Abstractions; @@ -264,187 +163,129 @@ await differ.CleanAsync(oldFile, newFile, patchFile); await differ.DirtyAsync(oldFile, outputFile, patchFile); ``` -## Relationship with GeneralUpdate.Core {#relationship-with-generalupdatecore} +#### Scenario 3: StreamingHdiffDiffer Parameter Tuning -`GeneralUpdate.Core` builds directory-level differential updates on top of Differential through `DiffPipeline`. It is responsible for: - -1. Comparing the old and new directories. -2. Finding changed files and calling `IBinaryDiffer.CleanAsync` to generate `.patch` files. -3. Copying added files into the patch directory. -4. Writing `generalupdate.delete.json` for deleted files. -5. Applying patches on the client by calling `IBinaryDiffer.DirtyAsync` in parallel, writing temporary files first, then replacing originals after success. - -If you use `GeneralUpdate.Core` in an application update flow, Core integrates Differential by default and includes the differential pipeline out of the box. In other words, normal update integration does not require extra installation, initialization, or direct calls to `GeneralUpdate.Differential`. As long as you use the Core update flow and enable patch update behavior as needed, Core creates the differ, applies patches, and performs directory-level orchestration internally. - -Use `UseDiffPipeline` only for advanced configuration, such as replacing the default differ algorithm, tuning parallelism, changing error behavior, or plugging in custom matchers: +**Description:** Large single files (200MB+) need adjusted window budget to avoid OOM. ```csharp -using GeneralUpdate.Core; -using GeneralUpdate.Core.Models; -using GeneralUpdate.Differential.Differ; +var differ = new StreamingHdiffDiffer( + compressionProvider: new DeflateCompressionProvider(optimalLevel: true), + blockSize: 32 * 1024, // 32 KB blocks for denser hash indexing + maxWindowSize: 256 * 1024 * 1024); // 256 MB for large files -await new GeneralUpdateBootstrap() - .SetSource( - updateUrl: "https://update.example.com/api/upgrade/verification", - appSecretKey: "your-app-secret") - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.PatchEnabled, true) - .UseDiffPipeline(builder => builder - .UseDiffer(new StreamingHdiffDiffer()) - .WithParallelism(4) - .WithStopOnFirstError(true)) - .LaunchAsync(); +await differ.CleanAsync(oldLargeFile, newLargeFile, patchFile); ``` -There are two default layers in the current source: - -| Usage | Default differ | -| --- | --- | -| Direct `new DiffPipeline()` or `new DiffPipelineBuilder().Build()` | `StreamingHdiffDiffer` | -| `GeneralUpdateBootstrap` without an explicit `UseDiffPipeline(...)` | `BsdiffDiffer`, parallelism 2, with `DiffProgressReporter` | - -For regular users, Differential can be treated as a built-in Core capability that does not need separate integration. Call `UseDiffPipeline(...)` only when you want Core to use a specific algorithm or custom differential behavior. +--- -## Relationship with GeneralUpdate.Tools {#relationship-with-generalupdatetools} +## 5. Basic Usage Examples -`GeneralUpdate.Tools` targets the publishing side and helps developers build update artifacts. The current `DiffService` creates `new DiffPipeline()` and calls: +### 5.1 Quick Start (Minimal Demo) ```csharp -await pipeline.CleanAsync(oldDir, newDir, patchDir); -``` - -In other words, Tools uses Core's `DiffPipeline` to build directory-level differential packages, and `DiffPipeline` calls Differential's `IBinaryDiffer` for each changed file. For most developers, the recommended path is: - -1. Use Tools to compare the old and new version directories and generate the patch directory plus manifest artifacts. -2. Use Core on the client to check versions, download packages, and apply patches. -3. Use Differential directly only when experimenting with custom algorithms, compression formats, or single-file patching. - -This layering keeps business code simple: Tools builds artifacts, Core runs updates, and Differential handles low-level file diffs. - -## Concurrency model and performance guidance +using GeneralUpdate.Differential.Abstractions; +using GeneralUpdate.Differential.Differ; -A Differential differ instance does not store mutable shared state for a specific patch operation. As long as the supplied `ICompressionProvider` is thread-safe, the built-in differ implementations can be called concurrently by Core's pipeline. The built-in BZip2 and Deflate providers create a new compression stream for each call and are suitable for concurrent use. +IBinaryDiffer differ = new BsdiffDiffer(); -Real "multi-threaded diff" normally happens at the Core `DiffPipeline` layer: +var oldFile = @"D:\releases\1.0.0\app.dll"; +var newFile = @"D:\releases\1.0.1\app.dll"; +var patchFile = @"D:\patches\app.dll.patch"; +var outputFile = @"D:\restore\app.dll"; -```csharp -var pipeline = new DiffPipelineBuilder() - .UseDiffer(new StreamingHdiffDiffer()) - .WithParallelism(4) - .Build(); +// Generate patch: oldFile + newFile → patchFile +await differ.CleanAsync(oldFile, newFile, patchFile); -await pipeline.CleanAsync(oldDir, newDir, patchDir); +// Apply patch: oldFile + patchFile → outputFile +await differ.DirtyAsync(oldFile, outputFile, patchFile); ``` -### Parallel differential work for large projects {#parallel-differential-work-for-large-projects} - -Large desktop projects are usually not one huge file. They are often composed of the main executable, many DLLs, plugins, resource files, runtime files, and configuration files. Core `DiffPipeline` splits the directory comparison result into file-level tasks, and each changed file independently calls `IBinaryDiffer.CleanAsync` to produce a patch. That means `WithParallelism(...)` can process multiple files at the same time. +### 5.2 Basic Parameter Combination -This parallel model matters for large projects: - -1. The publishing side can generate `.patch` files for multiple changed files at once, shortening package build time. -2. The client side can also apply multiple file patches in parallel, reducing the upgrade window. -3. Core orchestrates new-file copying, delete manifest handling, and differential patch generation, so developers do not need to write custom thread scheduling. -4. Parallelism can be tuned for the machine: higher on build servers, lower on resource-sensitive clients. - -| Parameter/strategy | Guidance | -| --- | --- | -| `WithParallelism(1)` | Resource-sensitive devices, HDDs, or low-memory environments. | -| `WithParallelism(2)` | Balanced default for most desktop applications. | -| `WithParallelism(4-8)` | Multi-core CPUs, SSDs, build machines, or publishing servers. | -| BZip2 | Better compatibility, but higher client-side decompression cost. | -| Deflate | Friendlier decompression speed for applying many client patches. | -| Large files | Measure generation time, memory peak, and restoration correctness; do not look only at patch size. | - -Parallel differential work is best for large projects with many independently processable files. A single very large file is still handled internally by the selected differ algorithm; `WithParallelism(8)` does not split one file into eight parallel chunks. It improves throughput across multiple files. +```csharp +// Option A: Classic BSDIFF + BZip2 → smallest patch size +var differA = new BsdiffDiffer(); -Downloads and differential work can run in parallel at the upper update-flow level: Core can download multiple resources concurrently, and the patch application phase can process files in parallel. Differential itself only computes the patch for one file and does not manage network download threads. +// Option B: Classic BSDIFF + Deflate → small patch + faster apply +var differB = new BsdiffDiffer(new DeflateCompressionProvider(optimalLevel: true)); -## Extension points +// Option C: Streaming HDiff + Deflate → fast generation + fastest apply +var differC = new StreamingHdiffDiffer( + new DeflateCompressionProvider(optimalLevel: true), + blockSize: 64 * 1024, + maxWindowSize: 128 * 1024 * 1024); +``` -### Custom differ algorithm +### 5.3 Production Usage (via Core DiffPipeline) -Implement `IBinaryDiffer` to plug another algorithm into Core. This is useful when integrating a native library, optimizing specific file types, or replacing the built-in algorithms. +Most scenarios use Differential indirectly through Core's `DiffPipeline`: ```csharp -using GeneralUpdate.Differential.Abstractions; - -public sealed class MyBinaryDiffer : IBinaryDiffer -{ - public Task CleanAsync( - string oldFilePath, - string newFilePath, - string patchFilePath, - CancellationToken cancellationToken = default) - { - // Generate patchFilePath from oldFilePath and newFilePath. - throw new NotImplementedException(); - } - - public Task DirtyAsync( - string oldFilePath, - string newFilePath, - string patchFilePath, - CancellationToken cancellationToken = default) - { - // Restore newFilePath from oldFilePath and patchFilePath. - throw new NotImplementedException(); - } -} -``` +using GeneralUpdate.Core; +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Differential.Differ; -```csharp +// Build side: compare old/new version directories, generate patches var pipeline = new DiffPipelineBuilder() - .UseDiffer(new MyBinaryDiffer()) - .WithParallelism(4) + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(8) + .WithProgress(new Progress(p => + Console.WriteLine($"[Build] {p.Completed}/{p.Total} {p.CurrentFile}"))) .Build(); -``` -Your custom algorithm must ensure that patches produced by `CleanAsync` can be applied correctly by the same algorithm's `DirtyAsync`. If the patch will be consumed by Core clients, the publishing side and client side must use the same differ implementation. +await pipeline.CleanAsync(@"D:\builds\v1.0.0", @"D:\builds\v1.0.1", @"D:\patches\v1.0.0-to-v1.0.1"); -### Custom compression provider +// Client side: via GeneralUpdateBootstrap +await new GeneralUpdateBootstrap() + .SetSource(updateUrl: "https://update.mycompany.com/api/upgrade/verification", appSecretKey: "prod-key") + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.PatchEnabled, true) + .UseDiffPipeline(builder => builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4)) + .LaunchAsync(); +``` -If you want to keep the BSDIFF-compatible patch structure but change how the control, diff, and extra sections are compressed, implement `ICompressionProvider`. +--- -```csharp -using GeneralUpdate.Differential.Abstractions; +## 6. Algorithm Selection Guide -public sealed class MyCompressionProvider : ICompressionProvider -{ - public byte FormatVersion => 0x01; +### Clean vs Dirty Semantics - public Stream CreateCompressStream( - Stream output, - CancellationToken cancellationToken = default) - { - return new DeflateStream( - output, - CompressionLevel.Optimal, - leaveOpen: true); - } +| Term | Method | Input | Output | Typical Location | +| --- | --- | --- | --- | --- | +| Clean | `CleanAsync` | Old file, new file, patch output path | `.patch` file | Build/release phase | +| Dirty | `DirtyAsync` | Old file, output new file path, patch path | Restored new file | Client upgrade phase | - public Stream CreateDecompressStream( - Stream input, - CancellationToken cancellationToken = default) - { - return new DeflateStream( - input, - CompressionMode.Decompress, - leaveOpen: true); - } -} -``` +### Algorithm Comparison -Do not allocate a new `FormatVersion` casually. Current `BsdiffDiffer.DirtyAsync` recognizes only BZip2 (`0x00`) and Deflate (`0x01`). If you introduce a new format, you must also extend the patch reader, otherwise clients cannot apply the patch. +| Dimension | `BsdiffDiffer` | `StreamingHdiffDiffer` | +| --- | --- | --- | +| Core Approach | Classic BSDIFF 4.0, suffix sort + longest match | FNV-1a block hash indexing + byte-level extension | +| Default Compression | BZip2 (0x00) | Deflate (0x01) | +| Patch Application | Self-implemented BSDIFF Dirty | Delegates to `BsdiffDiffer` (BSDIFF compatible) | +| Patch Size | More stable, typically smaller | Sensitive to change distribution | +| Client Apply Speed | BZip2 decompression slower | Deflate decompression faster (~1.5-5x) | +| Generation Memory | Full read of old & new files | Budgeted by `maxWindowSize` | +| Compatibility | Compatible with legacy BSDIFF/BZip2 patches | Better for new projects | -## Practical guidance +### Recommendations by Scenario -| Scenario | Recommended approach | +| Scenario | Recommendation | | --- | --- | -| Regular application differential updates | Use `GeneralUpdate.Tools` to build artifacts and Core on the client. | -| Need control over directory-level parallelism, error handling, and progress | Use Core's `DiffPipelineBuilder`. | -| Need to validate one file's patch behavior | Use `IBinaryDiffer` directly. | -| Patch size and application speed both matter | Benchmark BZip2, Deflate, and different algorithms on the same file set before choosing a default. | -| Packages must remain compatible with older clients | Conservatively use `BsdiffDiffer` + BZip2, or ensure clients already support the Deflate extended header. | +| Patch size priority | `BsdiffDiffer` + BZip2 | +| Client apply speed priority | `StreamingHdiffDiffer` (default Deflate) | +| Legacy patch format compatibility | `BsdiffDiffer` (32-byte legacy header auto-treated as BZip2) | +| Large files (>500MB) | Benchmark first; may need `StreamingHdiffDiffer` + larger `maxWindowSize` | +| Directory-level batch diff | Via Core `DiffPipeline` with `WithParallelism` | +| New project | Run baseline with defaults, adjust based on size/speed needs | + +--- + +## Related Resources -Differential's value is that it compresses complex binary diff behavior into a stable file-level abstraction. Application developers can focus on when to update, what to download, and how to inform users, while Core, Tools, and Differential handle patch generation and application together. +- [GeneralUpdate Repository](https://github.com/GeneralLibrary/GeneralUpdate) +- [Differential Sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Hub/Samples/DifferentialSample.cs) +- [Core DiffPipeline Docs](GeneralUpdate.Core.md) +- [Packaging Guide](../guide/Packaging.md) diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Drivelution.md b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Drivelution.md index 282c40c..ab8974d 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Drivelution.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Drivelution.md @@ -2,372 +2,356 @@ sidebar_position: 12 --- -### Definition +# GeneralUpdate.Drivelution -Namespace: `GeneralUpdate.Drivelution` +**Namespace:** `GeneralUpdate.Drivelution` | **Main Entry Point:** `GeneralDrivelution` (static class) | **NuGet Package:** `GeneralUpdate.Drivelution` -Assembly: `GeneralUpdate.Drivelution.dll` +## 1. Component Overview -```c# -public static class GeneralDrivelution -``` +### 1.1 Introduction -`GeneralUpdate.Drivelution` is a cross-platform component for operating-system driver updates. It turns the risky parts of driver servicing into a unified pipeline: platform detection, permission checks, file validation, backup, installation, post-install verification, and rollback entry points. Windows, Linux, and macOS implementations call the native tools that each platform expects for driver installation. +**GeneralUpdate.Drivelution** is a cross-platform driver update component. It structures the error-prone steps of driver updates into a unified pipeline: platform detection → permission check → file validation (hash/signature/compatibility) → backup → install → verification → rollback on failure, invoking native system tools on Windows, Linux, and macOS respectively. -Driver updates are different from normal application file updates. Application updates usually download, extract, replace files, and restart a process. Driver updates can affect the kernel, device nodes, system extensions, or the system driver store, so they also need administrator/root permissions, trusted signatures, target OS and CPU architecture checks, command-result handling, restart planning, and a recovery path. Drivelution covers operating-system driver updates only; it does not handle device-internal flashing workflows. +Driver updates differ fundamentally from application file replacement. Drivers affect the kernel, device nodes, system extensions, or driver repositories, requiring special attention to administrator privileges, signature trust, target OS and CPU architecture, install command return values, reboot requirements, and failure recovery paths. -### Capability overview +**Core Capabilities:** -| Capability | Current implementation | +| Capability | Description | | --- | --- | -| Platform adaptation | `GeneralDrivelution.Create()` automatically chooses the Windows, Linux, or macOS implementation. | -| Standard pipeline | Windows/Linux run a permission step before `Validate -> Backup -> Install -> Verify`. macOS currently includes a `CheckSudo` step and then runs system commands; actual installation still depends on system permissions. | -| Validation | File existence, optional hash validation, optional signature validation, target OS and architecture compatibility. | -| Backup | `UpdateStrategy.RequireBackup` defaults to `true`; the backup root comes from `UpdateStrategy.BackupPath`. | -| Installation | Windows uses `pnputil.exe`; Linux uses `insmod`/`modprobe`, `dpkg`, `rpm`/`dnf`; macOS uses system tools such as `kextload` and `installer`. | -| Rollback | `RollbackAsync(backupPath)` is exposed. Windows reinstalls backed-up `.inf` files, Linux restores `.ko` modules, and macOS restores `.kext` bundles. | -| Batch/parallel work | `BatchUpdateAsync` supports `BatchMode.Sequential` and `BatchMode.Parallel` for large driver sets. | -| Logging | `GeneralTracer` writes to console and `Logs\generalupdate-trace yyyy-MM-dd.log` by default, and can be disabled with `SetTracingEnabled(false)`. | - -### When to use Drivelution - -Use Drivelution when: - -- A hardware-vendor client needs to ship NIC, capture-card, USB, virtual-device, or similar drivers. -- Enterprise or industrial environments need to scan driver packages and update them from a manifest-like list. -- An installer, maintenance tool, or device-management service needs one API across Windows/Linux/macOS. -- You need hash, signature, OS, and architecture validation before installation, plus a backup path for recovery. +| Cross-Platform Abstraction | `GeneralDrivelution.Create()` auto-detects platform and creates the appropriate implementation | +| Standard Update Pipeline | Permission check → file validation → backup → install → verify → rollback | +| File Validation | File existence, SHA256/MD5 hash, Authenticode/GPG/codesign signature, OS/arch compatibility | +| Backup & Rollback | `RequireBackup` defaults to `true`; auto-rollback on failure with `RollbackAsync` for explicit recovery | +| Windows Installation | Via `pnputil.exe /add-driver /install` for INF driver packages | +| Linux Installation | Supports `.ko` (`insmod`/`modprobe`), `.deb` (`dpkg -i`), `.rpm` (`rpm -ivh`/`dnf install`) | +| macOS Installation | Supports `.kext` (`kextload`), `.dext` (SystemExtensions), `.pkg` (`installer`) | +| Batch Updates | `BatchUpdateAsync` with `BatchMode.Sequential` and `BatchMode.Parallel` | +| Progress Reporting | Via `IProgress` reporting step-level progress, status, and messages | +| Trace Logging | `GeneralTracer` with console + daily-rotating file output | + +**Business Problems Solved:** +- Hardware vendor clients need to deliver driver updates alongside apps, but OS-specific driver installation methods vary widely +- Driver installation requires administrator privileges, signature verification, architecture compatibility, and other safeguards +- Failed driver installations need reliable rollback to avoid leaving devices unusable + +**Use Cases:** +- Hardware vendor clients: network cards, capture cards, USB devices, virtual device drivers +- Enterprise/industrial sites: batch scan driver packages and update by manifest +- Installer/maintenance tools: unified handling of Windows/Linux/macOS driver differences + +### 1.2 Environment & Dependencies + +| Item | Description | +| --- | --- | +| **Version** | `10.5.0-beta.2` | +| **Target Frameworks** | `net8.0` / `net10.0` (multi-target) | +| **Dependencies** | `Microsoft.Extensions.DependencyInjection`, `Microsoft.Extensions.Logging.Abstractions`, `Microsoft.Extensions.Options` | +| **Compatibility** | Windows (full, admin required) / Linux (root/sudo) / macOS (SIP & system extension policies apply) | -Do not use Drivelution as a generic application file updater. If you only need to update your own exe, dll, assets, or plugins, use the `GeneralUpdate.Core` application update flow instead. +--- -### Installation +## 2. Feature List + +| Feature | Description | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| Single Driver Quick Update | `QuickUpdateAsync` with default safe strategy | Core | Optional | Custom strategy recommended for production | +| Custom Strategy Update | `UpdateAsync` with `UpdateStrategy` and `DrivelutionOptions` | Core | Recommended | Control backup, retry, timeout, restart | +| Driver Validation | File existence, SHA256 hash, signature, OS/arch compatibility | Core | Optional | `ValidateAsync`; can skip via strategy flags | +| Driver Backup | Backup driver files before update | Core | Automatic | `RequireBackup` defaults to `true` | +| Driver Rollback | Restore driver from backup path | Core | Optional | `RollbackAsync(backupPath)` | +| Directory Scanning | Scan and parse driver info (`.inf`/`.ko`/`.kext` etc.) | Core | Optional | `GetDriversFromDirectoryAsync` | +| Batch Updates | Batch update by manifest, sequential or parallel | Extended | Optional | `BatchUpdateAsync` | +| Platform Info Query | Get current OS/arch/version/support status | Core | Optional | `GetPlatformInfo()` | +| Restart Behavior Control | `UpdateStrategy.RestartMode` sets restart intent | Extended | Optional | Pipeline does not auto-restart; use `RestartHelper` | +| DI Registration | `AddDrivelution` extension registers platform services | Extended | Optional | Supports Generic Host / ASP.NET Core | +| Trace Logging | `GeneralTracer` runtime diagnostic logs | Extended | Optional | Enabled by default, can be disabled | -```bash -dotnet add package GeneralUpdate.Drivelution -``` +--- -Or add the package reference: +## 3. API Configuration Reference + +### 3.1 Configuration Properties (Props) + +**DriverInfo:** + +| Field | Type | Default | Required | Values | Description | +| --- | --- | --- | --- | --- | --- | +| `Name` | `string` | `""` | Yes | — | Driver name | +| `Version` | `string` | `""` | Recommended | SemVer format | Driver version | +| `FilePath` | `string` | `""` | Yes | Valid file path | Win: `.inf`; Linux: `.ko`/`.deb`/`.rpm`; macOS: `.kext`/`.dext`/`.pkg` | +| `TargetOS` | `string` | `""` | Optional | `"Windows"`/`"Linux"`/`"MacOS"` | No restriction when empty | +| `Architecture` | `string` | `""` | Optional | `"x64"`/`"amd64"`/`"x86"`/`"arm64"`/`"arm"` | Common alias normalization; no restriction when empty | +| `HardwareId` | `string` | `""` | Optional | — | Hardware ID or module alias | +| `Hash` | `string` | `""` | Optional | SHA256/MD5 value | Hash verified when non-empty and `SkipHashValidation = false` | +| `HashAlgorithm` | `string` | `"SHA256"` | Optional | `"SHA256"` / `"MD5"` | Hash algorithm | +| `TrustedPublishers` | `List` | `new()` | Optional | — | Signature verified when non-empty and `SkipSignatureValidation = false` | +| `Description` | `string` | `""` | Optional | — | Driver description | +| `ReleaseDate` | `DateTime` | — | Optional | — | Release date | +| `Metadata` | `Dictionary` | `new()` | Optional | — | Extended metadata | + +**UpdateStrategy:** + +| Field | Type | Default | Required | Values | Description | +| --- | --- | --- | --- | --- | --- | +| `RequireBackup` | `bool` | `true` | Optional | `true` / `false` | Execute backup step | +| `BackupPath` | `string` | `""` | Recommended | Valid directory path | Backup root path | +| `RestartMode` | `RestartMode` | `Prompt` | Optional | `None`, `Prompt`, `Delayed`, `Immediate` | Pipeline does not auto-restart system | +| `SkipHashValidation` | `bool` | `false` | Optional | `true` / `false` | Skip hash check (debug only) | +| `SkipSignatureValidation` | `bool` | `false` | Optional | `true` / `false` | Skip signature check (debug only) | +| `TimeoutSeconds` | `int` | `300` | Optional | Positive integer | Single update timeout | +| `RetryCount` | `int` | `3` | Optional | Positive integer | Retry count (actual retry from `DrivelutionOptions`) | +| `Mode` | `UpdateMode` | `Full` | Optional | `Full`, `Incremental` | Update mode | +| `ForceUpdate` | `bool` | `false` | Optional | `true` / `false` | Force update | +| `Priority` | `int` | `0` | Optional | — | Priority | + +**DrivelutionOptions:** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `DefaultBackupPath` | `string` | `"./DriverBackups"` | Optional | Default backup path | +| `DefaultRetryCount` | `int` | `3` | Optional | Default retry count | +| `DefaultRetryIntervalSeconds` | `int` | `5` | Optional | Default retry interval | +| `DefaultTimeoutSeconds` | `int` | `300` | Optional | Default timeout | +| `DebugModeSkipSignature` | `bool` | `false` | Optional | Debug: skip signature | +| `DebugModeSkipHash` | `bool` | `false` | Optional | Debug: skip hash | +| `ForceTerminateOnPermissionFailure` | `bool` | `true` | Optional | Terminate on permission failure | +| `AutoCleanupBackups` | `bool` | `true` | Optional | Auto-clean old backups | +| `BackupsToKeep` | `int` | `5` | Optional | Number of backups to keep | +| `UseExponentialBackoff` | `bool` | `false` | Optional | Use exponential backoff | + +### 3.2 Instance Methods + +**GeneralDrivelution (static):** + +| Method | Parameters | Returns | Use Case | Notes | +| --- | --- | --- | --- | --- | +| `Create(DrivelutionOptions?)` | `options` | `IGeneralDrivelution` | Create platform driver updater | Auto-detects platform | +| `Create(IServiceProvider)` | `serviceProvider` | `IGeneralDrivelution` | From DI container | Falls back to auto-detect if not registered | +| `QuickUpdateAsync(DriverInfo, UpdateStrategy?, IProgress?, CancellationToken)` | `driverInfo`, `strategy?`, `progress?`, `ct` | `Task` | Quick single driver update | Uses safe defaults | +| `ValidateAsync(DriverInfo, CancellationToken)` | `driverInfo`, `ct` | `Task` | Standalone validation | — | +| `GetPlatformInfo()` | None | `PlatformInfo` | Query platform info | OS/arch/version/support status | +| `GetDriversFromDirectoryAsync(string, string?, CancellationToken)` | `path`, `searchPattern?`, `ct` | `Task>` | Scan directory for drivers | Default pattern varies by platform | +| `BatchUpdateAsync(IEnumerable, UpdateStrategy, BatchMode, IProgress?, CancellationToken)` | `drivers`, `strategy`, `mode`, `progress?`, `ct` | `Task` | Batch update multiple drivers | Parallel may contend for system resources | + +**IGeneralDrivelution:** + +| Method | Parameters | Returns | Use Case | +| --- | --- | --- | --- | +| `UpdateAsync(...)` | Same as `QuickUpdateAsync` | `Task` | Full update pipeline | +| `ValidateAsync(...)` | — | `Task` | Standalone validation | +| `BackupAsync(DriverInfo, string, CancellationToken)` | `driverInfo`, `backupPath`, `ct` | `Task` | Standalone backup | +| `RollbackAsync(string, CancellationToken)` | `backupPath`, `ct` | `Task` | Restore from backup | +| `GetDriversFromDirectoryAsync(...)` | — | `Task>` | Scan directory | +| `BatchUpdateAsync(...)` | — | `Task` | Batch update | + +### 3.3 Callback Events + +Drivelution reports progress via `IProgress` rather than a dedicated event system. + +| Progress Field | Type | Description | +| --- | --- | --- | +| `CurrentStatus` | `UpdateStatus` | Current status (`Validating`/`BackingUp`/`Updating`/`Verifying`/`Succeeded`/`Failed`/`RolledBack`) | +| `StepName` | `string` | Current step name | +| `Percentage` | `int` | Progress percentage (0-100) | +| `Message` | `string` | Progress message | +| `StepIndex` | `int` | Current step index | +| `TotalSteps` | `int` | Total step count | -```xml - -``` +--- -### Quick start: update one driver +## 4. Advanced Examples -```c# -using GeneralUpdate.Drivelution; -using GeneralUpdate.Drivelution.Abstractions.Models; +### 4.1 Extension Points Overview -var driver = new DriverInfo -{ - Name = "MyDevice Driver", - Version = "1.2.0", - FilePath = @"C:\Drivers\mydevice.inf", - TargetOS = "Windows", - Architecture = "x64", - Hash = "driver-file-sha256", - HashAlgorithm = "SHA256", - TrustedPublishers = { "Contoso Hardware" } -}; +| Extension Interface | Description | +| --- | --- | +| `IGeneralDrivelution` | Fully replace driver updater behavior | +| `IDriverValidator` | Custom file validation logic | +| `IDriverBackup` | Custom backup/restore strategy | +| `ICommandRunner` | Custom system command executor | +| `INetworkDownloader` | Reserved interface (network download) | +| `BaseDriverUpdater` | Abstract base class for new platform implementations | -var result = await GeneralDrivelution.QuickUpdateAsync(driver); +### 4.2 Examples by Scenario -if (result.Success) -{ - Console.WriteLine($"Driver updated. Duration={result.DurationMs}ms"); -} -else -{ - Console.WriteLine($"Driver update failed: {result.Error?.Message}"); - Console.WriteLine(string.Join(Environment.NewLine, result.StepLogs)); -} -``` +#### Scenario 1: Custom Strategy + Rollback -`QuickUpdateAsync` creates the updater for the current platform and uses safe defaults: backup is required, failures can be retried 3 times, and the retry interval is 5 seconds. In production, pass an explicit `UpdateStrategy`, especially for backup location, timeout, and restart behavior. +**Description:** Production driver update with all security checks enabled; explicit rollback on failure. -### Custom strategy - -```c# +```csharp using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions.Configuration; using GeneralUpdate.Drivelution.Abstractions.Models; -var options = new DrivelutionOptions +var updater = GeneralDrivelution.Create(new DrivelutionOptions { DefaultBackupPath = @"C:\DriverBackups", DefaultRetryCount = 3, - DefaultRetryIntervalSeconds = 5, DefaultTimeoutSeconds = 600, - UseExponentialBackoff = true, - ForceTerminateOnPermissionFailure = true -}; - -var updater = GeneralDrivelution.Create(options); + UseExponentialBackoff = true +}); var strategy = new UpdateStrategy { RequireBackup = true, BackupPath = @"C:\DriverBackups\graphics", - RetryCount = 3, - RetryIntervalSeconds = 5, TimeoutSeconds = 600, - RestartMode = RestartMode.Prompt, - SkipHashValidation = false, - SkipSignatureValidation = false + RestartMode = RestartMode.Prompt }; -var progress = new Progress(p => +var driver = new DriverInfo { - Console.WriteLine($"{p.Percentage}% {p.StepName}: {p.Message}"); -}); + Name = "Graphics Driver", + Version = "2.1.0", + FilePath = @"C:\Drivers\graphics.inf", + TargetOS = "Windows", + Architecture = "x64", + Hash = "expected-sha256...", + TrustedPublishers = { "Contoso Hardware Inc." } +}; + +var progress = new Progress(p => + Console.WriteLine($"[{p.StepName}] {p.Percentage}%: {p.Message}")); var result = await updater.UpdateAsync(driver, strategy, progress); if (!result.Success && result.BackupPath is not null) -{ await updater.RollbackAsync(result.BackupPath); -} -``` -> Note: `UpdateStrategy.RetryCount` and `RetryIntervalSeconds` exist on the strategy model. The current pipeline retry policy is created from `DrivelutionOptions.DefaultRetryCount`, `DefaultRetryIntervalSeconds`, and `UseExponentialBackoff`. Configure `DrivelutionOptions` when you need to control retry behavior consistently. +if (result.Success && RestartHelper.IsRestartRequired(strategy.RestartMode)) + await RestartHelper.HandleRestartAsync(strategy.RestartMode, 60, "Driver updated. Restart now?"); +``` -### Dependency injection +#### Scenario 2: DI Container Integration -Generic Host, ASP.NET Core, and custom containers can register the current-platform implementation: +**Description:** Register driver update services via DI in ASP.NET Core / Generic Host apps. -```c# +```csharp using GeneralUpdate.Drivelution.Core; builder.Services.AddDrivelution(options => { options.DefaultBackupPath = "./DriverBackups"; options.DefaultTimeoutSeconds = 600; + options.DefaultRetryCount = 3; }); -var updater = GeneralDrivelution.Create(builder.Services.BuildServiceProvider()); +// Use in controller or service +app.MapPost("/drivers/update", async (DriverInfo driver, IGeneralDrivelution updater) => +{ + var result = await updater.UpdateAsync(driver, new UpdateStrategy()); + return result.Success ? Results.Ok(result) : Results.BadRequest(result.Error); +}); ``` -`AddDrivelution` registers `ICommandRunner`, the platform `IDriverValidator`, `IDriverBackup`, and `IGeneralDrivelution`. - -### API overview - -#### `GeneralDrivelution` - -| Method | Description | -| --- | --- | -| `Create(DrivelutionOptions? options = null)` | Detects the current OS and creates the platform driver updater. | -| `Create(IServiceProvider serviceProvider)` | Resolves `IGeneralDrivelution` from DI; falls back to automatic platform creation if not registered. | -| `QuickUpdateAsync(driverInfo, strategy?, progress?, token?)` | Updates one driver with default or custom strategy. | -| `ValidateAsync(driverInfo, token?)` | Validates a driver with the current platform validator. | -| `GetPlatformInfo()` | Returns platform, OS, architecture, system version, and support status. | -| `GetDriversFromDirectoryAsync(path, pattern?, token?)` | Scans a directory and parses driver information. | -| `BatchUpdateAsync(drivers, strategy, mode, progress?, token?)` | Updates multiple drivers sequentially or in parallel. | - -#### `IGeneralDrivelution` +#### Scenario 3: Batch Parallel Scanning + Sequential Installation -| Method | Description | -| --- | --- | -| `UpdateAsync(driverInfo, strategy, progress?, token?)` | Runs the full update pipeline. | -| `ValidateAsync(driverInfo, token?)` | Validates a driver only. | -| `BackupAsync(driverInfo, backupPath, token?)` | Backs up the driver file. | -| `RollbackAsync(backupPath, token?)` | Attempts platform-specific recovery from a backup. | -| `GetDriversFromDirectoryAsync(path, pattern?, token?)` | Scans a directory. | -| `BatchUpdateAsync(drivers, strategy, mode, progress?, token?)` | Processes multiple drivers. | +**Description:** Large projects scan and validate all drivers in parallel, then install core drivers sequentially by risk group. -### Data models +```csharp +var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync(@"C:\DriverPackages"); -#### `DriverInfo` +var validDrivers = new List(); +foreach (var d in drivers) + if (await GeneralDrivelution.ValidateAsync(d)) + validDrivers.Add(d); -| Property | Description | -| --- | --- | -| `Name` | Driver name. | -| `Version` | Driver version. Directory scanning tries to read it from INF, `modinfo`, package metadata, or plist data, and falls back to `1.0.0`. | -| `FilePath` | Driver file path. Windows usually uses `.inf`, Linux uses `.ko`/`.deb`/`.rpm`, and macOS uses `.kext`/`.dext`/`.pkg`. | -| `TargetOS` | Target operating system. Empty means no OS restriction; otherwise it must contain the current OS name, such as `Windows`, `Linux`, or `MacOS`. | -| `Architecture` | Target architecture. Common aliases are normalized: `x64/amd64/x86_64`, `x86/i386/i686`, `arm64/aarch64`, `arm/armv7`. | -| `HardwareId` | Hardware ID or module alias. Windows parses INF metadata; Linux can read `modinfo alias`. | -| `Hash` / `HashAlgorithm` | Integrity validation. `SHA256` and compatibility `MD5` are supported. | -| `TrustedPublishers` | Trusted publisher list. Signature validation runs only when this list is not empty and signature validation is not skipped. | -| `Description`, `ReleaseDate`, `Metadata` | Display and extension data. | - -#### `UpdateStrategy` - -| Property | Description | -| --- | --- | -| `RequireBackup` | Whether to run the backup step. Defaults to `true`. | -| `BackupPath` | Backup root. The pipeline creates `backup_{Name}_{yyyyMMddHHmmss}` under this path. | -| `RestartMode` | Restart intent: `None`, `Prompt`, `Delayed`, or `Immediate`. The current update pipeline does not restart the system automatically; call `RestartHelper.HandleRestartAsync(...)` after success if needed. | -| `SkipHashValidation` | Skips hash validation. Recommended only for debugging or controlled environments. | -| `SkipSignatureValidation` | Skips signature validation. Recommended only for debugging or controlled environments. | -| `TimeoutSeconds` | Per-update timeout; values less than or equal to 0 use `DrivelutionOptions.DefaultTimeoutSeconds`. | -| `Mode`, `ForceUpdate`, `Priority` | Reserved strategy fields that can be used by upper-level scheduling or UI logic. | - -#### `UpdateResult` - -| Property | Description | -| --- | --- | -| `Success` / `Status` | Success flag and status: `NotStarted`, `Validating`, `BackingUp`, `Updating`, `Verifying`, `Succeeded`, `Failed`, `RolledBack`. | -| `Error` | Error type, code, message, details, and stack trace. | -| `BackupPath` | Backup path for this update. | -| `RolledBack` | Whether the pipeline entered the rollback path after failure. If you need strong recovery, explicitly call `RollbackAsync(BackupPath)`. | -| `StepLogs` | Step-by-step logs suitable for an installer result page or diagnostics upload. | -| `DurationMs` | Total duration. | +var coreDrivers = validDrivers.Where(d => IsCoreDriver(d)).ToList(); +var optionalDrivers = validDrivers.Except(coreDrivers).ToList(); -### Update pipeline +// Core drivers: sequential install +if (coreDrivers.Any()) + await GeneralDrivelution.BatchUpdateAsync(coreDrivers, new UpdateStrategy { RequireBackup = true }, BatchMode.Sequential); -`BaseDriverUpdater.UpdateAsync` runs the current platform steps in order: - -1. Platform permission step: `CheckPermissions` on Windows, `CheckSudo` on Linux, and `CheckSudo` on macOS. -2. `Validate`: file existence, hash, signature, and compatibility. -3. `Backup`: runs when `RequireBackup == true`. -4. `Install`: calls the platform installation command. -5. `Verify`: checks installation result. Windows runs `pnputil.exe /enum-drivers`; inconclusive verification logs a warning but does not fail the whole update. - -Each step reports `StepName`, `Percentage`, `Message`, `StepIndex`, and `TotalSteps` through `IProgress`. When a step fails or an exception occurs, `UpdateResult.Error` is mapped to displayable error information. If a backup path exists, the pipeline enters the rollback path and records it in `StepLogs`. - -### Validation strategy - -Validation is conditional: - -- File existence is always checked. -- If `DriverInfo.Hash` is not empty and `SkipHashValidation == false`, the file hash is computed and compared with the expected value. -- If `DriverInfo.TrustedPublishers.Count > 0` and `SkipSignatureValidation == false`, signature validation runs. -- Compatibility is always checked; empty `TargetOS` or `Architecture` means that dimension is unrestricted. - -Platform signature behavior: - -| Platform | Signature validation | -| --- | --- | -| Windows | Uses Authenticode-related logic and checks trusted publishers. | -| Linux | Looks for sibling `.sig` or `.asc` files and validates GPG signatures; unsigned files are accepted when no trusted publisher is configured. | -| macOS | Runs `codesign -v`, then `codesign -v --deep` if needed; when trusted publishers are provided, it matches `codesign -dvv` output. | - -### Platform differences - -#### Windows - -The Windows implementation targets INF driver packages: - -- Default scan pattern: `*.inf`. -- Permission: the process must run as administrator, otherwise `CheckPermissions` fails. -- Installation: `pnputil.exe /add-driver /install`. -- Verification: `pnputil.exe /enum-drivers`; inconclusive verification logs a warning but does not block the update. -- Metadata: parses `DriverVer`, `DriverDesc`, and `HardwareId`, and computes SHA256. -- Rollback: `RollbackAsync` scans backed-up `.inf` files and reinstalls them with PnPUtil. - -#### Linux - -The Linux implementation supports kernel modules and distro packages: - -- Default scanning includes `.ko`; when no search pattern is specified it also scans `.deb` and `.rpm`. -- Permission: sudo/root is required for normal driver installation. -- `.ko` installation: tries `insmod ` first, then falls back to `modprobe `. -- `.deb` installation: `dpkg -i `. -- `.rpm` installation: tries `rpm -ivh `, then falls back to `dnf install -y `. -- Metadata: `.ko` uses `modinfo`; `.deb` uses `dpkg-deb -I`; `.rpm` uses `rpm -qip`. -- Rollback: currently focuses on `.ko`, tries `modprobe -r `, then loads the backed-up module with `insmod `. - -#### macOS +// Optional drivers: parallel install +if (optionalDrivers.Any()) + await GeneralDrivelution.BatchUpdateAsync(optionalDrivers, new UpdateStrategy { RequireBackup = true }, BatchMode.Parallel); +``` -The macOS implementation targets kernel extensions, DriverKit extensions, and packages: +--- -- Default scanning includes `.kext`, `.dext`, and `.pkg`. -- `.kext` installation: copies to `/Library/Extensions/`, sets `root:wheel` and `755`, runs `kextload`, then `kextcache -i /`. -- `.dext` installation: copies to `/Library/SystemExtensions/`; DriverKit extensions usually still require user approval in system security settings. -- `.pkg` installation: `/usr/sbin/installer -pkg -target /`. -- Signature: uses `codesign`. -- Limitations: newer macOS versions impose SIP, user approval, and system-extension policies. A command succeeding does not necessarily mean the user approval flow is complete. -- Rollback: currently focuses on `.kext`, copies it back to `/Library/Extensions/`, and attempts `kextload`. +## 5. Basic Usage Examples -### Batch and parallel updates +### 5.1 Quick Start (Minimal Demo) -Batch mode is useful when a large project splits driver packages into a list: +```csharp +using GeneralUpdate.Drivelution; +using GeneralUpdate.Drivelution.Abstractions.Models; -```c# -var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync(@"C:\Drivers"); +var driver = new DriverInfo +{ + Name = "MyDevice Driver", + Version = "1.2.0", + FilePath = @"C:\Drivers\mydevice.inf", + TargetOS = "Windows", + Architecture = "x64", + Hash = "driver-file-sha256", + HashAlgorithm = "SHA256", + TrustedPublishers = { "Contoso Hardware" } +}; -var batch = await GeneralDrivelution.BatchUpdateAsync( - drivers, - strategy, - BatchMode.Parallel, - progress); +var result = await GeneralDrivelution.QuickUpdateAsync(driver); -Console.WriteLine(batch); +if (result.Success) + Console.WriteLine($"Driver updated successfully in {result.DurationMs}ms."); +else + Console.WriteLine($"Update failed: {result.Error?.Message}"); ``` -`BatchMode.Sequential` processes drivers one by one and is safer for core drivers, dependent drivers, or risk-sensitive installs. `BatchMode.Parallel` uses `Task.WhenAll` for multiple drivers and can improve throughput for independent scanning, validation, and installation work, but underlying system tools may still contend for the driver store, package-manager locks, or kernel module resources. For large projects, parallelize scanning and validation first, then group or serialize high-risk installation stages. +### 5.2 Basic Parameter Combination -### Restart behavior +```csharp +var platform = GeneralDrivelution.GetPlatformInfo(); +Console.WriteLine($"OS: {platform.OperatingSystem}, Arch: {platform.Architecture}"); -`UpdateStrategy.RestartMode` expresses the restart intent after driver installation: - -| Value | Meaning | -| --- | --- | -| `None` | No restart required. | -| `Prompt` | The app should prompt the user. Current `RestartHelper.PromptUserForRestart` writes a prompt and returns `false`, so GUI apps should handle the dialog themselves. | -| `Delayed` | Waits, then calls the platform restart command. | -| `Immediate` | Calls the platform restart command immediately. | - -`UpdateAsync` does not call `RestartHelper` automatically, so it will not restart the system right after installation. Decide in your application layer based on driver type and result: +var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync(@"C:\Drivers"); +Console.WriteLine($"Found {drivers.Count} driver(s)."); -```c# -if (result.Success && RestartHelper.IsRestartRequired(strategy.RestartMode)) +var updater = GeneralDrivelution.Create(new DrivelutionOptions { - await RestartHelper.HandleRestartAsync( - strategy.RestartMode, - delaySeconds: 60, - message: "Driver update completed. Restart now?"); -} -``` - -### Logging and performance switch - -Drivelution uses `GeneralTracer` for internal diagnostics: - -- Enabled by default. -- Console output through `TextWriterTraceListener(Console.Out)`. -- File output under the application base directory: `Logs\generalupdate-trace yyyy-MM-dd.log`, rotated by date. -- Windows debug output through `WindowsOutputDebugListener`. -- `DefaultTraceListener` is added when a debugger is attached. - -Driver updates often involve external commands and elevated permissions, so logs are important for troubleshooting. However, `GeneralTracer` creates timestamps, stack-frame location data, and Trace Listener writes. In performance-sensitive scans, batch validation, or high-parallelism scenarios, disable it temporarily: - -```c# -GeneralTracer.SetTracingEnabled(false); + DefaultBackupPath = @"C:\DriverBackups", + DefaultRetryCount = 3, + DefaultTimeoutSeconds = 600 +}); -// Run performance-sensitive scanning or batch validation. +var strategy = new UpdateStrategy +{ + RequireBackup = true, + BackupPath = @"C:\DriverBackups\mydevice", + TimeoutSeconds = 300 +}; -GeneralTracer.SetTracingEnabled(true); +var result = await updater.UpdateAsync(drivers[0], strategy, + new Progress(p => Console.WriteLine($"{p.StepName}: {p.Percentage}%"))); ``` -If you need to bridge logs into your own UI or logging stack, you can wrap the `DrivelutionLogger.LogMessage` event. The main update pipeline currently uses `GeneralTracer`. +### 5.3 Production-Ready Example -### Recommended practices - -| Scenario | Recommendation | -| --- | --- | -| Production updates | Keep `RequireBackup = true`, set an explicit `BackupPath`, and do not skip hash or signature validation. | -| First integration | Call `ValidateAsync` and `GetPlatformInfo()` first, then show target OS, architecture, version, and publisher in the UI. | -| Windows | Start the process as administrator and prefer vendor-signed INF packages. | -| Linux | Verify root/sudo, kernel version, and package-manager locks; install core modules sequentially. | -| macOS | Warn users that system-extension approval may be required; kext behavior is heavily affected by SIP and system policies. | -| Large driver sets | Scan and validate in parallel, group installation by risk, and keep `StepLogs` and `BackupPath` on failure. | -| High-performance flows | Temporarily disable `GeneralTracer` during bulk scans, then enable it again. | +See the [Chinese documentation](GeneralUpdate.Drivelution.md#53-真实业务落地示例) for a full workflow covering scan, validate, install, rollback, and restart. -### FAQ +--- -#### Why did signature validation not run? +## 6. Global Configuration -Signature validation runs only when `DriverInfo.TrustedPublishers` is not empty and `SkipSignatureValidation == false`. To require signature validation, provide trusted publishers and ensure the platform has the corresponding signature file or system signature metadata. +### Platform Quick Reference -#### Why did the system not restart after setting `RestartMode`? +| Platform | Driver Format | Install Command | Signature Verification | Permission Required | +| --- | --- | --- | --- | --- | +| Windows | `.inf` | `pnputil.exe /add-driver /install` | Authenticode | Administrator | +| Linux | `.ko` / `.deb` / `.rpm` | `insmod`/`modprobe` / `dpkg -i` / `rpm -ivh` | GPG (`.sig`/`.asc`) | root/sudo | +| macOS | `.kext` / `.dext` / `.pkg` | `kextload` / SystemExtensions / `installer` | `codesign -v` | root (SIP & user approval) | -`RestartMode` is currently a strategy field. The update pipeline does not restart the system automatically. Call `RestartHelper.HandleRestartAsync(...)` after a successful `UpdateAsync`, or handle restart with your own GUI or service logic. +### Logging Configuration -#### Is `BatchMode.Parallel` always faster? +```csharp +GeneralTracer.SetTracingEnabled(false); // Disable for performance +GeneralTracer.SetTracingEnabled(true); // Re-enable for troubleshooting +``` -No. Parallel work improves throughput for scanning, validation, and independent tasks, but installation calls system tools and may hit driver-store locks, package-manager locks, module dependencies, or restart requirements. For large projects, validate in parallel first, then control installation concurrency by group. +--- -#### How should rollback be designed? +## Related Resources -Keep the backup path before updating, and read `UpdateResult.BackupPath` and `StepLogs` on failure. If your business flow requires strong recovery, explicitly call `RollbackAsync(backupPath)` and tell the user that a restart or device replug may still be required. +- [Driver Update Sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Hub/Samples/ImDiskQuickInstallSample.cs) +- [GeneralUpdate Repository](https://github.com/GeneralLibrary/GeneralUpdate) +- [Driver Guide](../guide/Driver.md) diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Extension.md b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Extension.md index d8c4f56..414275d 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Extension.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Extension.md @@ -4,515 +4,477 @@ sidebar_position: 12 # GeneralUpdate.Extension -## Overview +**Namespace:** `GeneralUpdate.Extension` | **Main Entry Point:** `GeneralExtensionHost` (implements `IExtensionHost`) | **NuGet Package:** `GeneralUpdate.Extension` -**GeneralUpdate.Extension** is the extension management component for .NET applications. It gives a host application a VS Code-like extension model: query extensions from a remote service, download packages, install or update them into a local directory, and handle host-version compatibility, platform matching, extension dependencies, SHA256 validation, rollback, and update events. +## 1. Component Overview -It is useful when optional capabilities should ship independently from the main app, such as report modules, authentication plugins, industry-specific integrations, customer customizations, or scripting extensions. The host integrates `GeneralExtensionHost`; extension packages can then be produced by Tools or CI/CD, published as ZIP files, and consumed by the Extension component. +### 1.1 Introduction -**Namespace:** `GeneralUpdate.Extension` +**GeneralUpdate.Extension** is an extension management component for .NET applications, designed to give host apps VS Code-like extension ecosystem capabilities: query extensions from a remote service, download extension packages, install or update to local directories, and handle version compatibility, platform matching, dependency resolution, SHA256 verification, failure rollback, and event notifications. -**Assembly:** `GeneralUpdate.Extension.dll` -**NuGet package:** `GeneralUpdate.Extension` +It's suited for scenarios where the main app and optional capabilities are distributed separately — reports, authentication, industry plugins, customer-customized modules, script executors, etc. -```csharp -public interface IExtensionHost -{ - IExtensionCatalog ExtensionCatalog { get; } - event EventHandler? ExtensionUpdateStatusChanged; - - Task>> QueryExtensionsAsync(ExtensionQueryDTO query); - Task DownloadExtensionAsync(string extensionId, string savePath); - Task UpdateExtensionAsync(string extensionId); - Task InstallExtensionAsync(string extensionPath, bool rollbackOnFailure = true); - Task> UpdateExtensionsAsync(IEnumerable extensionIds, CancellationToken cancellationToken = default); - bool IsExtensionCompatible(ExtensionMetadata extension); - void SetAutoUpdate(string extensionId, bool autoUpdate); - void SetGlobalAutoUpdate(bool enabled); -} -``` +**Core Capabilities:** ---- - -## Navigation - -| Topic | What it covers | +| Capability | Description | +| --- | --- | +| Extension Query | Paginated query of available extensions from server API with multi-condition filtering | +| One-Click Update | `UpdateExtensionAsync` chains query→compatibility→dependencies→download→hash verify→install→catalog update | +| Safe Installation | Zip Slip path traversal protection, pre-install backup, auto-rollback to old version on failure | +| Batch Updates | `UpdateExtensionsAsync` processes multiple extensions sequentially, returns per-extension success/failure | +| Version Compatibility | `MinHostVersion` ≤ `HostVersion` ≤ `MaxHostVersion` required for installation | +| Platform Matching | `[Flags] TargetPlatform` bitwise check against current OS | +| Dependency Resolution | Topological sort dependency tree with circular dependency detection, recursive install of missing deps | +| Resumable Download | HTTP Range support for resuming interrupted downloads | +| Local Catalog | Per-extension `manifest.json` with atomic write (`.tmp` → rename), persistence and loading | +| Lifecycle Hooks | Business logic injection before/after install, activate/deactivate, uninstall | +| Auto-Update Policy | `SetGlobalAutoUpdate` / `SetAutoUpdate` toggles for global or per-extension auto-update | +| DI Integration | `ExtensionHostBuilder` registers default services; all services replaceable via DI | + +**Business Problems Solved:** +- Main app bloat; non-core features should be independently updatable extensions +- Different customers need different feature combinations; extension ecosystem enables on-demand installation +- Extensions have inter-dependencies; need automatic dependency management and version compatibility +- Need a unified extension management framework to reduce redundant development + +**Use Cases:** +- IDE plugin marketplace +- Enterprise ERP/CRM industry modules (report templates, auth methods, data exports) +- Independently distributed customer-customized features +- Componentized publishing for script executors/tool suites + +### 1.2 Environment & Dependencies + +| Item | Description | | --- | --- | -| [Quick start](#quick-start) | Minimal configuration, querying, and updating | -| [Core workflow](#core-workflow) | Query, download, install, update, rollback, and uninstall | -| [Metadata and manifest](#metadata-and-manifest) | `ExtensionMetadata`, server DTOs, and local `manifest.json` | -| [Package structure and Tools packaging](#package-structure-and-tools-packaging) | ZIP naming, package contents, producer/consumer roles | -| [Compatibility, platform, and dependencies](#compatibility-platform-and-dependencies) | Host version range, `TargetPlatform`, recursive dependency installation | -| [Events and auto-update settings](#events-and-auto-update-settings) | Status events and global/per-extension auto-update flags | -| [Server API contract](#server-api-contract) | The actual `/Query` and `/Download/{extensionId}` calls | -| [Advanced extension points](#advanced-extension-points) | DI builder, custom HttpClient, lifecycle hooks | -| [Best practices](#best-practices) | Production integration guidance | +| **Version** | `10.5.0-beta.2` | +| **Target Framework** | `netstandard2.0` (.NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 5+) | +| **Dependencies** | `Microsoft.Extensions.DependencyInjection`, `Microsoft.Extensions.Logging.Abstractions`, `Microsoft.Extensions.Options`, `Newtonsoft.Json`, `System.Net.Http`, `System.IO.Compression`, `System.IO.Compression.ZipFile` | +| **Compatibility** | All .NET Standard 2.0 platforms | --- -## Quick start +## 2. Feature List + +| Feature | Description | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| Extension Query | Paginated query of extensions from server API | Core | Recommended | Multi-condition filtering supported | +| Extension Download | Download extension ZIP from server with resume support | Core | Automatic | Triggered by `DownloadExtensionAsync` or one-click update | +| Extension Install | Safe ZIP extraction with Zip Slip protection and rollback | Core | Automatic | Only `.zip` format accepted | +| One-Click Update | Auto chain: query→compatibility→deps→download→verify→install | Core | Recommended | `UpdateExtensionAsync` | +| Batch Update | Sequential multi-extension update | Extended | Optional | `UpdateExtensionsAsync` | +| Extension Uninstall | Remove from catalog and delete extension directory | Core | Optional | `UninstallExtensionAsync` | +| Version Compatibility Check | Host version must fall within extension's Min/Max range | Core | Automatic | Checked automatically during update | +| Platform Matching | Auto-detect current OS, match extension's supported platforms | Core | Automatic | `PlatformMatcher` via `RuntimeInformation` | +| Recursive Dependency Install | Recursively update missing dependencies | Core | Automatic | Dependencies must be queryable from same server | +| Circular Dependency Detection | Detect cycles during topological sort | Core | Automatic | `DependencyResolver` | +| SHA256 Verification | Verify downloaded file integrity | Core | Automatic | When server `Hash` is non-empty | +| Local Catalog Management | Per-extension `manifest.json` with atomic write | Core | Automatic | Stored in extension directory | +| Auto-Update Policy | Global/per-extension auto-update toggles | Extended | Optional | In-memory only; no background polling | +| Lifecycle Hooks | Business logic before/after install/activate/deactivate/uninstall | Extended | Optional | Implement `IExtensionLifecycleHooks` or extend `DefaultExtensionLifecycleHooks` | +| DI Builder | `ExtensionHostBuilder` registers and replaces all services | Extended | Optional | Custom `IExtensionServiceFactory` supported | +| Download Queue Management | Concurrent download control (default 3) | Extended | Optional | `DownloadQueueManager` | -### Install - -```bash -dotnet add package GeneralUpdate.Extension -``` - -### Create an extension host - -```csharp -using GeneralUpdate.Extension.Common.DTOs; -using GeneralUpdate.Extension.Common.Enums; -using GeneralUpdate.Extension.Common.Models; -using GeneralUpdate.Extension.Core; +--- -var options = new ExtensionHostOptions -{ - ServerUrl = "https://extensions.example.com/Extension", - Scheme = "Bearer", - Token = "your-token", - HostVersion = "1.0.0", - ExtensionsDirectory = "./extensions" -}; +## 3. API Configuration Reference + +### 3.1 Configuration Properties (Props) + +**ExtensionHostOptions:** + +| Field | Type | Default | Required | Values | Description | +| --- | --- | --- | --- | --- | --- | +| `ServerUrl` | `string` | — | Yes | Valid absolute URL | Extension service root; client calls `{ServerUrl}/Query` and `{ServerUrl}/Download/{extensionId}` | +| `Scheme` | `string` | `""` | Optional | `"Bearer"` etc. | Auth scheme; no auth header when empty | +| `Token` | `string` | `""` | Optional | — | Auth token; must both be non-empty with `Scheme` | +| `HostVersion` | `string` | — | Recommended | SemVer format | Host app version for compatibility | +| `ExtensionsDirectory` | `string` | — | Yes | Valid directory path | Download, install, and `.backup` location | +| `CatalogPath` | `string` | `null` | Optional | Valid directory path | Catalog scan path; defaults to `ExtensionsDirectory` | + +**ExtensionMetadata (Local Model):** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `Id` | `string` | — | Yes | Unique extension ID; key for dependency, query, update, uninstall | +| `Name` | `string` | `null` | Recommended | Stable name for directory and package naming | +| `DisplayName` | `string` | `null` | Optional | Display name | +| `Version` | `string` | `null` | Recommended | Extension version; suggested `1.2.3` format | +| `Format` | `string` | `null` | Recommended | Package format; install requires `.zip` | +| `Hash` | `string` | `null` | Recommended | SHA256; verified during update when non-empty | +| `Publisher` | `string` | `null` | Optional | Publisher | +| `Categories` | `string` | `null` | Optional | Comma-separated categories | +| `SupportedPlatforms` | `TargetPlatform` | `All` | Recommended | `[Flags]`: `Windows(1)`, `Linux(2)`, `MacOS(4)`, `All(7)` | +| `MinHostVersion` | `string` | `null` | Optional | Minimum host version | +| `MaxHostVersion` | `string` | `null` | Optional | Maximum host version | +| `Dependencies` | `string` | `null` | Optional | Comma-separated dependency extension IDs | +| `IsPreRelease` | `bool` | `false` | Optional | Whether pre-release | +| `CustomProperties` | `string` | `null` | Optional | Custom properties as JSON string | + +**ExtensionQueryDTO (Query Filters):** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `Id` | `string?` | `null` | Optional | Exact match by ID | +| `Name` | `string?` | `null` | Optional | Partial match by name | +| `Publisher` | `string?` | `null` | Optional | Partial match by publisher | +| `Category` | `string?` | `null` | Optional | Filter by category | +| `Platform` | `TargetPlatform?` | `null` | Optional | Filter by target platform | +| `HostVersion` | `string?` | `null` | Optional | For server-side compatibility check | +| `PageNumber` | `int` | `1` | Optional | Page number (1-based) | +| `PageSize` | `int` | `10` | Optional | Page size | + +### 3.2 Instance Methods + +**IExtensionHost:** + +| Method | Parameters | Returns | Use Case | Notes | +| --- | --- | --- | --- | --- | +| `QueryExtensionsAsync(ExtensionQueryDTO)` | `query` | `Task>>` | Search/browse extensions | Response data in `Body.Items` | +| `DownloadExtensionAsync(string, string)` | `extensionId`, `savePath` | `Task` | Download extension separately | Supports HTTP Range resume | +| `UpdateExtensionAsync(string)` | `extensionId` | `Task` | One-click update (recommended) | Chains full update pipeline | +| `InstallExtensionAsync(string, bool)` | `extensionPath`, `rollbackOnFailure` | `Task` | Manual local install | Only `.zip` accepted | +| `UpdateExtensionsAsync(IEnumerable, CancellationToken)` | `extensionIds`, `ct` | `Task>` | Batch update | Sequential processing in order | +| `UninstallExtensionAsync(string, CancellationToken)` | `extensionId`, `ct` | `Task` | Uninstall extension | Removes from catalog and deletes directory | +| `ActivateExtensionAsync(string, CancellationToken)` | `extensionId`, `ct` | `Task` | Activate extension | Invokes lifecycle hooks | +| `DeactivateExtensionAsync(string, CancellationToken)` | `extensionId`, `ct` | `Task` | Deactivate extension | Invokes lifecycle hooks | +| `IsExtensionCompatible(ExtensionMetadata)` | `extension` | `bool` | Check compatibility | Based on HostVersion vs Min/MaxHostVersion | +| `SetAutoUpdate(string, bool)` | `extensionId`, `autoUpdate` | `void` | Set per-extension auto-update | In-memory only; no background polling | +| `SetGlobalAutoUpdate(bool)` | `enabled` | `void` | Set global default | In-memory only | + +**ExtensionHostBuilder:** + +| Method | Parameters | Returns | Use Case | +| --- | --- | --- | --- | +| `ConfigureOptions(Action)` | `configure` | `ExtensionHostBuilder` | Configure via lambda | +| `WithOptions(ExtensionHostOptions)` | `options` | `ExtensionHostBuilder` | Set options directly | +| `ConfigureServices(Action)` | `configure` | `ExtensionHostBuilder` | Replace or add DI services | +| `Build()` | None | `IExtensionHost` | Build host instance with auto-registered defaults | -var host = new GeneralExtensionHost(options); +### 3.3 Callback Events -host.ExtensionUpdateStatusChanged += (sender, e) => -{ - Console.WriteLine($"{e.ExtensionId} {e.Status} {e.Progress}% {e.ErrorMessage}"); -}; -``` +| Event | Callback Parameters | Trigger Timing | Usage Notes | +| --- | --- | --- | --- | +| `ExtensionUpdateStatusChanged` | `ExtensionUpdateEventArgs` — `ExtensionId`, `ExtensionName`, `Status`, `Progress`(0-100), `ErrorMessage` | During extension update lifecycle | Status: `Queued`→`Updating`→`UpdateSuccessful`/`UpdateFailed` | -`ExtensionHostOptions` currently contains: +**ExtensionUpdateStatus Enum:** -| Property | Description | +| Value | Description | | --- | --- | -| `ServerUrl` | Extension service base URL. The client calls `{ServerUrl}/Query` and `{ServerUrl}/Download/{extensionId}` | -| `Scheme` | Authorization scheme, for example `Bearer`. Empty values disable the header | -| `Token` | Authorization token. It is only used when both `Scheme` and `Token` are non-empty | -| `HostVersion` | Host application version used by `MinHostVersion` / `MaxHostVersion` checks | -| `ExtensionsDirectory` | Directory for downloaded packages, installed extensions, and `.backup` | -| `CatalogPath` | Optional local catalog scan path. Defaults to `ExtensionsDirectory` | +| `Queued` (0) | Queued for update | +| `Updating` (1) | Downloading / updating | +| `UpdateSuccessful` (2) | Update succeeded | +| `UpdateFailed` (3) | Update failed | -### Query and update +--- -```csharp -var query = new ExtensionQueryDTO -{ - Platform = TargetPlatform.Windows, - HostVersion = options.HostVersion, - Status = true, - PageNumber = 1, - PageSize = 20 -}; +## 4. Advanced Examples -var response = await host.QueryExtensionsAsync(query); -if (response.Body != null) -{ - foreach (var extension in response.Body.Items) - { - Console.WriteLine($"{extension.DisplayName} v{extension.Version}, compatible: {extension.IsCompatible}"); - } -} -else -{ - Console.WriteLine(response.Message); -} +### 4.1 Extension Points Overview -var success = await host.UpdateExtensionAsync("extension-id"); -``` +All services are replaceable via `ExtensionHostBuilder.ConfigureServices()`: ---- +| Service Interface | Default Implementation | Description | +| --- | --- | --- | +| `IExtensionHttpClient` | `ExtensionHttpClient` | HTTP communication | +| `IVersionCompatibilityChecker` | `VersionCompatibilityChecker` | Version compatibility check | +| `IDownloadQueueManager` | `DownloadQueueManager` | Download queue management | +| `IPlatformMatcher` | `PlatformMatcher` | Platform detection | +| `IPlatformServices` | `RuntimePlatformServices` | Runtime platform info | +| `IExtensionMetadataMapper` | `DefaultExtensionMetadataMapper` | DTO→model mapping | +| `IExtensionCatalog` | `ExtensionCatalog` | Local extension catalog | +| `IDependencyResolver` | `DependencyResolver` | Dependency resolution | +| `IExtensionLifecycleHooks` | `DefaultExtensionLifecycleHooks` | Lifecycle hooks (all virtual) | +| `IExtensionServiceFactory` | `ExtensionServiceFactory` | Service factory | -## Core workflow +### 4.2 Examples by Scenario -### 1. Query remote extensions +#### Scenario 1: Custom Lifecycle Hooks -`QueryExtensionsAsync` sends an `ExtensionQueryDTO` through `ExtensionHttpClient` and returns `HttpResponseDTO>`. Current response data is in `Body`, not `Data`. +**Description:** Custom logic before/after install: check license before install, initialize extension database after install. ```csharp -var response = await host.QueryExtensionsAsync(new ExtensionQueryDTO -{ - Name = "report", - Publisher = "general", - Category = "Tools", - Platform = TargetPlatform.Windows | TargetPlatform.Linux, - HostVersion = "1.2.0", - IsPreRelease = false, - PageNumber = 1, - PageSize = 10 -}); +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.Models; -if (response.Body == null) +public sealed class LicensedLifecycleHooks : DefaultExtensionLifecycleHooks { - Console.WriteLine($"Query failed: {response.Code} {response.Message}"); - return; + public override async Task OnBeforeInstallAsync( + ExtensionMetadata extension, string? packagePath, + CancellationToken cancellationToken = default) + { + if (!LicenseManager.IsLicensed(extension.Id)) + return false; // Block installation + return true; + } + + public override async Task OnAfterInstallAsync( + ExtensionMetadata extension, CancellationToken cancellationToken = default) + { + if (extension.CustomProperties != null) + { + var props = Newtonsoft.Json.JsonConvert + .DeserializeObject>(extension.CustomProperties); + if (props?.ContainsKey("DbInitScript") == true) + await DatabaseInitializer.RunAsync(props["DbInitScript"], cancellationToken); + } + Console.WriteLine($"Extension '{extension.DisplayName}' installed successfully."); + } } -Console.WriteLine($"Total: {response.Body.TotalCount}"); +var host = new ExtensionHostBuilder() + .WithOptions(options) + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .Build(); ``` -### 2. Download a package - -`DownloadExtensionAsync(extensionId, savePath)` downloads the remote package to `savePath`. The lower-level downloader supports: +#### Scenario 2: Custom HTTP Client with Shared Connection Pool -- HTTP Range resume when a partial file already exists; -- `Updating` progress notifications through `ExtensionUpdateStatusChanged`; -- detailed error classification in `DownloadExtensionWithResultAsync`, including network errors, 4xx, 5xx, cancellation, and I/O errors. +**Description:** Share `HttpClient` connection pool with main app; switch to POST query. ```csharp -var downloaded = await host.DownloadExtensionAsync( - extensionId: "report-extension", - savePath: "./extensions/report-extension_1.0.0.zip"); -``` +using GeneralUpdate.Extension.Communication; -### 3. Install a package +var sharedClient = new HttpClient(); -`InstallExtensionAsync` accepts `.zip` packages only. The target directory is inferred from the file name: `name_version.zip` installs to `{ExtensionsDirectory}/name`. +var httpClient = new ExtensionHttpClient( + serverUrl: "https://extensions.mycompany.com/Extension", + scheme: "Bearer", + token: "jwt-token", + httpClient: sharedClient, + ownsHttpClient: false) +{ + UsePostForQuery = true +}; -```csharp -var installed = await host.InstallExtensionAsync( - extensionPath: "./extensions/report-extension_1.0.0.zip", - rollbackOnFailure: true); +var host = new ExtensionHostBuilder() + .WithOptions(options) + .ConfigureServices(services => + { + services.AddSingleton(httpClient); + }) + .Build(); ``` -Install steps: - -1. Verify that the file exists and is a `.zip`. -2. Call `IExtensionLifecycleHooks.OnBeforeInstallAsync`; `false` cancels the install. -3. If the extension already exists and rollback is enabled, copy the old directory to `{ExtensionsDirectory}/.backup`. -4. Delete the old directory and create the target directory. -5. Safely extract the ZIP. Zip Slip path traversal entries are skipped. -6. Delete the backup and call `OnAfterInstallAsync` on success. -7. Attempt to restore from backup on failure. - -### 4. Update in one call +#### Scenario 3: Dependency Resolution + Conditional Batch Update -`UpdateExtensionAsync(extensionId)` is the recommended entry point. It connects querying, compatibility checks, platform checks, recursive dependency installation, download, SHA256 validation, install, and catalog updates. +**Description:** When user selects an extension to install, auto-resolve dependencies and install them together. ```csharp -var success = await host.UpdateExtensionAsync("report-extension"); -if (!success) -{ - Console.WriteLine("Update failed. Read ExtensionUpdateStatusChanged for details."); -} -``` - -Full flow: +var host = new GeneralExtensionHost(options); +host.ExtensionCatalog.LoadInstalledExtensions(); -1. Raise `Queued`. -2. Query server metadata with `Id = extensionId`. -3. Map `ExtensionDTO` to `ExtensionMetadata`. -4. Check the host version against `MinHostVersion` / `MaxHostVersion`. -5. Check whether the current OS is included in `SupportedPlatforms`. -6. Recursively call `UpdateExtensionAsync(depId)` for missing dependencies. -7. Download `{Name}_{Version}{Format}` into `ExtensionsDirectory`. -8. If `Hash` is non-empty, compute and compare the downloaded file SHA256. -9. Call `InstallExtensionAsync(..., rollbackOnFailure: true)`. -10. Add or update the local catalog `manifest.json`. -11. Raise `UpdateSuccessful` on success or `UpdateFailed` on failure. +var response = await host.QueryExtensionsAsync(new ExtensionQueryDTO { Id = "report-extension" }); -### 5. Bulk update +var ext = response.Body?.Items.FirstOrDefault(); +if (ext == null) return; -`UpdateExtensionsAsync` updates the given extension IDs sequentially and returns a success flag per extension. If you need parallelism, control concurrency in the application layer and call `UpdateExtensionAsync` for each extension. +var resolver = new DependencyResolver(host.ExtensionCatalog); +var deps = resolver.ResolveDependencies( + new ExtensionMetadata { Id = ext.Id, Dependencies = string.Join(",", ext.Dependencies ?? []) }); +var missing = resolver.GetMissingDependencies( + new ExtensionMetadata { Id = ext.Id, Dependencies = string.Join(",", ext.Dependencies ?? []) }); -```csharp -var result = await host.UpdateExtensionsAsync(new[] -{ - "report-extension", - "auth-extension", - "theme-extension" -}, cancellationToken); +var updateOrder = new List(); +updateOrder.AddRange(missing); +updateOrder.Add(ext.Id); -foreach (var item in result) -{ - Console.WriteLine($"{item.Key}: {item.Value}"); -} +var results = await host.UpdateExtensionsAsync(updateOrder); +foreach (var (id, success) in results) + Console.WriteLine($" {id}: {(success ? "OK" : "FAILED")}"); ``` -### 6. Rollback - -Rollback belongs to `InstallExtensionAsync`. It backs up the previous directory and restores it if replacement fails. It is useful for update/overwrite scenarios; a first install usually has no previous directory to restore. - -Backup path: - -```text -{ExtensionsDirectory}/.backup/{extensionName}_{yyyyMMddHHmmss} -``` +--- -### 7. Uninstall +## 5. Basic Usage Examples -`IExtensionHost` does not currently expose `UninstallExtensionAsync`. The available uninstall primitive is `IExtensionCatalog.RemoveInstalledExtension(extensionId)`, which removes the in-memory record and attempts to delete the extension directory. +### 5.1 Quick Start (Minimal Demo) ```csharp -host.ExtensionCatalog.RemoveInstalledExtension("report-extension"); -``` - -If you need approval, disable-before-delete, pre-uninstall checks, or cleanup hooks, wrap this in an application service and reuse the semantics of `IExtensionLifecycleHooks.OnBeforeUninstallAsync` / `OnAfterUninstallAsync`. - ---- - -## Metadata and manifest +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Models; -### ExtensionMetadata +var options = new ExtensionHostOptions +{ + ServerUrl = "https://extensions.example.com/Extension", + Scheme = "Bearer", + Token = "your-token", + HostVersion = "1.0.0", + ExtensionsDirectory = "./extensions" +}; -`ExtensionMetadata` is the core local model for installed extensions, catalog persistence, and compatibility checks. +var host = new GeneralExtensionHost(options); -| Property | Description | -| --- | --- | -| `Id` | Unique extension ID. Dependencies, query, update, and uninstall all use this key | -| `Name` | Stable extension name, recommended for package and directory naming | -| `DisplayName` | Human-readable name | -| `Version` | Extension version. Compatibility sorting uses .NET `Version.TryParse`; prefer `1.2.3` or `1.2.3.0` | -| `FileSize` | Package size in bytes | -| `UploadTime` | Upload timestamp | -| `Status` | Enabled flag | -| `Description` | Description | -| `Format` | Package format. Current install logic requires `.zip` | -| `Hash` | Optional SHA256. Non-empty values are verified during update | -| `Publisher` | Publisher | -| `License` | License identifier | -| `Categories` | Comma-separated categories | -| `SupportedPlatforms` | `TargetPlatform` flags | -| `MinHostVersion` | Minimum host version | -| `MaxHostVersion` | Maximum host version | -| `ReleaseDate` | Release date | -| `Dependencies` | Comma-separated dependency extension IDs | -| `IsPreRelease` | Pre-release flag | -| `DownloadUrl` | Download URL metadata; the default client still downloads from `{ServerUrl}/Download/{extensionId}` | -| `CustomProperties` | Custom metadata as a JSON string | - -### Server DTO and local manifest - -The server query returns `ExtensionDTO`, where `Categories` and `Dependencies` are `List`. The client maps those lists to comma-separated strings in local `ExtensionMetadata`. - -The local catalog is not a single `catalog.json`. Current code scans child directories under `CatalogPath` and reads each extension's: +host.ExtensionUpdateStatusChanged += (sender, e) => + Console.WriteLine($"[{e.Status}] {e.ExtensionId}: {e.Progress}% {e.ErrorMessage}"); -```text -manifest.json -``` +var response = await host.QueryExtensionsAsync(new ExtensionQueryDTO +{ + Platform = TargetPlatform.Windows, + PageNumber = 1, + PageSize = 20 +}); -`AddOrUpdateInstalledExtension` writes each extension to its own directory: +if (response.Body != null) + foreach (var ext in response.Body.Items) + Console.WriteLine($"{ext.DisplayName} v{ext.Version}"); -```text -{CatalogPath}/{safe-extension-name}/manifest.json +var success = await host.UpdateExtensionAsync("report-extension"); +Console.WriteLine(success ? "Extension updated." : "Update failed."); ``` -Writes use a `manifest.json.tmp -> manifest.json` replacement pattern. `LoadInstalledExtensions` cleans orphaned `.tmp` files and skips directories containing `.backup`. +### 5.2 Basic Parameter Combination -Example manifest: - -```json +```csharp +var host = new GeneralExtensionHost(new ExtensionHostOptions { - "Id": "report-extension", - "Name": "report-extension", - "DisplayName": "Report Extension", - "Version": "1.0.0", - "Status": true, - "Description": "Adds PDF and Excel reports.", - "Format": ".zip", - "Hash": "6f5902ac237024bdd0c176cb93063dc4...", - "Publisher": "GeneralLibrary", - "License": "MIT", - "Categories": "Reports,Tools", - "SupportedPlatforms": 7, - "MinHostVersion": "1.0.0", - "MaxHostVersion": "2.0.0", - "Dependencies": "base-extension", - "IsPreRelease": false -} -``` - -`SupportedPlatforms` is a `[Flags]` enum. `All = Windows | Linux | MacOS = 7`. - ---- - -## Package structure and Tools packaging - -The Extension component consumes extension packages: download, validate, extract, install, rollback, and register manifests. Tools or CI/CD produces them: build the extension, generate metadata, compute SHA256, create a ZIP, and publish it to the extension service. - -Recommended package name: - -```text -{Name}_{Version}.zip -``` + ServerUrl = "https://extensions.mycompany.com/Extension", + Scheme = "Bearer", + Token = Environment.GetEnvironmentVariable("EXTENSION_TOKEN") ?? "", + HostVersion = "2.0.0", + ExtensionsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "extensions") +}); -Example: +host.ExtensionUpdateStatusChanged += (_, e) => +{ + switch (e.Status) + { + case ExtensionUpdateStatus.Queued: + Console.WriteLine($"{e.ExtensionId}: queued"); break; + case ExtensionUpdateStatus.Updating: + Console.WriteLine($"{e.ExtensionId}: downloading... {e.Progress}%"); break; + case ExtensionUpdateStatus.UpdateSuccessful: + Console.WriteLine($"{e.ExtensionName ?? e.ExtensionId}: updated"); break; + case ExtensionUpdateStatus.UpdateFailed: + Console.WriteLine($"{e.ExtensionId}: failed — {e.ErrorMessage}"); break; + } +}; -```text -report-extension_1.0.0.zip -``` +// Install local package +var installed = await host.InstallExtensionAsync("./downloads/report-extension_1.0.0.zip", rollbackOnFailure: true); -Recommended ZIP contents: +// Query installed extensions +host.ExtensionCatalog.LoadInstalledExtensions(); +foreach (var ext in host.ExtensionCatalog.GetInstalledExtensions()) + Console.WriteLine($"{ext.DisplayName} v{ext.Version} — compatible: {host.IsExtensionCompatible(ext)}"); -```text -report-extension_1.0.0.zip -├─ manifest.json # Recommended for business and catalog reuse -├─ extension.dll # Extension assembly -├─ extension.deps.json # .NET dependency description -├─ README.md -├─ CHANGELOG.md -└─ LICENSE.txt +// Configure auto-update +host.SetGlobalAutoUpdate(true); +host.SetAutoUpdate("large-extension", false); ``` -`InstallExtensionAsync` does not require or parse the package `manifest.json` by itself; it focuses on safe extraction and rollback. `UpdateExtensionAsync` updates the local catalog from the server-provided `ExtensionDTO`. Therefore, the producer side must keep server metadata and ZIP contents aligned. - -Recommended publishing flow: +### 5.3 Production-Ready Example -1. Build the extension project. -2. Prepare `manifest.json` with fields matching `ExtensionMetadata`. -3. Create `{Name}_{Version}.zip`. -4. Compute the ZIP SHA256 and store it in the server `Hash`. -5. Upload the ZIP and `ExtensionDTO` metadata. -6. Let the host query with `QueryExtensionsAsync` and consume with `UpdateExtensionAsync`. - -For packaging basics, see [Packaging](../guide/Packaging.md). The advanced cookbook extension release pipeline will be covered in task [#54](https://github.com/GeneralLibrary/GeneralUpdate-Samples/issues/54), where Tools packaging, manifest generation, hash calculation, upload, and staged host consumption should be connected into one workflow. - ---- - -## Compatibility, platform, and dependencies - -### Version compatibility - -`VersionCompatibilityChecker.IsCompatible` compares `HostVersion` with `MinHostVersion` and `MaxHostVersion`: - -- Empty `HostVersion`: no constraint, compatible; -- Unparseable `HostVersion`: incompatible; -- Non-empty unparseable `MinHostVersion`: incompatible; -- Non-empty unparseable `MaxHostVersion`: incompatible; -- Host version must satisfy `MinHostVersion <= HostVersion <= MaxHostVersion`. - -| HostVersion | MinHostVersion | MaxHostVersion | Result | -| --- | --- | --- | --- | -| `1.5.0` | `1.0.0` | `2.0.0` | Compatible | -| `1.5.0` | `1.6.0` | `2.0.0` | Incompatible | -| `1.5.0` | `1.0.0` | `1.4.0` | Incompatible | -| `1.5.0` | empty | empty | Compatible | - -```csharp -var extension = host.ExtensionCatalog.GetInstalledExtensionById("report-extension"); -if (extension != null && host.IsExtensionCompatible(extension)) -{ - Console.WriteLine("Compatible"); -} -``` - -### Platform matching +Full workflow with exception handling, dependency management, and compatibility checking: ```csharp -[Flags] -public enum TargetPlatform -{ - None = 0, - Windows = 1, - Linux = 2, - MacOS = 4, - All = Windows | Linux | MacOS -} -``` - -`PlatformMatcher` detects the current OS through `RuntimeInformation` and checks support with flag operations. +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Enums; +using GeneralUpdate.Extension.Common.Models; -```csharp -var metadata = new ExtensionMetadata +var options = new ExtensionHostOptions { - Id = "report-extension", - Name = "report-extension", - SupportedPlatforms = TargetPlatform.Windows | TargetPlatform.Linux + ServerUrl = "https://extensions.mycompany.com/Extension", + Scheme = "Bearer", + Token = Configuration.GetExtensionToken(), + HostVersion = AppInfo.CurrentVersion.ToString(), + ExtensionsDirectory = Path.Combine(AppInfo.DataDirectory, "extensions") }; -``` - -### Dependencies -`ExtensionMetadata.Dependencies` is a comma-separated list of extension IDs. `DependencyList` parses it into a list. `UpdateExtensionAsync` recursively installs missing dependencies before the current extension. +var host = new ExtensionHostBuilder() + .WithOptions(options) + .ConfigureServices(services => + services.AddSingleton()) + .Build(); -```csharp -var metadata = new ExtensionMetadata +host.ExtensionUpdateStatusChanged += (_, e) => { - Id = "report-extension", - Dependencies = "base-extension,chart-extension" + if (e.Status == ExtensionUpdateStatus.UpdateFailed) + Log.Error($"Extension '{e.ExtensionId}' update failed: {e.ErrorMessage}"); + else if (e.Status == ExtensionUpdateStatus.UpdateSuccessful) + Log.Info($"Extension '{e.ExtensionName ?? e.ExtensionId}' updated."); }; -``` -`DependencyResolver` can also resolve dependencies from the local catalog, report missing dependencies, and detect circular dependencies. The current `UpdateExtensionAsync` path installs dependencies from the current extension's server metadata, so producer-side metadata must ensure every dependency can be queried and downloaded by ID from the same extension service. - ---- +host.ExtensionCatalog.LoadInstalledExtensions(); +var installed = host.ExtensionCatalog.GetInstalledExtensions(); +Console.WriteLine($"Loaded {installed.Count} installed extension(s)."); -## Events and auto-update settings +var response = await host.QueryExtensionsAsync(new ExtensionQueryDTO +{ + Platform = TargetPlatform.Windows | TargetPlatform.Linux, + HostVersion = options.HostVersion, + Status = true, + PageNumber = 1, + PageSize = 100 +}); -### ExtensionUpdateStatusChanged +if (response?.Body == null) return; -The update event reports status changes for one extension: +var toUpdate = new List(); +foreach (var ext in response.Body.Items) +{ + var local = host.ExtensionCatalog.GetInstalledExtensionById(ext.Id); + if (local == null) continue; -| Field | Description | -| --- | --- | -| `ExtensionId` | Extension ID | -| `ExtensionName` | Extension name; can be empty in some phases | -| `Status` | `Queued`, `Updating`, `UpdateSuccessful`, `UpdateFailed` | -| `Progress` | 0-100, updated during download | -| `ErrorMessage` | Failure reason | + var meta = new ExtensionMetadata + { + MinHostVersion = ext.MinHostVersion, + MaxHostVersion = ext.MaxHostVersion + }; -```csharp -host.ExtensionUpdateStatusChanged += (sender, e) => -{ - switch (e.Status) + if (!host.IsExtensionCompatible(meta)) { - case ExtensionUpdateStatus.Queued: - Console.WriteLine($"{e.ExtensionId} queued"); - break; - case ExtensionUpdateStatus.Updating: - Console.WriteLine($"{e.ExtensionId} downloading {e.Progress}%"); - break; - case ExtensionUpdateStatus.UpdateSuccessful: - Console.WriteLine($"{e.ExtensionName ?? e.ExtensionId} updated"); - break; - case ExtensionUpdateStatus.UpdateFailed: - Console.WriteLine($"{e.ExtensionId} failed: {e.ErrorMessage}"); - break; + Console.WriteLine($"[INCOMPATIBLE] {ext.DisplayName}"); + continue; } -}; -``` -### Auto-update settings - -`SetGlobalAutoUpdate` sets the global default, and `SetAutoUpdate` sets a per-extension override. `IExtensionHost` exposes the setters; concrete `GeneralExtensionHost` also provides `IsAutoUpdateEnabled(extensionId)` for reading the effective value. - -```csharp -var concreteHost = new GeneralExtensionHost(options); - -concreteHost.SetGlobalAutoUpdate(true); -concreteHost.SetAutoUpdate("large-extension", false); + if (Version.TryParse(ext.Version, out var remoteVer) && + Version.TryParse(local.Version, out var localVer) && + remoteVer > localVer && host.IsAutoUpdateEnabled(ext.Id)) + { + Console.WriteLine($"[UPDATE] {ext.DisplayName}: {local.Version} → {ext.Version}"); + toUpdate.Add(ext.Id); + } +} -var enabled = concreteHost.IsAutoUpdateEnabled("large-extension"); +if (toUpdate.Any()) +{ + Console.WriteLine($"\nUpdating {toUpdate.Count} extension(s)..."); + var results = await host.UpdateExtensionsAsync(toUpdate); + var succeeded = results.Count(r => r.Value); + var failed = results.Count(r => !r.Value); + Console.WriteLine($"Done: {succeeded} succeeded, {failed} failed."); +} +else +{ + Console.WriteLine("All extensions up to date."); +} ``` -These flags are stored in memory on the current `GeneralExtensionHost` instance. The component does not start a background polling job. Your application should decide when to scan for updates and call `UpdateExtensionAsync` according to the flags. - --- -## Server API contract +## 6. Global Configuration -`ExtensionHttpClient` currently uses two endpoints. +### Server API Contract -### Query +**Query Endpoint:** ```http GET {ServerUrl}/Query Content-Type: application/json Authorization: {Scheme} {Token} -ExtensionQueryDTO JSON body +Body: ExtensionQueryDTO (JSON) ``` -This is **GET with a JSON body**. That is not common HTTP style, but the current client implements this server contract explicitly. If proxies, gateways, or API platforms reject it, client and server should be changed together to POST or query-string parameters. - -Response: - -```csharp -HttpResponseDTO> -``` +> Note: Current implementation uses GET + JSON Body, which is non-standard HTTP. If going through proxies/gateways, may need to switch to POST or query string. -### Download +**Download Endpoint:** ```http GET {ServerUrl}/Download/{extensionId} @@ -520,72 +482,40 @@ Authorization: {Scheme} {Token} Range: bytes={existingLength}- ``` -The server should return a file stream and preferably support HTTP Range for resume. The client treats `416 RequestedRangeNotSatisfiable` as an already-complete download. - ---- - -## Advanced extension points +### Package Structure -### ExtensionHostBuilder and DI +Recommended package name: `{Name}_{Version}.zip` -`ExtensionHostBuilder` registers default services and allows the application to replace any of them: - -```csharp -var host = new ExtensionHostBuilder() - .WithOptions(options) - .ConfigureServices(services => - { - services.AddSingleton(); - services.AddSingleton(sp => - new ExtensionHttpClient(options.ServerUrl, options.Scheme, options.Token, sharedHttpClient)); - }) - .Build(); +```text +report-extension_1.0.0.zip +├── manifest.json +├── extension.dll +├── extension.deps.json +├── README.md +├── CHANGELOG.md +└── LICENSE.txt ``` -Default registrations: - -- `IExtensionHttpClient -> ExtensionHttpClient` -- `IVersionCompatibilityChecker -> VersionCompatibilityChecker` -- `IDownloadQueueManager -> DownloadQueueManager` -- `IPlatformMatcher -> PlatformMatcher` -- `IPlatformServices -> RuntimePlatformServices` -- `IExtensionMetadataMapper -> DefaultExtensionMetadataMapper` -- `IExtensionCatalog -> ExtensionCatalog` -- `IDependencyResolver -> DependencyResolver` -- `IExtensionLifecycleHooks -> DefaultExtensionLifecycleHooks` -- `IExtensionHost -> GeneralExtensionHost` - -### Lifecycle hooks +### Auto-Update Policy Priority -`IExtensionLifecycleHooks` lets applications add logic before or after install, activation, deactivation, and uninstall. Current `GeneralExtensionHost` calls `OnBeforeInstallAsync` and `OnAfterInstallAsync`; activation, deactivation, and uninstall hooks can be reused by application-level wrappers for those flows. - -```csharp -public sealed class MyLifecycleHooks : DefaultExtensionLifecycleHooks -{ - public override Task OnBeforeInstallAsync( - ExtensionMetadata extension, - string? packagePath, - CancellationToken cancellationToken = default) - { - Console.WriteLine($"Installing {packagePath}"); - return Task.FromResult(true); - } -} +``` +Per-extension setting > Global setting > Default (false) ``` -### Download queue +### Platform Compatibility Reference -`DownloadQueueManager` is an independent queue type with a default max concurrency of 3. It supports `Enqueue`, `GetTask`, `CancelTask`, `GetActiveTasks`, and `DownloadStatusChanged`. The queue currently manages status and concurrency slots; the actual download in the host update path is performed by `ExtensionHttpClient`. For application-level parallel downloads, compose the queue, HTTP client, and install flow in your own service. +| Enum Value | Code | Description | +| --- | --- | --- | +| `TargetPlatform.None` | 0 | Matches no platform | +| `TargetPlatform.Windows` | 1 | Windows | +| `TargetPlatform.Linux` | 2 | Linux | +| `TargetPlatform.MacOS` | 4 | macOS | +| `TargetPlatform.All` | 7 | All platforms (Windows \| Linux \| MacOS) | --- -## Best practices +## Related Resources -1. Use `.zip` extension packages in production and name them `{Name}_{Version}.zip`. -2. Store the ZIP SHA256 in server `Hash` so `UpdateExtensionAsync` can verify integrity. -3. Use parseable versions for `HostVersion`, `MinHostVersion`, and `MaxHostVersion`. -4. Use one `manifest.json` per extension directory; do not rely on the old single `catalog.json` model. -5. Set `SupportedPlatforms` to real OS support instead of defaulting everything to `All`. -6. Ensure every dependency can be queried and downloaded by ID from the same extension service. -7. For large packages, support HTTP Range on the server and show `ExtensionUpdateStatusChanged` progress in the UI. -8. Auto-update flags are policy state, not a scheduler. Scanning, timing, staged rollout, and approval belong in the application layer. +- [Extension Management Sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Hub/Samples/ExtensionSample.cs) +- [GeneralUpdate Repository](https://github.com/GeneralLibrary/GeneralUpdate) +- [Packaging Guide](../guide/Packaging.md) diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Bowl.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Bowl.md index 4ecc369..b9f65c7 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Bowl.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Bowl.md @@ -49,10 +49,6 @@ Bowl 是“升级后健康检查与回滚保护”,不是固件恢复、系统 ### 安装 -```bash -dotnet add package GeneralUpdate.Bowl -``` - ### 升级模式监控 升级模式适合放在升级程序或 Bowl helper 中运行。关键点是:`BackupDirectory` 指向升级前保留的备份,`TargetPath` 指向当前安装目录,`ExtendedField` 填本次升级版本号。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md index 80580b8..cf28a15 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md @@ -10,10 +10,6 @@ sidebar_position: 5 **主要入口:** `GeneralUpdateBootstrap` **NuGet 包:** `GeneralUpdate.Core` -```bash -dotnet add package GeneralUpdate.Core -``` - ## 文档大纲与知识点导航 {#knowledge-map} 如果你是第一次阅读 Core 文档,可以先看这个导航,再跳到对应知识点。本文按照“入口与配置 -> 执行策略 -> 差分/下载/并发 -> 扩展点 -> 工具链关系”的顺序组织。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Differential.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Differential.md index 04678d2..6856748 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Differential.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Differential.md @@ -12,10 +12,6 @@ sidebar_position: 6 **NuGet 包:** `GeneralUpdate.Differential` -```bash -dotnet add package GeneralUpdate.Differential -``` - ## 文档大纲与知识点导航 {#knowledge-map} 如果你第一次阅读 Differential 文档,可以先看这个导航,再跳到对应知识点。本文按照“能力边界 -> 文件级 API -> 算法选择 -> 压缩格式 -> 与 Core/Tools 集成 -> 性能与扩展”的顺序组织。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Drivelution.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Drivelution.md index 9a7940b..a9a72c2 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Drivelution.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Drivelution.md @@ -42,10 +42,6 @@ public static class GeneralDrivelution ### 安装 -```bash -dotnet add package GeneralUpdate.Drivelution -``` - 或在项目文件中添加: ```xml diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Extension.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Extension.md index d5aba2e..5b3a9f6 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Extension.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Extension.md @@ -54,10 +54,6 @@ public interface IExtensionHost ### 安装 -```bash -dotnet add package GeneralUpdate.Extension -``` - ### 初始化扩展宿主 ```csharp diff --git a/website/sidebars.js b/website/sidebars.js index 94df74a..2524f79 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -71,7 +71,6 @@ const sidebars = { collapsed: true, items: [ 'doc/GeneralSpacestation', - 'doc/UpgradeHub', ], },