diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 4b717e3..25f9311 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -4,565 +4,1209 @@ sidebar_position: 5 # GeneralUpdate.Core -## 组件概览 +`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心,重点提供可编程的启动器、配置模型、事件模型、下载子系统扩展点、生命周期钩子、状态上报、差分管道和平台策略扩展。本页聚焦组件 API、属性和扩展方式;完整端到端上手流程会放到 cookbook 中。 -**GeneralUpdate.Core** 是 GeneralUpdate 框架的统一核心组件。自最新版本起,原先独立的 `GeneralUpdate.ClientCore`(客户端更新管理)和 `GeneralUpdate.Common`(公共基础代码)已合并到 `GeneralUpdate.Core` 中。现在只需引用一个 `GeneralUpdate.Core` 包即可同时获得: +**命名空间:** `GeneralUpdate.Core` +**主要入口:** `GeneralUpdateBootstrap` +**NuGet 包:** `GeneralUpdate.Core` -- **客户端更新管理**(原 ClientCore):版本检查、更新包下载、完整性验证、拉起升级程序 -- **升级执行引擎**(原 Core):独立进程升级、文件替换、差分包应用、驱动安装 -- **公共基础设施**(原 Common):生命周期追踪、下载引擎、序列化等底层能力 +```bash +dotnet add package GeneralUpdate.Core +``` + +## 文档大纲与知识点导航 {#knowledge-map} + +如果你是第一次阅读 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-的关系) | -**命名空间:** `GeneralUpdate.Core` -**程序集:** `GeneralUpdate.Core.dll` +## 组件能力边界 + +Core 负责“执行更新”,不负责生成更新包,也不直接管理服务端后台。 + +| 能力 | Core 是否负责 | 说明 | +| --- | --- | --- | +| 读取更新配置 | 是 | 通过 `UpdateRequest`、配置文件、`SetSource` 或 IPC 恢复运行参数。 | +| 检查服务端版本 | 是 | `Client` / `OssClient` 角色会读取版本清单并生成下载计划。 | +| 下载更新包 | 是 | 可替换下载来源、执行器、重试策略、后处理管道或完整编排器。 | +| 校验与应用补丁 | 是 | 支持 Hash 校验、压缩包处理、差分补丁管道。 | +| 文件替换与重启应用 | 是 | `Upgrade` / `OssUpgrade` 角色用于独立升级程序。 | +| 生成差分包 | 否 | 推荐使用 `GeneralUpdate.Tools`。 | + +## 入口类:GeneralUpdateBootstrap + +`GeneralUpdateBootstrap` 是 Core 的主要门面类。它继承 `AbstractBootstrap`,因此同时拥有自身方法和基类提供的扩展注册方法。 ```csharp -public class GeneralUpdateBootstrap : AbstractBootstrap +using GeneralUpdate.Core; + +var bootstrap = new GeneralUpdateBootstrap(); ``` ---- +### 方法总览 -## 核心特性 +| 方法 | 用途 | 常用场景 | +| --- | --- | --- | +| `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()` | 批量注册事件监听器。 | 将事件处理封装成类。 | -### 1. 文件替换与版本管理 -- 安全的文件替换机制,避免文件占用问题 -- 支持多版本增量升级 -- 自动处理文件依赖关系 +### LaunchAsync -### 2. 驱动升级支持 -- 可选的驱动程序升级功能 -- 字段映射表配置 -- 安全的驱动安装流程 +```csharp +public Task LaunchAsync() +``` -### 3. 完整的事件通知 -- 下载进度实时监控 -- 多版本下载管理 -- 异常和错误完整捕获 +`LaunchAsync` 会读取 `Option.AppType` 并选择对应策略: -### 4. 跨平台支持 -- Windows、Linux、macOS 平台全支持 -- 自动平台检测和策略适配 +| `Option.AppType` | 策略 | 说明 | +| --- | --- | --- | +| `AppType.Client` | `ClientStrategy` | 主程序侧:检查版本、下载包、准备升级上下文、启动升级程序。 | +| `AppType.Upgrade` | `UpdateStrategy` | 升级程序侧:读取 IPC 上下文并执行文件替换。 | +| `AppType.OssClient` | `OssStrategy` | OSS 主程序侧更新流程。 | +| `AppType.OssUpgrade` | `OssStrategy` | OSS 升级程序侧更新流程。 | -![Multi Download](imgs/muti_donwload.png) +示例:独立升级程序入口。 ---- +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.Upgrade) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .LaunchAsync(); +``` -## 快速开始 +> 当升级程序由主程序启动时,Core 会通过加密文件 IPC 自动恢复更新上下文,通常不需要在升级程序里再次调用 `SetConfig`。 -### 安装 +## 执行策略总览 {#execution-strategies} -通过 NuGet 安装 GeneralUpdate.Core: +Core 内置三类上层执行策略:标准更新策略、OSS 更新策略和静默更新策略。它们不是互相独立的 API,而是由 `LaunchAsync()` 根据 `Option.AppType`、`Option.Silent` 和当前配置自动选择。 -```bash -dotnet add package GeneralUpdate.Core +| 策略 | 触发条件 | 主要角色 | 适用场景 | +| --- | --- | --- | --- | +| [标准更新策略](#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} + +标准更新由 `ClientStrategy` 和 `UpdateStrategy` 配合完成。`ClientStrategy` 运行在主程序中,负责发现本地清单、请求服务端版本、生成下载计划、下载更新包、准备 IPC 上下文并启动升级程序;`UpdateStrategy` 运行在独立升级程序中,负责读取 IPC 上下文、解压、应用差分补丁、替换文件、回写版本并按需启动主程序。 + +标准流程的核心顺序如下: + +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`,开发者不需要在业务代码中维护本地版本号。 + +| 场景 | 判断结果 | Core 行为 | +| --- | --- | --- | +| `None` | 主程序和升级程序都无需更新 | 分发“无更新”事件并结束。 | +| `UpgradeOnly` | 只有升级程序需要更新 | 主程序下载升级程序包,直接应用到升级程序目录,回写 `UpgradeClientVersion`,主程序继续运行。 | +| `MainOnly` | 只有主程序需要更新 | 主程序下载主程序包,写入 IPC 上下文,启动升级程序替换主程序文件。 | +| `Both` | 主程序和升级程序都需要更新 | 先更新升级程序并回写 `UpgradeClientVersion`,再把主程序包交给新的升级程序处理。 | + +```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 更新策略 {#oss-update-strategy} + +OSS 更新由同一个 `OssStrategy` 根据角色分成 `OssClient` 和 `OssUpgrade` 两段。它适合把版本配置 JSON 和更新包放在 OSS、S3、MinIO、CDN 或静态文件服务器上,不依赖标准服务端版本检查 API。 + +| 角色 | 本地行为 | 关键配置 | +| --- | --- | --- | +| `AppType.OssClient` | 从 `UpdateUrl` 下载 OSS 版本配置到安装目录,比较远端最新版本和本地 `ClientVersion`,需要更新时启动升级程序并退出。 | `UpdateUrl` 指向版本配置文件地址;`MainAppName` / `UpdateAppName` 可由 manifest 提供。 | +| `AppType.OssUpgrade` | 读取本地版本配置或自定义 `DownloadSource`,筛选高于本地版本的资源,下载到安装目录,解压 ZIP,删除压缩包,启动主程序并退出。 | 安装目录可写;资源列表中的版本号必须可比较。 | -以下示例展示了如何在升级助手程序中配置和启动升级流程: +OSS 版本配置文件会保存为 `{MainAppName}_versions.json` 或 `{UpdateAppName}_versions.json`。如果注册了 `DownloadSource()`,OSS 升级侧可以跳过默认文件读取逻辑,改为由你的下载源返回资源列表;如果注册了 `DownloadOrchestrator()`,下载过程也可以完全替换。 ```csharp -using GeneralUpdate.Core; +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://cdn.example.com/myapp_versions.json", + appSecretKey: "oss-mode-secret") + .SetOption(Option.AppType, AppType.OssClient) + .LaunchAsync(); +``` -try -{ - Console.WriteLine($"升级程序初始化,{DateTime.Now}!"); - Console.WriteLine("当前运行目录:" + Thread.GetDomain().BaseDirectory); - - // 启动升级流程 - await new GeneralUpdateBootstrap() - // 监听下载统计信息 - .AddListenerMultiDownloadStatistics(OnMultiDownloadStatistics) - // 监听单个下载完成 - .AddListenerMultiDownloadCompleted(OnMultiDownloadCompleted) - // 监听所有下载完成 - .AddListenerMultiAllDownloadCompleted(OnMultiAllDownloadCompleted) - // 监听下载错误 - .AddListenerMultiDownloadError(OnMultiDownloadError) - // 监听异常 - .AddListenerException(OnException) - // 启动异步升级 - .LaunchAsync(); - - Console.WriteLine($"升级程序已启动,{DateTime.Now}!"); - await Task.Delay(2000); -} -catch (Exception e) -{ - Console.WriteLine(e.Message + "\n" + e.StackTrace); -} +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.OssUpgrade) + .LaunchAsync(); +``` -// 事件处理方法 -void OnMultiDownloadStatistics(object arg1, MultiDownloadStatisticsEventArgs arg2) -{ - var version = arg2.Version as VersionInfo; - Console.WriteLine($"当前下载版本:{version.Version},下载速度:{arg2.Speed}," + - $"剩余时间:{arg2.Remaining},进度:{arg2.ProgressPercentage}%"); -} +### 静默更新策略 {#silent-update-strategy} -void OnMultiDownloadCompleted(object arg1, MultiDownloadCompletedEventArgs arg2) +静默更新只在 `AppType.Client` 下生效。启用后,`LaunchAsync()` 会进入静默启动分支,创建和标准更新相同的 `ClientStrategy`,但把 `LaunchAfterPrepare` 设为 `false`,再交给 `SilentPollOrchestrator` 做后台轮询。 + +静默模式不会重新实现更新逻辑;它只是把“检查和下载”放到后台,把“启动升级程序替换文件”延后到进程退出时。这样用户可以继续使用当前进程,更新包先准备好,真正替换发生在应用退出之后。 + +| 阶段 | 标准更新 | 静默更新 | +| --- | --- | --- | +| 版本检查 | 用户触发后立即执行一次 | 后台按 `Option.SilentPollIntervalMinutes` 周期执行 | +| 下载 | 发现更新后立即下载 | 发现更新后后台下载 | +| 启动升级程序 | 主程序准备完成后立即启动 | 主程序退出时由 `ProcessExit` 处理启动 | +| 用户体验 | 适合显式“检查更新/立即更新” | 适合无打扰准备更新 | + +```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(); +``` + +静默更新仍然会使用你注册的 `IUpdateHooks`、`IUpdateReporter`、下载扩展、证书策略、认证策略和差分管道。需要注意的是,静默模式适合“下载准备无感知”,不等于“文件替换无感知”;主程序文件仍应由独立升级程序在主程序退出后替换。 + +### Cancel + +```csharp +public void Cancel() +``` + +`Cancel` 会触发内部 `CancellationTokenSource`,更新策略会在安全检查点观察取消请求。适合 UI 应用把 bootstrap 保存为字段后绑定取消按钮。 + +```csharp +private GeneralUpdateBootstrap? _bootstrap; + +async Task StartUpdateAsync(UpdateRequest request) { - var version = arg2.Version as VersionInfo; - Console.WriteLine(arg2.IsComplated ? - $"版本 {version.Version} 下载完成!" : - $"版本 {version.Version} 下载失败!"); + _bootstrap = new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)); + + await _bootstrap.LaunchAsync(); } -void OnMultiAllDownloadCompleted(object arg1, MultiAllDownloadCompletedEventArgs arg2) +void CancelUpdate() { - Console.WriteLine(arg2.IsAllDownloadCompleted ? - "所有下载任务已完成!" : - $"下载任务失败!失败数量:{arg2.FailedVersions.Count}"); + _bootstrap?.Cancel(); } +``` + +### SetConfig(UpdateRequest) + +```csharp +public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) +``` + +`SetConfig(UpdateRequest)` 会调用 `UpdateRequest.Validate()`,并把外部配置映射为内部 `UpdateContext`。当角色不是 `AppType.Upgrade` 时,它还会初始化临时目录和黑名单匹配器。 -void OnMultiDownloadError(object arg1, MultiDownloadErrorEventArgs arg2) +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; + +var request = new UpdateRequest { - var version = arg2.Version as VersionInfo; - Console.WriteLine($"版本 {version.Version} 下载错误:{arg2.Exception}"); -} + 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" } +}; -void OnException(object arg1, ExceptionEventArgs arg2) +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +### SetConfig(string) + +```csharp +public GeneralUpdateBootstrap SetConfig(string filePath) +``` + +`SetConfig(string)` 从 UTF-8 JSON 文件读取 `UpdateRequest`。如果只传文件名,会从当前应用基目录解析;如果传相对或绝对路径,会按路径解析。 + +```json { - Console.WriteLine($"升级异常:{arg2.Exception}"); + "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` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json`,只在代码中指定服务端入口和密钥。 -## 核心 API 参考 +```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(); +``` -### GeneralUpdateBootstrap 类方法 +### UseDiffPipeline -#### LaunchAsync 方法 +```csharp +public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) +``` -异步启动升级流程。 +`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,引导类会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。关于算法差异、补丁阶段和并发设置,请看 [差分算法与补丁管道](#differential-pipeline)。 ```csharp -public async Task LaunchAsync() +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(); ``` -**返回值:** -- 返回当前 GeneralUpdateBootstrap 实例,支持链式调用 +## 差分算法与补丁管道 {#differential-pipeline} + +Core 的差分能力分两层:`IBinaryDiffer` 负责“单个文件如何生成/应用补丁”,`DiffPipeline` 负责“目录中哪些文件需要补丁、哪些文件是新增/删除、如何并行处理多个文件”。普通使用者只需要打开 `Option.PatchEnabled`;需要调优性能或兼容性时,再通过 `UseDiffPipeline(...)` 调整。 -#### Option 方法 +### 差分算法类型 -设置升级选项。 +| 算法/实现 | 默认位置 | 特点 | 适合场景 | +| --- | --- | --- | --- | +| `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 -public GeneralUpdateBootstrap Option(UpdateOption option, object value) +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(); ``` -**参数:** -- `option`: 升级选项枚举 -- `value`: 选项值 +### 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 -.Option(UpdateOption.Drive, true) // 启用驱动升级 +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 6) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4); + }) + .LaunchAsync(); ``` -#### AddListenerMultiDownloadStatistics 方法 +并发值不是越大越好。网络慢但磁盘快时可以提高 `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 -public GeneralUpdateBootstrap AddListenerMultiDownloadStatistics( - Action callbackAction) +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 ``` -#### AddListenerMultiDownloadCompleted 方法 +### 清单结构 -监听单个更新包下载完成事件。 +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/" +} +``` + +| 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 -public GeneralUpdateBootstrap AddListenerMultiDownloadCompleted( - Action callbackAction) +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(); ``` -#### AddListenerMultiAllDownloadCompleted 方法 +推荐的职责拆分是: + +| 由 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() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.MaxConcurrency, 4) + .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 -public GeneralUpdateBootstrap AddListenerMultiAllDownloadCompleted( - Action callbackAction) +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); + + // 当前实现中返回 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); + }) + .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(); ``` -#### AddListenerMultiDownloadError 方法 +### 批量事件监听器 -监听每个版本下载错误事件。 +实现 `IUpdateEventListener` 可以把事件处理集中到一个类。若只关心部分事件,继承 `UpdateEventListenerBase` 更简单。 ```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadError( - Action callbackAction) +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(); ``` -#### AddListenerException 方法 +## 日志与性能 {#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 -public GeneralUpdateBootstrap AddListenerException( - Action callbackAction) +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 证书校验。 | +| `UpdateAuth()` | `IHttpAuthProvider` | HTTP 请求认证。 | +| `DownloadSource()` | `IDownloadSource` | 版本清单和下载资源来源。 | +| `DownloadPolicy()` | `IDownloadPolicy` | 下载重试、超时、熔断等策略。 | +| `DownloadExecutor()` | `IDownloadExecutor` | 单文件下载实现。 | +| `DownloadPipeline()` | `IDownloadPipeline` | 下载后处理,例如校验、解密、扫描。 | +| `DownloadOrchestrator()` | `IDownloadOrchestrator` | 批量下载完整编排。 | +| `Strategy()` | `IStrategy` | 自定义平台级更新策略。 | -### UpdateOption 枚举 +> 通过这些方法注册的类型必须有无参构造函数,因为 Core 使用 `new()` 或反射创建实例。需要复杂依赖时,建议在自定义类型内部读取配置,或在应用层封装一个无参适配器。 + +## 生命周期钩子:IUpdateHooks + +`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。它也是一个非常灵活的开放点:在 Linux 或 macOS 上,更新后的可执行文件可能需要重新赋予执行权限,或者需要先执行企业内部的授权脚本、签名校验脚本、权限修复脚本,再启动主程序;这些操作都可以放在 `OnBeforeStartAppAsync` 中完成。 ```csharp -public enum UpdateOption +using GeneralUpdate.Core.Hooks; + +public sealed class ProductUpdateHooks : IUpdateHooks { - /// - /// 是否启用驱动升级功能 - /// - Drive + 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) + { + Console.WriteLine($"Starting app from {ctx.InstallPath}"); + return Task.CompletedTask; + } } + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); ``` -### Packet 类 +Linux/macOS 场景可以直接注册内置的 `UnixPermissionHooks`,让 Core 在启动应用前执行 `chmod +x`: -升级包信息类,由客户端(原 ClientCore,现已合并到 Core)通过参数传递给升级程序: +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + +如果需要执行自己的赋权脚本,可以封装一个无参 hook 适配器,再通过 `Hooks()` 注册: ```csharp -public class Packet +using GeneralUpdate.Core.Hooks; + +public sealed class ProductPermissionHooks : IUpdateHooks { - /// - /// 主更新检查 API 地址 - /// - public string MainUpdateUrl { get; set; } - - /// - /// 应用类型:1=客户端应用,2=更新应用 - /// - public int AppType { get; set; } - - /// - /// 更新检查 API 地址 - /// - public string UpdateUrl { get; set; } - - /// - /// 需要启动的应用程序名称 - /// - public string AppName { get; set; } - - /// - /// 主应用程序名称 - /// - public string MainAppName { get; set; } - - /// - /// 更新包文件格式(默认为 Zip) - /// - public string Format { get; set; } - - /// - /// 是否需要升级更新应用 - /// - public bool IsUpgradeUpdate { get; set; } - - /// - /// 是否需要更新主应用 - /// - public bool IsMainUpdate { get; set; } - - /// - /// 更新日志网页 URL - /// - public string UpdateLogUrl { get; set; } - - /// - /// 需要更新的版本信息列表 - /// - public List UpdateVersions { get; set; } - - /// - /// 文件操作编码格式 - /// - public Encoding Encoding { get; set; } - - /// - /// 下载超时时间(秒) - /// - public int DownloadTimeOut { get; set; } - - /// - /// 应用密钥,与服务器约定 - /// - public string AppSecretKey { get; set; } - - /// - /// 当前客户端版本 - /// - public string ClientVersion { get; set; } - - /// - /// 最新版本 - /// - public string LastVersion { get; set; } - - /// - /// 安装路径(用于更新文件逻辑) - /// - public string InstallPath { get; set; } - - /// - /// 下载文件的临时存储路径 - /// - public string TempPath { get; set; } - - /// - /// 升级终端程序的配置参数(Base64 编码) - /// - public string ProcessBase64 { get; set; } - - /// - /// 当前策略所属平台(Windows/Linux/Mac) - /// - public string Platform { get; set; } - - /// - /// 黑名单文件列表 - /// - public List BlackFiles { get; set; } - - /// - /// 黑名单文件格式列表 - /// - public List BlackFormats { get; set; } - - /// - /// 是否启用驱动升级功能 - /// - public bool DriveEnabled { get; set; } - - /// - /// 驱动程序目录路径,与 Configinfo.DriverDirectory 对应,由 ConfigurationMapper 自动填充 - /// - public string DriverDirectory { get; set; } + 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; } + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); ``` ---- +内置实现包括: + +| 类型 | 说明 | +| --- | --- | +| `NoOpUpdateHooks` | 默认空实现。 | +| `UnixPermissionHooks` | 在 Unix-like 系统启动前执行 `chmod +x`。 | +| `CustomPermissionHooks` | 执行自定义权限脚本;该类型构造函数需要参数,不适合直接用 `Hooks()` 注册,可自行包装无参适配器。 | -## 实际使用示例 +## 状态上报:IUpdateReporter -### 示例 1:基本升级流程 +`IUpdateReporter` 用于把更新状态上报到服务端。 ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Reporting; -try +public sealed class ConsoleUpdateReporter : IUpdateReporter { - Console.WriteLine("升级程序初始化..."); - - // 启动升级流程 - await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadStatistics((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"[{version.Version}] 下载进度: {args.ProgressPercentage}%"); - }) - .AddListenerException((sender, args) => - { - Console.WriteLine($"升级异常: {args.Exception.Message}"); - }) - .LaunchAsync(); - - Console.WriteLine("升级完成!"); + public Task ReportAsync(UpdateReport report, CancellationToken token = default) + { + Console.WriteLine($"Report: record={report.RecordId}, status={report.Status}, type={report.Type}"); + return Task.CompletedTask; + } } -catch (Exception e) + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateReporter() + .LaunchAsync(); +``` + +内置 `HttpUpdateReporter` 会向 `ReportUrl` 发送 JSON: + +```json { - Console.WriteLine($"升级失败: {e.Message}"); + "recordId": 123, + "status": 1, + "type": 1 } ``` -### 示例 2:启用驱动升级 +状态值: -驱动升级通过 `Configinfo.DriverDirectory` 字段传入驱动目录(`Configinfo` 现在位于 `GeneralUpdate.Core` 中),`DrivelutionMiddleware` 会自动处理驱动安装。 +| 枚举 | 值 | 说明 | +| --- | --- | --- | +| `UpdateStatus.Updating` | `1` | 更新中。 | +| `UpdateStatus.Success` | `2` | 更新成功。 | +| `UpdateStatus.Failure` | `3` | 更新失败。 | -在客户端侧配置: +## HTTP 认证:IHttpAuthProvider + +`IHttpAuthProvider` 可以为 Core 发出的 HTTP 请求追加认证头。 ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Security; -var config = new Configinfo +public sealed class StaticBearerAuthProvider : IHttpAuthProvider { - UpdateUrl = "http://your-server.com/api/update/check", - ClientVersion = "1.0.0.0", - InstallPath = AppDomain.CurrentDomain.BaseDirectory, - // 指定包含驱动文件的目录 - DriverDirectory = @"C:\Drivers\Updates" -}; - -await new GeneralClientBootstrap() - .SetConfig(config) - .AddListenerException((sender, args) => + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) { - Console.WriteLine($"更新异常: {args.Exception.Message}"); - }) + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "access-token"); + + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateAuth() .LaunchAsync(); ``` -在 `Core`(升级助手)侧,无需额外配置,`DrivelutionMiddleware` 会自动从 `PipelineContext` 获取驱动目录并执行驱动安装: +Core 内置的认证类型包括 `NoOpAuthProvider`、`BearerTokenAuthProvider`、`ApiKeyAuthProvider` 和 `HmacAuthProvider`。这些类型中部分构造函数需要参数,因此如果要通过 `UpdateAuth()` 注册,通常需要写一个无参包装类。 + +## HTTPS 证书策略:ISslValidationPolicy + +`ISslValidationPolicy` 用于控制 HTTPS 证书校验。默认 `StrictSslValidationPolicy` 只接受没有 SSL policy errors 的证书。 ```csharp -using GeneralUpdate.Core; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using GeneralUpdate.Core.Security; -await new GeneralUpdateBootstrap() - .Option(UpdateOption.Drive, true) // 启用驱动升级 - .AddListenerException((sender, args) => +public sealed class DevelopmentSslPolicy : ISslValidationPolicy +{ + public bool ValidateCertificate( + X509Certificate2? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) { - Console.WriteLine($"升级异常: {args.Exception.Message}"); - }) + return sslPolicyErrors == SslPolicyErrors.None + || certificate?.Issuer.Contains("CN=Local Dev Root") == true; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SslPolicy() .LaunchAsync(); ``` -### 示例 3:完整事件监听 +生产环境不建议无条件返回 `true`,否则会绕过 HTTPS 的安全保证。 + +## 下载来源:IDownloadSource + +`IDownloadSource` 负责返回待下载资源列表。适合接入私有服务、文件服务器、配置中心或自定义云存储。 ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; -await new GeneralUpdateBootstrap() - // 下载统计 - .AddListenerMultiDownloadStatistics((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"[{version.Version}]"); - Console.WriteLine($" 速度: {args.Speed}"); - Console.WriteLine($" 进度: {args.ProgressPercentage}%"); - Console.WriteLine($" 已下载: {args.BytesReceived} / {args.TotalBytesToReceive}"); - Console.WriteLine($" 剩余时间: {args.Remaining}"); - }) - // 单个下载完成 - .AddListenerMultiDownloadCompleted((sender, args) => +public sealed class StaticDownloadSource : IDownloadSource +{ + public Task ListAsync(CancellationToken token = default) { - var version = args.Version as VersionInfo; - string status = args.IsComplated ? "✓ 成功" : "✗ 失败"; - Console.WriteLine($"版本 {version.Version} 下载{status}"); - }) - // 所有下载完成 - .AddListenerMultiAllDownloadCompleted((sender, args) => + 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) { - if (args.IsAllDownloadCompleted) + try { - Console.WriteLine("✓ 所有版本下载完成,开始安装..."); + return await action(token); } - else + catch when (!token.IsCancellationRequested) { - Console.WriteLine($"✗ 下载失败,{args.FailedVersions.Count} 个版本失败:"); - foreach (var version in args.FailedVersions) - { - Console.WriteLine($" - {version}"); - } + await Task.Delay(TimeSpan.FromSeconds(2), token); + return await action(token); } - }) - // 下载错误 - .AddListenerMultiDownloadError((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"✗ 版本 {version.Version} 错误:"); - Console.WriteLine($" {args.Exception.Message}"); - }) - // 异常处理 - .AddListenerException((sender, args) => - { - Console.WriteLine("⚠ 升级过程异常:"); - Console.WriteLine($" 错误: {args.Exception.Message}"); - Console.WriteLine($" 堆栈: {args.Exception.StackTrace}"); - }) + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadPolicy() .LaunchAsync(); ``` -### 示例 4:自定义升级流程 +如果同时注册 `DownloadOrchestrator()`,自定义 orchestrator 会接管完整下载流程,`DownloadPolicy()` 是否生效取决于 orchestrator 自己是否使用该策略。 + +## 单文件下载:IDownloadExecutor + +`IDownloadExecutor` 负责把一个 `DownloadAsset` 下载到目标路径。适合支持 FTP、SFTP、私有协议或自定义 HTTP 客户端。 ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; -// 记录升级开始时间 -var startTime = DateTime.Now; -var downloadedVersions = new List(); +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() - .AddListenerMultiDownloadCompleted((sender, args) => + .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 (args.IsComplated) - { - var version = args.Version as VersionInfo; - downloadedVersions.Add(version.Version); - } - }) - .AddListenerMultiAllDownloadCompleted((sender, args) => + 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) { - if (args.IsAllDownloadCompleted) + var results = new List(); + var started = DateTimeOffset.Now; + + foreach (var asset in plan.Assets) { - var duration = DateTime.Now - startTime; - Console.WriteLine($"升级完成!"); - Console.WriteLine($"总耗时: {duration.TotalSeconds:F2} 秒"); - Console.WriteLine($"已更新版本: {string.Join(", ", downloadedVersions)}"); + var destPath = Path.Combine(destDir, asset.Name); + results.Add(await _executor.ExecuteAsync(asset, destPath, progress, token)); } - }) - .AddListenerException((sender, args) => - { - // 记录日志到文件 - File.AppendAllText("upgrade_error.log", - $"[{DateTime.Now}] {args.Exception}\n"); - }) + + 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)); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadOrchestrator() .LaunchAsync(); ``` ---- - -## 注意事项与警告 +只有当你需要完整替换下载行为时才建议实现 orchestrator。多数情况下替换 `IDownloadExecutor`、`IDownloadPolicy` 或 `IDownloadPipeline` 就够了。 -### ⚠️ 重要提示 +## 平台策略:IStrategy -1. **进程隔离** - - Core 必须作为独立进程运行,不能在主程序中直接调用 - - 升级时主程序必须完全关闭,否则文件替换会失败 +`IStrategy` 是最高级别的更新策略接口。Core 内置 `ClientStrategy`、`UpdateStrategy`、`OssStrategy` 以及 Windows/Linux/macOS 平台策略。只有在你需要替换平台级文件操作或启动逻辑时,才应实现它。 -2. **参数传递** - - 客户端通过 Base64 编码的参数传递配置给 Core(原 ClientCore 现已合并到 Core) - - 确保参数传递过程中不会被截断或损坏 +```csharp +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Strategy; -3. **文件权限** - - 在 Windows 上可能需要管理员权限替换系统目录中的文件 - - 在 Linux/macOS 上需要适当的文件系统权限 +public sealed class LoggingStrategy : IStrategy +{ + private UpdateContext? _context; -4. **驱动升级** - - 驱动升级功能需要系统级权限 - - 建议在测试环境充分验证后再使用 + public IUpdateHooks Hooks { get; set; } = new NoOpUpdateHooks(); + public IUpdateReporter Reporter { get; set; } = new HttpUpdateReporter(); -5. **回滚机制** - - Core 不直接提供回滚功能,但保留了备份文件 - - Core 提供内置的备份与回滚机制,无需额外组件 + public void Create(UpdateContext parameter) + { + _context = parameter; + } -### 💡 最佳实践 + 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)); + } + + public Task StartAppAsync() + { + Console.WriteLine("Custom start app logic."); + return Task.CompletedTask; + } +} -- **日志记录**:实现完整的异常监听,记录升级过程中的所有问题 -- **超时设置**:根据网络环境合理设置下载超时时间 -- **进度反馈**:向用户显示升级进度,提升用户体验 -- **错误处理**:升级失败时提供清晰的错误信息和解决方案 -- **测试验证**:在各种网络条件下测试升级流程的稳定性 +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Strategy() + .LaunchAsync(); +``` ---- +> 静默更新不是单独的扩展接口,而是内置执行策略。配置方式和生命周期见 [静默更新策略](#silent-update-strategy)。 -## 适用平台 +## 与 GeneralUpdate.Tools 的关系 -| 产品 | 版本 | -| ------------------ | ----------------- | -| .NET | 5, 6, 7, 8, 9, 10 | -| .NET Framework | 4.6.1 | -| .NET Standard | 2.0 | -| .NET Core | 2.0 | +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`、下载后校验和状态上报。 | -## 相关资源 +## 相关示例 -- **示例代码**:[查看 GitHub 示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/blob/main/src/Upgrade/Program.cs) -- **主仓库**:[GeneralUpdate 项目](https://github.com/GeneralLibrary/GeneralUpdate) -- **相关组件**:[GeneralUpdate.Bowl](./GeneralUpdate.Bowl.md) | [GeneralUpdate.Drivelution](./GeneralUpdate.Drivelution.md) -- **迁移说明**:`GeneralUpdate.ClientCore` 和 `GeneralUpdate.Common` 已合并到 `GeneralUpdate.Core`。迁移时只需将 `using GeneralUpdate.ClientCore` / `using GeneralUpdate.Common.*` 替换为 `using GeneralUpdate.Core`,并移除旧的 NuGet 包引用。 +- [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) 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 3eb8c64..1ac79c2 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,566 +4,1206 @@ sidebar_position: 5 # GeneralUpdate.Core -## Component Overview +`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. -**GeneralUpdate.Core** is the unified core component of the GeneralUpdate framework. As of the latest version, the previously separate `GeneralUpdate.ClientCore` (client-side update management) and `GeneralUpdate.Common` (shared foundation) have been merged into `GeneralUpdate.Core`. A single `GeneralUpdate.Core` package reference now provides: +**Namespace:** `GeneralUpdate.Core` +**Primary entry point:** `GeneralUpdateBootstrap` +**NuGet package:** `GeneralUpdate.Core` -- **Client update management** (formerly ClientCore): version checking, package download, integrity validation, launching the upgrade process -- **Upgrade execution engine** (formerly Core): standalone process upgrade, file replacement, differential patch application, driver installation -- **Shared infrastructure** (formerly Common): lifecycle tracing, download engine, serialization, and other low-level capabilities +```bash +dotnet add package GeneralUpdate.Core +``` + +## Outline and topic map {#knowledge-map} + +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". + +| What you want to learn | Read | +| --- | --- | +| 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) | -**Namespace:** `GeneralUpdate.Core` -**Assembly:** `GeneralUpdate.Core.dll` +## 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 -public class GeneralUpdateBootstrap : AbstractBootstrap +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 -## Core Features +```csharp +public Task LaunchAsync() +``` -### 1. File Replacement and Version Management -- Safe file replacement mechanism to avoid file locking issues -- Support multi-version incremental upgrades -- Automatic handling of file dependencies +`LaunchAsync` reads `Option.AppType` and selects a role strategy: -### 2. Driver Upgrade Support -- Optional driver upgrade functionality -- Field mapping table configuration -- Safe driver installation process +| `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. | -### 3. Comprehensive Event Notifications -- Real-time download progress monitoring -- Multi-version download management -- Complete exception and error capture +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.Upgrade) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .LaunchAsync(); +``` -### 4. Cross-Platform Support -- Full support for Windows, Linux, macOS platforms -- Automatic platform detection and strategy adaptation +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. -![Multi Download](imgs/muti_donwload.png) +## 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. -## Quick Start +| 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 | -### Installation +### Standard update strategy {#standard-update-strategy} -Install GeneralUpdate.Core via NuGet: +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. -```bash -dotnet add package GeneralUpdate.Core +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(); ``` -### Initialization and Usage +### OSS update strategy {#oss-update-strategy} -The following example demonstrates how to configure and launch the upgrade process in the upgrade assistant program: +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 -using GeneralUpdate.Core; +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://cdn.example.com/myapp_versions.json", + appSecretKey: "oss-mode-secret") + .SetOption(Option.AppType, AppType.OssClient) + .LaunchAsync(); +``` -try -{ - Console.WriteLine($"Upgrade program initialization, {DateTime.Now}!"); - Console.WriteLine("Current directory: " + Thread.GetDomain().BaseDirectory); - - // Launch upgrade process - await new GeneralUpdateBootstrap() - // Listen for download statistics - .AddListenerMultiDownloadStatistics(OnMultiDownloadStatistics) - // Listen for single download completion - .AddListenerMultiDownloadCompleted(OnMultiDownloadCompleted) - // Listen for all downloads completion - .AddListenerMultiAllDownloadCompleted(OnMultiAllDownloadCompleted) - // Listen for download errors - .AddListenerMultiDownloadError(OnMultiDownloadError) - // Listen for exceptions - .AddListenerException(OnException) - // Launch async upgrade - .LaunchAsync(); - - Console.WriteLine($"Upgrade program started, {DateTime.Now}!"); - await Task.Delay(2000); -} -catch (Exception e) -{ - Console.WriteLine(e.Message + "\n" + e.StackTrace); -} +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.OssUpgrade) + .LaunchAsync(); +``` -// Event handler methods -void OnMultiDownloadStatistics(object arg1, MultiDownloadStatisticsEventArgs arg2) -{ - var version = arg2.Version as VersionInfo; - Console.WriteLine($"Current download version: {version.Version}, Download speed: {arg2.Speed}, " + - $"Remaining time: {arg2.Remaining}, Progress: {arg2.ProgressPercentage}%"); -} +### 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. -void OnMultiDownloadCompleted(object arg1, MultiDownloadCompletedEventArgs arg2) +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) { - var version = arg2.Version as VersionInfo; - Console.WriteLine(arg2.IsComplated ? - $"Version {version.Version} download complete!" : - $"Version {version.Version} download failed!"); + _bootstrap = new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)); + + await _bootstrap.LaunchAsync(); } -void OnMultiAllDownloadCompleted(object arg1, MultiAllDownloadCompletedEventArgs arg2) +void CancelUpdate() { - Console.WriteLine(arg2.IsAllDownloadCompleted ? - "All download tasks completed!" : - $"Download tasks failed! Failed count: {arg2.FailedVersions.Count}"); + _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; -void OnMultiDownloadError(object arg1, MultiDownloadErrorEventArgs arg2) +var request = new UpdateRequest { - var version = arg2.Version as VersionInfo; - Console.WriteLine($"Version {version.Version} download error: {arg2.Exception}"); -} + 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(); +``` -void OnException(object arg1, ExceptionEventArgs arg2) +### 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 { - Console.WriteLine($"Upgrade exception: {arg2.Exception}"); + "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(); +``` -## Core API Reference +### SetSource -### GeneralUpdateBootstrap Class Methods +```csharp +public GeneralUpdateBootstrap SetSource( + string updateUrl, + string appSecretKey, + string? reportUrl = null, + string? scheme = null, + string? token = null) +``` -#### LaunchAsync Method +`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. -Launch the upgrade process asynchronously. +```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 async Task LaunchAsync() +public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) ``` -**Return Value:** -- Returns the current GeneralUpdateBootstrap instance, supporting method chaining +`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(); +``` -#### Option Method +## Differential algorithms and patch pipeline {#differential-pipeline} -Set upgrade options. +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 -public GeneralUpdateBootstrap Option(UpdateOption option, object value) +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(); ``` -**Parameters:** -- `option`: Upgrade option enum -- `value`: Option value +### 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. | -**Example:** ```csharp -.Option(UpdateOption.Drive, true) // Enable driver upgrade +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 6) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4); + }) + .LaunchAsync(); ``` -#### AddListenerMultiDownloadStatistics Method +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. | -Listen for download statistics (speed, progress, remaining time, etc.). +### 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 -public GeneralUpdateBootstrap AddListenerMultiDownloadStatistics( - Action callbackAction) +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 ``` -#### AddListenerMultiDownloadCompleted Method +### Manifest structure -Listen for single update package download completion event. +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 | +| --- | --- | +| `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. + +### 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 -public GeneralUpdateBootstrap AddListenerMultiDownloadCompleted( - Action callbackAction) +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(); ``` -#### AddListenerMultiAllDownloadCompleted Method +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: -Listen for all version downloads completion event. +| 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 -public GeneralUpdateBootstrap AddListenerMultiAllDownloadCompleted( - Action callbackAction) +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.MaxConcurrency, 4) + .SetOption(Option.VerifyChecksum, true) + .LaunchAsync(); ``` -#### AddListenerMultiDownloadError Method +| 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. -Listen for download error events for each version. +| Method | Argument type | Trigger in the current code | Important fields | Recommended use | +| --- | --- | --- | --- | --- | +| `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. + +```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 -public GeneralUpdateBootstrap AddListenerMultiDownloadError( - Action callbackAction) +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(); ``` -#### AddListenerException Method +## Logging and performance {#logging-performance} -Listen for all internal exceptions in the upgrade component. +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. + +| 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. ```csharp -public GeneralUpdateBootstrap AddListenerException( - Action callbackAction) +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(); ``` ---- +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. + +## Extension points -## Configuration Class Details +All extension registration methods are provided by `AbstractBootstrap` and can be chained. -### UpdateOption Enum +| Registration method | Interface | Scope | +| --- | --- | --- | +| `Hooks()` | `IUpdateHooks` | Lifecycle callbacks. | +| `UpdateReporter()` | `IUpdateReporter` | Update status reporting. | +| `SslPolicy()` | `ISslValidationPolicy` | HTTPS certificate validation. | +| `UpdateAuth()` | `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. | + +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. + +## Lifecycle hooks: IUpdateHooks + +`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`. ```csharp -public enum UpdateOption +using GeneralUpdate.Core.Hooks; + +public sealed class ProductUpdateHooks : IUpdateHooks { - /// - /// Whether to enable driver upgrade functionality - /// - Drive + 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) + { + Console.WriteLine($"Starting app from {ctx.InstallPath}"); + return Task.CompletedTask; + } } + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); ``` -### Packet Class +On Linux/macOS, you can register the built-in `UnixPermissionHooks` to let Core run `chmod +x` before starting the app: -Upgrade package information class, passed from the client (formerly ClientCore, now merged into Core) to the upgrade process via parameters: +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + +If you need to run your own permission script, wrap it in a parameterless hook adapter and register that adapter with `Hooks()`: ```csharp -public class Packet +using GeneralUpdate.Core.Hooks; + +public sealed class ProductPermissionHooks : IUpdateHooks { - /// - /// Main update check API address - /// - public string MainUpdateUrl { get; set; } - - /// - /// Application type: 1=ClientApp, 2=UpdateApp - /// - public int AppType { get; set; } - - /// - /// Update check API address - /// - public string UpdateUrl { get; set; } - - /// - /// Name of the application to be launched - /// - public string AppName { get; set; } - - /// - /// Main application name - /// - public string MainAppName { get; set; } - - /// - /// Update package file format (default is Zip) - /// - public string Format { get; set; } - - /// - /// Indicates if the update application needs to be upgraded - /// - public bool IsUpgradeUpdate { get; set; } - - /// - /// Indicates if the main application needs to be updated - /// - public bool IsMainUpdate { get; set; } - - /// - /// Update log webpage URL - /// - public string UpdateLogUrl { get; set; } - - /// - /// List of version information that needs updating - /// - public List UpdateVersions { get; set; } - - /// - /// File operation encoding format - /// - public Encoding Encoding { get; set; } - - /// - /// Download timeout duration (seconds) - /// - public int DownloadTimeOut { get; set; } - - /// - /// Application secret key, agreed upon with the server - /// - public string AppSecretKey { get; set; } - - /// - /// Current client version - /// - public string ClientVersion { get; set; } - - /// - /// Latest version - /// - public string LastVersion { get; set; } - - /// - /// Installation path (used for update file logic) - /// - public string InstallPath { get; set; } - - /// - /// Temporary storage path for downloaded files - /// - public string TempPath { get; set; } - - /// - /// Configuration parameters for the upgrade terminal program (Base64 encoded) - /// - public string ProcessBase64 { get; set; } - - /// - /// Platform to which the current strategy belongs (Windows/Linux/Mac) - /// - public string Platform { get; set; } - - /// - /// Files in the blacklist - /// - public List BlackFiles { get; set; } - - /// - /// File formats in the blacklist - /// - public List BlackFormats { get; set; } - - /// - /// Indicates if the driver upgrade feature is enabled - /// - public bool DriveEnabled { get; set; } - - /// - /// Driver directory path, corresponds to Configinfo.DriverDirectory and is auto-populated by ConfigurationMapper - /// - public string DriverDirectory { get; set; } + 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; } + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .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. | -## Practical Usage Examples +## Status reporting: IUpdateReporter -### Example 1: Basic Upgrade Process +`IUpdateReporter` reports update status to a server or local telemetry. ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Reporting; -try +public sealed class ConsoleUpdateReporter : IUpdateReporter { - Console.WriteLine("Upgrade program initialization..."); - - // Launch upgrade process - await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadStatistics((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"[{version.Version}] Download progress: {args.ProgressPercentage}%"); - }) - .AddListenerException((sender, args) => - { - Console.WriteLine($"Upgrade exception: {args.Exception.Message}"); - }) - .LaunchAsync(); - - Console.WriteLine("Upgrade complete!"); + public Task ReportAsync(UpdateReport report, CancellationToken token = default) + { + Console.WriteLine($"Report: record={report.RecordId}, status={report.Status}, type={report.Type}"); + return Task.CompletedTask; + } } -catch (Exception e) + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateReporter() + .LaunchAsync(); +``` + +The built-in `HttpUpdateReporter` posts JSON to `ReportUrl`: + +```json { - Console.WriteLine($"Upgrade failed: {e.Message}"); + "recordId": 123, + "status": 1, + "type": 1 } ``` -### Example 2: Enable Driver Upgrade +| Enum | Value | Description | +| --- | --- | --- | +| `UpdateStatus.Updating` | `1` | Updating. | +| `UpdateStatus.Success` | `2` | Update succeeded. | +| `UpdateStatus.Failure` | `3` | Update failed. | -Driver upgrades are configured via the `DriverDirectory` field in `Configinfo` (now located in `GeneralUpdate.Core`). The `DrivelutionMiddleware` automatically processes driver installation. +## HTTP authentication: IHttpAuthProvider -On the client side: +`IHttpAuthProvider` adds authentication to outgoing Core HTTP requests. ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Security; -var config = new Configinfo +public sealed class StaticBearerAuthProvider : IHttpAuthProvider { - UpdateUrl = "http://your-server.com/api/update/check", - ClientVersion = "1.0.0.0", - InstallPath = AppDomain.CurrentDomain.BaseDirectory, - // Specify the directory containing driver files - DriverDirectory = @"C:\Drivers\Updates" -}; - -await new GeneralClientBootstrap() - .SetConfig(config) - .AddListenerException((sender, args) => + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) { - Console.WriteLine($"Update exception: {args.Exception.Message}"); - }) + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "access-token"); + + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateAuth() .LaunchAsync(); ``` -On the `Core` (upgrade assistant) side, no additional configuration is needed — `DrivelutionMiddleware` automatically retrieves the driver directory from `PipelineContext` and performs driver installation: +Core includes `NoOpAuthProvider`, `BearerTokenAuthProvider`, `ApiKeyAuthProvider`, and `HmacAuthProvider`. Some require constructor parameters, so create a parameterless wrapper when registering through `UpdateAuth()`. + +## HTTPS certificate policy: ISslValidationPolicy + +`ISslValidationPolicy` controls HTTPS certificate validation. The default `StrictSslValidationPolicy` accepts only certificates without SSL policy errors. ```csharp -using GeneralUpdate.Core; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using GeneralUpdate.Core.Security; -await new GeneralUpdateBootstrap() - .Option(UpdateOption.Drive, true) // Enable driver upgrade - .AddListenerException((sender, args) => +public sealed class DevelopmentSslPolicy : ISslValidationPolicy +{ + public bool ValidateCertificate( + X509Certificate2? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) { - Console.WriteLine($"Upgrade exception: {args.Exception.Message}"); - }) + return sslPolicyErrors == SslPolicyErrors.None + || certificate?.Issuer.Contains("CN=Local Dev Root") == true; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SslPolicy() .LaunchAsync(); ``` -### Example 3: Complete Event Listening +Do not unconditionally return `true` in production. + +## Download source: IDownloadSource + +`IDownloadSource` returns assets to download. Use it for private services, file servers, config centers, or custom cloud storage. ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; -await new GeneralUpdateBootstrap() - // Download statistics - .AddListenerMultiDownloadStatistics((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"[{version.Version}]"); - Console.WriteLine($" Speed: {args.Speed}"); - Console.WriteLine($" Progress: {args.ProgressPercentage}%"); - Console.WriteLine($" Downloaded: {args.BytesReceived} / {args.TotalBytesToReceive}"); - Console.WriteLine($" Remaining time: {args.Remaining}"); - }) - // Single download completed - .AddListenerMultiDownloadCompleted((sender, args) => +public sealed class StaticDownloadSource : IDownloadSource +{ + public Task ListAsync(CancellationToken token = default) { - var version = args.Version as VersionInfo; - string status = args.IsComplated ? "✓ Success" : "✗ Failed"; - Console.WriteLine($"Version {version.Version} download {status}"); - }) - // All downloads completed - .AddListenerMultiAllDownloadCompleted((sender, args) => + 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(); +``` + +## Retry policy: IDownloadPolicy + +`IDownloadPolicy` wraps download actions and can implement retry, timeout, circuit breaker, or throttling. + +```csharp +using GeneralUpdate.Core.Download.Abstractions; + +public sealed class TwoAttemptDownloadPolicy : IDownloadPolicy +{ + public async Task ExecuteAsync( + Func> action, + CancellationToken token = default) { - if (args.IsAllDownloadCompleted) + try { - Console.WriteLine("✓ All versions downloaded, starting installation..."); + return await action(token); } - else + catch when (!token.IsCancellationRequested) { - Console.WriteLine($"✗ Download failed, {args.FailedVersions.Count} versions failed:"); - foreach (var version in args.FailedVersions) - { - Console.WriteLine($" - {version}"); - } + await Task.Delay(TimeSpan.FromSeconds(2), token); + return await action(token); } - }) - // Download error - .AddListenerMultiDownloadError((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"✗ Version {version.Version} error:"); - Console.WriteLine($" {args.Exception.Message}"); - }) - // Exception handling - .AddListenerException((sender, args) => - { - Console.WriteLine("⚠ Upgrade process exception:"); - Console.WriteLine($" Error: {args.Exception.Message}"); - Console.WriteLine($" Stack: {args.Exception.StackTrace}"); - }) + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadPolicy() .LaunchAsync(); ``` -### Example 4: Custom Upgrade Process +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. ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; +public sealed class MirrorDownloadExecutor : IDownloadExecutor +{ + private readonly HttpClient _client = new(); -// Record upgrade start time -var startTime = DateTime.Now; -var downloadedVersions = new List(); + 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() - .AddListenerMultiDownloadCompleted((sender, args) => + .SetConfig(request) + .DownloadExecutor() + .LaunchAsync(); +``` + +## Post-download pipeline: IDownloadPipeline + +`IDownloadPipeline` runs after a file is downloaded. Use it for hash verification, decryption, antivirus scanning, or format conversion. + +```csharp +using GeneralUpdate.Core.Download.Abstractions; + +public sealed class AntivirusPipeline : IDownloadPipeline +{ + public Task ProcessAsync(string downloadedPath, CancellationToken token = default) { - if (args.IsComplated) - { - var version = args.Version as VersionInfo; - downloadedVersions.Add(version.Version); - } - }) - .AddListenerMultiAllDownloadCompleted((sender, args) => + 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 first tries to construct a pipeline with a `string` constructor for the expected hash. If not available, it uses the parameterless constructor. + +## Batch download orchestration: IDownloadOrchestrator + +`IDownloadOrchestrator` is the highest-level download extension point. It owns batch download, concurrency, retry, progress, and result aggregation. + +```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) { - if (args.IsAllDownloadCompleted) + var results = new List(); + var started = DateTimeOffset.Now; + + foreach (var asset in plan.Assets) { - var duration = DateTime.Now - startTime; - Console.WriteLine($"Upgrade complete!"); - Console.WriteLine($"Total time: {duration.TotalSeconds:F2} seconds"); - Console.WriteLine($"Updated versions: {string.Join(", ", downloadedVersions)}"); + var destPath = Path.Combine(destDir, asset.Name); + results.Add(await _executor.ExecuteAsync(asset, destPath, progress, token)); } - }) - .AddListenerException((sender, args) => - { - // Log to file - File.AppendAllText("upgrade_error.log", - $"[{DateTime.Now}] {args.Exception}\n"); - }) + + 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)); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadOrchestrator() .LaunchAsync(); ``` ---- - -## Notes and Warnings +Implement an orchestrator only when you need to replace the entire download behavior. Most cases only need `IDownloadExecutor`, `IDownloadPolicy`, or `IDownloadPipeline`. -### ⚠️ Important Notes +## Platform strategy: IStrategy -1. **Process Isolation** - - Core must run as an independent process, cannot be called directly in the main program - - The main program must be completely closed during upgrade, otherwise file replacement will fail +`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. -2. **Parameter Passing** - - Client passes configuration to Core via Base64 encoded parameters (formerly ClientCore, now merged into Core) - - Ensure parameters are not truncated or corrupted during passing +```csharp +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Strategy; -3. **File Permissions** - - Administrator privileges may be required on Windows to replace files in system directories - - Appropriate file system permissions are required on Linux/macOS +public sealed class LoggingStrategy : IStrategy +{ + private UpdateContext? _context; -4. **Driver Upgrade** - - Driver upgrade functionality requires system-level permissions - - Recommended to thoroughly validate in test environment before use + public IUpdateHooks Hooks { get; set; } = new NoOpUpdateHooks(); + public IUpdateReporter Reporter { get; set; } = new HttpUpdateReporter(); -5. **Rollback Mechanism** - - Core does not directly provide rollback functionality, but backup files are preserved - - Core provides built-in backup and rollback, no extra components needed + public void Create(UpdateContext parameter) + { + _context = parameter; + } -### 💡 Best Practices + 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)); + } + + public Task StartAppAsync() + { + Console.WriteLine("Custom start app logic."); + return Task.CompletedTask; + } +} -- **Logging**: Implement complete exception listening to record all issues during the upgrade process -- **Timeout Settings**: Set download timeout appropriately based on network environment -- **Progress Feedback**: Display upgrade progress to users to improve user experience -- **Error Handling**: Provide clear error messages and solutions when upgrade fails -- **Testing**: Test upgrade process stability under various network conditions +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Strategy() + .LaunchAsync(); +``` ---- +> 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. -## Applicable Platforms +## Relationship with GeneralUpdate.Tools -| Product | Version | -| -------------- | ------------- | -| .NET | 5, 6, 7, 8, 9, 10 | -| .NET Framework | 4.6.1 | -| .NET Standard | 2.0 | -| .NET Core | 2.0 | +Core consumes manifests and packages. `GeneralUpdate.Tools` helps generate and validate those artifacts. ---- +| 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 Resources +## Related samples -- **Sample Code**: [View GitHub Examples](https://github.com/GeneralLibrary/GeneralUpdate-Samples/blob/main/src/Upgrade/Program.cs) -- **Main Repository**: [GeneralUpdate Project](https://github.com/GeneralLibrary/GeneralUpdate) -- **Related Components**: [GeneralUpdate.Bowl](./GeneralUpdate.Bowl.md) | [GeneralUpdate.Drivelution](./GeneralUpdate.Drivelution.md) -- **Migration note**: `GeneralUpdate.ClientCore` and `GeneralUpdate.Common` have been merged into `GeneralUpdate.Core`. When migrating, replace `using GeneralUpdate.ClientCore` / `using GeneralUpdate.Common.*` with `using GeneralUpdate.Core` and remove the old NuGet package references. +- [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) 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 4b717e3..25f9311 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,565 +4,1209 @@ sidebar_position: 5 # GeneralUpdate.Core -## 组件概览 +`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心,重点提供可编程的启动器、配置模型、事件模型、下载子系统扩展点、生命周期钩子、状态上报、差分管道和平台策略扩展。本页聚焦组件 API、属性和扩展方式;完整端到端上手流程会放到 cookbook 中。 -**GeneralUpdate.Core** 是 GeneralUpdate 框架的统一核心组件。自最新版本起,原先独立的 `GeneralUpdate.ClientCore`(客户端更新管理)和 `GeneralUpdate.Common`(公共基础代码)已合并到 `GeneralUpdate.Core` 中。现在只需引用一个 `GeneralUpdate.Core` 包即可同时获得: +**命名空间:** `GeneralUpdate.Core` +**主要入口:** `GeneralUpdateBootstrap` +**NuGet 包:** `GeneralUpdate.Core` -- **客户端更新管理**(原 ClientCore):版本检查、更新包下载、完整性验证、拉起升级程序 -- **升级执行引擎**(原 Core):独立进程升级、文件替换、差分包应用、驱动安装 -- **公共基础设施**(原 Common):生命周期追踪、下载引擎、序列化等底层能力 +```bash +dotnet add package GeneralUpdate.Core +``` + +## 文档大纲与知识点导航 {#knowledge-map} + +如果你是第一次阅读 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-的关系) | -**命名空间:** `GeneralUpdate.Core` -**程序集:** `GeneralUpdate.Core.dll` +## 组件能力边界 + +Core 负责“执行更新”,不负责生成更新包,也不直接管理服务端后台。 + +| 能力 | Core 是否负责 | 说明 | +| --- | --- | --- | +| 读取更新配置 | 是 | 通过 `UpdateRequest`、配置文件、`SetSource` 或 IPC 恢复运行参数。 | +| 检查服务端版本 | 是 | `Client` / `OssClient` 角色会读取版本清单并生成下载计划。 | +| 下载更新包 | 是 | 可替换下载来源、执行器、重试策略、后处理管道或完整编排器。 | +| 校验与应用补丁 | 是 | 支持 Hash 校验、压缩包处理、差分补丁管道。 | +| 文件替换与重启应用 | 是 | `Upgrade` / `OssUpgrade` 角色用于独立升级程序。 | +| 生成差分包 | 否 | 推荐使用 `GeneralUpdate.Tools`。 | + +## 入口类:GeneralUpdateBootstrap + +`GeneralUpdateBootstrap` 是 Core 的主要门面类。它继承 `AbstractBootstrap`,因此同时拥有自身方法和基类提供的扩展注册方法。 ```csharp -public class GeneralUpdateBootstrap : AbstractBootstrap +using GeneralUpdate.Core; + +var bootstrap = new GeneralUpdateBootstrap(); ``` ---- +### 方法总览 -## 核心特性 +| 方法 | 用途 | 常用场景 | +| --- | --- | --- | +| `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()` | 批量注册事件监听器。 | 将事件处理封装成类。 | -### 1. 文件替换与版本管理 -- 安全的文件替换机制,避免文件占用问题 -- 支持多版本增量升级 -- 自动处理文件依赖关系 +### LaunchAsync -### 2. 驱动升级支持 -- 可选的驱动程序升级功能 -- 字段映射表配置 -- 安全的驱动安装流程 +```csharp +public Task LaunchAsync() +``` -### 3. 完整的事件通知 -- 下载进度实时监控 -- 多版本下载管理 -- 异常和错误完整捕获 +`LaunchAsync` 会读取 `Option.AppType` 并选择对应策略: -### 4. 跨平台支持 -- Windows、Linux、macOS 平台全支持 -- 自动平台检测和策略适配 +| `Option.AppType` | 策略 | 说明 | +| --- | --- | --- | +| `AppType.Client` | `ClientStrategy` | 主程序侧:检查版本、下载包、准备升级上下文、启动升级程序。 | +| `AppType.Upgrade` | `UpdateStrategy` | 升级程序侧:读取 IPC 上下文并执行文件替换。 | +| `AppType.OssClient` | `OssStrategy` | OSS 主程序侧更新流程。 | +| `AppType.OssUpgrade` | `OssStrategy` | OSS 升级程序侧更新流程。 | -![Multi Download](imgs/muti_donwload.png) +示例:独立升级程序入口。 ---- +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.Upgrade) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .LaunchAsync(); +``` -## 快速开始 +> 当升级程序由主程序启动时,Core 会通过加密文件 IPC 自动恢复更新上下文,通常不需要在升级程序里再次调用 `SetConfig`。 -### 安装 +## 执行策略总览 {#execution-strategies} -通过 NuGet 安装 GeneralUpdate.Core: +Core 内置三类上层执行策略:标准更新策略、OSS 更新策略和静默更新策略。它们不是互相独立的 API,而是由 `LaunchAsync()` 根据 `Option.AppType`、`Option.Silent` 和当前配置自动选择。 -```bash -dotnet add package GeneralUpdate.Core +| 策略 | 触发条件 | 主要角色 | 适用场景 | +| --- | --- | --- | --- | +| [标准更新策略](#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} + +标准更新由 `ClientStrategy` 和 `UpdateStrategy` 配合完成。`ClientStrategy` 运行在主程序中,负责发现本地清单、请求服务端版本、生成下载计划、下载更新包、准备 IPC 上下文并启动升级程序;`UpdateStrategy` 运行在独立升级程序中,负责读取 IPC 上下文、解压、应用差分补丁、替换文件、回写版本并按需启动主程序。 + +标准流程的核心顺序如下: + +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`,开发者不需要在业务代码中维护本地版本号。 + +| 场景 | 判断结果 | Core 行为 | +| --- | --- | --- | +| `None` | 主程序和升级程序都无需更新 | 分发“无更新”事件并结束。 | +| `UpgradeOnly` | 只有升级程序需要更新 | 主程序下载升级程序包,直接应用到升级程序目录,回写 `UpgradeClientVersion`,主程序继续运行。 | +| `MainOnly` | 只有主程序需要更新 | 主程序下载主程序包,写入 IPC 上下文,启动升级程序替换主程序文件。 | +| `Both` | 主程序和升级程序都需要更新 | 先更新升级程序并回写 `UpgradeClientVersion`,再把主程序包交给新的升级程序处理。 | + +```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 更新策略 {#oss-update-strategy} + +OSS 更新由同一个 `OssStrategy` 根据角色分成 `OssClient` 和 `OssUpgrade` 两段。它适合把版本配置 JSON 和更新包放在 OSS、S3、MinIO、CDN 或静态文件服务器上,不依赖标准服务端版本检查 API。 + +| 角色 | 本地行为 | 关键配置 | +| --- | --- | --- | +| `AppType.OssClient` | 从 `UpdateUrl` 下载 OSS 版本配置到安装目录,比较远端最新版本和本地 `ClientVersion`,需要更新时启动升级程序并退出。 | `UpdateUrl` 指向版本配置文件地址;`MainAppName` / `UpdateAppName` 可由 manifest 提供。 | +| `AppType.OssUpgrade` | 读取本地版本配置或自定义 `DownloadSource`,筛选高于本地版本的资源,下载到安装目录,解压 ZIP,删除压缩包,启动主程序并退出。 | 安装目录可写;资源列表中的版本号必须可比较。 | -以下示例展示了如何在升级助手程序中配置和启动升级流程: +OSS 版本配置文件会保存为 `{MainAppName}_versions.json` 或 `{UpdateAppName}_versions.json`。如果注册了 `DownloadSource()`,OSS 升级侧可以跳过默认文件读取逻辑,改为由你的下载源返回资源列表;如果注册了 `DownloadOrchestrator()`,下载过程也可以完全替换。 ```csharp -using GeneralUpdate.Core; +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://cdn.example.com/myapp_versions.json", + appSecretKey: "oss-mode-secret") + .SetOption(Option.AppType, AppType.OssClient) + .LaunchAsync(); +``` -try -{ - Console.WriteLine($"升级程序初始化,{DateTime.Now}!"); - Console.WriteLine("当前运行目录:" + Thread.GetDomain().BaseDirectory); - - // 启动升级流程 - await new GeneralUpdateBootstrap() - // 监听下载统计信息 - .AddListenerMultiDownloadStatistics(OnMultiDownloadStatistics) - // 监听单个下载完成 - .AddListenerMultiDownloadCompleted(OnMultiDownloadCompleted) - // 监听所有下载完成 - .AddListenerMultiAllDownloadCompleted(OnMultiAllDownloadCompleted) - // 监听下载错误 - .AddListenerMultiDownloadError(OnMultiDownloadError) - // 监听异常 - .AddListenerException(OnException) - // 启动异步升级 - .LaunchAsync(); - - Console.WriteLine($"升级程序已启动,{DateTime.Now}!"); - await Task.Delay(2000); -} -catch (Exception e) -{ - Console.WriteLine(e.Message + "\n" + e.StackTrace); -} +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.OssUpgrade) + .LaunchAsync(); +``` -// 事件处理方法 -void OnMultiDownloadStatistics(object arg1, MultiDownloadStatisticsEventArgs arg2) -{ - var version = arg2.Version as VersionInfo; - Console.WriteLine($"当前下载版本:{version.Version},下载速度:{arg2.Speed}," + - $"剩余时间:{arg2.Remaining},进度:{arg2.ProgressPercentage}%"); -} +### 静默更新策略 {#silent-update-strategy} -void OnMultiDownloadCompleted(object arg1, MultiDownloadCompletedEventArgs arg2) +静默更新只在 `AppType.Client` 下生效。启用后,`LaunchAsync()` 会进入静默启动分支,创建和标准更新相同的 `ClientStrategy`,但把 `LaunchAfterPrepare` 设为 `false`,再交给 `SilentPollOrchestrator` 做后台轮询。 + +静默模式不会重新实现更新逻辑;它只是把“检查和下载”放到后台,把“启动升级程序替换文件”延后到进程退出时。这样用户可以继续使用当前进程,更新包先准备好,真正替换发生在应用退出之后。 + +| 阶段 | 标准更新 | 静默更新 | +| --- | --- | --- | +| 版本检查 | 用户触发后立即执行一次 | 后台按 `Option.SilentPollIntervalMinutes` 周期执行 | +| 下载 | 发现更新后立即下载 | 发现更新后后台下载 | +| 启动升级程序 | 主程序准备完成后立即启动 | 主程序退出时由 `ProcessExit` 处理启动 | +| 用户体验 | 适合显式“检查更新/立即更新” | 适合无打扰准备更新 | + +```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(); +``` + +静默更新仍然会使用你注册的 `IUpdateHooks`、`IUpdateReporter`、下载扩展、证书策略、认证策略和差分管道。需要注意的是,静默模式适合“下载准备无感知”,不等于“文件替换无感知”;主程序文件仍应由独立升级程序在主程序退出后替换。 + +### Cancel + +```csharp +public void Cancel() +``` + +`Cancel` 会触发内部 `CancellationTokenSource`,更新策略会在安全检查点观察取消请求。适合 UI 应用把 bootstrap 保存为字段后绑定取消按钮。 + +```csharp +private GeneralUpdateBootstrap? _bootstrap; + +async Task StartUpdateAsync(UpdateRequest request) { - var version = arg2.Version as VersionInfo; - Console.WriteLine(arg2.IsComplated ? - $"版本 {version.Version} 下载完成!" : - $"版本 {version.Version} 下载失败!"); + _bootstrap = new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)); + + await _bootstrap.LaunchAsync(); } -void OnMultiAllDownloadCompleted(object arg1, MultiAllDownloadCompletedEventArgs arg2) +void CancelUpdate() { - Console.WriteLine(arg2.IsAllDownloadCompleted ? - "所有下载任务已完成!" : - $"下载任务失败!失败数量:{arg2.FailedVersions.Count}"); + _bootstrap?.Cancel(); } +``` + +### SetConfig(UpdateRequest) + +```csharp +public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) +``` + +`SetConfig(UpdateRequest)` 会调用 `UpdateRequest.Validate()`,并把外部配置映射为内部 `UpdateContext`。当角色不是 `AppType.Upgrade` 时,它还会初始化临时目录和黑名单匹配器。 -void OnMultiDownloadError(object arg1, MultiDownloadErrorEventArgs arg2) +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; + +var request = new UpdateRequest { - var version = arg2.Version as VersionInfo; - Console.WriteLine($"版本 {version.Version} 下载错误:{arg2.Exception}"); -} + 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" } +}; -void OnException(object arg1, ExceptionEventArgs arg2) +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +### SetConfig(string) + +```csharp +public GeneralUpdateBootstrap SetConfig(string filePath) +``` + +`SetConfig(string)` 从 UTF-8 JSON 文件读取 `UpdateRequest`。如果只传文件名,会从当前应用基目录解析;如果传相对或绝对路径,会按路径解析。 + +```json { - Console.WriteLine($"升级异常:{arg2.Exception}"); + "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` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json`,只在代码中指定服务端入口和密钥。 -## 核心 API 参考 +```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(); +``` -### GeneralUpdateBootstrap 类方法 +### UseDiffPipeline -#### LaunchAsync 方法 +```csharp +public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) +``` -异步启动升级流程。 +`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,引导类会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。关于算法差异、补丁阶段和并发设置,请看 [差分算法与补丁管道](#differential-pipeline)。 ```csharp -public async Task LaunchAsync() +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(); ``` -**返回值:** -- 返回当前 GeneralUpdateBootstrap 实例,支持链式调用 +## 差分算法与补丁管道 {#differential-pipeline} + +Core 的差分能力分两层:`IBinaryDiffer` 负责“单个文件如何生成/应用补丁”,`DiffPipeline` 负责“目录中哪些文件需要补丁、哪些文件是新增/删除、如何并行处理多个文件”。普通使用者只需要打开 `Option.PatchEnabled`;需要调优性能或兼容性时,再通过 `UseDiffPipeline(...)` 调整。 -#### Option 方法 +### 差分算法类型 -设置升级选项。 +| 算法/实现 | 默认位置 | 特点 | 适合场景 | +| --- | --- | --- | --- | +| `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 -public GeneralUpdateBootstrap Option(UpdateOption option, object value) +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(); ``` -**参数:** -- `option`: 升级选项枚举 -- `value`: 选项值 +### 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 -.Option(UpdateOption.Drive, true) // 启用驱动升级 +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 6) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4); + }) + .LaunchAsync(); ``` -#### AddListenerMultiDownloadStatistics 方法 +并发值不是越大越好。网络慢但磁盘快时可以提高 `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 -public GeneralUpdateBootstrap AddListenerMultiDownloadStatistics( - Action callbackAction) +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 ``` -#### AddListenerMultiDownloadCompleted 方法 +### 清单结构 -监听单个更新包下载完成事件。 +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/" +} +``` + +| 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 -public GeneralUpdateBootstrap AddListenerMultiDownloadCompleted( - Action callbackAction) +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(); ``` -#### AddListenerMultiAllDownloadCompleted 方法 +推荐的职责拆分是: + +| 由 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() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.MaxConcurrency, 4) + .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 -public GeneralUpdateBootstrap AddListenerMultiAllDownloadCompleted( - Action callbackAction) +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); + + // 当前实现中返回 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); + }) + .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(); ``` -#### AddListenerMultiDownloadError 方法 +### 批量事件监听器 -监听每个版本下载错误事件。 +实现 `IUpdateEventListener` 可以把事件处理集中到一个类。若只关心部分事件,继承 `UpdateEventListenerBase` 更简单。 ```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadError( - Action callbackAction) +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(); ``` -#### AddListenerException 方法 +## 日志与性能 {#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 -public GeneralUpdateBootstrap AddListenerException( - Action callbackAction) +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 证书校验。 | +| `UpdateAuth()` | `IHttpAuthProvider` | HTTP 请求认证。 | +| `DownloadSource()` | `IDownloadSource` | 版本清单和下载资源来源。 | +| `DownloadPolicy()` | `IDownloadPolicy` | 下载重试、超时、熔断等策略。 | +| `DownloadExecutor()` | `IDownloadExecutor` | 单文件下载实现。 | +| `DownloadPipeline()` | `IDownloadPipeline` | 下载后处理,例如校验、解密、扫描。 | +| `DownloadOrchestrator()` | `IDownloadOrchestrator` | 批量下载完整编排。 | +| `Strategy()` | `IStrategy` | 自定义平台级更新策略。 | -### UpdateOption 枚举 +> 通过这些方法注册的类型必须有无参构造函数,因为 Core 使用 `new()` 或反射创建实例。需要复杂依赖时,建议在自定义类型内部读取配置,或在应用层封装一个无参适配器。 + +## 生命周期钩子:IUpdateHooks + +`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。它也是一个非常灵活的开放点:在 Linux 或 macOS 上,更新后的可执行文件可能需要重新赋予执行权限,或者需要先执行企业内部的授权脚本、签名校验脚本、权限修复脚本,再启动主程序;这些操作都可以放在 `OnBeforeStartAppAsync` 中完成。 ```csharp -public enum UpdateOption +using GeneralUpdate.Core.Hooks; + +public sealed class ProductUpdateHooks : IUpdateHooks { - /// - /// 是否启用驱动升级功能 - /// - Drive + 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) + { + Console.WriteLine($"Starting app from {ctx.InstallPath}"); + return Task.CompletedTask; + } } + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); ``` -### Packet 类 +Linux/macOS 场景可以直接注册内置的 `UnixPermissionHooks`,让 Core 在启动应用前执行 `chmod +x`: -升级包信息类,由客户端(原 ClientCore,现已合并到 Core)通过参数传递给升级程序: +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + +如果需要执行自己的赋权脚本,可以封装一个无参 hook 适配器,再通过 `Hooks()` 注册: ```csharp -public class Packet +using GeneralUpdate.Core.Hooks; + +public sealed class ProductPermissionHooks : IUpdateHooks { - /// - /// 主更新检查 API 地址 - /// - public string MainUpdateUrl { get; set; } - - /// - /// 应用类型:1=客户端应用,2=更新应用 - /// - public int AppType { get; set; } - - /// - /// 更新检查 API 地址 - /// - public string UpdateUrl { get; set; } - - /// - /// 需要启动的应用程序名称 - /// - public string AppName { get; set; } - - /// - /// 主应用程序名称 - /// - public string MainAppName { get; set; } - - /// - /// 更新包文件格式(默认为 Zip) - /// - public string Format { get; set; } - - /// - /// 是否需要升级更新应用 - /// - public bool IsUpgradeUpdate { get; set; } - - /// - /// 是否需要更新主应用 - /// - public bool IsMainUpdate { get; set; } - - /// - /// 更新日志网页 URL - /// - public string UpdateLogUrl { get; set; } - - /// - /// 需要更新的版本信息列表 - /// - public List UpdateVersions { get; set; } - - /// - /// 文件操作编码格式 - /// - public Encoding Encoding { get; set; } - - /// - /// 下载超时时间(秒) - /// - public int DownloadTimeOut { get; set; } - - /// - /// 应用密钥,与服务器约定 - /// - public string AppSecretKey { get; set; } - - /// - /// 当前客户端版本 - /// - public string ClientVersion { get; set; } - - /// - /// 最新版本 - /// - public string LastVersion { get; set; } - - /// - /// 安装路径(用于更新文件逻辑) - /// - public string InstallPath { get; set; } - - /// - /// 下载文件的临时存储路径 - /// - public string TempPath { get; set; } - - /// - /// 升级终端程序的配置参数(Base64 编码) - /// - public string ProcessBase64 { get; set; } - - /// - /// 当前策略所属平台(Windows/Linux/Mac) - /// - public string Platform { get; set; } - - /// - /// 黑名单文件列表 - /// - public List BlackFiles { get; set; } - - /// - /// 黑名单文件格式列表 - /// - public List BlackFormats { get; set; } - - /// - /// 是否启用驱动升级功能 - /// - public bool DriveEnabled { get; set; } - - /// - /// 驱动程序目录路径,与 Configinfo.DriverDirectory 对应,由 ConfigurationMapper 自动填充 - /// - public string DriverDirectory { get; set; } + 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; } + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); ``` ---- +内置实现包括: + +| 类型 | 说明 | +| --- | --- | +| `NoOpUpdateHooks` | 默认空实现。 | +| `UnixPermissionHooks` | 在 Unix-like 系统启动前执行 `chmod +x`。 | +| `CustomPermissionHooks` | 执行自定义权限脚本;该类型构造函数需要参数,不适合直接用 `Hooks()` 注册,可自行包装无参适配器。 | -## 实际使用示例 +## 状态上报:IUpdateReporter -### 示例 1:基本升级流程 +`IUpdateReporter` 用于把更新状态上报到服务端。 ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Reporting; -try +public sealed class ConsoleUpdateReporter : IUpdateReporter { - Console.WriteLine("升级程序初始化..."); - - // 启动升级流程 - await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadStatistics((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"[{version.Version}] 下载进度: {args.ProgressPercentage}%"); - }) - .AddListenerException((sender, args) => - { - Console.WriteLine($"升级异常: {args.Exception.Message}"); - }) - .LaunchAsync(); - - Console.WriteLine("升级完成!"); + public Task ReportAsync(UpdateReport report, CancellationToken token = default) + { + Console.WriteLine($"Report: record={report.RecordId}, status={report.Status}, type={report.Type}"); + return Task.CompletedTask; + } } -catch (Exception e) + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateReporter() + .LaunchAsync(); +``` + +内置 `HttpUpdateReporter` 会向 `ReportUrl` 发送 JSON: + +```json { - Console.WriteLine($"升级失败: {e.Message}"); + "recordId": 123, + "status": 1, + "type": 1 } ``` -### 示例 2:启用驱动升级 +状态值: -驱动升级通过 `Configinfo.DriverDirectory` 字段传入驱动目录(`Configinfo` 现在位于 `GeneralUpdate.Core` 中),`DrivelutionMiddleware` 会自动处理驱动安装。 +| 枚举 | 值 | 说明 | +| --- | --- | --- | +| `UpdateStatus.Updating` | `1` | 更新中。 | +| `UpdateStatus.Success` | `2` | 更新成功。 | +| `UpdateStatus.Failure` | `3` | 更新失败。 | -在客户端侧配置: +## HTTP 认证:IHttpAuthProvider + +`IHttpAuthProvider` 可以为 Core 发出的 HTTP 请求追加认证头。 ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Security; -var config = new Configinfo +public sealed class StaticBearerAuthProvider : IHttpAuthProvider { - UpdateUrl = "http://your-server.com/api/update/check", - ClientVersion = "1.0.0.0", - InstallPath = AppDomain.CurrentDomain.BaseDirectory, - // 指定包含驱动文件的目录 - DriverDirectory = @"C:\Drivers\Updates" -}; - -await new GeneralClientBootstrap() - .SetConfig(config) - .AddListenerException((sender, args) => + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) { - Console.WriteLine($"更新异常: {args.Exception.Message}"); - }) + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "access-token"); + + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateAuth() .LaunchAsync(); ``` -在 `Core`(升级助手)侧,无需额外配置,`DrivelutionMiddleware` 会自动从 `PipelineContext` 获取驱动目录并执行驱动安装: +Core 内置的认证类型包括 `NoOpAuthProvider`、`BearerTokenAuthProvider`、`ApiKeyAuthProvider` 和 `HmacAuthProvider`。这些类型中部分构造函数需要参数,因此如果要通过 `UpdateAuth()` 注册,通常需要写一个无参包装类。 + +## HTTPS 证书策略:ISslValidationPolicy + +`ISslValidationPolicy` 用于控制 HTTPS 证书校验。默认 `StrictSslValidationPolicy` 只接受没有 SSL policy errors 的证书。 ```csharp -using GeneralUpdate.Core; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using GeneralUpdate.Core.Security; -await new GeneralUpdateBootstrap() - .Option(UpdateOption.Drive, true) // 启用驱动升级 - .AddListenerException((sender, args) => +public sealed class DevelopmentSslPolicy : ISslValidationPolicy +{ + public bool ValidateCertificate( + X509Certificate2? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) { - Console.WriteLine($"升级异常: {args.Exception.Message}"); - }) + return sslPolicyErrors == SslPolicyErrors.None + || certificate?.Issuer.Contains("CN=Local Dev Root") == true; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SslPolicy() .LaunchAsync(); ``` -### 示例 3:完整事件监听 +生产环境不建议无条件返回 `true`,否则会绕过 HTTPS 的安全保证。 + +## 下载来源:IDownloadSource + +`IDownloadSource` 负责返回待下载资源列表。适合接入私有服务、文件服务器、配置中心或自定义云存储。 ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; -await new GeneralUpdateBootstrap() - // 下载统计 - .AddListenerMultiDownloadStatistics((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"[{version.Version}]"); - Console.WriteLine($" 速度: {args.Speed}"); - Console.WriteLine($" 进度: {args.ProgressPercentage}%"); - Console.WriteLine($" 已下载: {args.BytesReceived} / {args.TotalBytesToReceive}"); - Console.WriteLine($" 剩余时间: {args.Remaining}"); - }) - // 单个下载完成 - .AddListenerMultiDownloadCompleted((sender, args) => +public sealed class StaticDownloadSource : IDownloadSource +{ + public Task ListAsync(CancellationToken token = default) { - var version = args.Version as VersionInfo; - string status = args.IsComplated ? "✓ 成功" : "✗ 失败"; - Console.WriteLine($"版本 {version.Version} 下载{status}"); - }) - // 所有下载完成 - .AddListenerMultiAllDownloadCompleted((sender, args) => + 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) { - if (args.IsAllDownloadCompleted) + try { - Console.WriteLine("✓ 所有版本下载完成,开始安装..."); + return await action(token); } - else + catch when (!token.IsCancellationRequested) { - Console.WriteLine($"✗ 下载失败,{args.FailedVersions.Count} 个版本失败:"); - foreach (var version in args.FailedVersions) - { - Console.WriteLine($" - {version}"); - } + await Task.Delay(TimeSpan.FromSeconds(2), token); + return await action(token); } - }) - // 下载错误 - .AddListenerMultiDownloadError((sender, args) => - { - var version = args.Version as VersionInfo; - Console.WriteLine($"✗ 版本 {version.Version} 错误:"); - Console.WriteLine($" {args.Exception.Message}"); - }) - // 异常处理 - .AddListenerException((sender, args) => - { - Console.WriteLine("⚠ 升级过程异常:"); - Console.WriteLine($" 错误: {args.Exception.Message}"); - Console.WriteLine($" 堆栈: {args.Exception.StackTrace}"); - }) + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadPolicy() .LaunchAsync(); ``` -### 示例 4:自定义升级流程 +如果同时注册 `DownloadOrchestrator()`,自定义 orchestrator 会接管完整下载流程,`DownloadPolicy()` 是否生效取决于 orchestrator 自己是否使用该策略。 + +## 单文件下载:IDownloadExecutor + +`IDownloadExecutor` 负责把一个 `DownloadAsset` 下载到目标路径。适合支持 FTP、SFTP、私有协议或自定义 HTTP 客户端。 ```csharp -using GeneralUpdate.Core; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; -// 记录升级开始时间 -var startTime = DateTime.Now; -var downloadedVersions = new List(); +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() - .AddListenerMultiDownloadCompleted((sender, args) => + .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 (args.IsComplated) - { - var version = args.Version as VersionInfo; - downloadedVersions.Add(version.Version); - } - }) - .AddListenerMultiAllDownloadCompleted((sender, args) => + 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) { - if (args.IsAllDownloadCompleted) + var results = new List(); + var started = DateTimeOffset.Now; + + foreach (var asset in plan.Assets) { - var duration = DateTime.Now - startTime; - Console.WriteLine($"升级完成!"); - Console.WriteLine($"总耗时: {duration.TotalSeconds:F2} 秒"); - Console.WriteLine($"已更新版本: {string.Join(", ", downloadedVersions)}"); + var destPath = Path.Combine(destDir, asset.Name); + results.Add(await _executor.ExecuteAsync(asset, destPath, progress, token)); } - }) - .AddListenerException((sender, args) => - { - // 记录日志到文件 - File.AppendAllText("upgrade_error.log", - $"[{DateTime.Now}] {args.Exception}\n"); - }) + + 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)); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadOrchestrator() .LaunchAsync(); ``` ---- - -## 注意事项与警告 +只有当你需要完整替换下载行为时才建议实现 orchestrator。多数情况下替换 `IDownloadExecutor`、`IDownloadPolicy` 或 `IDownloadPipeline` 就够了。 -### ⚠️ 重要提示 +## 平台策略:IStrategy -1. **进程隔离** - - Core 必须作为独立进程运行,不能在主程序中直接调用 - - 升级时主程序必须完全关闭,否则文件替换会失败 +`IStrategy` 是最高级别的更新策略接口。Core 内置 `ClientStrategy`、`UpdateStrategy`、`OssStrategy` 以及 Windows/Linux/macOS 平台策略。只有在你需要替换平台级文件操作或启动逻辑时,才应实现它。 -2. **参数传递** - - 客户端通过 Base64 编码的参数传递配置给 Core(原 ClientCore 现已合并到 Core) - - 确保参数传递过程中不会被截断或损坏 +```csharp +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Strategy; -3. **文件权限** - - 在 Windows 上可能需要管理员权限替换系统目录中的文件 - - 在 Linux/macOS 上需要适当的文件系统权限 +public sealed class LoggingStrategy : IStrategy +{ + private UpdateContext? _context; -4. **驱动升级** - - 驱动升级功能需要系统级权限 - - 建议在测试环境充分验证后再使用 + public IUpdateHooks Hooks { get; set; } = new NoOpUpdateHooks(); + public IUpdateReporter Reporter { get; set; } = new HttpUpdateReporter(); -5. **回滚机制** - - Core 不直接提供回滚功能,但保留了备份文件 - - Core 提供内置的备份与回滚机制,无需额外组件 + public void Create(UpdateContext parameter) + { + _context = parameter; + } -### 💡 最佳实践 + 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)); + } + + public Task StartAppAsync() + { + Console.WriteLine("Custom start app logic."); + return Task.CompletedTask; + } +} -- **日志记录**:实现完整的异常监听,记录升级过程中的所有问题 -- **超时设置**:根据网络环境合理设置下载超时时间 -- **进度反馈**:向用户显示升级进度,提升用户体验 -- **错误处理**:升级失败时提供清晰的错误信息和解决方案 -- **测试验证**:在各种网络条件下测试升级流程的稳定性 +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Strategy() + .LaunchAsync(); +``` ---- +> 静默更新不是单独的扩展接口,而是内置执行策略。配置方式和生命周期见 [静默更新策略](#silent-update-strategy)。 -## 适用平台 +## 与 GeneralUpdate.Tools 的关系 -| 产品 | 版本 | -| ------------------ | ----------------- | -| .NET | 5, 6, 7, 8, 9, 10 | -| .NET Framework | 4.6.1 | -| .NET Standard | 2.0 | -| .NET Core | 2.0 | +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`、下载后校验和状态上报。 | -## 相关资源 +## 相关示例 -- **示例代码**:[查看 GitHub 示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/blob/main/src/Upgrade/Program.cs) -- **主仓库**:[GeneralUpdate 项目](https://github.com/GeneralLibrary/GeneralUpdate) -- **相关组件**:[GeneralUpdate.Bowl](./GeneralUpdate.Bowl.md) | [GeneralUpdate.Drivelution](./GeneralUpdate.Drivelution.md) -- **迁移说明**:`GeneralUpdate.ClientCore` 和 `GeneralUpdate.Common` 已合并到 `GeneralUpdate.Core`。迁移时只需将 `using GeneralUpdate.ClientCore` / `using GeneralUpdate.Common.*` 替换为 `using GeneralUpdate.Core`,并移除旧的 NuGet 包引用。 +- [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)