From 08ad2637c6da5dc18228c29049e6312791582ef5 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Thu, 4 Jun 2026 01:30:41 +0800 Subject: [PATCH] docs: unify layout and style across 5 GeneralUpdate component docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix zh-Hans Bowl: remove stray '## 简介' heading, restructure to follow standard 6-section template - Fix zh-Hans Drivelution: replace '### 定义' with proper title + metadata + numbered sections - Fix zh-Hans Extension: replace '## 组件概览' with standard '## 1. 组件简介' - Fix zh-Hans Core/Differential: merge multi-line metadata into single inline line - Fix duplicate sidebar_position 12 between Drivelution and Extension (→ 8 and 7) - Apply consistent formatting: inline metadata, --- separators, numbered sections (1-6) across all 5 components in all 3 languages (EN source, zh-Hans, en) Co-Authored-By: Claude Opus 4.8 --- website/docs/doc/GeneralUpdate.Drivelution.md | 2 +- website/docs/doc/GeneralUpdate.Extension.md | 2 +- .../current/doc/GeneralUpdate.Drivelution.md | 2 +- .../current/doc/GeneralUpdate.Extension.md | 2 +- .../current/doc/GeneralUpdate.Bowl.md | 504 ++++++---- .../current/doc/GeneralUpdate.Core.md | 6 +- .../current/doc/GeneralUpdate.Differential.md | 8 +- .../current/doc/GeneralUpdate.Drivelution.md | 670 ++++++++----- .../current/doc/GeneralUpdate.Extension.md | 946 ++++++++++-------- 9 files changed, 1268 insertions(+), 874 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Drivelution.md b/website/docs/doc/GeneralUpdate.Drivelution.md index fa6dcc4..50362ad 100644 --- a/website/docs/doc/GeneralUpdate.Drivelution.md +++ b/website/docs/doc/GeneralUpdate.Drivelution.md @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 8 --- # GeneralUpdate.Drivelution diff --git a/website/docs/doc/GeneralUpdate.Extension.md b/website/docs/doc/GeneralUpdate.Extension.md index 62be37a..569f5d7 100644 --- a/website/docs/doc/GeneralUpdate.Extension.md +++ b/website/docs/doc/GeneralUpdate.Extension.md @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 7 --- # GeneralUpdate.Extension 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 ab8974d..c386095 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 @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 8 --- # GeneralUpdate.Drivelution 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 414275d..899ecfd 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 @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 7 --- # GeneralUpdate.Extension 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 b9f65c7..b15cfd1 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 @@ -4,54 +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 环境与依赖 + +| 项目 | 说明 | +| --- | --- | +| **版本** | `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 和调试权限限制) | + +--- + +## 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` 运行时诊断日志 | 拓展 | 可选 | 默认开启,可关闭 | + +--- + +## 3. API 配置说明 + +### 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 后触发 | + +**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 日志控制:** + +| 方法 | 说明 | | --- | --- | -| [生命周期位置](#生命周期位置) | Bowl 应该在升级流程的哪个阶段运行 | -| [快速接入](#快速接入) | 用当前 `BowlContext` API 完成一次监控 | -| [崩溃检测与恢复流程](#崩溃检测与恢复流程) | 崩溃后组件具体做了什么 | -| [BowlContext 参数](#bowlcontext-参数) | 每个配置项的含义和推荐值 | -| [输出文件](#输出文件) | Dump、失败报告、系统诊断、追踪日志在哪里 | -| [事件回调](#事件回调) | 如何在崩溃时上传报告或通知用户 | -| [日志开关](#日志开关) | 如何为了性能关闭组件追踪日志 | -| [平台差异](#平台差异) | Windows、Linux、macOS 的监控能力差异 | -| [恢复场景](#恢复场景) | 一次真实升级失败回滚过程 | -| [旧 API 迁移](#旧-api-迁移) | 从 `MonitorParameter` 迁移到 `BowlContext` | +| `GeneralTracer.SetTracingEnabled(false)` | 关闭 Bowl 日志输出 | +| `GeneralTracer.SetTracingEnabled(true)` | 重新开启日志输出 | +| `GeneralTracer.IsTracingEnabled()` | 查询当前日志开关状态 | +| `GeneralTracer.Dispose()` | 释放文件监听器并清空 Trace listeners | -## 生命周期位置 +--- -在 GeneralUpdate 的完整升级链路中,Bowl 位于**文件替换完成之后、用户正式使用新版本之前**: +## 4. 扩展示例(高阶用法) -1. Core 获取更新信息、下载包、校验并应用更新。 -2. Core/Upgrade 进程准备启动主程序。 -3. Bowl 作为守护逻辑启动,附加到目标进程并等待启动期异常。 -4. 主程序正常启动:没有 Dump 产生,Bowl 返回本次监控结果。 -5. 主程序启动崩溃:Bowl 进入故障处理管线,生成诊断文件并按配置恢复备份。 +### 4.1 组件可扩展能力总览 -在当前 Core 代码中,Windows 的 `UpdateStrategy` 会在更新完成后通过 OS 策略启动主程序,并在配置了 Bowl 进程名时一并启动 Bowl 辅助进程。Linux/macOS 侧 Core 策略没有同等的 Bowl helper 自动启动能力,通常需要由你的启动器、服务脚本或独立进程显式调用 `LaunchAsync`。 +Bowl 的主要扩展点是 `BowlContext.OnCrash` 回调。内部策略接口(`IBowlStrategy`、`ICrashReporter`、`ISystemInfoProvider`)为 internal,如需新增平台支持或自定义报告逻辑,可在 GeneralUpdate 仓库贡献代码。 -:::tip -Bowl 是“升级后健康检查与回滚保护”,不是固件恢复、系统还原或升级包安装器。它处理的是应用启动崩溃后的诊断与应用目录级备份恢复。 -::: +| 扩展点 | 类型 | 说明 | +| --- | --- | --- | +| `OnCrash` 回调 | `Func?` | `BowlContext` 中配置,崩溃时触发 | +| `GeneralTracer` 日志 | 静态类 | 可通过 `SetTracingEnabled` 开关日志 | -## 快速接入 +### 4.2 分场景示例 -### 安装 +#### 场景 1:升级模式监控 + 崩溃告警上传 -### 升级模式监控 +【场景说明】桌面应用升级到新版本后,Bowl 监控启动崩溃;发生崩溃时自动回退,并上传诊断包到内部日志平台。 -升级模式适合放在升级程序或 Bowl helper 中运行。关键点是:`BackupDirectory` 指向升级前保留的备份,`TargetPath` 指向当前安装目录,`ExtendedField` 填本次升级版本号。 +【示例代码】 ```csharp using GeneralUpdate.Bowl; @@ -69,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` +- 回调异常不会阻断恢复流程 -`Normal` 模式只做崩溃捕获、报告输出和回调通知,不会自动恢复备份,也不会写入 `UpgradeFail` 失败版本标记。 +#### 场景 2:独立监控模式(非升级场景) + +【场景说明】对 Worker 进程做通用启动崩溃监控,只采集诊断信息,不自动回滚。 + +【示例代码】 ```csharp var context = new BowlContext @@ -104,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/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 cf28a15..556adc1 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 @@ -4,11 +4,9 @@ sidebar_position: 5 # GeneralUpdate.Core -`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心,重点提供可编程的启动器、配置模型、事件模型、下载子系统扩展点、生命周期钩子、状态上报、差分管道和平台策略扩展。本页聚焦组件 API、属性和扩展方式;完整端到端上手流程会放到 cookbook 中。 +**命名空间:** `GeneralUpdate.Core` | **主要入口:** `GeneralUpdateBootstrap` | **NuGet 包:** `GeneralUpdate.Core` -**命名空间:** `GeneralUpdate.Core` -**主要入口:** `GeneralUpdateBootstrap` -**NuGet 包:** `GeneralUpdate.Core` +`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心,重点提供可编程的启动器、配置模型、事件模型、下载子系统扩展点、生命周期钩子、状态上报、差分管道和平台策略扩展。本页聚焦组件 API、属性和扩展方式;完整端到端上手流程会放到 cookbook 中。 ## 文档大纲与知识点导航 {#knowledge-map} 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 6856748..60c72b8 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 @@ -4,13 +4,9 @@ sidebar_position: 6 # GeneralUpdate.Differential -`GeneralUpdate.Differential` 是 GeneralUpdate 的二进制差分组件,专注解决“一个旧文件 + 一个补丁文件 = 一个新文件”的问题。它提供可替换的文件级差分算法、补丁压缩抽象和 BSDIFF 兼容补丁读写能力;目录级对比、批量补丁生成、并行调度、删除文件处理和更新流程编排由 `GeneralUpdate.Core` 的 `DiffPipeline` 或 `GeneralUpdate.Tools` 承担。 +**命名空间:** `GeneralUpdate.Differential`、`GeneralUpdate.Differential.Differ`、`GeneralUpdate.Differential.Abstractions` | **主要入口:** `IBinaryDiffer`、`BsdiffDiffer`、`StreamingHdiffDiffer` | **NuGet 包:** `GeneralUpdate.Differential` -**命名空间:** `GeneralUpdate.Differential`、`GeneralUpdate.Differential.Differ`、`GeneralUpdate.Differential.Abstractions` - -**主要入口:** `IBinaryDiffer`、`BsdiffDiffer`、`StreamingHdiffDiffer` - -**NuGet 包:** `GeneralUpdate.Differential` +`GeneralUpdate.Differential` 是 GeneralUpdate 的二进制差分组件,专注解决”一个旧文件 + 一个补丁文件 = 一个新文件”的问题。它提供可替换的文件级差分算法、补丁压缩抽象和 BSDIFF 兼容补丁读写能力;目录级对比、批量补丁生成、并行调度、删除文件处理和更新流程编排由 `GeneralUpdate.Core` 的 `DiffPipeline` 或 `GeneralUpdate.Tools` 承担。 ## 文档大纲与知识点导航 {#knowledge-map} 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 a9a72c2..50362ad 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 @@ -1,89 +1,187 @@ --- -sidebar_position: 12 +sidebar_position: 8 --- -### 定义 +# 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 +| 跨平台抽象 | `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 的场景: +--- -- 硬件厂商客户端需要随应用一起交付网卡、采集卡、USB、虚拟设备等驱动。 -- 企业或工业现场需要批量扫描驱动包并按清单更新。 -- 安装器、维护工具、设备管理服务需要统一处理 Windows/Linux/macOS 驱动差异。 -- 需要在更新前进行哈希、签名、系统架构校验,并在失败后保留可恢复的备份。 +## 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` 运行时诊断日志 | 拓展 | 可选 | 默认开启,可关闭 | -不适合把 Drivelution 当作普通文件更新器使用。如果只是更新应用自身的 exe、dll、资源文件或插件,请优先使用 `GeneralUpdate.Core` 的应用更新流程。 +--- -### 安装 +## 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. 扩展示例(高阶用法) -### 快速开始:更新单个驱动 +### 4.1 组件可扩展能力总览 -```c# -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" } -}; +| 扩展接口 | 说明 | +| --- | --- | +| `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; @@ -104,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`。 +【效果&注意事项】 +- 生产环境不要跳过哈希和签名校验 +- 回滚后驱动恢复到安装前状态,但仍建议提示用户可能需要重启 + +#### 场景 2:DI 容器集成 -### DI 注册 +【场景说明】在 ASP.NET Core / Generic Host 应用中通过 DI 注册驱动更新服务。 -在 Generic Host、ASP.NET Core 或自己的服务容器中,可以通过扩展方法注册当前平台实现: +【示例代码】 -```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}"); +} +``` -#### Windows +### 5.2 基础参数组合示例 -Windows 实现面向 INF 驱动包: +```csharp +using GeneralUpdate.Drivelution; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Abstractions.Models; -- 扫描默认模式:`*.inf`。 -- 权限:必须以管理员身份运行,否则 `CheckPermissions` 会失败。 -- 安装:`pnputil.exe /add-driver /install`。 -- 验证:`pnputil.exe /enum-drivers`,验证不确定时记录警告但不阻断更新。 -- 元数据:解析 `DriverVer`、`DriverDesc`、`HardwareId`,并计算 SHA256。 -- 回滚:`RollbackAsync` 会扫描备份目录中的 `.inf` 并重新调用 PnPUtil 安装。 +// 查询平台信息 +var platform = GeneralDrivelution.GetPlatformInfo(); +Console.WriteLine($"OS: {platform.OperatingSystem}, Arch: {platform.Architecture}"); -#### Linux +// 扫描驱动目录 +var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync(@"C:\Drivers"); +Console.WriteLine($"Found {drivers.Count} driver(s)."); -Linux 实现支持内核模块和发行版包: +foreach (var d in drivers) + Console.WriteLine($" {d.Name} v{d.Version} ({d.FilePath})"); -- 扫描默认包括 `.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 updater = GeneralDrivelution.Create(new DrivelutionOptions +{ + DefaultBackupPath = @"C:\DriverBackups", + DefaultRetryCount = 3, + DefaultTimeoutSeconds = 600 +}); -#### macOS +// 更新,带进度 +var progress = new Progress(p => + Console.WriteLine($"{p.StepName}: {p.Percentage}%")); -macOS 实现面向内核扩展、DriverKit 扩展和安装包: +var strategy = new UpdateStrategy +{ + RequireBackup = true, + BackupPath = @"C:\DriverBackups\mydevice", + TimeoutSeconds = 300 +}; -- 扫描默认包括 `.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`。 +var result = await updater.UpdateAsync(drivers[0], strategy, progress); +Console.WriteLine($"Result: {(result.Success ? "Success" : "Failed")}"); -### 批量与并行更新 +if (result.Success && RestartHelper.IsRestartRequired(strategy.RestartMode)) + await RestartHelper.HandleRestartAsync(strategy.RestartMode, 60); +``` -批量更新适合大型项目把驱动包拆成清单后统一处理: +### 5.3 真实业务落地示例 -```c# -var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync(@"C:\Drivers"); +完整驱动更新工作流,覆盖扫描、验证、安装、回滚、重启: -var batch = await GeneralDrivelution.BatchUpdateAsync( - drivers, - strategy, - BatchMode.Parallel, - progress); +```csharp +using GeneralUpdate.Drivelution; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Abstractions.Models; -Console.WriteLine(batch); -``` +// 1. 全局配置 +var options = new DrivelutionOptions +{ + DefaultBackupPath = @"C:\ProgramData\MyProduct\DriverBackups", + DefaultRetryCount = 2, + DefaultRetryIntervalSeconds = 10, + DefaultTimeoutSeconds = 900, + UseExponentialBackoff = true, + ForceTerminateOnPermissionFailure = true, + AutoCleanupBackups = true, + BackupsToKeep = 3 +}; -`BatchMode.Sequential` 会按顺序逐个更新,适合核心驱动、互相依赖的驱动或需要降低系统风险的场景。`BatchMode.Parallel` 使用 `Task.WhenAll` 并行处理多个驱动,适合互不依赖的驱动包扫描、验证和安装任务,但底层系统工具可能仍会竞争驱动仓库、包管理器锁或内核模块资源。大型项目建议先并行验证和扫描,再对高风险安装阶段做分组或顺序控制。 +// 2. 创建更新器 +var updater = GeneralDrivelution.Create(options); -### 重启行为 +// 3. 扫描驱动目录 +var platform = GeneralDrivelution.GetPlatformInfo(); +Console.WriteLine($"Running on {platform.OperatingSystem} {platform.Architecture}"); -`UpdateStrategy.RestartMode` 表示本次驱动更新完成后的重启意图: +var drivers = await GeneralDrivelution.GetDriversFromDirectoryAsync( + @"C:\ProgramData\MyProduct\DriverPackages"); -| 值 | 含义 | -| --- | --- | -| `None` | 不需要重启。 | -| `Prompt` | 应用提示用户重启。当前 `RestartHelper.PromptUserForRestart` 只输出提示并返回 `false`,适合由 GUI 自行接管。 | -| `Delayed` | 延迟后调用系统重启命令。 | -| `Immediate` | 立即调用系统重启命令。 | +var compatible = drivers + .Where(d => string.IsNullOrEmpty(d.TargetOS) || d.TargetOS == platform.OperatingSystem) + .ToList(); -当前 `UpdateAsync` 不会自动调用 `RestartHelper`,因此不会在驱动安装后直接重启系统。推荐在业务层根据驱动类型和安装结果决定是否调用: +Console.WriteLine($"Found {drivers.Count} driver(s), {compatible.Count} compatible."); -```c# -if (result.Success && RestartHelper.IsRestartRequired(strategy.RestartMode)) +// 4. 逐驱动验证并更新 +var results = new List<(DriverInfo Driver, UpdateResult Result)>(); +foreach (var driver in compatible) { - await RestartHelper.HandleRestartAsync( - strategy.RestartMode, - delaySeconds: 60, - message: "Driver update completed. Restart now?"); + // 先验证 + 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)"); + } } -``` - -### 日志与性能开关 -Drivelution 使用 `GeneralTracer` 输出内部诊断信息: +// 5. 汇总并处理重启 +var succeeded = results.Count(r => r.Result.Success); +var failed = results.Count(r => !r.Result.Success); +Console.WriteLine($"\nSummary: {succeeded} succeeded, {failed} failed."); -- 默认启用。 -- 控制台输出:通过 `TextWriterTraceListener(Console.Out)`。 -- 文件输出:应用基目录下的 `Logs\generalupdate-trace yyyy-MM-dd.log`,按日期切换。 -- Windows 调试输出:Windows 下会额外添加 `WindowsOutputDebugListener`。 -- 调试器附加时会添加 `DefaultTraceListener`。 - -驱动更新通常涉及外部命令和系统权限,日志对排查失败很重要。但 `GeneralTracer` 会生成时间戳、调用栈位置并写入 Trace Listener;在性能敏感、批量验证或大量并行处理场景中,可以关闭它降低额外开销: - -```c# -GeneralTracer.SetTracingEnabled(false); - -// 执行性能敏感的扫描或批量验证 - -GeneralTracer.SetTracingEnabled(true); +if (succeeded > 0 && results.Any(r => RestartHelper.IsRestartRequired(r.Result.Status == UpdateStatus.Succeeded + ? RestartMode.Prompt : RestartMode.None))) +{ + Console.WriteLine("Some drivers may require a restart."); + var userAccepted = RestartHelper.PromptUserForRestart("Driver update completed. Restart now?"); + if (userAccepted) + await RestartHelper.RestartSystemAsync(); +} ``` -如果需要把日志桥接到自己的 UI 或日志系统,可以使用 `DrivelutionLogger` 的 `LogMessage` 事件自行包装;当前主更新流水线主要使用 `GeneralTracer`。 - -### 推荐实践 - -| 场景 | 建议 | -| --- | --- | -| 生产更新 | 保持 `RequireBackup = true`,设置明确的 `BackupPath`,不要跳过哈希和签名。 | -| 首次集成 | 先调用 `ValidateAsync` 和 `GetPlatformInfo()`,在 UI 中展示目标 OS、架构、版本和发布者。 | -| Windows | 以管理员启动进程,并优先使用厂商签名的 INF 包。 | -| Linux | 确认 root/sudo 权限、内核版本和包管理器锁;核心模块建议顺序更新。 | -| macOS | 提前告知用户可能需要批准系统扩展;kext 受 SIP 和系统策略影响较大。 | -| 大批量驱动 | 扫描和验证可并行,安装阶段按驱动风险分组;失败时保留 `StepLogs` 和 `BackupPath`。 | -| 高性能场景 | 批量扫描时可临时关闭 `GeneralTracer`,结束后再恢复。 | +--- -### 常见问题 +## 6. 全局配置 -#### 为什么有时签名校验没有执行? +### 平台差异速查 -签名校验只在 `DriverInfo.TrustedPublishers` 非空且 `SkipSignatureValidation == false` 时执行。如果你希望强制校验签名,请提供可信发布者列表,并确保平台对应的签名文件或系统签名信息可用。 +| 平台 | 驱动格式 | 安装命令 | 签名验证 | 权限要求 | +| --- | --- | --- | --- | --- | +| 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 和用户批准策略影响) | -#### 为什么设置了 `RestartMode` 但系统没有重启? +### 日志配置 -`RestartMode` 当前是策略字段,更新流水线不会自动重启系统。应用需要在 `UpdateAsync` 成功后调用 `RestartHelper.HandleRestartAsync(...)`,或用自己的 GUI/服务逻辑接管重启。 +```csharp +// 性能敏感场景关闭日志 +GeneralTracer.SetTracingEnabled(false); -#### `BatchMode.Parallel` 是否一定更快? +// 排查问题时重新开启 +GeneralTracer.SetTracingEnabled(true); +``` -不一定。并行可以提升扫描、验证和互不依赖任务的吞吐,但驱动安装会调用系统工具,可能遇到驱动仓库锁、包管理器锁、模块依赖或重启要求。大型项目建议先并行验证,再对安装阶段分组控制并发。 +--- -#### 回滚应该如何设计? +## 相关资源 -更新前保留备份路径,失败时读取 `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/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 5b3a9f6..569f5d7 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 @@ -1,514 +1,629 @@ --- -sidebar_position: 12 +sidebar_position: 7 --- # 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 组件可扩展能力总览 -### 初始化扩展宿主 +所有服务均可通过 `ExtensionHostBuilder.ConfigureServices()` 替换: -```csharp -using GeneralUpdate.Extension.Common.DTOs; -using GeneralUpdate.Extension.Common.Enums; -using GeneralUpdate.Extension.Common.Models; -using GeneralUpdate.Extension.Core; +| 服务接口 | 默认实现 | 说明 | +| --- | --- | --- | +| `IExtensionHttpClient` | `ExtensionHttpClient` | HTTP 通信(查询/下载) | +| `IVersionCompatibilityChecker` | `VersionCompatibilityChecker` | 版本兼容性检查 | +| `IDownloadQueueManager` | `DownloadQueueManager` | 下载队列管理 | +| `IPlatformMatcher` | `PlatformMatcher` | 平台检测 | +| `IPlatformServices` | `RuntimePlatformServices` | 运行时平台信息 | +| `IExtensionMetadataMapper` | `DefaultExtensionMetadataMapper` | DTO→模型映射 | +| `IExtensionCatalog` | `ExtensionCatalog` | 本地扩展目录管理 | +| `IDependencyResolver` | `DependencyResolver` | 依赖解析 | +| `IExtensionLifecycleHooks` | `DefaultExtensionLifecycleHooks` | 生命周期钩子(所有方法 virtual) | +| `IExtensionServiceFactory` | `ExtensionServiceFactory` | 服务工厂 | -var options = new ExtensionHostOptions -{ - ServerUrl = "https://extensions.example.com/Extension", - Scheme = "Bearer", - Token = "your-token", - HostVersion = "1.0.0", - ExtensionsDirectory = "./extensions" -}; +### 4.2 分场景示例 -var host = new GeneralExtensionHost(options); +#### 场景 1:自定义生命周期钩子 -host.ExtensionUpdateStatusChanged += (sender, e) => -{ - Console.WriteLine($"{e.ExtensionId} {e.Status} {e.Progress}% {e.ErrorMessage}"); -}; -``` - -`ExtensionHostOptions` 当前可配置项如下: +【场景说明】在扩展安装前后执行自定义逻辑:安装前检查许可证、安装后初始化扩展数据库。 -| 属性 | 说明 | -| --- | --- | -| `ServerUrl` | 扩展服务根地址。客户端会调用 `{ServerUrl}/Query` 和 `{ServerUrl}/Download/{extensionId}` | -| `Scheme` | Authorization 认证方案,例如 `Bearer`。为空时不设置认证头 | -| `Token` | Authorization token。需要和 `Scheme` 同时非空才会生效 | -| `HostVersion` | 宿主应用版本,用于 `MinHostVersion` / `MaxHostVersion` 兼容性判断 | -| `ExtensionsDirectory` | 扩展包下载、安装和 `.backup` 目录所在位置 | -| `CatalogPath` | 可选,本地扩展目录扫描路径;为空时使用 `ExtensionsDirectory` | - -### 查询和更新扩展 +【示例代码】 ```csharp -var query = new ExtensionQueryDTO -{ - Platform = TargetPlatform.Windows, - HostVersion = options.HostVersion, - Status = true, - PageNumber = 1, - PageSize = 20 -}; +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.Models; -var response = await host.QueryExtensionsAsync(query); -if (response.Body != null) +public sealed class LicensedLifecycleHooks : DefaultExtensionLifecycleHooks { - foreach (var extension in response.Body.Items) + public override async Task OnBeforeInstallAsync( + ExtensionMetadata extension, + string? packagePath, + CancellationToken cancellationToken = default) { - Console.WriteLine($"{extension.DisplayName} v{extension.Version}, compatible: {extension.IsCompatible}"); + // 检查许可证 + if (!LicenseManager.IsLicensed(extension.Id)) + { + Console.WriteLine($"Extension '{extension.Id}' is not licensed."); + return false; // 阻止安装 + } + return true; } -} -else -{ - Console.WriteLine(response.Message); -} - -var success = await host.UpdateExtensionAsync("extension-id"); -``` - ---- -## 核心流程 - -### 1. 查询远程扩展 - -`QueryExtensionsAsync` 直接把 `ExtensionQueryDTO` 交给 `ExtensionHttpClient`,返回 `HttpResponseDTO>`。当前响应数据在 `Body` 属性中,不是 `Data`。 - -```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 -}); + 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."); + } -if (response.Body == null) -{ - Console.WriteLine($"Query failed: {response.Code} {response.Message}"); - return; + 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; + } } -Console.WriteLine($"Total: {response.Body.TotalCount}"); +// 使用 Builder 注册 +var host = new ExtensionHostBuilder() + .WithOptions(options) + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .Build(); ``` -### 2. 下载扩展包 +【效果&注意事项】 +- 返回 `false` 会阻止操作继续执行 +- 所有钩子方法都是 `virtual`,只需覆写需要的部分 -`DownloadExtensionAsync(extensionId, savePath)` 会调用远程下载接口并写入 `savePath`。底层下载器支持: +#### 场景 2:自定义 HTTP 客户端 + 共享连接池 -- 已存在部分文件时使用 HTTP Range 续传; -- 下载过程中通过 `ExtensionUpdateStatusChanged` 报告 `Updating` 和进度; -- `DownloadExtensionWithResultAsync` 在底层提供更细的错误分类,例如网络错误、4xx、5xx、取消、I/O 错误。 +【场景说明】与主应用共享 `HttpClient` 连接池,避免 socket 耗尽;同时切换为 POST 查询。 + +【示例代码】 ```csharp -var downloaded = await host.DownloadExtensionAsync( - extensionId: "report-extension", - savePath: "./extensions/report-extension_1.0.0.zip"); -``` +using GeneralUpdate.Extension.Communication; -### 3. 安装扩展包 +// 共享主应用的 HttpClient +var sharedClient = new HttpClient(); // 或从 IHttpClientFactory 获取 -`InstallExtensionAsync` 只接受 `.zip` 包。安装时会根据文件名推导目录名:`name_version.zip` 会安装到 `{ExtensionsDirectory}/name`。 +var httpClient = new ExtensionHttpClient( + serverUrl: "https://extensions.mycompany.com/Extension", + scheme: "Bearer", + token: "jwt-token", + httpClient: sharedClient, + ownsHttpClient: false) // 不拥有,不 Dispose +{ + UsePostForQuery = true // 服务端要求 POST 查询 +}; -```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(); ``` -安装过程: +【效果&注意事项】 +- `ownsHttpClient: false` 确保 Dispose 时不关闭共享连接 +- `UsePostForQuery = true` 将默认 GET+JSON Body 改为 POST+JSON Body -1. 检查文件是否存在,并确认扩展包是 `.zip`。 -2. 调用 `IExtensionLifecycleHooks.OnBeforeInstallAsync`,返回 `false` 时取消安装。 -3. 如果本地已有同名扩展且开启回滚,复制旧目录到 `{ExtensionsDirectory}/.backup`。 -4. 删除旧目录,创建目标目录。 -5. 安全解压 ZIP。解压时会校验目标路径,跳过 Zip Slip 路径穿越条目。 -6. 安装成功后删除备份,并调用 `OnAfterInstallAsync`。 -7. 安装失败时尝试从备份目录恢复。 +#### 场景 3:依赖解析 + 条件批量更新 -### 4. 一键更新扩展 +【场景说明】用户选择安装一个扩展时,自动解析依赖并一起安装。 -`UpdateExtensionAsync(extensionId)` 是推荐入口。它会串起查询、兼容性检查、平台检查、依赖递归安装、下载、SHA256 校验、安装和 catalog 更新。 +【示例代码】 ```csharp -var success = await host.UpdateExtensionAsync("report-extension"); -if (!success) -{ - Console.WriteLine("Update failed. Read ExtensionUpdateStatusChanged for details."); -} -``` - -完整流程: - -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`。 - -### 5. 批量更新 +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Enums; -`UpdateExtensionsAsync` 会按传入顺序逐个更新扩展,并返回每个扩展的成功/失败结果。当前实现是顺序处理;如需并发策略,应在业务层控制并发数量后分别调用 `UpdateExtensionAsync`。 +var host = new GeneralExtensionHost(options); -```csharp -var result = await host.UpdateExtensionsAsync(new[] +// 查询目标扩展 +var response = await host.QueryExtensionsAsync(new ExtensionQueryDTO { - "report-extension", - "auth-extension", - "theme-extension" -}, cancellationToken); + Id = "report-extension", + PageSize = 1 +}); -foreach (var item in result) +if (response.Body?.Items.Any() != true) { - Console.WriteLine($"{item.Key}: {item.Value}"); + Console.WriteLine("Extension not found."); + return; } -``` -### 6. 回滚 +var ext = response.Body.Items.First(); -回滚由 `InstallExtensionAsync` 负责,核心是备份旧目录、失败时恢复旧目录。它适合覆盖安装或更新失败场景;首次安装失败时因为没有旧目录,通常没有可恢复内容。 +// 解析依赖 +var catalog = host.ExtensionCatalog; +catalog.LoadInstalledExtensions(); -备份目录位于: +var resolver = new GeneralUpdate.Extension.Dependencies.DependencyResolver(catalog); +var deps = resolver.ResolveDependencies( + new ExtensionMetadata { Id = ext.Id, Dependencies = string.Join(",", ext.Dependencies ?? []) }); -```text -{ExtensionsDirectory}/.backup/{extensionName}_{yyyyMMddHHmmss} -``` +var missingDeps = resolver.GetMissingDependencies( + new ExtensionMetadata { Id = ext.Id, Dependencies = string.Join(",", ext.Dependencies ?? []) }); -### 7. 卸载 +Console.WriteLine($"Dependencies for {ext.DisplayName}: {deps.Count} total, {missingDeps.Count} missing."); -`IExtensionHost` 当前没有暴露 `UninstallExtensionAsync`。卸载能力在 `IExtensionCatalog.RemoveInstalledExtension(extensionId)` 中,调用后会移除内存记录,并尝试删除对应扩展目录。 +// 先安装缺失依赖 +var updateOrder = new List(); +updateOrder.AddRange(missingDeps); +updateOrder.Add(ext.Id); -```csharp -host.ExtensionCatalog.RemoveInstalledExtension("report-extension"); +var results = await host.UpdateExtensionsAsync(updateOrder); + +foreach (var (id, success) in results) + Console.WriteLine($" {id}: {(success ? "OK" : "FAILED")}"); ``` -如果业务需要审批、停用、卸载前检查或卸载后清理,可以在应用层封装卸载服务,并复用 `IExtensionLifecycleHooks.OnBeforeUninstallAsync` / `OnAfterUninstallAsync` 的语义保持一致。 +【效果&注意事项】 +- `DependencyResolver.ResolveDependencies` 返回拓扑排序后的完整依赖列表 +- `GetMissingDependencies` 过滤出未安装在本地 catalog 中的依赖 +- 循环依赖会被检测并抛出异常 --- -## 扩展元数据与 manifest +## 5. 常规使用示例 -### ExtensionMetadata +### 5.1 快速入门示例(最简 demo) -`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 -``` +```csharp +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Models; -`AddOrUpdateInstalledExtension` 会把每个扩展写入独立目录: +var options = new ExtensionHostOptions +{ + ServerUrl = "https://extensions.example.com/Extension", + Scheme = "Bearer", + Token = "your-token", + HostVersion = "1.0.0", + ExtensionsDirectory = "./extensions" +}; -```text -{CatalogPath}/{safe-extension-name}/manifest.json -``` +var host = new GeneralExtensionHost(options); -写入时使用 `manifest.json.tmp -> manifest.json` 的方式尽量保证原子替换;`LoadInstalledExtensions` 会清理遗留的 `.tmp` 文件,并跳过包含 `.backup` 的目录。 +host.ExtensionUpdateStatusChanged += (sender, e) => +{ + Console.WriteLine($"[{e.Status}] {e.ExtensionId}: {e.Progress}% {e.ErrorMessage}"); +}; -示例 manifest: +// 查询可用扩展 +var response = await host.QueryExtensionsAsync(new ExtensionQueryDTO +{ + Platform = TargetPlatform.Windows, + PageNumber = 1, + PageSize = 20 +}); -```json +if (response.Body != null) { - "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 + foreach (var ext in response.Body.Items) + Console.WriteLine($"{ext.DisplayName} v{ext.Version} [{ext.Id}]"); } -``` - -`SupportedPlatforms` 是 `[Flags]` 枚举,`All = Windows | Linux | MacOS = 7`。 ---- +// 更新指定扩展 +var success = await host.UpdateExtensionAsync("report-extension"); +Console.WriteLine(success ? "Extension updated." : "Update failed."); +``` -## 扩展包结构与 Tools 打包关系 +### 5.2 基础参数组合示例 -Extension 组件负责“消费”扩展包:下载、校验、解压、安装、回滚和登记 manifest。Tools 或 CI/CD 负责“生产”扩展包:编译扩展、生成元数据、计算 SHA256、压缩为 ZIP、上传到扩展服务。 +```csharp +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Common.DTOs; +using GeneralUpdate.Extension.Common.Enums; -推荐包名: +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") +}); -```text -{Name}_{Version}.zip -``` +// 事件监听 +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; + } +}; -例如: +// 安装本地扩展包 +var installed = await host.InstallExtensionAsync( + "./downloads/report-extension_1.0.0.zip", + rollbackOnFailure: true); +Console.WriteLine(installed ? "Installed." : "Installation failed."); -```text -report-extension_1.0.0.zip -``` +// 查询已安装扩展 +host.ExtensionCatalog.LoadInstalledExtensions(); +var installedExts = host.ExtensionCatalog.GetInstalledExtensions(); +foreach (var ext in installedExts) +{ + var compat = host.IsExtensionCompatible(ext); + Console.WriteLine($"{ext.DisplayName} v{ext.Version} — compatible: {compat}"); +} -推荐 ZIP 内容: +// 配置自动更新策略 +host.SetGlobalAutoUpdate(true); +host.SetAutoUpdate("large-extension", false); // 大型扩展关闭自动更新 -```text -report-extension_1.0.0.zip -├─ manifest.json # 推荐放入包内,供业务和 catalog 复用 -├─ extension.dll # 扩展主体程序集 -├─ extension.deps.json # .NET 依赖描述 -├─ README.md -├─ CHANGELOG.md -└─ LICENSE.txt +var checkResult = host.IsAutoUpdateEnabled("large-extension"); +Console.WriteLine($"Auto-update for large-extension: {checkResult}"); ``` -当前 `InstallExtensionAsync` 本身不强制读取包内 `manifest.json`,它主要负责安全解压和回滚;`UpdateExtensionAsync` 会使用服务端返回的 `ExtensionDTO` 更新本地 catalog。因此生产侧必须保证服务端元数据与 ZIP 包内容一致。 +### 5.3 真实业务落地示例 -发布侧建议流程: - -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} @@ -516,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)