From c9da86c4b88ce18f3d59af096f848bf9c4d3e8f8 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 00:56:28 +0800 Subject: [PATCH 01/10] Draft GeneralUpdate.Core documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 663 +++++------------- .../current/doc/GeneralUpdate.Core.md | 662 +++++------------ .../current/doc/GeneralUpdate.Core.md | 663 +++++------------- 3 files changed, 575 insertions(+), 1413 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 4b717e3..391ea07 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -4,565 +4,286 @@ sidebar_position: 5 # GeneralUpdate.Core -## 组件概览 +## 组件定位 -**GeneralUpdate.Core** 是 GeneralUpdate 框架的统一核心组件。自最新版本起,原先独立的 `GeneralUpdate.ClientCore`(客户端更新管理)和 `GeneralUpdate.Common`(公共基础代码)已合并到 `GeneralUpdate.Core` 中。现在只需引用一个 `GeneralUpdate.Core` 包即可同时获得: +`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心。它负责把“检查版本、下载更新包、校验、解压/合并、替换文件、启动目标程序”等步骤串成一个完整流程,并通过 `GeneralUpdateBootstrap` 提供统一入口。 -- **客户端更新管理**(原 ClientCore):版本检查、更新包下载、完整性验证、拉起升级程序 -- **升级执行引擎**(原 Core):独立进程升级、文件替换、差分包应用、驱动安装 -- **公共基础设施**(原 Common):生命周期追踪、下载引擎、序列化等底层能力 +Core 既可以运行在主程序内执行 `Client` / `OssClient` 流程,也可以作为独立升级程序执行 `Upgrade` / `OssUpgrade` 流程。实际项目中最常见的部署方式是: -**命名空间:** `GeneralUpdate.Core` -**程序集:** `GeneralUpdate.Core.dll` +1. 主程序负责启动更新检查。 +2. Core 在客户端流程中获取版本清单并下载更新包。 +3. Core 启动独立升级程序,升级程序关闭占用进程后完成文件替换。 +4. 升级完成后重新启动主程序。 -```csharp -public class GeneralUpdateBootstrap : AbstractBootstrap -``` - ---- - -## 核心特性 +> 固件升级不属于本页范围;驱动安装能力请参考后续 `GeneralUpdate.Drivelution` 文档。 -### 1. 文件替换与版本管理 -- 安全的文件替换机制,避免文件占用问题 -- 支持多版本增量升级 -- 自动处理文件依赖关系 +**命名空间:** `GeneralUpdate.Core` +**主要入口:** `GeneralUpdateBootstrap` +**NuGet 包:** `GeneralUpdate.Core` -### 2. 驱动升级支持 -- 可选的驱动程序升级功能 -- 字段映射表配置 -- 安全的驱动安装流程 - -### 3. 完整的事件通知 -- 下载进度实时监控 -- 多版本下载管理 -- 异常和错误完整捕获 - -### 4. 跨平台支持 -- Windows、Linux、macOS 平台全支持 -- 自动平台检测和策略适配 +```bash +dotnet add package GeneralUpdate.Core +``` -![Multi Download](imgs/muti_donwload.png) +## 适用场景 ---- +| 场景 | 是否适合使用 Core | +| --- | --- | +| 桌面应用自更新 | 适合。主程序检查更新,升级程序替换文件。 | +| 需要多版本连续升级 | 适合。Core 可以按服务端返回的版本序列处理多个包。 | +| 需要差分补丁更新 | 适合。配合差分包和 `Option.PatchEnabled` / `Option.DiffMode` 使用。 | +| 需要云存储分发更新包 | 适合。使用 `OssClient` / `OssUpgrade` 角色。 | +| 固件刷写 | 不适合。本页不覆盖固件升级组件。 | -## 快速开始 +## 运行角色 -### 安装 +Core 通过 `Option.AppType` 决定当前进程承担的更新角色: -通过 NuGet 安装 GeneralUpdate.Core: +| AppType | 角色 | 说明 | +| --- | --- | --- | +| `Client` | 主程序侧更新流程 | 检查服务端版本、下载包、准备升级上下文,并启动升级程序。默认值。 | +| `Upgrade` | 独立升级程序流程 | 读取主程序传入的 IPC 数据,执行文件替换,再启动主程序。 | +| `OssClient` | OSS 主程序侧流程 | 从 OSS 配置检查更新并启动 OSS 升级程序。 | +| `OssUpgrade` | OSS 升级程序流程 | 从 OSS 下载并部署更新包。 | -```bash -dotnet add package GeneralUpdate.Core -``` +升级程序通常不需要手动调用 `SetConfig`。当它由主程序启动时,Core 会通过加密文件 IPC 自动读取 `ProcessContract`,恢复安装路径、版本、临时目录、下载配置、更新包列表等上下文。 -### 初始化与使用 +## 最小升级程序 -以下示例展示了如何在升级助手程序中配置和启动升级流程: +`GeneralUpdate-Samples/src/Upgrade/Program.cs` 展示了独立升级程序的最小形态。该程序只需要注册必要事件,然后调用 `LaunchAsync()`: ```csharp +using GeneralUpdate.Common.Download; +using GeneralUpdate.Common.Internal; +using GeneralUpdate.Common.Shared.Object; using GeneralUpdate.Core; try { - Console.WriteLine($"升级程序初始化,{DateTime.Now}!"); - Console.WriteLine("当前运行目录:" + Thread.GetDomain().BaseDirectory); - - // 启动升级流程 - await new GeneralUpdateBootstrap() - // 监听下载统计信息 + Console.WriteLine($"Updater started at {DateTime.Now}"); + + _ = 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); } - -// 事件处理方法 -void OnMultiDownloadStatistics(object arg1, MultiDownloadStatisticsEventArgs arg2) +catch (Exception ex) { - var version = arg2.Version as VersionInfo; - Console.WriteLine($"当前下载版本:{version.Version},下载速度:{arg2.Speed}," + - $"剩余时间:{arg2.Remaining},进度:{arg2.ProgressPercentage}%"); + Console.WriteLine(ex); } -void OnMultiDownloadCompleted(object arg1, MultiDownloadCompletedEventArgs arg2) +void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs args) { - var version = arg2.Version as VersionInfo; - Console.WriteLine(arg2.IsComplated ? - $"版本 {version.Version} 下载完成!" : - $"版本 {version.Version} 下载失败!"); + var version = args.Version as VersionInfo; + Console.WriteLine( + $"Version: {version?.Version}, Speed: {args.Speed}, Progress: {args.ProgressPercentage}%"); } -void OnMultiAllDownloadCompleted(object arg1, MultiAllDownloadCompletedEventArgs arg2) +void OnMultiDownloadCompleted(object sender, MultiDownloadCompletedEventArgs args) { - Console.WriteLine(arg2.IsAllDownloadCompleted ? - "所有下载任务已完成!" : - $"下载任务失败!失败数量:{arg2.FailedVersions.Count}"); + var version = args.Version as VersionInfo; + Console.WriteLine(args.IsComplated + ? $"Version {version?.Version} download completed." + : $"Version {version?.Version} download failed."); } -void OnMultiDownloadError(object arg1, MultiDownloadErrorEventArgs arg2) +void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs args) { - var version = arg2.Version as VersionInfo; - Console.WriteLine($"版本 {version.Version} 下载错误:{arg2.Exception}"); + Console.WriteLine(args.IsAllDownloadCompleted + ? "All download tasks completed." + : $"Download failed. Failed versions: {args.FailedVersions.Count}"); } -void OnException(object arg1, ExceptionEventArgs arg2) +void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs args) { - Console.WriteLine($"升级异常:{arg2.Exception}"); + var version = args.Version as VersionInfo; + Console.WriteLine($"Version {version?.Version} download error: {args.Exception}"); } -``` - ---- - -## 核心 API 参考 - -### GeneralUpdateBootstrap 类方法 - -#### LaunchAsync 方法 - -异步启动升级流程。 - -```csharp -public async Task LaunchAsync() -``` - -**返回值:** -- 返回当前 GeneralUpdateBootstrap 实例,支持链式调用 - -#### Option 方法 - -设置升级选项。 - -```csharp -public GeneralUpdateBootstrap Option(UpdateOption option, object value) -``` -**参数:** -- `option`: 升级选项枚举 -- `value`: 选项值 - -**示例:** -```csharp -.Option(UpdateOption.Drive, true) // 启用驱动升级 -``` - -#### AddListenerMultiDownloadStatistics 方法 - -监听下载统计信息(速度、进度、剩余时间等)。 - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadStatistics( - Action callbackAction) -``` - -#### AddListenerMultiDownloadCompleted 方法 - -监听单个更新包下载完成事件。 - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadCompleted( - Action callbackAction) -``` - -#### AddListenerMultiAllDownloadCompleted 方法 - -监听所有版本下载完成事件。 - -```csharp -public GeneralUpdateBootstrap AddListenerMultiAllDownloadCompleted( - Action callbackAction) -``` - -#### AddListenerMultiDownloadError 方法 - -监听每个版本下载错误事件。 - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadError( - Action callbackAction) -``` - -#### AddListenerException 方法 - -监听升级组件内部所有异常。 - -```csharp -public GeneralUpdateBootstrap AddListenerException( - Action callbackAction) -``` - ---- - -## 配置类详解 - -### UpdateOption 枚举 - -```csharp -public enum UpdateOption +void OnException(object sender, ExceptionEventArgs args) { - /// - /// 是否启用驱动升级功能 - /// - Drive + Console.WriteLine(args.Exception); } ``` -### Packet 类 +## 主程序侧配置示例 -升级包信息类,由客户端(原 ClientCore,现已合并到 Core)通过参数传递给升级程序: - -```csharp -public class Packet -{ - /// - /// 主更新检查 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; } -} -``` - ---- - -## 实际使用示例 - -### 示例 1:基本升级流程 - -```csharp -using GeneralUpdate.Core; - -try -{ - 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("升级完成!"); -} -catch (Exception e) -{ - Console.WriteLine($"升级失败: {e.Message}"); -} -``` - -### 示例 2:启用驱动升级 - -驱动升级通过 `Configinfo.DriverDirectory` 字段传入驱动目录(`Configinfo` 现在位于 `GeneralUpdate.Core` 中),`DrivelutionMiddleware` 会自动处理驱动安装。 - -在客户端侧配置: +如果你在主程序内直接使用 Core 的 `Client` 流程,可以显式传入 `UpdateRequest`: ```csharp using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; -var config = new Configinfo -{ - 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) +await new GeneralUpdateBootstrap() + .SetConfig(new UpdateRequest + { + UpdateUrl = "https://update.example.com/api/upgrade/verification", + ReportUrl = "https://update.example.com/api/upgrade/report", + UpdateAppName = "UpgradeSample.exe", + MainAppName = "ClientSample.exe", + InstallPath = AppDomain.CurrentDomain.BaseDirectory, + ClientVersion = "1.0.0", + AppSecretKey = "your-app-secret", + ProductId = "your-product-id" + }) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.DownloadTimeout, 60) + .SetOption(Option.MaxConcurrency, 3) + .AddListenerUpdateInfo((sender, args) => + { + Console.WriteLine($"Server returned {args.Info.Body?.Count ?? 0} update versions."); + }) .AddListenerException((sender, args) => { - Console.WriteLine($"更新异常: {args.Exception.Message}"); + Console.WriteLine(args.Exception); }) .LaunchAsync(); ``` -在 `Core`(升级助手)侧,无需额外配置,`DrivelutionMiddleware` 会自动从 `PipelineContext` 获取驱动目录并执行驱动安装: +也可以使用 `SetSource` 走简化配置路径: ```csharp -using GeneralUpdate.Core; - await new GeneralUpdateBootstrap() - .Option(UpdateOption.Drive, true) // 启用驱动升级 - .AddListenerException((sender, args) => - { - Console.WriteLine($"升级异常: {args.Exception.Message}"); - }) + .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(); ``` -### 示例 3:完整事件监听 +## 核心执行流程 + +1. `SetConfig` / `SetSource` 载入更新地址、应用名称、版本号、密钥、安装目录等配置。 +2. `LaunchAsync` 根据 `Option.AppType` 选择 `ClientStrategy`、`UpdateStrategy` 或 OSS 策略。 +3. Client 流程向服务端请求版本信息,并触发 `AddListenerUpdateInfo` / `AddListenerUpdatePrecheck`。 +4. Core 下载需要更新的版本包,并通过下载事件持续回调速度、进度和错误。 +5. 下载完成后执行校验、解压、差分合并和文件替换等管道步骤。 +6. Upgrade 流程完成后根据 `Option.LaunchClientAfterUpdate` 决定是否启动主程序。 +7. 如配置了 `ReportUrl` 或自定义 `UpdateReporter`,Core 会上报更新状态。 + +## 常用配置项 + +使用 `SetOption(Option.Xxx, value)` 设置运行时选项: + +| 选项 | 默认值 | 说明 | +| --- | --- | --- | +| `Option.AppType` | `AppType.Client` | 当前进程角色。 | +| `Option.Encoding` | `Encoding.UTF8` | 压缩包文件名/内容处理编码。 | +| `Option.Format` | `Format.Zip` | 更新包压缩格式。 | +| `Option.DownloadTimeout` | `30` | 下载超时时间,单位为秒。 | +| `Option.PatchEnabled` | `true` | 是否启用差分补丁处理。 | +| `Option.BackupEnabled` | `true` | 是否在更新前备份被替换文件。 | +| `Option.MaxConcurrency` | `3` | 多文件下载最大并发数。 | +| `Option.EnableResume` | `true` | 是否启用断点续传。 | +| `Option.RetryCount` | `3` | 下载失败重试次数。 | +| `Option.RetryInterval` | `1s` | 下载重试间隔。 | +| `Option.VerifyChecksum` | `true` | 是否校验文件 Hash。 | +| `Option.DiffMode` | `DiffMode.Serial` | 差分合并执行模式。 | +| `Option.Silent` | `false` | 是否启用静默轮询更新。 | +| `Option.SilentPollIntervalMinutes` | `60` | 静默模式检查更新间隔。 | +| `Option.LaunchClientAfterUpdate` | `true` | 升级后是否启动主程序。 | + +示例: ```csharp -using GeneralUpdate.Core; - 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) => - { - var version = args.Version as VersionInfo; - string status = args.IsComplated ? "✓ 成功" : "✗ 失败"; - Console.WriteLine($"版本 {version.Version} 下载{status}"); - }) - // 所有下载完成 - .AddListenerMultiAllDownloadCompleted((sender, args) => - { - if (args.IsAllDownloadCompleted) - { - Console.WriteLine("✓ 所有版本下载完成,开始安装..."); - } - else - { - Console.WriteLine($"✗ 下载失败,{args.FailedVersions.Count} 个版本失败:"); - foreach (var version in args.FailedVersions) - { - Console.WriteLine($" - {version}"); - } - } - }) - // 下载错误 - .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}"); - }) + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.PatchEnabled, true) + .SetOption(Option.BackupEnabled, true) + .SetOption(Option.VerifyChecksum, true) + .SetOption(Option.MaxConcurrency, 4) .LaunchAsync(); ``` -### 示例 4:自定义升级流程 +## 事件监听 -```csharp -using GeneralUpdate.Core; +Core 通过事件监听器暴露更新过程状态: + +| 方法 | 触发时机 | 典型用途 | +| --- | --- | --- | +| `AddListenerUpdateInfo` | 服务端返回版本信息后 | 展示更新日志、版本列表、更新大小。 | +| `AddListenerUpdatePrecheck` | 下载开始前 | 检查磁盘空间、网络环境、用户确认。 | +| `AddListenerMultiDownloadStatistics` | 下载过程中持续触发 | 展示速度、剩余时间、百分比。 | +| `AddListenerMultiDownloadCompleted` | 单个版本包下载完成 | 记录每个版本下载结果。 | +| `AddListenerMultiAllDownloadCompleted` | 全部下载任务完成 | 切换 UI 状态或写日志。 | +| `AddListenerMultiDownloadError` | 下载任务失败 | 输出失败版本和异常。 | +| `AddListenerProgress` | 通用更新进度变化 | 对接统一进度条。 | +| `AddListenerException` | Core 捕获到异常 | 写入日志、提示用户或上报。 | + +如果不想逐个注册事件,可以实现 `IUpdateEventListener` 后使用 `AddEventListener()` 批量注册。 -// 记录升级开始时间 -var startTime = DateTime.Now; -var downloadedVersions = new List(); +## 静默更新 +静默更新适合后台定期检查更新。启用后,`Client` 流程会启动后台轮询并立即返回,更新准备完成后在进程退出时继续升级。 + +```csharp await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadCompleted((sender, args) => - { - if (args.IsComplated) - { - var version = args.Version as VersionInfo; - downloadedVersions.Add(version.Version); - } - }) - .AddListenerMultiAllDownloadCompleted((sender, args) => - { - if (args.IsAllDownloadCompleted) - { - var duration = DateTime.Now - startTime; - Console.WriteLine($"升级完成!"); - Console.WriteLine($"总耗时: {duration.TotalSeconds:F2} 秒"); - Console.WriteLine($"已更新版本: {string.Join(", ", downloadedVersions)}"); - } - }) - .AddListenerException((sender, args) => - { - // 记录日志到文件 - File.AppendAllText("upgrade_error.log", - $"[{DateTime.Now}] {args.Exception}\n"); - }) + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 30) + .SetOption(Option.LaunchClientAfterUpdate, true) .LaunchAsync(); ``` ---- +静默更新仍然需要正确配置更新地址、应用密钥、主程序名称、升级程序名称和安装目录。 -## 注意事项与警告 +## 扩展点 -### ⚠️ 重要提示 +`GeneralUpdateBootstrap` 继承自 `AbstractBootstrap`,可以替换多个内部组件: -1. **进程隔离** - - Core 必须作为独立进程运行,不能在主程序中直接调用 - - 升级时主程序必须完全关闭,否则文件替换会失败 +| 方法 | 用途 | +| --- | --- | +| `Strategy()` | 指定自定义平台策略。 | +| `Hooks()` | 注入更新前、下载后、更新后、启动前、异常时的生命周期钩子。 | +| `UpdateReporter()` | 自定义更新状态上报。 | +| `SslPolicy()` | 自定义 HTTPS 证书校验策略。 | +| `UpdateAuth()` | 为 HTTP 请求添加认证信息。 | +| `DownloadSource()` | 自定义版本清单和文件来源。 | +| `DownloadPolicy()` | 自定义下载重试/超时策略。 | +| `DownloadExecutor()` | 自定义单文件下载实现。 | +| `DownloadPipeline()` | 自定义下载后处理,例如解密、杀毒、校验。 | +| `DownloadOrchestrator()` | 完全接管批量下载流程。 | -2. **参数传递** - - 客户端通过 Base64 编码的参数传递配置给 Core(原 ClientCore 现已合并到 Core) - - 确保参数传递过程中不会被截断或损坏 +高级项目可以只替换某一个环节,而不需要 fork Core。 -3. **文件权限** - - 在 Windows 上可能需要管理员权限替换系统目录中的文件 - - 在 Linux/macOS 上需要适当的文件系统权限 +## 与 GeneralUpdate.Tools 的关系 -4. **驱动升级** - - 驱动升级功能需要系统级权限 - - 建议在测试环境充分验证后再使用 +Core 消费的是更新服务端或 OSS 返回的版本清单和更新包。`GeneralUpdate.Tools` 用来帮助生成和验证这些输入: -5. **回滚机制** - - Core 不直接提供回滚功能,但保留了备份文件 - - Core 提供内置的备份与回滚机制,无需额外组件 +- 使用 Patch Package 生成差分更新包。 +- 使用 Extension Package 生成扩展包。 +- 使用 OSS Config 准备云存储分发配置。 +- 使用模拟、报告和 Hash 相关能力提前验证包结构与完整性。 -### 💡 最佳实践 +推荐流程是:先用 Tools 生成并校验更新包,再把包和版本清单发布到服务端或 OSS,最后由 Core 在客户端执行更新。 -- **日志记录**:实现完整的异常监听,记录升级过程中的所有问题 -- **超时设置**:根据网络环境合理设置下载超时时间 -- **进度反馈**:向用户显示升级进度,提升用户体验 -- **错误处理**:升级失败时提供清晰的错误信息和解决方案 -- **测试验证**:在各种网络条件下测试升级流程的稳定性 +## 常见问题 ---- +### 升级程序启动后没有执行更新 -## 适用平台 +确认它是否由主程序启动。如果直接双击独立升级程序,通常没有 IPC 上下文,`Upgrade` 流程无法知道安装目录和待更新版本。开发调试时可以先从主程序触发完整流程。 -| 产品 | 版本 | -| ------------------ | ----------------- | -| .NET | 5, 6, 7, 8, 9, 10 | -| .NET Framework | 4.6.1 | -| .NET Standard | 2.0 | -| .NET Core | 2.0 | +### 文件替换失败 ---- +通常是主程序或 Bowl 进程仍占用文件。确认主程序已退出,并正确配置 `Bowl` 进程名或相关关闭逻辑。 + +### 下载成功但校验失败 + +确认服务端或 OSS 上的包没有被重新压缩、截断或替换。若启用了 `Option.VerifyChecksum`,客户端收到的 Hash 必须和清单中的 Hash 一致。 + +### 差分包没有生效 + +确认服务端返回的是差分包清单,并且 `Option.PatchEnabled` 为 `true`。差分包建议通过 `GeneralUpdate.Tools` 生成,避免手工组织目录导致清单和包内容不一致。 -## 相关资源 +## 相关示例 -- **示例代码**:[查看 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..6778d6a 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,286 @@ sidebar_position: 5 # GeneralUpdate.Core -## Component Overview +## Component role -**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: +`GeneralUpdate.Core` is the execution core of GeneralUpdate. It connects update checking, package download, checksum verification, extraction or patch merge, file replacement, process restart, and reporting through a single entry point: `GeneralUpdateBootstrap`. -- **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 +Core can run inside the main application for the `Client` / `OssClient` workflow, or inside an independent updater process for the `Upgrade` / `OssUpgrade` workflow. A typical desktop deployment is: -**Namespace:** `GeneralUpdate.Core` -**Assembly:** `GeneralUpdate.Core.dll` +1. The main application starts the update check. +2. Core obtains the server manifest and downloads update packages. +3. Core starts an independent updater process after the main application is ready to exit. +4. The updater process replaces files and restarts the main application. -```csharp -public class GeneralUpdateBootstrap : AbstractBootstrap -``` - ---- - -## Core Features +> Firmware update is outside the scope of this page. Driver installation belongs to `GeneralUpdate.Drivelution` and is documented separately. -### 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 +**Namespace:** `GeneralUpdate.Core` +**Primary entry point:** `GeneralUpdateBootstrap` +**NuGet package:** `GeneralUpdate.Core` -### 2. Driver Upgrade Support -- Optional driver upgrade functionality -- Field mapping table configuration -- Safe driver installation process - -### 3. Comprehensive Event Notifications -- Real-time download progress monitoring -- Multi-version download management -- Complete exception and error capture - -### 4. Cross-Platform Support -- Full support for Windows, Linux, macOS platforms -- Automatic platform detection and strategy adaptation +```bash +dotnet add package GeneralUpdate.Core +``` -![Multi Download](imgs/muti_donwload.png) +## When to use Core ---- +| Scenario | Fit | +| --- | --- | +| Desktop application self-update | Yes. Use the main app to check updates and the updater process to replace files. | +| Multi-version sequential update | Yes. Core can process the version sequence returned by the server. | +| Differential package update | Yes. Use differential packages with `Option.PatchEnabled` / `Option.DiffMode`. | +| OSS or cloud-storage distribution | Yes. Use `OssClient` / `OssUpgrade`. | +| Firmware flashing | No. This page does not cover firmware update components. | -## Quick Start +## Runtime roles -### Installation +Core uses `Option.AppType` to decide the process role: -Install GeneralUpdate.Core via NuGet: +| AppType | Role | Description | +| --- | --- | --- | +| `Client` | Main application workflow | Checks versions, downloads packages, prepares upgrade context, and starts the updater. Default value. | +| `Upgrade` | Independent updater workflow | Reads IPC data from the main app, replaces files, and starts the main app. | +| `OssClient` | OSS client workflow | Checks OSS update configuration and starts the OSS updater. | +| `OssUpgrade` | OSS updater workflow | Downloads from OSS and deploys packages. | -```bash -dotnet add package GeneralUpdate.Core -``` +The updater process usually does not call `SetConfig` manually. When it is launched by the main application, Core reads the encrypted file IPC contract and restores paths, versions, temporary directories, download settings, and package lists automatically. -### Initialization and Usage +## Minimal updater process -The following example demonstrates how to configure and launch the upgrade process in the upgrade assistant program: +`GeneralUpdate-Samples/src/Upgrade/Program.cs` shows the minimal independent updater shape. Register the events you care about and call `LaunchAsync()`: ```csharp +using GeneralUpdate.Common.Download; +using GeneralUpdate.Common.Internal; +using GeneralUpdate.Common.Shared.Object; using GeneralUpdate.Core; 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 + Console.WriteLine($"Updater started at {DateTime.Now}"); + + _ = await new GeneralUpdateBootstrap() .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); } - -// Event handler methods -void OnMultiDownloadStatistics(object arg1, MultiDownloadStatisticsEventArgs arg2) +catch (Exception ex) { - var version = arg2.Version as VersionInfo; - Console.WriteLine($"Current download version: {version.Version}, Download speed: {arg2.Speed}, " + - $"Remaining time: {arg2.Remaining}, Progress: {arg2.ProgressPercentage}%"); + Console.WriteLine(ex); } -void OnMultiDownloadCompleted(object arg1, MultiDownloadCompletedEventArgs arg2) +void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs args) { - var version = arg2.Version as VersionInfo; - Console.WriteLine(arg2.IsComplated ? - $"Version {version.Version} download complete!" : - $"Version {version.Version} download failed!"); + var version = args.Version as VersionInfo; + Console.WriteLine( + $"Version: {version?.Version}, Speed: {args.Speed}, Progress: {args.ProgressPercentage}%"); } -void OnMultiAllDownloadCompleted(object arg1, MultiAllDownloadCompletedEventArgs arg2) +void OnMultiDownloadCompleted(object sender, MultiDownloadCompletedEventArgs args) { - Console.WriteLine(arg2.IsAllDownloadCompleted ? - "All download tasks completed!" : - $"Download tasks failed! Failed count: {arg2.FailedVersions.Count}"); + var version = args.Version as VersionInfo; + Console.WriteLine(args.IsComplated + ? $"Version {version?.Version} download completed." + : $"Version {version?.Version} download failed."); } -void OnMultiDownloadError(object arg1, MultiDownloadErrorEventArgs arg2) -{ - var version = arg2.Version as VersionInfo; - Console.WriteLine($"Version {version.Version} download error: {arg2.Exception}"); -} - -void OnException(object arg1, ExceptionEventArgs arg2) -{ - Console.WriteLine($"Upgrade exception: {arg2.Exception}"); -} -``` - ---- - -## Core API Reference - -### GeneralUpdateBootstrap Class Methods - -#### LaunchAsync Method - -Launch the upgrade process asynchronously. - -```csharp -public async Task LaunchAsync() -``` - -**Return Value:** -- Returns the current GeneralUpdateBootstrap instance, supporting method chaining - -#### Option Method - -Set upgrade options. - -```csharp -public GeneralUpdateBootstrap Option(UpdateOption option, object value) -``` - -**Parameters:** -- `option`: Upgrade option enum -- `value`: Option value - -**Example:** -```csharp -.Option(UpdateOption.Drive, true) // Enable driver upgrade -``` - -#### AddListenerMultiDownloadStatistics Method - -Listen for download statistics (speed, progress, remaining time, etc.). - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadStatistics( - Action callbackAction) -``` - -#### AddListenerMultiDownloadCompleted Method - -Listen for single update package download completion event. - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadCompleted( - Action callbackAction) -``` - -#### AddListenerMultiAllDownloadCompleted Method - -Listen for all version downloads completion event. - -```csharp -public GeneralUpdateBootstrap AddListenerMultiAllDownloadCompleted( - Action callbackAction) -``` - -#### AddListenerMultiDownloadError Method - -Listen for download error events for each version. - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadError( - Action callbackAction) -``` - -#### AddListenerException Method - -Listen for all internal exceptions in the upgrade component. - -```csharp -public GeneralUpdateBootstrap AddListenerException( - Action callbackAction) -``` - ---- - -## Configuration Class Details - -### UpdateOption Enum - -```csharp -public enum UpdateOption +void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs args) { - /// - /// Whether to enable driver upgrade functionality - /// - Drive + Console.WriteLine(args.IsAllDownloadCompleted + ? "All download tasks completed." + : $"Download failed. Failed versions: {args.FailedVersions.Count}"); } -``` - -### Packet Class -Upgrade package information class, passed from the client (formerly ClientCore, now merged into Core) to the upgrade process via parameters: - -```csharp -public class Packet +void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs args) { - /// - /// 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; } + var version = args.Version as VersionInfo; + Console.WriteLine($"Version {version?.Version} download error: {args.Exception}"); } -``` - ---- - -## Practical Usage Examples - -### Example 1: Basic Upgrade Process - -```csharp -using GeneralUpdate.Core; -try +void OnException(object sender, ExceptionEventArgs args) { - 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!"); -} -catch (Exception e) -{ - Console.WriteLine($"Upgrade failed: {e.Message}"); + Console.WriteLine(args.Exception); } ``` -### Example 2: Enable Driver Upgrade - -Driver upgrades are configured via the `DriverDirectory` field in `Configinfo` (now located in `GeneralUpdate.Core`). The `DrivelutionMiddleware` automatically processes driver installation. +## Main application configuration -On the client side: +When using Core directly from the main application, pass an `UpdateRequest` explicitly: ```csharp using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; -var config = new Configinfo -{ - 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) +await new GeneralUpdateBootstrap() + .SetConfig(new UpdateRequest + { + UpdateUrl = "https://update.example.com/api/upgrade/verification", + ReportUrl = "https://update.example.com/api/upgrade/report", + UpdateAppName = "UpgradeSample.exe", + MainAppName = "ClientSample.exe", + InstallPath = AppDomain.CurrentDomain.BaseDirectory, + ClientVersion = "1.0.0", + AppSecretKey = "your-app-secret", + ProductId = "your-product-id" + }) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.DownloadTimeout, 60) + .SetOption(Option.MaxConcurrency, 3) + .AddListenerUpdateInfo((sender, args) => + { + Console.WriteLine($"Server returned {args.Info.Body?.Count ?? 0} update versions."); + }) .AddListenerException((sender, args) => { - Console.WriteLine($"Update exception: {args.Exception.Message}"); + Console.WriteLine(args.Exception); }) .LaunchAsync(); ``` -On the `Core` (upgrade assistant) side, no additional configuration is needed — `DrivelutionMiddleware` automatically retrieves the driver directory from `PipelineContext` and performs driver installation: +For a simplified setup, use `SetSource`: ```csharp -using GeneralUpdate.Core; - await new GeneralUpdateBootstrap() - .Option(UpdateOption.Drive, true) // Enable driver upgrade - .AddListenerException((sender, args) => - { - Console.WriteLine($"Upgrade exception: {args.Exception.Message}"); - }) + .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(); ``` -### Example 3: Complete Event Listening +## Execution flow + +1. `SetConfig` / `SetSource` loads update URLs, app names, versions, secret keys, and install paths. +2. `LaunchAsync` selects `ClientStrategy`, `UpdateStrategy`, or an OSS strategy based on `Option.AppType`. +3. The Client workflow requests version metadata and triggers `AddListenerUpdateInfo` / `AddListenerUpdatePrecheck`. +4. Core downloads required version packages and reports speed, progress, completion, and errors through events. +5. After download, Core verifies checksums, extracts packages, merges patches, and replaces files. +6. The Upgrade workflow starts the main app according to `Option.LaunchClientAfterUpdate`. +7. If `ReportUrl` or a custom `UpdateReporter` is configured, Core reports update status. + +## Common options + +Use `SetOption(Option.Xxx, value)` for runtime settings: + +| Option | Default | Description | +| --- | --- | --- | +| `Option.AppType` | `AppType.Client` | Current process role. | +| `Option.Encoding` | `Encoding.UTF8` | Encoding for package processing. | +| `Option.Format` | `Format.Zip` | Update package compression format. | +| `Option.DownloadTimeout` | `30` | Download timeout in seconds. | +| `Option.PatchEnabled` | `true` | Enables differential patch processing. | +| `Option.BackupEnabled` | `true` | Backs up replaced files before update. | +| `Option.MaxConcurrency` | `3` | Max concurrent file downloads. | +| `Option.EnableResume` | `true` | Enables resumable downloads. | +| `Option.RetryCount` | `3` | Download retry count. | +| `Option.RetryInterval` | `1s` | Delay between retries. | +| `Option.VerifyChecksum` | `true` | Verifies file checksums. | +| `Option.DiffMode` | `DiffMode.Serial` | Differential merge mode. | +| `Option.Silent` | `false` | Enables silent polling update mode. | +| `Option.SilentPollIntervalMinutes` | `60` | Silent polling interval. | +| `Option.LaunchClientAfterUpdate` | `true` | Starts the main app after update. | + +Example: ```csharp -using GeneralUpdate.Core; - 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) => - { - 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) => - { - if (args.IsAllDownloadCompleted) - { - Console.WriteLine("✓ All versions downloaded, starting installation..."); - } - else - { - Console.WriteLine($"✗ Download failed, {args.FailedVersions.Count} versions failed:"); - foreach (var version in args.FailedVersions) - { - Console.WriteLine($" - {version}"); - } - } - }) - // 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}"); - }) + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.PatchEnabled, true) + .SetOption(Option.BackupEnabled, true) + .SetOption(Option.VerifyChecksum, true) + .SetOption(Option.MaxConcurrency, 4) .LaunchAsync(); ``` -### Example 4: Custom Upgrade Process +## Events -```csharp -using GeneralUpdate.Core; +Core exposes update state through listener methods: + +| Method | Trigger | Typical use | +| --- | --- | --- | +| `AddListenerUpdateInfo` | After server version metadata is returned | Show update notes, version list, or package size. | +| `AddListenerUpdatePrecheck` | Before download starts | Check disk space, network conditions, or user confirmation. | +| `AddListenerMultiDownloadStatistics` | During download | Show speed, remaining time, and percentage. | +| `AddListenerMultiDownloadCompleted` | One version package completes | Log per-version download result. | +| `AddListenerMultiAllDownloadCompleted` | All download tasks complete | Update UI state or write logs. | +| `AddListenerMultiDownloadError` | A download task fails | Capture failed version and exception. | +| `AddListenerProgress` | Generic update progress changes | Drive a unified progress bar. | +| `AddListenerException` | Core catches an exception | Log, notify the user, or report telemetry. | +You can also implement `IUpdateEventListener` and register all handlers with `AddEventListener()`. -// Record upgrade start time -var startTime = DateTime.Now; -var downloadedVersions = new List(); +## Silent update +Silent mode is designed for background polling. When enabled, the `Client` workflow starts a background poll loop and returns immediately. Prepared updates continue when the process exits. + +```csharp await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadCompleted((sender, args) => - { - if (args.IsComplated) - { - var version = args.Version as VersionInfo; - downloadedVersions.Add(version.Version); - } - }) - .AddListenerMultiAllDownloadCompleted((sender, args) => - { - if (args.IsAllDownloadCompleted) - { - var duration = DateTime.Now - startTime; - Console.WriteLine($"Upgrade complete!"); - Console.WriteLine($"Total time: {duration.TotalSeconds:F2} seconds"); - Console.WriteLine($"Updated versions: {string.Join(", ", downloadedVersions)}"); - } - }) - .AddListenerException((sender, args) => - { - // Log to file - File.AppendAllText("upgrade_error.log", - $"[{DateTime.Now}] {args.Exception}\n"); - }) + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 30) + .SetOption(Option.LaunchClientAfterUpdate, true) .LaunchAsync(); ``` ---- +Silent mode still requires valid update URL, secret key, main app name, updater name, and install path configuration. -## Notes and Warnings +## Extension points -### ⚠️ Important Notes +`GeneralUpdateBootstrap` inherits from `AbstractBootstrap` and can replace multiple internal components: -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 +| Method | Purpose | +| --- | --- | +| `Strategy()` | Selects a custom platform strategy. | +| `Hooks()` | Adds lifecycle hooks before update, after download, after update, before app start, and on error. | +| `UpdateReporter()` | Customizes update status reporting. | +| `SslPolicy()` | Customizes HTTPS certificate validation. | +| `UpdateAuth()` | Adds authentication to HTTP requests. | +| `DownloadSource()` | Customizes manifest and file source. | +| `DownloadPolicy()` | Customizes retry and timeout behavior. | +| `DownloadExecutor()` | Customizes single-file download implementation. | +| `DownloadPipeline()` | Adds post-download processing such as decryption, scanning, or validation. | +| `DownloadOrchestrator()` | Fully owns batch download orchestration. | -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 +Advanced projects can replace one part of the workflow without forking Core. -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 +## Relationship with GeneralUpdate.Tools -4. **Driver Upgrade** - - Driver upgrade functionality requires system-level permissions - - Recommended to thoroughly validate in test environment before use +Core consumes version manifests and update packages from the update server or OSS. `GeneralUpdate.Tools` helps produce and verify those inputs: -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 +- Patch Package generates differential packages. +- Extension Package generates extension packages. +- OSS Config prepares cloud-storage distribution configuration. +- Simulation, report, and hash features help validate package structure and integrity before release. -### 💡 Best Practices +Recommended workflow: use Tools to generate and validate packages, publish packages and manifests to the server or OSS, then let Core execute the update on client machines. -- **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 +## Troubleshooting ---- +### The updater starts but does nothing -## Applicable Platforms +Confirm it was launched by the main application. If the independent updater is started by double-clicking, it usually has no IPC context and cannot know the install path or pending versions. -| Product | Version | -| -------------- | ------------- | -| .NET | 5, 6, 7, 8, 9, 10 | -| .NET Framework | 4.6.1 | -| .NET Standard | 2.0 | -| .NET Core | 2.0 | +### File replacement fails ---- +The main app or Bowl process is usually still holding files. Confirm the main app has exited and configure the `Bowl` process name or shutdown logic correctly. + +### Download succeeds but checksum validation fails + +Confirm the package was not recompressed, truncated, or replaced on the server or OSS. When `Option.VerifyChecksum` is enabled, the received file hash must match the manifest. + +### Differential packages are ignored + +Confirm the server returns a differential package manifest and `Option.PatchEnabled` is `true`. Generate differential packages with `GeneralUpdate.Tools` to avoid mismatched manifests and package contents. -## 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..391ea07 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,286 @@ sidebar_position: 5 # GeneralUpdate.Core -## 组件概览 +## 组件定位 -**GeneralUpdate.Core** 是 GeneralUpdate 框架的统一核心组件。自最新版本起,原先独立的 `GeneralUpdate.ClientCore`(客户端更新管理)和 `GeneralUpdate.Common`(公共基础代码)已合并到 `GeneralUpdate.Core` 中。现在只需引用一个 `GeneralUpdate.Core` 包即可同时获得: +`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心。它负责把“检查版本、下载更新包、校验、解压/合并、替换文件、启动目标程序”等步骤串成一个完整流程,并通过 `GeneralUpdateBootstrap` 提供统一入口。 -- **客户端更新管理**(原 ClientCore):版本检查、更新包下载、完整性验证、拉起升级程序 -- **升级执行引擎**(原 Core):独立进程升级、文件替换、差分包应用、驱动安装 -- **公共基础设施**(原 Common):生命周期追踪、下载引擎、序列化等底层能力 +Core 既可以运行在主程序内执行 `Client` / `OssClient` 流程,也可以作为独立升级程序执行 `Upgrade` / `OssUpgrade` 流程。实际项目中最常见的部署方式是: -**命名空间:** `GeneralUpdate.Core` -**程序集:** `GeneralUpdate.Core.dll` +1. 主程序负责启动更新检查。 +2. Core 在客户端流程中获取版本清单并下载更新包。 +3. Core 启动独立升级程序,升级程序关闭占用进程后完成文件替换。 +4. 升级完成后重新启动主程序。 -```csharp -public class GeneralUpdateBootstrap : AbstractBootstrap -``` - ---- - -## 核心特性 +> 固件升级不属于本页范围;驱动安装能力请参考后续 `GeneralUpdate.Drivelution` 文档。 -### 1. 文件替换与版本管理 -- 安全的文件替换机制,避免文件占用问题 -- 支持多版本增量升级 -- 自动处理文件依赖关系 +**命名空间:** `GeneralUpdate.Core` +**主要入口:** `GeneralUpdateBootstrap` +**NuGet 包:** `GeneralUpdate.Core` -### 2. 驱动升级支持 -- 可选的驱动程序升级功能 -- 字段映射表配置 -- 安全的驱动安装流程 - -### 3. 完整的事件通知 -- 下载进度实时监控 -- 多版本下载管理 -- 异常和错误完整捕获 - -### 4. 跨平台支持 -- Windows、Linux、macOS 平台全支持 -- 自动平台检测和策略适配 +```bash +dotnet add package GeneralUpdate.Core +``` -![Multi Download](imgs/muti_donwload.png) +## 适用场景 ---- +| 场景 | 是否适合使用 Core | +| --- | --- | +| 桌面应用自更新 | 适合。主程序检查更新,升级程序替换文件。 | +| 需要多版本连续升级 | 适合。Core 可以按服务端返回的版本序列处理多个包。 | +| 需要差分补丁更新 | 适合。配合差分包和 `Option.PatchEnabled` / `Option.DiffMode` 使用。 | +| 需要云存储分发更新包 | 适合。使用 `OssClient` / `OssUpgrade` 角色。 | +| 固件刷写 | 不适合。本页不覆盖固件升级组件。 | -## 快速开始 +## 运行角色 -### 安装 +Core 通过 `Option.AppType` 决定当前进程承担的更新角色: -通过 NuGet 安装 GeneralUpdate.Core: +| AppType | 角色 | 说明 | +| --- | --- | --- | +| `Client` | 主程序侧更新流程 | 检查服务端版本、下载包、准备升级上下文,并启动升级程序。默认值。 | +| `Upgrade` | 独立升级程序流程 | 读取主程序传入的 IPC 数据,执行文件替换,再启动主程序。 | +| `OssClient` | OSS 主程序侧流程 | 从 OSS 配置检查更新并启动 OSS 升级程序。 | +| `OssUpgrade` | OSS 升级程序流程 | 从 OSS 下载并部署更新包。 | -```bash -dotnet add package GeneralUpdate.Core -``` +升级程序通常不需要手动调用 `SetConfig`。当它由主程序启动时,Core 会通过加密文件 IPC 自动读取 `ProcessContract`,恢复安装路径、版本、临时目录、下载配置、更新包列表等上下文。 -### 初始化与使用 +## 最小升级程序 -以下示例展示了如何在升级助手程序中配置和启动升级流程: +`GeneralUpdate-Samples/src/Upgrade/Program.cs` 展示了独立升级程序的最小形态。该程序只需要注册必要事件,然后调用 `LaunchAsync()`: ```csharp +using GeneralUpdate.Common.Download; +using GeneralUpdate.Common.Internal; +using GeneralUpdate.Common.Shared.Object; using GeneralUpdate.Core; try { - Console.WriteLine($"升级程序初始化,{DateTime.Now}!"); - Console.WriteLine("当前运行目录:" + Thread.GetDomain().BaseDirectory); - - // 启动升级流程 - await new GeneralUpdateBootstrap() - // 监听下载统计信息 + Console.WriteLine($"Updater started at {DateTime.Now}"); + + _ = 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); } - -// 事件处理方法 -void OnMultiDownloadStatistics(object arg1, MultiDownloadStatisticsEventArgs arg2) +catch (Exception ex) { - var version = arg2.Version as VersionInfo; - Console.WriteLine($"当前下载版本:{version.Version},下载速度:{arg2.Speed}," + - $"剩余时间:{arg2.Remaining},进度:{arg2.ProgressPercentage}%"); + Console.WriteLine(ex); } -void OnMultiDownloadCompleted(object arg1, MultiDownloadCompletedEventArgs arg2) +void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs args) { - var version = arg2.Version as VersionInfo; - Console.WriteLine(arg2.IsComplated ? - $"版本 {version.Version} 下载完成!" : - $"版本 {version.Version} 下载失败!"); + var version = args.Version as VersionInfo; + Console.WriteLine( + $"Version: {version?.Version}, Speed: {args.Speed}, Progress: {args.ProgressPercentage}%"); } -void OnMultiAllDownloadCompleted(object arg1, MultiAllDownloadCompletedEventArgs arg2) +void OnMultiDownloadCompleted(object sender, MultiDownloadCompletedEventArgs args) { - Console.WriteLine(arg2.IsAllDownloadCompleted ? - "所有下载任务已完成!" : - $"下载任务失败!失败数量:{arg2.FailedVersions.Count}"); + var version = args.Version as VersionInfo; + Console.WriteLine(args.IsComplated + ? $"Version {version?.Version} download completed." + : $"Version {version?.Version} download failed."); } -void OnMultiDownloadError(object arg1, MultiDownloadErrorEventArgs arg2) +void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs args) { - var version = arg2.Version as VersionInfo; - Console.WriteLine($"版本 {version.Version} 下载错误:{arg2.Exception}"); + Console.WriteLine(args.IsAllDownloadCompleted + ? "All download tasks completed." + : $"Download failed. Failed versions: {args.FailedVersions.Count}"); } -void OnException(object arg1, ExceptionEventArgs arg2) +void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs args) { - Console.WriteLine($"升级异常:{arg2.Exception}"); + var version = args.Version as VersionInfo; + Console.WriteLine($"Version {version?.Version} download error: {args.Exception}"); } -``` - ---- - -## 核心 API 参考 - -### GeneralUpdateBootstrap 类方法 - -#### LaunchAsync 方法 - -异步启动升级流程。 - -```csharp -public async Task LaunchAsync() -``` - -**返回值:** -- 返回当前 GeneralUpdateBootstrap 实例,支持链式调用 - -#### Option 方法 - -设置升级选项。 - -```csharp -public GeneralUpdateBootstrap Option(UpdateOption option, object value) -``` -**参数:** -- `option`: 升级选项枚举 -- `value`: 选项值 - -**示例:** -```csharp -.Option(UpdateOption.Drive, true) // 启用驱动升级 -``` - -#### AddListenerMultiDownloadStatistics 方法 - -监听下载统计信息(速度、进度、剩余时间等)。 - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadStatistics( - Action callbackAction) -``` - -#### AddListenerMultiDownloadCompleted 方法 - -监听单个更新包下载完成事件。 - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadCompleted( - Action callbackAction) -``` - -#### AddListenerMultiAllDownloadCompleted 方法 - -监听所有版本下载完成事件。 - -```csharp -public GeneralUpdateBootstrap AddListenerMultiAllDownloadCompleted( - Action callbackAction) -``` - -#### AddListenerMultiDownloadError 方法 - -监听每个版本下载错误事件。 - -```csharp -public GeneralUpdateBootstrap AddListenerMultiDownloadError( - Action callbackAction) -``` - -#### AddListenerException 方法 - -监听升级组件内部所有异常。 - -```csharp -public GeneralUpdateBootstrap AddListenerException( - Action callbackAction) -``` - ---- - -## 配置类详解 - -### UpdateOption 枚举 - -```csharp -public enum UpdateOption +void OnException(object sender, ExceptionEventArgs args) { - /// - /// 是否启用驱动升级功能 - /// - Drive + Console.WriteLine(args.Exception); } ``` -### Packet 类 +## 主程序侧配置示例 -升级包信息类,由客户端(原 ClientCore,现已合并到 Core)通过参数传递给升级程序: - -```csharp -public class Packet -{ - /// - /// 主更新检查 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; } -} -``` - ---- - -## 实际使用示例 - -### 示例 1:基本升级流程 - -```csharp -using GeneralUpdate.Core; - -try -{ - 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("升级完成!"); -} -catch (Exception e) -{ - Console.WriteLine($"升级失败: {e.Message}"); -} -``` - -### 示例 2:启用驱动升级 - -驱动升级通过 `Configinfo.DriverDirectory` 字段传入驱动目录(`Configinfo` 现在位于 `GeneralUpdate.Core` 中),`DrivelutionMiddleware` 会自动处理驱动安装。 - -在客户端侧配置: +如果你在主程序内直接使用 Core 的 `Client` 流程,可以显式传入 `UpdateRequest`: ```csharp using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; -var config = new Configinfo -{ - 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) +await new GeneralUpdateBootstrap() + .SetConfig(new UpdateRequest + { + UpdateUrl = "https://update.example.com/api/upgrade/verification", + ReportUrl = "https://update.example.com/api/upgrade/report", + UpdateAppName = "UpgradeSample.exe", + MainAppName = "ClientSample.exe", + InstallPath = AppDomain.CurrentDomain.BaseDirectory, + ClientVersion = "1.0.0", + AppSecretKey = "your-app-secret", + ProductId = "your-product-id" + }) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.DownloadTimeout, 60) + .SetOption(Option.MaxConcurrency, 3) + .AddListenerUpdateInfo((sender, args) => + { + Console.WriteLine($"Server returned {args.Info.Body?.Count ?? 0} update versions."); + }) .AddListenerException((sender, args) => { - Console.WriteLine($"更新异常: {args.Exception.Message}"); + Console.WriteLine(args.Exception); }) .LaunchAsync(); ``` -在 `Core`(升级助手)侧,无需额外配置,`DrivelutionMiddleware` 会自动从 `PipelineContext` 获取驱动目录并执行驱动安装: +也可以使用 `SetSource` 走简化配置路径: ```csharp -using GeneralUpdate.Core; - await new GeneralUpdateBootstrap() - .Option(UpdateOption.Drive, true) // 启用驱动升级 - .AddListenerException((sender, args) => - { - Console.WriteLine($"升级异常: {args.Exception.Message}"); - }) + .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(); ``` -### 示例 3:完整事件监听 +## 核心执行流程 + +1. `SetConfig` / `SetSource` 载入更新地址、应用名称、版本号、密钥、安装目录等配置。 +2. `LaunchAsync` 根据 `Option.AppType` 选择 `ClientStrategy`、`UpdateStrategy` 或 OSS 策略。 +3. Client 流程向服务端请求版本信息,并触发 `AddListenerUpdateInfo` / `AddListenerUpdatePrecheck`。 +4. Core 下载需要更新的版本包,并通过下载事件持续回调速度、进度和错误。 +5. 下载完成后执行校验、解压、差分合并和文件替换等管道步骤。 +6. Upgrade 流程完成后根据 `Option.LaunchClientAfterUpdate` 决定是否启动主程序。 +7. 如配置了 `ReportUrl` 或自定义 `UpdateReporter`,Core 会上报更新状态。 + +## 常用配置项 + +使用 `SetOption(Option.Xxx, value)` 设置运行时选项: + +| 选项 | 默认值 | 说明 | +| --- | --- | --- | +| `Option.AppType` | `AppType.Client` | 当前进程角色。 | +| `Option.Encoding` | `Encoding.UTF8` | 压缩包文件名/内容处理编码。 | +| `Option.Format` | `Format.Zip` | 更新包压缩格式。 | +| `Option.DownloadTimeout` | `30` | 下载超时时间,单位为秒。 | +| `Option.PatchEnabled` | `true` | 是否启用差分补丁处理。 | +| `Option.BackupEnabled` | `true` | 是否在更新前备份被替换文件。 | +| `Option.MaxConcurrency` | `3` | 多文件下载最大并发数。 | +| `Option.EnableResume` | `true` | 是否启用断点续传。 | +| `Option.RetryCount` | `3` | 下载失败重试次数。 | +| `Option.RetryInterval` | `1s` | 下载重试间隔。 | +| `Option.VerifyChecksum` | `true` | 是否校验文件 Hash。 | +| `Option.DiffMode` | `DiffMode.Serial` | 差分合并执行模式。 | +| `Option.Silent` | `false` | 是否启用静默轮询更新。 | +| `Option.SilentPollIntervalMinutes` | `60` | 静默模式检查更新间隔。 | +| `Option.LaunchClientAfterUpdate` | `true` | 升级后是否启动主程序。 | + +示例: ```csharp -using GeneralUpdate.Core; - 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) => - { - var version = args.Version as VersionInfo; - string status = args.IsComplated ? "✓ 成功" : "✗ 失败"; - Console.WriteLine($"版本 {version.Version} 下载{status}"); - }) - // 所有下载完成 - .AddListenerMultiAllDownloadCompleted((sender, args) => - { - if (args.IsAllDownloadCompleted) - { - Console.WriteLine("✓ 所有版本下载完成,开始安装..."); - } - else - { - Console.WriteLine($"✗ 下载失败,{args.FailedVersions.Count} 个版本失败:"); - foreach (var version in args.FailedVersions) - { - Console.WriteLine($" - {version}"); - } - } - }) - // 下载错误 - .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}"); - }) + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.PatchEnabled, true) + .SetOption(Option.BackupEnabled, true) + .SetOption(Option.VerifyChecksum, true) + .SetOption(Option.MaxConcurrency, 4) .LaunchAsync(); ``` -### 示例 4:自定义升级流程 +## 事件监听 -```csharp -using GeneralUpdate.Core; +Core 通过事件监听器暴露更新过程状态: + +| 方法 | 触发时机 | 典型用途 | +| --- | --- | --- | +| `AddListenerUpdateInfo` | 服务端返回版本信息后 | 展示更新日志、版本列表、更新大小。 | +| `AddListenerUpdatePrecheck` | 下载开始前 | 检查磁盘空间、网络环境、用户确认。 | +| `AddListenerMultiDownloadStatistics` | 下载过程中持续触发 | 展示速度、剩余时间、百分比。 | +| `AddListenerMultiDownloadCompleted` | 单个版本包下载完成 | 记录每个版本下载结果。 | +| `AddListenerMultiAllDownloadCompleted` | 全部下载任务完成 | 切换 UI 状态或写日志。 | +| `AddListenerMultiDownloadError` | 下载任务失败 | 输出失败版本和异常。 | +| `AddListenerProgress` | 通用更新进度变化 | 对接统一进度条。 | +| `AddListenerException` | Core 捕获到异常 | 写入日志、提示用户或上报。 | + +如果不想逐个注册事件,可以实现 `IUpdateEventListener` 后使用 `AddEventListener()` 批量注册。 -// 记录升级开始时间 -var startTime = DateTime.Now; -var downloadedVersions = new List(); +## 静默更新 +静默更新适合后台定期检查更新。启用后,`Client` 流程会启动后台轮询并立即返回,更新准备完成后在进程退出时继续升级。 + +```csharp await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadCompleted((sender, args) => - { - if (args.IsComplated) - { - var version = args.Version as VersionInfo; - downloadedVersions.Add(version.Version); - } - }) - .AddListenerMultiAllDownloadCompleted((sender, args) => - { - if (args.IsAllDownloadCompleted) - { - var duration = DateTime.Now - startTime; - Console.WriteLine($"升级完成!"); - Console.WriteLine($"总耗时: {duration.TotalSeconds:F2} 秒"); - Console.WriteLine($"已更新版本: {string.Join(", ", downloadedVersions)}"); - } - }) - .AddListenerException((sender, args) => - { - // 记录日志到文件 - File.AppendAllText("upgrade_error.log", - $"[{DateTime.Now}] {args.Exception}\n"); - }) + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 30) + .SetOption(Option.LaunchClientAfterUpdate, true) .LaunchAsync(); ``` ---- +静默更新仍然需要正确配置更新地址、应用密钥、主程序名称、升级程序名称和安装目录。 -## 注意事项与警告 +## 扩展点 -### ⚠️ 重要提示 +`GeneralUpdateBootstrap` 继承自 `AbstractBootstrap`,可以替换多个内部组件: -1. **进程隔离** - - Core 必须作为独立进程运行,不能在主程序中直接调用 - - 升级时主程序必须完全关闭,否则文件替换会失败 +| 方法 | 用途 | +| --- | --- | +| `Strategy()` | 指定自定义平台策略。 | +| `Hooks()` | 注入更新前、下载后、更新后、启动前、异常时的生命周期钩子。 | +| `UpdateReporter()` | 自定义更新状态上报。 | +| `SslPolicy()` | 自定义 HTTPS 证书校验策略。 | +| `UpdateAuth()` | 为 HTTP 请求添加认证信息。 | +| `DownloadSource()` | 自定义版本清单和文件来源。 | +| `DownloadPolicy()` | 自定义下载重试/超时策略。 | +| `DownloadExecutor()` | 自定义单文件下载实现。 | +| `DownloadPipeline()` | 自定义下载后处理,例如解密、杀毒、校验。 | +| `DownloadOrchestrator()` | 完全接管批量下载流程。 | -2. **参数传递** - - 客户端通过 Base64 编码的参数传递配置给 Core(原 ClientCore 现已合并到 Core) - - 确保参数传递过程中不会被截断或损坏 +高级项目可以只替换某一个环节,而不需要 fork Core。 -3. **文件权限** - - 在 Windows 上可能需要管理员权限替换系统目录中的文件 - - 在 Linux/macOS 上需要适当的文件系统权限 +## 与 GeneralUpdate.Tools 的关系 -4. **驱动升级** - - 驱动升级功能需要系统级权限 - - 建议在测试环境充分验证后再使用 +Core 消费的是更新服务端或 OSS 返回的版本清单和更新包。`GeneralUpdate.Tools` 用来帮助生成和验证这些输入: -5. **回滚机制** - - Core 不直接提供回滚功能,但保留了备份文件 - - Core 提供内置的备份与回滚机制,无需额外组件 +- 使用 Patch Package 生成差分更新包。 +- 使用 Extension Package 生成扩展包。 +- 使用 OSS Config 准备云存储分发配置。 +- 使用模拟、报告和 Hash 相关能力提前验证包结构与完整性。 -### 💡 最佳实践 +推荐流程是:先用 Tools 生成并校验更新包,再把包和版本清单发布到服务端或 OSS,最后由 Core 在客户端执行更新。 -- **日志记录**:实现完整的异常监听,记录升级过程中的所有问题 -- **超时设置**:根据网络环境合理设置下载超时时间 -- **进度反馈**:向用户显示升级进度,提升用户体验 -- **错误处理**:升级失败时提供清晰的错误信息和解决方案 -- **测试验证**:在各种网络条件下测试升级流程的稳定性 +## 常见问题 ---- +### 升级程序启动后没有执行更新 -## 适用平台 +确认它是否由主程序启动。如果直接双击独立升级程序,通常没有 IPC 上下文,`Upgrade` 流程无法知道安装目录和待更新版本。开发调试时可以先从主程序触发完整流程。 -| 产品 | 版本 | -| ------------------ | ----------------- | -| .NET | 5, 6, 7, 8, 9, 10 | -| .NET Framework | 4.6.1 | -| .NET Standard | 2.0 | -| .NET Core | 2.0 | +### 文件替换失败 ---- +通常是主程序或 Bowl 进程仍占用文件。确认主程序已退出,并正确配置 `Bowl` 进程名或相关关闭逻辑。 + +### 下载成功但校验失败 + +确认服务端或 OSS 上的包没有被重新压缩、截断或替换。若启用了 `Option.VerifyChecksum`,客户端收到的 Hash 必须和清单中的 Hash 一致。 + +### 差分包没有生效 + +确认服务端返回的是差分包清单,并且 `Option.PatchEnabled` 为 `true`。差分包建议通过 `GeneralUpdate.Tools` 生成,避免手工组织目录导致清单和包内容不一致。 -## 相关资源 +## 相关示例 -- **示例代码**:[查看 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) From d2b3f810bf1ddd28c4de9b617f290fd7749671b7 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 01:16:57 +0800 Subject: [PATCH 02/10] Expand GeneralUpdate.Core API documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 907 ++++++++++++++---- .../current/doc/GeneralUpdate.Core.md | 901 +++++++++++++---- .../current/doc/GeneralUpdate.Core.md | 907 ++++++++++++++---- 3 files changed, 2173 insertions(+), 542 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 391ea07..9467421 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -4,18 +4,7 @@ sidebar_position: 5 # GeneralUpdate.Core -## 组件定位 - -`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心。它负责把“检查版本、下载更新包、校验、解压/合并、替换文件、启动目标程序”等步骤串成一个完整流程,并通过 `GeneralUpdateBootstrap` 提供统一入口。 - -Core 既可以运行在主程序内执行 `Client` / `OssClient` 流程,也可以作为独立升级程序执行 `Upgrade` / `OssUpgrade` 流程。实际项目中最常见的部署方式是: - -1. 主程序负责启动更新检查。 -2. Core 在客户端流程中获取版本清单并下载更新包。 -3. Core 启动独立升级程序,升级程序关闭占用进程后完成文件替换。 -4. 升级完成后重新启动主程序。 - -> 固件升级不属于本页范围;驱动安装能力请参考后续 `GeneralUpdate.Drivelution` 文档。 +`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心,重点提供可编程的启动器、配置模型、事件模型、下载子系统扩展点、生命周期钩子、状态上报、差分管道和平台策略扩展。本页聚焦组件 API、属性和扩展方式;完整端到端上手流程会放到 cookbook 中。 **命名空间:** `GeneralUpdate.Core` **主要入口:** `GeneralUpdateBootstrap` @@ -25,261 +14,817 @@ Core 既可以运行在主程序内执行 `Client` / `OssClient` 流程,也可 dotnet add package GeneralUpdate.Core ``` -## 适用场景 +## 组件能力边界 -| 场景 | 是否适合使用 Core | -| --- | --- | -| 桌面应用自更新 | 适合。主程序检查更新,升级程序替换文件。 | -| 需要多版本连续升级 | 适合。Core 可以按服务端返回的版本序列处理多个包。 | -| 需要差分补丁更新 | 适合。配合差分包和 `Option.PatchEnabled` / `Option.DiffMode` 使用。 | -| 需要云存储分发更新包 | 适合。使用 `OssClient` / `OssUpgrade` 角色。 | -| 固件刷写 | 不适合。本页不覆盖固件升级组件。 | +Core 负责“执行更新”,不负责生成更新包,也不直接管理服务端后台。 -## 运行角色 +| 能力 | Core 是否负责 | 说明 | +| --- | --- | --- | +| 读取更新配置 | 是 | 通过 `UpdateRequest`、配置文件、`SetSource` 或 IPC 恢复运行参数。 | +| 检查服务端版本 | 是 | `Client` / `OssClient` 角色会读取版本清单并生成下载计划。 | +| 下载更新包 | 是 | 可替换下载来源、执行器、重试策略、后处理管道或完整编排器。 | +| 校验与应用补丁 | 是 | 支持 Hash 校验、压缩包处理、差分补丁管道。 | +| 文件替换与重启应用 | 是 | `Upgrade` / `OssUpgrade` 角色用于独立升级程序。 | +| 生成差分包 | 否 | 推荐使用 `GeneralUpdate.Tools`。 | +| 固件升级 | 否 | 固件升级组件不在本页范围内。 | + +## 入口类:GeneralUpdateBootstrap + +`GeneralUpdateBootstrap` 是 Core 的主要门面类。它继承 `AbstractBootstrap`,因此同时拥有自身方法和基类提供的扩展注册方法。 -Core 通过 `Option.AppType` 决定当前进程承担的更新角色: +```csharp +using GeneralUpdate.Core; -| AppType | 角色 | 说明 | +var bootstrap = new GeneralUpdateBootstrap(); +``` + +### 方法总览 + +| 方法 | 用途 | 常用场景 | | --- | --- | --- | -| `Client` | 主程序侧更新流程 | 检查服务端版本、下载包、准备升级上下文,并启动升级程序。默认值。 | -| `Upgrade` | 独立升级程序流程 | 读取主程序传入的 IPC 数据,执行文件替换,再启动主程序。 | -| `OssClient` | OSS 主程序侧流程 | 从 OSS 配置检查更新并启动 OSS 升级程序。 | -| `OssUpgrade` | OSS 升级程序流程 | 从 OSS 下载并部署更新包。 | +| `LaunchAsync()` | 按当前 `Option.AppType` 启动更新流程。 | 所有 Core 使用场景最终都会调用。 | +| `Cancel()` | 请求取消当前更新操作。 | UI 中提供“取消更新”按钮。 | +| `SetConfig(UpdateRequest)` | 使用强类型对象配置更新。 | 主程序内显式配置更新参数。 | +| `SetConfig(string)` | 从 JSON 文件读取 `UpdateRequest`。 | 将更新参数放到 `update_config.json` 或自定义配置文件。 | +| `SetSource(...)` | 只提供更新地址、密钥、报告地址等基础参数。 | 零配置/轻配置入口。 | +| `SetOption(Option, T)` | 设置运行时选项。 | 设置角色、超时、并发、差分、静默更新等。 | +| `UseDiffPipeline(Action)` | 自定义差分补丁管道。 | 替换 differ、调整并行度、接入补丁进度。 | +| `AddListenerUpdatePrecheck(Func)` | 下载前预检查。 | 检查磁盘空间、网络状态或弹窗确认。 | +| `AddListener...` | 注册单个事件回调。 | 更新 UI、写日志、上报监控。 | +| `AddEventListener()` | 批量注册事件监听器。 | 将事件处理封装成类。 | + +### LaunchAsync -升级程序通常不需要手动调用 `SetConfig`。当它由主程序启动时,Core 会通过加密文件 IPC 自动读取 `ProcessContract`,恢复安装路径、版本、临时目录、下载配置、更新包列表等上下文。 +```csharp +public Task LaunchAsync() +``` -## 最小升级程序 +`LaunchAsync` 会读取 `Option.AppType` 并选择对应策略: -`GeneralUpdate-Samples/src/Upgrade/Program.cs` 展示了独立升级程序的最小形态。该程序只需要注册必要事件,然后调用 `LaunchAsync()`: +| `Option.AppType` | 策略 | 说明 | +| --- | --- | --- | +| `AppType.Client` | `ClientStrategy` | 主程序侧:检查版本、下载包、准备升级上下文、启动升级程序。 | +| `AppType.Upgrade` | `UpdateStrategy` | 升级程序侧:读取 IPC 上下文并执行文件替换。 | +| `AppType.OssClient` | `OssStrategy` | OSS 主程序侧更新流程。 | +| `AppType.OssUpgrade` | `OssStrategy` | OSS 升级程序侧更新流程。 | + +示例:独立升级程序入口。 ```csharp -using GeneralUpdate.Common.Download; -using GeneralUpdate.Common.Internal; -using GeneralUpdate.Common.Shared.Object; -using GeneralUpdate.Core; +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.Upgrade) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .LaunchAsync(); +``` -try -{ - Console.WriteLine($"Updater started at {DateTime.Now}"); - - _ = await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadStatistics(OnMultiDownloadStatistics) - .AddListenerMultiDownloadCompleted(OnMultiDownloadCompleted) - .AddListenerMultiAllDownloadCompleted(OnMultiAllDownloadCompleted) - .AddListenerMultiDownloadError(OnMultiDownloadError) - .AddListenerException(OnException) - .LaunchAsync(); -} -catch (Exception ex) -{ - Console.WriteLine(ex); -} +> 当升级程序由主程序启动时,Core 会通过加密文件 IPC 自动恢复更新上下文,通常不需要在升级程序里再次调用 `SetConfig`。 -void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs args) -{ - var version = args.Version as VersionInfo; - Console.WriteLine( - $"Version: {version?.Version}, Speed: {args.Speed}, Progress: {args.ProgressPercentage}%"); -} +### Cancel -void OnMultiDownloadCompleted(object sender, MultiDownloadCompletedEventArgs args) +```csharp +public void Cancel() +``` + +`Cancel` 会触发内部 `CancellationTokenSource`,更新策略会在安全检查点观察取消请求。适合 UI 应用把 bootstrap 保存为字段后绑定取消按钮。 + +```csharp +private GeneralUpdateBootstrap? _bootstrap; + +async Task StartUpdateAsync(UpdateRequest request) { - var version = args.Version as VersionInfo; - Console.WriteLine(args.IsComplated - ? $"Version {version?.Version} download completed." - : $"Version {version?.Version} download failed."); + _bootstrap = new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)); + + await _bootstrap.LaunchAsync(); } -void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs args) +void CancelUpdate() { - Console.WriteLine(args.IsAllDownloadCompleted - ? "All download tasks completed." - : $"Download failed. Failed versions: {args.FailedVersions.Count}"); + _bootstrap?.Cancel(); } +``` + +### SetConfig(UpdateRequest) + +```csharp +public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) +``` + +`SetConfig(UpdateRequest)` 会调用 `UpdateRequest.Validate()`,并把外部配置映射为内部 `UpdateContext`。当角色不是 `AppType.Upgrade` 时,它还会初始化临时目录和黑名单匹配器。 + +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; -void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs args) +var request = new UpdateRequest { - var version = args.Version as VersionInfo; - Console.WriteLine($"Version {version?.Version} download error: {args.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 sender, ExceptionEventArgs args) +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(args.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 的 `Client` 流程,可以显式传入 `UpdateRequest`: +### SetSource ```csharp -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; +public GeneralUpdateBootstrap SetSource( + string updateUrl, + string appSecretKey, + string? reportUrl = null, + string? scheme = null, + string? token = null) +``` + +`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。 +```csharp await new GeneralUpdateBootstrap() - .SetConfig(new UpdateRequest + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret", + reportUrl: "https://update.example.com/api/upgrade/report", + scheme: "Bearer", + token: "access-token") + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +### UseDiffPipeline + +```csharp +public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) +``` + +`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,Core 会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。 + +```csharp +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Differential.Differ; + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UseDiffPipeline(builder => { - 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" + 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(); +``` + +## 配置模型: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` | 更新时跳过的目录。 | +| `DriverDirectory` | 驱动目录;驱动安装属于 Drivelution 文档范围。 | + +### 使用 UpdateRequestBuilder + +`UpdateRequestBuilder` 提供链式构建 API,并在 `Build()` 时执行校验。 + +```csharp +using GeneralUpdate.Core.Configuration; + +var request = new UpdateRequestBuilder() + .SetUpdateUrl("https://update.example.com/api/upgrade/verification") + .SetReportUrl("https://update.example.com/api/upgrade/report") + .SetUpgradeAppName("UpgradeSample.exe") + .SetMainAppName("ClientSample.exe") + .SetClientVersion("1.0.0") + .SetAppSecretKey("your-app-secret") + .SetProductId("your-product-id") + .SetInstallPath(AppDomain.CurrentDomain.BaseDirectory) + .SetFiles(new List { "appsettings.json" }) + .SetFormats(new List { ".log", ".tmp" }) + .SetDirectories(new List { "logs" }) + .Build(); +``` + +`UpdateRequestBuilder.Create()` 会尝试从应用运行目录的 `update_config.json` 读取配置。如果文件不存在,会抛出 `FileNotFoundException`。 + +```csharp +var request = UpdateRequestBuilder.Create().Build(); +``` + +## 运行选项:Option + +Core 使用强类型 `Option` 注册运行时选项,并通过 `SetOption` 设置值。 + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.DownloadTimeout, 60) - .SetOption(Option.MaxConcurrency, 3) - .AddListenerUpdateInfo((sender, args) => + .SetOption(Option.MaxConcurrency, 4) + .SetOption(Option.VerifyChecksum, true) + .LaunchAsync(); +``` + +| 选项 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `Option.AppType` | `AppType` | `Client` | 当前进程角色。 | +| `Option.DiffMode` | `DiffMode` | `Serial` | 差分执行模式。 | +| `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 + +事件适合观察更新过程,不应该承载复杂业务流程。复杂流程建议封装成 `IUpdateHooks` 或 cookbook 中的完整方案。 + +### 单个事件回调 + +| 方法 | 参数类型 | 触发时机 | +| --- | --- | --- | +| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` | 服务端版本信息返回后。 | +| `AddListenerUpdatePrecheck` | `Func` | 下载开始前,返回 `true` 继续,返回 `false` 中止。 | +| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` | 下载过程中持续触发。 | +| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` | 单个版本下载结束。 | +| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` | 所有下载任务结束。 | +| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` | 下载失败。 | +| `AddListenerProgress` | `ProgressEventArgs` | 下载进度或差分补丁进度变化。 | +| `AddListenerException` | `ExceptionEventArgs` | Core 捕获异常。 | + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerUpdateInfo((_, e) => { - Console.WriteLine($"Server returned {args.Info.Body?.Count ?? 0} update versions."); + Console.WriteLine($"Versions from server: {e.Info?.Body?.Count ?? 0}"); }) - .AddListenerException((sender, args) => + .AddListenerUpdatePrecheck(e => { - Console.WriteLine(args.Exception); + var hasUpdate = (e.Info?.Body?.Count ?? 0) > 0; + var enoughDisk = DriveInfo.GetDrives() + .Where(d => d.IsReady) + .Any(d => d.AvailableFreeSpace > 1024L * 1024 * 1024); + + return hasUpdate && enoughDisk; + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + Console.WriteLine($"{e.ProgressPercentage}% {e.Speed} {e.BytesReceived}/{e.TotalBytesToReceive}"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + Console.WriteLine(e.IsCompleted ? "Download completed." : "Download failed."); + }) + .AddListenerProgress((_, e) => + { + if (e.Progress != null) + Console.WriteLine($"Download: {e.Progress.Percentage}%"); + + if (e.DiffProgress != null) + Console.WriteLine($"Patch: {e.DiffProgress.Completed}/{e.DiffProgress.Total}"); + }) + .AddListenerException((_, e) => + { + Console.WriteLine(e.Message); + Console.WriteLine(e.Exception); }) .LaunchAsync(); ``` -也可以使用 `SetSource` 走简化配置路径: +### 批量事件监听器 + +实现 `IUpdateEventListener` 可以把事件处理集中到一个类。若只关心部分事件,继承 `UpdateEventListenerBase` 更简单。 ```csharp +using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Event; + +public sealed class ConsoleUpdateListener : UpdateEventListenerBase +{ + public override void OnUpdateInfo(UpdateInfoEventArgs args) + { + Console.WriteLine($"Update count: {args.Info?.Body?.Count ?? 0}"); + } + + public override void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) + { + Console.WriteLine($"{args.ProgressPercentage}% {args.Speed}"); + } + + public override void OnException(ExceptionEventArgs args) + { + Console.WriteLine(args.Exception); + } +} + 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) + .SetConfig(request) + .AddEventListener() .LaunchAsync(); ``` -## 核心执行流程 +## 扩展点总览 -1. `SetConfig` / `SetSource` 载入更新地址、应用名称、版本号、密钥、安装目录等配置。 -2. `LaunchAsync` 根据 `Option.AppType` 选择 `ClientStrategy`、`UpdateStrategy` 或 OSS 策略。 -3. Client 流程向服务端请求版本信息,并触发 `AddListenerUpdateInfo` / `AddListenerUpdatePrecheck`。 -4. Core 下载需要更新的版本包,并通过下载事件持续回调速度、进度和错误。 -5. 下载完成后执行校验、解压、差分合并和文件替换等管道步骤。 -6. Upgrade 流程完成后根据 `Option.LaunchClientAfterUpdate` 决定是否启动主程序。 -7. 如配置了 `ReportUrl` 或自定义 `UpdateReporter`,Core 会上报更新状态。 +扩展点由 `AbstractBootstrap` 提供,所有注册方法都返回当前 bootstrap,可链式调用。 -## 常用配置项 +| 注册方法 | 接口 | 影响范围 | +| --- | --- | --- | +| `Hooks()` | `IUpdateHooks` | 更新生命周期前后置逻辑。 | +| `UpdateReporter()` | `IUpdateReporter` | 更新状态上报。 | +| `SslPolicy()` | `ISslValidationPolicy` | HTTPS 证书校验。 | +| `UpdateAuth()` | `IHttpAuthProvider` | HTTP 请求认证。 | +| `DownloadSource()` | `IDownloadSource` | 版本清单和下载资源来源。 | +| `DownloadPolicy()` | `IDownloadPolicy` | 下载重试、超时、熔断等策略。 | +| `DownloadExecutor()` | `IDownloadExecutor` | 单文件下载实现。 | +| `DownloadPipeline()` | `IDownloadPipeline` | 下载后处理,例如校验、解密、扫描。 | +| `DownloadOrchestrator()` | `IDownloadOrchestrator` | 批量下载完整编排。 | +| `Strategy()` | `IStrategy` | 自定义平台级更新策略。 | -使用 `SetOption(Option.Xxx, value)` 设置运行时选项: +> 通过这些方法注册的类型必须有无参构造函数,因为 Core 使用 `new()` 或反射创建实例。需要复杂依赖时,建议在自定义类型内部读取配置,或在应用层封装一个无参适配器。 -| 选项 | 默认值 | 说明 | -| --- | --- | --- | -| `Option.AppType` | `AppType.Client` | 当前进程角色。 | -| `Option.Encoding` | `Encoding.UTF8` | 压缩包文件名/内容处理编码。 | -| `Option.Format` | `Format.Zip` | 更新包压缩格式。 | -| `Option.DownloadTimeout` | `30` | 下载超时时间,单位为秒。 | -| `Option.PatchEnabled` | `true` | 是否启用差分补丁处理。 | -| `Option.BackupEnabled` | `true` | 是否在更新前备份被替换文件。 | -| `Option.MaxConcurrency` | `3` | 多文件下载最大并发数。 | -| `Option.EnableResume` | `true` | 是否启用断点续传。 | -| `Option.RetryCount` | `3` | 下载失败重试次数。 | -| `Option.RetryInterval` | `1s` | 下载重试间隔。 | -| `Option.VerifyChecksum` | `true` | 是否校验文件 Hash。 | -| `Option.DiffMode` | `DiffMode.Serial` | 差分合并执行模式。 | -| `Option.Silent` | `false` | 是否启用静默轮询更新。 | -| `Option.SilentPollIntervalMinutes` | `60` | 静默模式检查更新间隔。 | -| `Option.LaunchClientAfterUpdate` | `true` | 升级后是否启动主程序。 | - -示例: +## 生命周期钩子:IUpdateHooks + +`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。 ```csharp +using GeneralUpdate.Core.Hooks; + +public sealed class ProductUpdateHooks : IUpdateHooks +{ + public Task OnBeforeUpdateAsync(HookContext ctx) + { + Console.WriteLine($"Before update: {ctx.CurrentVersion} -> {ctx.TargetVersion}"); + return Task.FromResult(true); + } + + public Task OnDownloadCompletedAsync(DownloadContext ctx) + { + Console.WriteLine($"Downloaded {ctx.AssetName}, success={ctx.Success}, path={ctx.LocalPath}"); + return Task.CompletedTask; + } + + public Task OnAfterUpdateAsync(HookContext ctx) + { + File.WriteAllText(Path.Combine(ctx.InstallPath, "last-update.txt"), DateTimeOffset.Now.ToString("O")); + return Task.CompletedTask; + } + + public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) + { + File.AppendAllText(Path.Combine(ctx.InstallPath, "update-error.log"), ex + Environment.NewLine); + return Task.CompletedTask; + } + + public Task OnBeforeStartAppAsync(HookContext ctx) + { + Console.WriteLine($"Starting app from {ctx.InstallPath}"); + return Task.CompletedTask; + } +} + await new GeneralUpdateBootstrap() .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.PatchEnabled, true) - .SetOption(Option.BackupEnabled, true) - .SetOption(Option.VerifyChecksum, true) - .SetOption(Option.MaxConcurrency, 4) + .Hooks() + .LaunchAsync(); +``` + +内置实现包括: + +| 类型 | 说明 | +| --- | --- | +| `NoOpUpdateHooks` | 默认空实现。 | +| `UnixPermissionHooks` | 在 Unix-like 系统启动前执行 `chmod +x`。 | +| `CustomPermissionHooks` | 执行自定义权限脚本;该类型构造函数需要参数,不适合直接用 `Hooks()` 注册,可自行包装无参适配器。 | + +## 状态上报:IUpdateReporter + +`IUpdateReporter` 用于把更新状态上报到服务端。 + +```csharp +using GeneralUpdate.Core.Download.Reporting; + +public sealed class ConsoleUpdateReporter : IUpdateReporter +{ + public Task ReportAsync(UpdateReport report, CancellationToken token = default) + { + Console.WriteLine($"Report: record={report.RecordId}, status={report.Status}, type={report.Type}"); + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateReporter() .LaunchAsync(); ``` -## 事件监听 +内置 `HttpUpdateReporter` 会向 `ReportUrl` 发送 JSON: + +```json +{ + "recordId": 123, + "status": 1, + "type": 1 +} +``` -Core 通过事件监听器暴露更新过程状态: +状态值: -| 方法 | 触发时机 | 典型用途 | +| 枚举 | 值 | 说明 | | --- | --- | --- | -| `AddListenerUpdateInfo` | 服务端返回版本信息后 | 展示更新日志、版本列表、更新大小。 | -| `AddListenerUpdatePrecheck` | 下载开始前 | 检查磁盘空间、网络环境、用户确认。 | -| `AddListenerMultiDownloadStatistics` | 下载过程中持续触发 | 展示速度、剩余时间、百分比。 | -| `AddListenerMultiDownloadCompleted` | 单个版本包下载完成 | 记录每个版本下载结果。 | -| `AddListenerMultiAllDownloadCompleted` | 全部下载任务完成 | 切换 UI 状态或写日志。 | -| `AddListenerMultiDownloadError` | 下载任务失败 | 输出失败版本和异常。 | -| `AddListenerProgress` | 通用更新进度变化 | 对接统一进度条。 | -| `AddListenerException` | Core 捕获到异常 | 写入日志、提示用户或上报。 | +| `UpdateStatus.Updating` | `1` | 更新中。 | +| `UpdateStatus.Success` | `2` | 更新成功。 | +| `UpdateStatus.Failure` | `3` | 更新失败。 | -如果不想逐个注册事件,可以实现 `IUpdateEventListener` 后使用 `AddEventListener()` 批量注册。 +## HTTP 认证:IHttpAuthProvider -## 静默更新 +`IHttpAuthProvider` 可以为 Core 发出的 HTTP 请求追加认证头。 -静默更新适合后台定期检查更新。启用后,`Client` 流程会启动后台轮询并立即返回,更新准备完成后在进程退出时继续升级。 +```csharp +using GeneralUpdate.Core.Security; + +public sealed class StaticBearerAuthProvider : IHttpAuthProvider +{ + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) + { + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "access-token"); + + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateAuth() + .LaunchAsync(); +``` + +Core 内置的认证类型包括 `NoOpAuthProvider`、`BearerTokenAuthProvider`、`ApiKeyAuthProvider` 和 `HmacAuthProvider`。这些类型中部分构造函数需要参数,因此如果要通过 `UpdateAuth()` 注册,通常需要写一个无参包装类。 + +## HTTPS 证书策略:ISslValidationPolicy + +`ISslValidationPolicy` 用于控制 HTTPS 证书校验。默认 `StrictSslValidationPolicy` 只接受没有 SSL policy errors 的证书。 ```csharp +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using GeneralUpdate.Core.Security; + +public sealed class DevelopmentSslPolicy : ISslValidationPolicy +{ + public bool ValidateCertificate( + X509Certificate2? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) + { + return sslPolicyErrors == SslPolicyErrors.None + || certificate?.Issuer.Contains("CN=Local Dev Root") == true; + } +} + await new GeneralUpdateBootstrap() .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.Silent, true) - .SetOption(Option.SilentPollIntervalMinutes, 30) - .SetOption(Option.LaunchClientAfterUpdate, true) + .SslPolicy() .LaunchAsync(); ``` -静默更新仍然需要正确配置更新地址、应用密钥、主程序名称、升级程序名称和安装目录。 +生产环境不建议无条件返回 `true`,否则会绕过 HTTPS 的安全保证。 -## 扩展点 +## 下载来源:IDownloadSource -`GeneralUpdateBootstrap` 继承自 `AbstractBootstrap`,可以替换多个内部组件: +`IDownloadSource` 负责返回待下载资源列表。适合接入私有服务、文件服务器、配置中心或自定义云存储。 -| 方法 | 用途 | -| --- | --- | -| `Strategy()` | 指定自定义平台策略。 | -| `Hooks()` | 注入更新前、下载后、更新后、启动前、异常时的生命周期钩子。 | -| `UpdateReporter()` | 自定义更新状态上报。 | -| `SslPolicy()` | 自定义 HTTPS 证书校验策略。 | -| `UpdateAuth()` | 为 HTTP 请求添加认证信息。 | -| `DownloadSource()` | 自定义版本清单和文件来源。 | -| `DownloadPolicy()` | 自定义下载重试/超时策略。 | -| `DownloadExecutor()` | 自定义单文件下载实现。 | -| `DownloadPipeline()` | 自定义下载后处理,例如解密、杀毒、校验。 | -| `DownloadOrchestrator()` | 完全接管批量下载流程。 | - -高级项目可以只替换某一个环节,而不需要 fork Core。 +```csharp +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; -## 与 GeneralUpdate.Tools 的关系 +public sealed class StaticDownloadSource : IDownloadSource +{ + public Task ListAsync(CancellationToken token = default) + { + var assets = new[] + { + new DownloadAsset( + Name: "app-1.0.1.zip", + Url: "https://cdn.example.com/releases/app-1.0.1.zip", + Size: 25_000_000, + SHA256: "expected-sha256", + Version: "1.0.1") + }; + + return Task.FromResult(new DownloadSourceResult + { + Assets = assets, + HasMainUpdate = true, + HasUpgradeUpdate = false + }); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadSource() + .LaunchAsync(); +``` + +## 下载重试策略:IDownloadPolicy + +`IDownloadPolicy` 包装单次下载动作,适合实现重试、超时、熔断或限流。 + +```csharp +using GeneralUpdate.Core.Download.Abstractions; + +public sealed class TwoAttemptDownloadPolicy : IDownloadPolicy +{ + public async Task ExecuteAsync( + Func> action, + CancellationToken token = default) + { + try + { + return await action(token); + } + catch when (!token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(2), token); + return await action(token); + } + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadPolicy() + .LaunchAsync(); +``` + +如果同时注册 `DownloadOrchestrator()`,自定义 orchestrator 会接管完整下载流程,`DownloadPolicy()` 是否生效取决于 orchestrator 自己是否使用该策略。 + +## 单文件下载:IDownloadExecutor + +`IDownloadExecutor` 负责把一个 `DownloadAsset` 下载到目标路径。适合支持 FTP、SFTP、私有协议或自定义 HTTP 客户端。 + +```csharp +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; + +public sealed class MirrorDownloadExecutor : IDownloadExecutor +{ + private readonly HttpClient _client = new(); + + public async Task ExecuteAsync( + DownloadAsset asset, + string destPath, + IProgress? progress = null, + CancellationToken token = default) + { + var started = DateTimeOffset.Now; + await using var input = await _client.GetStreamAsync(asset.Url, token); + await using var output = File.Create(destPath); + await input.CopyToAsync(output, token); + + var fileInfo = new FileInfo(destPath); + return new DownloadResult(asset, destPath, fileInfo.Length, DateTimeOffset.Now - started, 0, true, null); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadExecutor() + .LaunchAsync(); +``` -Core 消费的是更新服务端或 OSS 返回的版本清单和更新包。`GeneralUpdate.Tools` 用来帮助生成和验证这些输入: +## 下载后处理:IDownloadPipeline -- 使用 Patch Package 生成差分更新包。 -- 使用 Extension Package 生成扩展包。 -- 使用 OSS Config 准备云存储分发配置。 -- 使用模拟、报告和 Hash 相关能力提前验证包结构与完整性。 +`IDownloadPipeline` 在文件下载完成后运行。适合做 Hash 校验、解密、病毒扫描、格式转换等。 -推荐流程是:先用 Tools 生成并校验更新包,再把包和版本清单发布到服务端或 OSS,最后由 Core 在客户端执行更新。 +```csharp +using GeneralUpdate.Core.Download.Abstractions; -## 常见问题 +public sealed class AntivirusPipeline : IDownloadPipeline +{ + public Task ProcessAsync(string downloadedPath, CancellationToken token = default) + { + if (!File.Exists(downloadedPath)) + throw new FileNotFoundException("Downloaded file not found.", downloadedPath); -### 升级程序启动后没有执行更新 + Console.WriteLine($"Scanning {downloadedPath}"); + return Task.FromResult(downloadedPath); + } +} -确认它是否由主程序启动。如果直接双击独立升级程序,通常没有 IPC 上下文,`Upgrade` 流程无法知道安装目录和待更新版本。开发调试时可以先从主程序触发完整流程。 +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadPipeline() + .LaunchAsync(); +``` -### 文件替换失败 +Core 在创建下载管道时会优先尝试使用 `string` 构造函数传入期望 Hash;如果没有该构造函数,则使用无参构造函数。 -通常是主程序或 Bowl 进程仍占用文件。确认主程序已退出,并正确配置 `Bowl` 进程名或相关关闭逻辑。 +## 批量下载编排:IDownloadOrchestrator -### 下载成功但校验失败 +`IDownloadOrchestrator` 是下载子系统的最高层扩展点。注册后,它会接管批量下载、并发控制、重试、进度和结果汇总。 -确认服务端或 OSS 上的包没有被重新压缩、截断或替换。若启用了 `Option.VerifyChecksum`,客户端收到的 Hash 必须和清单中的 Hash 一致。 +```csharp +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Executors; +using GeneralUpdate.Core.Download.Models; + +public sealed class SerialDownloadOrchestrator : IDownloadOrchestrator +{ + private readonly IDownloadExecutor _executor = new HttpDownloadExecutor(new HttpClient()); + + public async Task ExecuteAsync( + DownloadPlan plan, + string destDir, + int maxConcurrency = 3, + IProgress? progress = null, + CancellationToken token = default) + { + var results = new List(); + var started = DateTimeOffset.Now; + + foreach (var asset in plan.Assets) + { + var destPath = Path.Combine(destDir, asset.Name); + results.Add(await _executor.ExecuteAsync(asset, destPath, progress, token)); + } + + return new DownloadReport( + results, + results.Where(r => r.Success).Sum(r => r.DownloadedBytes), + DateTimeOffset.Now - started, + results.Count(r => r.Success), + results.Count(r => !r.Success)); + } +} -### 差分包没有生效 +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadOrchestrator() + .LaunchAsync(); +``` -确认服务端返回的是差分包清单,并且 `Option.PatchEnabled` 为 `true`。差分包建议通过 `GeneralUpdate.Tools` 生成,避免手工组织目录导致清单和包内容不一致。 +只有当你需要完整替换下载行为时才建议实现 orchestrator。多数情况下替换 `IDownloadExecutor`、`IDownloadPolicy` 或 `IDownloadPipeline` 就够了。 + +## 平台策略:IStrategy + +`IStrategy` 是最高级别的更新策略接口。Core 内置 `ClientStrategy`、`UpdateStrategy`、`OssStrategy` 以及 Windows/Linux/macOS 平台策略。只有在你需要替换平台级文件操作或启动逻辑时,才应实现它。 + +```csharp +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Strategy; + +public sealed class LoggingStrategy : IStrategy +{ + private UpdateContext? _context; + + public IUpdateHooks Hooks { get; set; } = new NoOpUpdateHooks(); + public IUpdateReporter Reporter { get; set; } = new HttpUpdateReporter(); + + public void Create(UpdateContext parameter) + { + _context = parameter; + } + + 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(); +``` + +## 静默更新选项 + +静默更新通过选项启用,不需要额外接口。启用后,`Client` 角色会启动后台轮询并立即返回。 + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 30) + .SetOption(Option.LaunchClientAfterUpdate, true) + .LaunchAsync(); +``` + +适合把“是否启用、何时提示用户、如何处理退出时升级”等完整策略写到 cookbook,而组件文档只需要说明相关 API。 + +## 与 GeneralUpdate.Tools 的关系 + +Core 消费更新清单和更新包;`GeneralUpdate.Tools` 负责辅助生成和验证这些产物。 + +| Tools 能力 | Core 中对应消费点 | +| --- | --- | +| Patch Package | `Option.PatchEnabled`、`UseDiffPipeline`、差分补丁处理。 | +| Extension Package | 作为更新包内容或扩展包分发,由下载和部署流程消费。 | +| OSS Config | `OssClient` / `OssUpgrade` 角色读取 OSS 配置并下载。 | +| Hash / Simulation / Report | 对应 `Option.VerifyChecksum`、下载后校验和状态上报。 | ## 相关示例 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 6778d6a..1aeea45 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,18 +4,7 @@ sidebar_position: 5 # GeneralUpdate.Core -## Component role - -`GeneralUpdate.Core` is the execution core of GeneralUpdate. It connects update checking, package download, checksum verification, extraction or patch merge, file replacement, process restart, and reporting through a single entry point: `GeneralUpdateBootstrap`. - -Core can run inside the main application for the `Client` / `OssClient` workflow, or inside an independent updater process for the `Upgrade` / `OssUpgrade` workflow. A typical desktop deployment is: - -1. The main application starts the update check. -2. Core obtains the server manifest and downloads update packages. -3. Core starts an independent updater process after the main application is ready to exit. -4. The updater process replaces files and restarts the main application. - -> Firmware update is outside the scope of this page. Driver installation belongs to `GeneralUpdate.Drivelution` and is documented separately. +`GeneralUpdate.Core` is the update execution core of GeneralUpdate. It provides the programmable bootstrapper, configuration model, event model, download extension points, lifecycle hooks, reporting, differential pipeline, and platform strategy extension model. This page focuses on component APIs and extension examples; end-to-end workflows belong in the cookbook. **Namespace:** `GeneralUpdate.Core` **Primary entry point:** `GeneralUpdateBootstrap` @@ -25,261 +14,813 @@ Core can run inside the main application for the `Client` / `OssClient` workflow dotnet add package GeneralUpdate.Core ``` -## When to use Core +## Responsibility boundary -| Scenario | Fit | -| --- | --- | -| Desktop application self-update | Yes. Use the main app to check updates and the updater process to replace files. | -| Multi-version sequential update | Yes. Core can process the version sequence returned by the server. | -| Differential package update | Yes. Use differential packages with `Option.PatchEnabled` / `Option.DiffMode`. | -| OSS or cloud-storage distribution | Yes. Use `OssClient` / `OssUpgrade`. | -| Firmware flashing | No. This page does not cover firmware update components. | +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`. | +| Firmware update | No | Firmware components are out of scope for this page. | + +## Entry point: GeneralUpdateBootstrap + +`GeneralUpdateBootstrap` is Core's main facade. It inherits `AbstractBootstrap`, so it includes both its own methods and the extension registration methods from the base class. + +```csharp +using GeneralUpdate.Core; -## Runtime roles +var bootstrap = new GeneralUpdateBootstrap(); +``` -Core uses `Option.AppType` to decide the process role: +### Method overview -| AppType | Role | Description | +| Method | Purpose | Typical use | | --- | --- | --- | -| `Client` | Main application workflow | Checks versions, downloads packages, prepares upgrade context, and starts the updater. Default value. | -| `Upgrade` | Independent updater workflow | Reads IPC data from the main app, replaces files, and starts the main app. | -| `OssClient` | OSS client workflow | Checks OSS update configuration and starts the OSS updater. | -| `OssUpgrade` | OSS updater workflow | Downloads from OSS and deploys packages. | +| `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 -The updater process usually does not call `SetConfig` manually. When it is launched by the main application, Core reads the encrypted file IPC contract and restores paths, versions, temporary directories, download settings, and package lists automatically. +```csharp +public Task LaunchAsync() +``` -## Minimal updater process +`LaunchAsync` reads `Option.AppType` and selects a role strategy: -`GeneralUpdate-Samples/src/Upgrade/Program.cs` shows the minimal independent updater shape. Register the events you care about and call `LaunchAsync()`: +| `Option.AppType` | Strategy | Description | +| --- | --- | --- | +| `AppType.Client` | `ClientStrategy` | Main-app side: check version, download packages, prepare context, launch updater. | +| `AppType.Upgrade` | `UpdateStrategy` | Updater side: read IPC context and replace files. | +| `AppType.OssClient` | `OssStrategy` | OSS main-app update role. | +| `AppType.OssUpgrade` | `OssStrategy` | OSS updater role. | ```csharp -using GeneralUpdate.Common.Download; -using GeneralUpdate.Common.Internal; -using GeneralUpdate.Common.Shared.Object; -using GeneralUpdate.Core; +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.Upgrade) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .LaunchAsync(); +``` -try -{ - Console.WriteLine($"Updater started at {DateTime.Now}"); - - _ = await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadStatistics(OnMultiDownloadStatistics) - .AddListenerMultiDownloadCompleted(OnMultiDownloadCompleted) - .AddListenerMultiAllDownloadCompleted(OnMultiAllDownloadCompleted) - .AddListenerMultiDownloadError(OnMultiDownloadError) - .AddListenerException(OnException) - .LaunchAsync(); -} -catch (Exception ex) -{ - Console.WriteLine(ex); -} +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. -void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs args) -{ - var version = args.Version as VersionInfo; - Console.WriteLine( - $"Version: {version?.Version}, Speed: {args.Speed}, Progress: {args.ProgressPercentage}%"); -} +### Cancel -void OnMultiDownloadCompleted(object sender, MultiDownloadCompletedEventArgs args) -{ - var version = args.Version as VersionInfo; - Console.WriteLine(args.IsComplated - ? $"Version {version?.Version} download completed." - : $"Version {version?.Version} download failed."); -} +```csharp +public void Cancel() +``` -void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs args) -{ - Console.WriteLine(args.IsAllDownloadCompleted - ? "All download tasks completed." - : $"Download failed. Failed versions: {args.FailedVersions.Count}"); -} +`Cancel` signals the internal `CancellationTokenSource`. Strategies observe the token at safe checkpoints. -void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs args) +```csharp +private GeneralUpdateBootstrap? _bootstrap; + +async Task StartUpdateAsync(UpdateRequest request) { - var version = args.Version as VersionInfo; - Console.WriteLine($"Version {version?.Version} download error: {args.Exception}"); + _bootstrap = new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)); + + await _bootstrap.LaunchAsync(); } -void OnException(object sender, ExceptionEventArgs args) +void CancelUpdate() { - Console.WriteLine(args.Exception); + _bootstrap?.Cancel(); } ``` -## Main application configuration +### SetConfig(UpdateRequest) -When using Core directly from the main application, pass an `UpdateRequest` explicitly: +```csharp +public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) +``` + +`SetConfig(UpdateRequest)` validates the request and maps it into the internal `UpdateContext`. For non-`Upgrade` roles it also initializes the temporary update directory and blacklist matcher. ```csharp using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; +var request = new UpdateRequest +{ + UpdateUrl = "https://update.example.com/api/upgrade/verification", + ReportUrl = "https://update.example.com/api/upgrade/report", + UpdateAppName = "UpgradeSample.exe", + MainAppName = "ClientSample.exe", + InstallPath = AppDomain.CurrentDomain.BaseDirectory, + ClientVersion = "1.0.0", + AppSecretKey = "your-app-secret", + ProductId = "your-product-id", + Files = new List { "appsettings.json" }, + Formats = new List { ".log", ".tmp" }, + Directories = new List { "logs", "cache" } +}; + await new GeneralUpdateBootstrap() - .SetConfig(new UpdateRequest - { - UpdateUrl = "https://update.example.com/api/upgrade/verification", - ReportUrl = "https://update.example.com/api/upgrade/report", - UpdateAppName = "UpgradeSample.exe", - MainAppName = "ClientSample.exe", - InstallPath = AppDomain.CurrentDomain.BaseDirectory, - ClientVersion = "1.0.0", - AppSecretKey = "your-app-secret", - ProductId = "your-product-id" - }) + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +### SetConfig(string) + +```csharp +public GeneralUpdateBootstrap SetConfig(string filePath) +``` + +`SetConfig(string)` reads a UTF-8 JSON `UpdateRequest`. A bare filename is resolved from the app base directory; relative and absolute paths are used as provided. + +```json +{ + "updateUrl": "https://update.example.com/api/upgrade/verification", + "reportUrl": "https://update.example.com/api/upgrade/report", + "updateAppName": "UpgradeSample.exe", + "mainAppName": "ClientSample.exe", + "installPath": "C:\\Program Files\\MyApp", + "clientVersion": "1.0.0", + "appSecretKey": "your-app-secret", + "productId": "your-product-id" +} +``` + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig("update_config.json") .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.DownloadTimeout, 60) - .SetOption(Option.MaxConcurrency, 3) - .AddListenerUpdateInfo((sender, args) => - { - Console.WriteLine($"Server returned {args.Info.Body?.Count ?? 0} update versions."); - }) - .AddListenerException((sender, args) => - { - Console.WriteLine(args.Exception); - }) .LaunchAsync(); ``` -For a simplified setup, use `SetSource`: +### SetSource + +```csharp +public GeneralUpdateBootstrap SetSource( + string updateUrl, + string appSecretKey, + string? reportUrl = null, + string? scheme = null, + string? token = null) +``` + +`SetSource` is a lightweight entry point when identity metadata is discovered elsewhere, such as from `generalupdate.manifest.json`. ```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") + reportUrl: "https://update.example.com/api/upgrade/report", + scheme: "Bearer", + token: "access-token") .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -## Execution flow +### UseDiffPipeline -1. `SetConfig` / `SetSource` loads update URLs, app names, versions, secret keys, and install paths. -2. `LaunchAsync` selects `ClientStrategy`, `UpdateStrategy`, or an OSS strategy based on `Option.AppType`. -3. The Client workflow requests version metadata and triggers `AddListenerUpdateInfo` / `AddListenerUpdatePrecheck`. -4. Core downloads required version packages and reports speed, progress, completion, and errors through events. -5. After download, Core verifies checksums, extracts packages, merges patches, and replaces files. -6. The Upgrade workflow starts the main app according to `Option.LaunchClientAfterUpdate`. -7. If `ReportUrl` or a custom `UpdateReporter` is configured, Core reports update status. +```csharp +public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) +``` -## Common options +`UseDiffPipeline` customizes differential patch processing. Without it, Core builds a default pipeline using `BsdiffDiffer`, `DefaultCleanMatcher`, `DefaultDirtyMatcher`, parallelism `2`, and the Core progress reporter. -Use `SetOption(Option.Xxx, value)` for runtime settings: +```csharp +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Differential.Differ; -| Option | Default | Description | -| --- | --- | --- | -| `Option.AppType` | `AppType.Client` | Current process role. | -| `Option.Encoding` | `Encoding.UTF8` | Encoding for package processing. | -| `Option.Format` | `Format.Zip` | Update package compression format. | -| `Option.DownloadTimeout` | `30` | Download timeout in seconds. | -| `Option.PatchEnabled` | `true` | Enables differential patch processing. | -| `Option.BackupEnabled` | `true` | Backs up replaced files before update. | -| `Option.MaxConcurrency` | `3` | Max concurrent file downloads. | -| `Option.EnableResume` | `true` | Enables resumable downloads. | -| `Option.RetryCount` | `3` | Download retry count. | -| `Option.RetryInterval` | `1s` | Delay between retries. | -| `Option.VerifyChecksum` | `true` | Verifies file checksums. | -| `Option.DiffMode` | `DiffMode.Serial` | Differential merge mode. | -| `Option.Silent` | `false` | Enables silent polling update mode. | -| `Option.SilentPollIntervalMinutes` | `60` | Silent polling interval. | -| `Option.LaunchClientAfterUpdate` | `true` | Starts the main app after update. | - -Example: +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(); +``` + +## 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. | +| `DriverDirectory` | Driver directory; driver installation is documented under Drivelution. | + +### UpdateRequestBuilder + +`UpdateRequestBuilder` provides a fluent builder and validates on `Build()`. + +```csharp +using GeneralUpdate.Core.Configuration; + +var request = new UpdateRequestBuilder() + .SetUpdateUrl("https://update.example.com/api/upgrade/verification") + .SetReportUrl("https://update.example.com/api/upgrade/report") + .SetUpgradeAppName("UpgradeSample.exe") + .SetMainAppName("ClientSample.exe") + .SetClientVersion("1.0.0") + .SetAppSecretKey("your-app-secret") + .SetProductId("your-product-id") + .SetInstallPath(AppDomain.CurrentDomain.BaseDirectory) + .SetFiles(new List { "appsettings.json" }) + .SetFormats(new List { ".log", ".tmp" }) + .SetDirectories(new List { "logs" }) + .Build(); +``` + +`UpdateRequestBuilder.Create()` attempts to load `update_config.json` from the app runtime directory and throws `FileNotFoundException` if it is missing. + +```csharp +var request = UpdateRequestBuilder.Create().Build(); +``` + +## Runtime options: Option + +Core uses strongly typed `Option` values and sets them with `SetOption`. ```csharp await new GeneralUpdateBootstrap() .SetConfig(request) .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.PatchEnabled, true) - .SetOption(Option.BackupEnabled, true) - .SetOption(Option.VerifyChecksum, true) .SetOption(Option.MaxConcurrency, 4) + .SetOption(Option.VerifyChecksum, true) .LaunchAsync(); ``` +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `Option.AppType` | `AppType` | `Client` | Current process role. | +| `Option.DiffMode` | `DiffMode` | `Serial` | Differential execution mode. | +| `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 concurrent downloads. | +| `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 -Core exposes update state through listener methods: +Events are for observing update state. Complex business flow should be implemented with `IUpdateHooks` or documented in cookbook workflows. -| Method | Trigger | Typical use | +### Individual callbacks + +| Method | Argument type | Trigger | | --- | --- | --- | -| `AddListenerUpdateInfo` | After server version metadata is returned | Show update notes, version list, or package size. | -| `AddListenerUpdatePrecheck` | Before download starts | Check disk space, network conditions, or user confirmation. | -| `AddListenerMultiDownloadStatistics` | During download | Show speed, remaining time, and percentage. | -| `AddListenerMultiDownloadCompleted` | One version package completes | Log per-version download result. | -| `AddListenerMultiAllDownloadCompleted` | All download tasks complete | Update UI state or write logs. | -| `AddListenerMultiDownloadError` | A download task fails | Capture failed version and exception. | -| `AddListenerProgress` | Generic update progress changes | Drive a unified progress bar. | -| `AddListenerException` | Core catches an exception | Log, notify the user, or report telemetry. | +| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` | After server version metadata is returned. | +| `AddListenerUpdatePrecheck` | `Func` | Before download starts; `true` continues, `false` aborts. | +| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` | During download. | +| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` | One version download completes. | +| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` | All download tasks complete. | +| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` | Download failure. | +| `AddListenerProgress` | `ProgressEventArgs` | Download or differential progress changes. | +| `AddListenerException` | `ExceptionEventArgs` | Core catches an exception. | + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerUpdateInfo((_, e) => + { + Console.WriteLine($"Versions from server: {e.Info?.Body?.Count ?? 0}"); + }) + .AddListenerUpdatePrecheck(e => + { + var hasUpdate = (e.Info?.Body?.Count ?? 0) > 0; + var enoughDisk = DriveInfo.GetDrives() + .Where(d => d.IsReady) + .Any(d => d.AvailableFreeSpace > 1024L * 1024 * 1024); + + return hasUpdate && enoughDisk; + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + Console.WriteLine($"{e.ProgressPercentage}% {e.Speed} {e.BytesReceived}/{e.TotalBytesToReceive}"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + Console.WriteLine(e.IsCompleted ? "Download completed." : "Download failed."); + }) + .AddListenerProgress((_, e) => + { + if (e.Progress != null) + Console.WriteLine($"Download: {e.Progress.Percentage}%"); -You can also implement `IUpdateEventListener` and register all handlers with `AddEventListener()`. + if (e.DiffProgress != null) + Console.WriteLine($"Patch: {e.DiffProgress.Completed}/{e.DiffProgress.Total}"); + }) + .AddListenerException((_, e) => + { + Console.WriteLine(e.Message); + Console.WriteLine(e.Exception); + }) + .LaunchAsync(); +``` -## Silent update +### Listener class -Silent mode is designed for background polling. When enabled, the `Client` workflow starts a background poll loop and returns immediately. Prepared updates continue when the process exits. +Implement `IUpdateEventListener` to centralize event handling. Inherit `UpdateEventListenerBase` if you only need some events. ```csharp +using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Event; + +public sealed class ConsoleUpdateListener : UpdateEventListenerBase +{ + public override void OnUpdateInfo(UpdateInfoEventArgs args) + { + Console.WriteLine($"Update count: {args.Info?.Body?.Count ?? 0}"); + } + + public override void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) + { + Console.WriteLine($"{args.ProgressPercentage}% {args.Speed}"); + } + + public override void OnException(ExceptionEventArgs args) + { + Console.WriteLine(args.Exception); + } +} + await new GeneralUpdateBootstrap() .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.Silent, true) - .SetOption(Option.SilentPollIntervalMinutes, 30) - .SetOption(Option.LaunchClientAfterUpdate, true) + .AddEventListener() .LaunchAsync(); ``` -Silent mode still requires valid update URL, secret key, main app name, updater name, and install path configuration. - ## Extension points -`GeneralUpdateBootstrap` inherits from `AbstractBootstrap` and can replace multiple internal components: +All extension registration methods are provided by `AbstractBootstrap` and can be chained. + +| 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. -| Method | Purpose | +## Lifecycle hooks: IUpdateHooks + +`IUpdateHooks` is best for business logic before update, after download, after update, before app start, and on errors. + +```csharp +using GeneralUpdate.Core.Hooks; + +public sealed class ProductUpdateHooks : IUpdateHooks +{ + public Task OnBeforeUpdateAsync(HookContext ctx) + { + Console.WriteLine($"Before update: {ctx.CurrentVersion} -> {ctx.TargetVersion}"); + return Task.FromResult(true); + } + + public Task OnDownloadCompletedAsync(DownloadContext ctx) + { + Console.WriteLine($"Downloaded {ctx.AssetName}, success={ctx.Success}, path={ctx.LocalPath}"); + return Task.CompletedTask; + } + + public Task OnAfterUpdateAsync(HookContext ctx) + { + File.WriteAllText(Path.Combine(ctx.InstallPath, "last-update.txt"), DateTimeOffset.Now.ToString("O")); + return Task.CompletedTask; + } + + public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) + { + File.AppendAllText(Path.Combine(ctx.InstallPath, "update-error.log"), ex + Environment.NewLine); + return Task.CompletedTask; + } + + public Task OnBeforeStartAppAsync(HookContext ctx) + { + Console.WriteLine($"Starting app from {ctx.InstallPath}"); + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + +Built-in hook types: + +| Type | Description | | --- | --- | -| `Strategy()` | Selects a custom platform strategy. | -| `Hooks()` | Adds lifecycle hooks before update, after download, after update, before app start, and on error. | -| `UpdateReporter()` | Customizes update status reporting. | -| `SslPolicy()` | Customizes HTTPS certificate validation. | -| `UpdateAuth()` | Adds authentication to HTTP requests. | -| `DownloadSource()` | Customizes manifest and file source. | -| `DownloadPolicy()` | Customizes retry and timeout behavior. | -| `DownloadExecutor()` | Customizes single-file download implementation. | -| `DownloadPipeline()` | Adds post-download processing such as decryption, scanning, or validation. | -| `DownloadOrchestrator()` | Fully owns batch download orchestration. | - -Advanced projects can replace one part of the workflow without forking Core. +| `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. | -## Relationship with GeneralUpdate.Tools +## Status reporting: IUpdateReporter + +`IUpdateReporter` reports update status to a server or local telemetry. + +```csharp +using GeneralUpdate.Core.Download.Reporting; + +public sealed class ConsoleUpdateReporter : IUpdateReporter +{ + public Task ReportAsync(UpdateReport report, CancellationToken token = default) + { + Console.WriteLine($"Report: record={report.RecordId}, status={report.Status}, type={report.Type}"); + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateReporter() + .LaunchAsync(); +``` + +The built-in `HttpUpdateReporter` posts JSON to `ReportUrl`: + +```json +{ + "recordId": 123, + "status": 1, + "type": 1 +} +``` + +| Enum | Value | Description | +| --- | --- | --- | +| `UpdateStatus.Updating` | `1` | Updating. | +| `UpdateStatus.Success` | `2` | Update succeeded. | +| `UpdateStatus.Failure` | `3` | Update failed. | + +## HTTP authentication: IHttpAuthProvider + +`IHttpAuthProvider` adds authentication to outgoing Core HTTP requests. + +```csharp +using GeneralUpdate.Core.Security; + +public sealed class StaticBearerAuthProvider : IHttpAuthProvider +{ + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) + { + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "access-token"); + + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateAuth() + .LaunchAsync(); +``` + +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 System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using GeneralUpdate.Core.Security; + +public sealed class DevelopmentSslPolicy : ISslValidationPolicy +{ + public bool ValidateCertificate( + X509Certificate2? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) + { + return sslPolicyErrors == SslPolicyErrors.None + || certificate?.Issuer.Contains("CN=Local Dev Root") == true; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SslPolicy() + .LaunchAsync(); +``` + +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.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; + +public sealed class StaticDownloadSource : IDownloadSource +{ + public Task ListAsync(CancellationToken token = default) + { + var assets = new[] + { + new DownloadAsset( + Name: "app-1.0.1.zip", + Url: "https://cdn.example.com/releases/app-1.0.1.zip", + Size: 25_000_000, + SHA256: "expected-sha256", + Version: "1.0.1") + }; + + return Task.FromResult(new DownloadSourceResult + { + Assets = assets, + HasMainUpdate = true, + HasUpgradeUpdate = false + }); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadSource() + .LaunchAsync(); +``` + +## 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) + { + try + { + return await action(token); + } + catch when (!token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(2), token); + return await action(token); + } + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadPolicy() + .LaunchAsync(); +``` + +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.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; + +public sealed class MirrorDownloadExecutor : IDownloadExecutor +{ + private readonly HttpClient _client = new(); + + public async Task ExecuteAsync( + DownloadAsset asset, + string destPath, + IProgress? progress = null, + CancellationToken token = default) + { + var started = DateTimeOffset.Now; + await using var input = await _client.GetStreamAsync(asset.Url, token); + await using var output = File.Create(destPath); + await input.CopyToAsync(output, token); + + var fileInfo = new FileInfo(destPath); + return new DownloadResult(asset, destPath, fileInfo.Length, DateTimeOffset.Now - started, 0, true, null); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadExecutor() + .LaunchAsync(); +``` + +## 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 (!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 -Core consumes version manifests and update packages from the update server or OSS. `GeneralUpdate.Tools` helps produce and verify those inputs: +`IDownloadOrchestrator` is the highest-level download extension point. It owns batch download, concurrency, retry, progress, and result aggregation. -- Patch Package generates differential packages. -- Extension Package generates extension packages. -- OSS Config prepares cloud-storage distribution configuration. -- Simulation, report, and hash features help validate package structure and integrity before release. +```csharp +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Executors; +using GeneralUpdate.Core.Download.Models; + +public sealed class SerialDownloadOrchestrator : IDownloadOrchestrator +{ + private readonly IDownloadExecutor _executor = new HttpDownloadExecutor(new HttpClient()); + + public async Task ExecuteAsync( + DownloadPlan plan, + string destDir, + int maxConcurrency = 3, + IProgress? progress = null, + CancellationToken token = default) + { + var results = new List(); + var started = DateTimeOffset.Now; + + foreach (var asset in plan.Assets) + { + var destPath = Path.Combine(destDir, asset.Name); + results.Add(await _executor.ExecuteAsync(asset, destPath, progress, token)); + } + + return new DownloadReport( + results, + results.Where(r => r.Success).Sum(r => r.DownloadedBytes), + DateTimeOffset.Now - started, + results.Count(r => r.Success), + results.Count(r => !r.Success)); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadOrchestrator() + .LaunchAsync(); +``` + +Implement an orchestrator only when you need to replace the entire download behavior. Most cases only need `IDownloadExecutor`, `IDownloadPolicy`, or `IDownloadPipeline`. + +## Platform strategy: IStrategy + +`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. + +```csharp +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Strategy; + +public sealed class LoggingStrategy : IStrategy +{ + private UpdateContext? _context; + + public IUpdateHooks Hooks { get; set; } = new NoOpUpdateHooks(); + public IUpdateReporter Reporter { get; set; } = new HttpUpdateReporter(); -Recommended workflow: use Tools to generate and validate packages, publish packages and manifests to the server or OSS, then let Core execute the update on client machines. + public void Create(UpdateContext parameter) + { + _context = parameter; + } -## Troubleshooting + 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; + } +} -### The updater starts but does nothing +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Strategy() + .LaunchAsync(); +``` -Confirm it was launched by the main application. If the independent updater is started by double-clicking, it usually has no IPC context and cannot know the install path or pending versions. +## Silent update options -### File replacement fails +Silent update is enabled through options rather than a separate interface. When enabled, the `Client` role starts background polling and returns immediately. -The main app or Bowl process is usually still holding files. Confirm the main app has exited and configure the `Bowl` process name or shutdown logic correctly. +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 30) + .SetOption(Option.LaunchClientAfterUpdate, true) + .LaunchAsync(); +``` -### Download succeeds but checksum validation fails +The full product decision around prompting users, exit-time upgrade, and rollout policy should be covered in cookbooks. -Confirm the package was not recompressed, truncated, or replaced on the server or OSS. When `Option.VerifyChecksum` is enabled, the received file hash must match the manifest. +## Relationship with GeneralUpdate.Tools -### Differential packages are ignored +Core consumes manifests and packages. `GeneralUpdate.Tools` helps generate and validate those artifacts. -Confirm the server returns a differential package manifest and `Option.PatchEnabled` is `true`. Generate differential packages with `GeneralUpdate.Tools` to avoid mismatched manifests and package contents. +| Tools capability | Core consumption point | +| --- | --- | +| Patch Package | `Option.PatchEnabled`, `UseDiffPipeline`, differential patch processing. | +| Extension Package | Distributed as package content and consumed by download/deploy flow. | +| OSS Config | `OssClient` / `OssUpgrade` roles. | +| Hash / Simulation / Report | `Option.VerifyChecksum`, post-download verification, and status reporting. | ## Related samples 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 391ea07..9467421 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,18 +4,7 @@ sidebar_position: 5 # GeneralUpdate.Core -## 组件定位 - -`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心。它负责把“检查版本、下载更新包、校验、解压/合并、替换文件、启动目标程序”等步骤串成一个完整流程,并通过 `GeneralUpdateBootstrap` 提供统一入口。 - -Core 既可以运行在主程序内执行 `Client` / `OssClient` 流程,也可以作为独立升级程序执行 `Upgrade` / `OssUpgrade` 流程。实际项目中最常见的部署方式是: - -1. 主程序负责启动更新检查。 -2. Core 在客户端流程中获取版本清单并下载更新包。 -3. Core 启动独立升级程序,升级程序关闭占用进程后完成文件替换。 -4. 升级完成后重新启动主程序。 - -> 固件升级不属于本页范围;驱动安装能力请参考后续 `GeneralUpdate.Drivelution` 文档。 +`GeneralUpdate.Core` 是 GeneralUpdate 的更新执行核心,重点提供可编程的启动器、配置模型、事件模型、下载子系统扩展点、生命周期钩子、状态上报、差分管道和平台策略扩展。本页聚焦组件 API、属性和扩展方式;完整端到端上手流程会放到 cookbook 中。 **命名空间:** `GeneralUpdate.Core` **主要入口:** `GeneralUpdateBootstrap` @@ -25,261 +14,817 @@ Core 既可以运行在主程序内执行 `Client` / `OssClient` 流程,也可 dotnet add package GeneralUpdate.Core ``` -## 适用场景 +## 组件能力边界 -| 场景 | 是否适合使用 Core | -| --- | --- | -| 桌面应用自更新 | 适合。主程序检查更新,升级程序替换文件。 | -| 需要多版本连续升级 | 适合。Core 可以按服务端返回的版本序列处理多个包。 | -| 需要差分补丁更新 | 适合。配合差分包和 `Option.PatchEnabled` / `Option.DiffMode` 使用。 | -| 需要云存储分发更新包 | 适合。使用 `OssClient` / `OssUpgrade` 角色。 | -| 固件刷写 | 不适合。本页不覆盖固件升级组件。 | +Core 负责“执行更新”,不负责生成更新包,也不直接管理服务端后台。 -## 运行角色 +| 能力 | Core 是否负责 | 说明 | +| --- | --- | --- | +| 读取更新配置 | 是 | 通过 `UpdateRequest`、配置文件、`SetSource` 或 IPC 恢复运行参数。 | +| 检查服务端版本 | 是 | `Client` / `OssClient` 角色会读取版本清单并生成下载计划。 | +| 下载更新包 | 是 | 可替换下载来源、执行器、重试策略、后处理管道或完整编排器。 | +| 校验与应用补丁 | 是 | 支持 Hash 校验、压缩包处理、差分补丁管道。 | +| 文件替换与重启应用 | 是 | `Upgrade` / `OssUpgrade` 角色用于独立升级程序。 | +| 生成差分包 | 否 | 推荐使用 `GeneralUpdate.Tools`。 | +| 固件升级 | 否 | 固件升级组件不在本页范围内。 | + +## 入口类:GeneralUpdateBootstrap + +`GeneralUpdateBootstrap` 是 Core 的主要门面类。它继承 `AbstractBootstrap`,因此同时拥有自身方法和基类提供的扩展注册方法。 -Core 通过 `Option.AppType` 决定当前进程承担的更新角色: +```csharp +using GeneralUpdate.Core; -| AppType | 角色 | 说明 | +var bootstrap = new GeneralUpdateBootstrap(); +``` + +### 方法总览 + +| 方法 | 用途 | 常用场景 | | --- | --- | --- | -| `Client` | 主程序侧更新流程 | 检查服务端版本、下载包、准备升级上下文,并启动升级程序。默认值。 | -| `Upgrade` | 独立升级程序流程 | 读取主程序传入的 IPC 数据,执行文件替换,再启动主程序。 | -| `OssClient` | OSS 主程序侧流程 | 从 OSS 配置检查更新并启动 OSS 升级程序。 | -| `OssUpgrade` | OSS 升级程序流程 | 从 OSS 下载并部署更新包。 | +| `LaunchAsync()` | 按当前 `Option.AppType` 启动更新流程。 | 所有 Core 使用场景最终都会调用。 | +| `Cancel()` | 请求取消当前更新操作。 | UI 中提供“取消更新”按钮。 | +| `SetConfig(UpdateRequest)` | 使用强类型对象配置更新。 | 主程序内显式配置更新参数。 | +| `SetConfig(string)` | 从 JSON 文件读取 `UpdateRequest`。 | 将更新参数放到 `update_config.json` 或自定义配置文件。 | +| `SetSource(...)` | 只提供更新地址、密钥、报告地址等基础参数。 | 零配置/轻配置入口。 | +| `SetOption(Option, T)` | 设置运行时选项。 | 设置角色、超时、并发、差分、静默更新等。 | +| `UseDiffPipeline(Action)` | 自定义差分补丁管道。 | 替换 differ、调整并行度、接入补丁进度。 | +| `AddListenerUpdatePrecheck(Func)` | 下载前预检查。 | 检查磁盘空间、网络状态或弹窗确认。 | +| `AddListener...` | 注册单个事件回调。 | 更新 UI、写日志、上报监控。 | +| `AddEventListener()` | 批量注册事件监听器。 | 将事件处理封装成类。 | + +### LaunchAsync -升级程序通常不需要手动调用 `SetConfig`。当它由主程序启动时,Core 会通过加密文件 IPC 自动读取 `ProcessContract`,恢复安装路径、版本、临时目录、下载配置、更新包列表等上下文。 +```csharp +public Task LaunchAsync() +``` -## 最小升级程序 +`LaunchAsync` 会读取 `Option.AppType` 并选择对应策略: -`GeneralUpdate-Samples/src/Upgrade/Program.cs` 展示了独立升级程序的最小形态。该程序只需要注册必要事件,然后调用 `LaunchAsync()`: +| `Option.AppType` | 策略 | 说明 | +| --- | --- | --- | +| `AppType.Client` | `ClientStrategy` | 主程序侧:检查版本、下载包、准备升级上下文、启动升级程序。 | +| `AppType.Upgrade` | `UpdateStrategy` | 升级程序侧:读取 IPC 上下文并执行文件替换。 | +| `AppType.OssClient` | `OssStrategy` | OSS 主程序侧更新流程。 | +| `AppType.OssUpgrade` | `OssStrategy` | OSS 升级程序侧更新流程。 | + +示例:独立升级程序入口。 ```csharp -using GeneralUpdate.Common.Download; -using GeneralUpdate.Common.Internal; -using GeneralUpdate.Common.Shared.Object; -using GeneralUpdate.Core; +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.Upgrade) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)) + .LaunchAsync(); +``` -try -{ - Console.WriteLine($"Updater started at {DateTime.Now}"); - - _ = await new GeneralUpdateBootstrap() - .AddListenerMultiDownloadStatistics(OnMultiDownloadStatistics) - .AddListenerMultiDownloadCompleted(OnMultiDownloadCompleted) - .AddListenerMultiAllDownloadCompleted(OnMultiAllDownloadCompleted) - .AddListenerMultiDownloadError(OnMultiDownloadError) - .AddListenerException(OnException) - .LaunchAsync(); -} -catch (Exception ex) -{ - Console.WriteLine(ex); -} +> 当升级程序由主程序启动时,Core 会通过加密文件 IPC 自动恢复更新上下文,通常不需要在升级程序里再次调用 `SetConfig`。 -void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs args) -{ - var version = args.Version as VersionInfo; - Console.WriteLine( - $"Version: {version?.Version}, Speed: {args.Speed}, Progress: {args.ProgressPercentage}%"); -} +### Cancel -void OnMultiDownloadCompleted(object sender, MultiDownloadCompletedEventArgs args) +```csharp +public void Cancel() +``` + +`Cancel` 会触发内部 `CancellationTokenSource`,更新策略会在安全检查点观察取消请求。适合 UI 应用把 bootstrap 保存为字段后绑定取消按钮。 + +```csharp +private GeneralUpdateBootstrap? _bootstrap; + +async Task StartUpdateAsync(UpdateRequest request) { - var version = args.Version as VersionInfo; - Console.WriteLine(args.IsComplated - ? $"Version {version?.Version} download completed." - : $"Version {version?.Version} download failed."); + _bootstrap = new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerException((_, e) => Console.WriteLine(e.Exception)); + + await _bootstrap.LaunchAsync(); } -void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs args) +void CancelUpdate() { - Console.WriteLine(args.IsAllDownloadCompleted - ? "All download tasks completed." - : $"Download failed. Failed versions: {args.FailedVersions.Count}"); + _bootstrap?.Cancel(); } +``` + +### SetConfig(UpdateRequest) + +```csharp +public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) +``` + +`SetConfig(UpdateRequest)` 会调用 `UpdateRequest.Validate()`,并把外部配置映射为内部 `UpdateContext`。当角色不是 `AppType.Upgrade` 时,它还会初始化临时目录和黑名单匹配器。 + +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; -void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs args) +var request = new UpdateRequest { - var version = args.Version as VersionInfo; - Console.WriteLine($"Version {version?.Version} download error: {args.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 sender, ExceptionEventArgs args) +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(args.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 的 `Client` 流程,可以显式传入 `UpdateRequest`: +### SetSource ```csharp -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; +public GeneralUpdateBootstrap SetSource( + string updateUrl, + string appSecretKey, + string? reportUrl = null, + string? scheme = null, + string? token = null) +``` + +`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。 +```csharp await new GeneralUpdateBootstrap() - .SetConfig(new UpdateRequest + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret", + reportUrl: "https://update.example.com/api/upgrade/report", + scheme: "Bearer", + token: "access-token") + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +### UseDiffPipeline + +```csharp +public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) +``` + +`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,Core 会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。 + +```csharp +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Differential.Differ; + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UseDiffPipeline(builder => { - 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" + 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(); +``` + +## 配置模型: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` | 更新时跳过的目录。 | +| `DriverDirectory` | 驱动目录;驱动安装属于 Drivelution 文档范围。 | + +### 使用 UpdateRequestBuilder + +`UpdateRequestBuilder` 提供链式构建 API,并在 `Build()` 时执行校验。 + +```csharp +using GeneralUpdate.Core.Configuration; + +var request = new UpdateRequestBuilder() + .SetUpdateUrl("https://update.example.com/api/upgrade/verification") + .SetReportUrl("https://update.example.com/api/upgrade/report") + .SetUpgradeAppName("UpgradeSample.exe") + .SetMainAppName("ClientSample.exe") + .SetClientVersion("1.0.0") + .SetAppSecretKey("your-app-secret") + .SetProductId("your-product-id") + .SetInstallPath(AppDomain.CurrentDomain.BaseDirectory) + .SetFiles(new List { "appsettings.json" }) + .SetFormats(new List { ".log", ".tmp" }) + .SetDirectories(new List { "logs" }) + .Build(); +``` + +`UpdateRequestBuilder.Create()` 会尝试从应用运行目录的 `update_config.json` 读取配置。如果文件不存在,会抛出 `FileNotFoundException`。 + +```csharp +var request = UpdateRequestBuilder.Create().Build(); +``` + +## 运行选项:Option + +Core 使用强类型 `Option` 注册运行时选项,并通过 `SetOption` 设置值。 + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.DownloadTimeout, 60) - .SetOption(Option.MaxConcurrency, 3) - .AddListenerUpdateInfo((sender, args) => + .SetOption(Option.MaxConcurrency, 4) + .SetOption(Option.VerifyChecksum, true) + .LaunchAsync(); +``` + +| 选项 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `Option.AppType` | `AppType` | `Client` | 当前进程角色。 | +| `Option.DiffMode` | `DiffMode` | `Serial` | 差分执行模式。 | +| `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 + +事件适合观察更新过程,不应该承载复杂业务流程。复杂流程建议封装成 `IUpdateHooks` 或 cookbook 中的完整方案。 + +### 单个事件回调 + +| 方法 | 参数类型 | 触发时机 | +| --- | --- | --- | +| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` | 服务端版本信息返回后。 | +| `AddListenerUpdatePrecheck` | `Func` | 下载开始前,返回 `true` 继续,返回 `false` 中止。 | +| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` | 下载过程中持续触发。 | +| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` | 单个版本下载结束。 | +| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` | 所有下载任务结束。 | +| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` | 下载失败。 | +| `AddListenerProgress` | `ProgressEventArgs` | 下载进度或差分补丁进度变化。 | +| `AddListenerException` | `ExceptionEventArgs` | Core 捕获异常。 | + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .AddListenerUpdateInfo((_, e) => { - Console.WriteLine($"Server returned {args.Info.Body?.Count ?? 0} update versions."); + Console.WriteLine($"Versions from server: {e.Info?.Body?.Count ?? 0}"); }) - .AddListenerException((sender, args) => + .AddListenerUpdatePrecheck(e => { - Console.WriteLine(args.Exception); + var hasUpdate = (e.Info?.Body?.Count ?? 0) > 0; + var enoughDisk = DriveInfo.GetDrives() + .Where(d => d.IsReady) + .Any(d => d.AvailableFreeSpace > 1024L * 1024 * 1024); + + return hasUpdate && enoughDisk; + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + Console.WriteLine($"{e.ProgressPercentage}% {e.Speed} {e.BytesReceived}/{e.TotalBytesToReceive}"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + Console.WriteLine(e.IsCompleted ? "Download completed." : "Download failed."); + }) + .AddListenerProgress((_, e) => + { + if (e.Progress != null) + Console.WriteLine($"Download: {e.Progress.Percentage}%"); + + if (e.DiffProgress != null) + Console.WriteLine($"Patch: {e.DiffProgress.Completed}/{e.DiffProgress.Total}"); + }) + .AddListenerException((_, e) => + { + Console.WriteLine(e.Message); + Console.WriteLine(e.Exception); }) .LaunchAsync(); ``` -也可以使用 `SetSource` 走简化配置路径: +### 批量事件监听器 + +实现 `IUpdateEventListener` 可以把事件处理集中到一个类。若只关心部分事件,继承 `UpdateEventListenerBase` 更简单。 ```csharp +using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Event; + +public sealed class ConsoleUpdateListener : UpdateEventListenerBase +{ + public override void OnUpdateInfo(UpdateInfoEventArgs args) + { + Console.WriteLine($"Update count: {args.Info?.Body?.Count ?? 0}"); + } + + public override void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) + { + Console.WriteLine($"{args.ProgressPercentage}% {args.Speed}"); + } + + public override void OnException(ExceptionEventArgs args) + { + Console.WriteLine(args.Exception); + } +} + 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) + .SetConfig(request) + .AddEventListener() .LaunchAsync(); ``` -## 核心执行流程 +## 扩展点总览 -1. `SetConfig` / `SetSource` 载入更新地址、应用名称、版本号、密钥、安装目录等配置。 -2. `LaunchAsync` 根据 `Option.AppType` 选择 `ClientStrategy`、`UpdateStrategy` 或 OSS 策略。 -3. Client 流程向服务端请求版本信息,并触发 `AddListenerUpdateInfo` / `AddListenerUpdatePrecheck`。 -4. Core 下载需要更新的版本包,并通过下载事件持续回调速度、进度和错误。 -5. 下载完成后执行校验、解压、差分合并和文件替换等管道步骤。 -6. Upgrade 流程完成后根据 `Option.LaunchClientAfterUpdate` 决定是否启动主程序。 -7. 如配置了 `ReportUrl` 或自定义 `UpdateReporter`,Core 会上报更新状态。 +扩展点由 `AbstractBootstrap` 提供,所有注册方法都返回当前 bootstrap,可链式调用。 -## 常用配置项 +| 注册方法 | 接口 | 影响范围 | +| --- | --- | --- | +| `Hooks()` | `IUpdateHooks` | 更新生命周期前后置逻辑。 | +| `UpdateReporter()` | `IUpdateReporter` | 更新状态上报。 | +| `SslPolicy()` | `ISslValidationPolicy` | HTTPS 证书校验。 | +| `UpdateAuth()` | `IHttpAuthProvider` | HTTP 请求认证。 | +| `DownloadSource()` | `IDownloadSource` | 版本清单和下载资源来源。 | +| `DownloadPolicy()` | `IDownloadPolicy` | 下载重试、超时、熔断等策略。 | +| `DownloadExecutor()` | `IDownloadExecutor` | 单文件下载实现。 | +| `DownloadPipeline()` | `IDownloadPipeline` | 下载后处理,例如校验、解密、扫描。 | +| `DownloadOrchestrator()` | `IDownloadOrchestrator` | 批量下载完整编排。 | +| `Strategy()` | `IStrategy` | 自定义平台级更新策略。 | -使用 `SetOption(Option.Xxx, value)` 设置运行时选项: +> 通过这些方法注册的类型必须有无参构造函数,因为 Core 使用 `new()` 或反射创建实例。需要复杂依赖时,建议在自定义类型内部读取配置,或在应用层封装一个无参适配器。 -| 选项 | 默认值 | 说明 | -| --- | --- | --- | -| `Option.AppType` | `AppType.Client` | 当前进程角色。 | -| `Option.Encoding` | `Encoding.UTF8` | 压缩包文件名/内容处理编码。 | -| `Option.Format` | `Format.Zip` | 更新包压缩格式。 | -| `Option.DownloadTimeout` | `30` | 下载超时时间,单位为秒。 | -| `Option.PatchEnabled` | `true` | 是否启用差分补丁处理。 | -| `Option.BackupEnabled` | `true` | 是否在更新前备份被替换文件。 | -| `Option.MaxConcurrency` | `3` | 多文件下载最大并发数。 | -| `Option.EnableResume` | `true` | 是否启用断点续传。 | -| `Option.RetryCount` | `3` | 下载失败重试次数。 | -| `Option.RetryInterval` | `1s` | 下载重试间隔。 | -| `Option.VerifyChecksum` | `true` | 是否校验文件 Hash。 | -| `Option.DiffMode` | `DiffMode.Serial` | 差分合并执行模式。 | -| `Option.Silent` | `false` | 是否启用静默轮询更新。 | -| `Option.SilentPollIntervalMinutes` | `60` | 静默模式检查更新间隔。 | -| `Option.LaunchClientAfterUpdate` | `true` | 升级后是否启动主程序。 | - -示例: +## 生命周期钩子:IUpdateHooks + +`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。 ```csharp +using GeneralUpdate.Core.Hooks; + +public sealed class ProductUpdateHooks : IUpdateHooks +{ + public Task OnBeforeUpdateAsync(HookContext ctx) + { + Console.WriteLine($"Before update: {ctx.CurrentVersion} -> {ctx.TargetVersion}"); + return Task.FromResult(true); + } + + public Task OnDownloadCompletedAsync(DownloadContext ctx) + { + Console.WriteLine($"Downloaded {ctx.AssetName}, success={ctx.Success}, path={ctx.LocalPath}"); + return Task.CompletedTask; + } + + public Task OnAfterUpdateAsync(HookContext ctx) + { + File.WriteAllText(Path.Combine(ctx.InstallPath, "last-update.txt"), DateTimeOffset.Now.ToString("O")); + return Task.CompletedTask; + } + + public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) + { + File.AppendAllText(Path.Combine(ctx.InstallPath, "update-error.log"), ex + Environment.NewLine); + return Task.CompletedTask; + } + + public Task OnBeforeStartAppAsync(HookContext ctx) + { + Console.WriteLine($"Starting app from {ctx.InstallPath}"); + return Task.CompletedTask; + } +} + await new GeneralUpdateBootstrap() .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.PatchEnabled, true) - .SetOption(Option.BackupEnabled, true) - .SetOption(Option.VerifyChecksum, true) - .SetOption(Option.MaxConcurrency, 4) + .Hooks() + .LaunchAsync(); +``` + +内置实现包括: + +| 类型 | 说明 | +| --- | --- | +| `NoOpUpdateHooks` | 默认空实现。 | +| `UnixPermissionHooks` | 在 Unix-like 系统启动前执行 `chmod +x`。 | +| `CustomPermissionHooks` | 执行自定义权限脚本;该类型构造函数需要参数,不适合直接用 `Hooks()` 注册,可自行包装无参适配器。 | + +## 状态上报:IUpdateReporter + +`IUpdateReporter` 用于把更新状态上报到服务端。 + +```csharp +using GeneralUpdate.Core.Download.Reporting; + +public sealed class ConsoleUpdateReporter : IUpdateReporter +{ + public Task ReportAsync(UpdateReport report, CancellationToken token = default) + { + Console.WriteLine($"Report: record={report.RecordId}, status={report.Status}, type={report.Type}"); + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateReporter() .LaunchAsync(); ``` -## 事件监听 +内置 `HttpUpdateReporter` 会向 `ReportUrl` 发送 JSON: + +```json +{ + "recordId": 123, + "status": 1, + "type": 1 +} +``` -Core 通过事件监听器暴露更新过程状态: +状态值: -| 方法 | 触发时机 | 典型用途 | +| 枚举 | 值 | 说明 | | --- | --- | --- | -| `AddListenerUpdateInfo` | 服务端返回版本信息后 | 展示更新日志、版本列表、更新大小。 | -| `AddListenerUpdatePrecheck` | 下载开始前 | 检查磁盘空间、网络环境、用户确认。 | -| `AddListenerMultiDownloadStatistics` | 下载过程中持续触发 | 展示速度、剩余时间、百分比。 | -| `AddListenerMultiDownloadCompleted` | 单个版本包下载完成 | 记录每个版本下载结果。 | -| `AddListenerMultiAllDownloadCompleted` | 全部下载任务完成 | 切换 UI 状态或写日志。 | -| `AddListenerMultiDownloadError` | 下载任务失败 | 输出失败版本和异常。 | -| `AddListenerProgress` | 通用更新进度变化 | 对接统一进度条。 | -| `AddListenerException` | Core 捕获到异常 | 写入日志、提示用户或上报。 | +| `UpdateStatus.Updating` | `1` | 更新中。 | +| `UpdateStatus.Success` | `2` | 更新成功。 | +| `UpdateStatus.Failure` | `3` | 更新失败。 | -如果不想逐个注册事件,可以实现 `IUpdateEventListener` 后使用 `AddEventListener()` 批量注册。 +## HTTP 认证:IHttpAuthProvider -## 静默更新 +`IHttpAuthProvider` 可以为 Core 发出的 HTTP 请求追加认证头。 -静默更新适合后台定期检查更新。启用后,`Client` 流程会启动后台轮询并立即返回,更新准备完成后在进程退出时继续升级。 +```csharp +using GeneralUpdate.Core.Security; + +public sealed class StaticBearerAuthProvider : IHttpAuthProvider +{ + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) + { + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "access-token"); + + return Task.CompletedTask; + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UpdateAuth() + .LaunchAsync(); +``` + +Core 内置的认证类型包括 `NoOpAuthProvider`、`BearerTokenAuthProvider`、`ApiKeyAuthProvider` 和 `HmacAuthProvider`。这些类型中部分构造函数需要参数,因此如果要通过 `UpdateAuth()` 注册,通常需要写一个无参包装类。 + +## HTTPS 证书策略:ISslValidationPolicy + +`ISslValidationPolicy` 用于控制 HTTPS 证书校验。默认 `StrictSslValidationPolicy` 只接受没有 SSL policy errors 的证书。 ```csharp +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using GeneralUpdate.Core.Security; + +public sealed class DevelopmentSslPolicy : ISslValidationPolicy +{ + public bool ValidateCertificate( + X509Certificate2? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) + { + return sslPolicyErrors == SslPolicyErrors.None + || certificate?.Issuer.Contains("CN=Local Dev Root") == true; + } +} + await new GeneralUpdateBootstrap() .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.Silent, true) - .SetOption(Option.SilentPollIntervalMinutes, 30) - .SetOption(Option.LaunchClientAfterUpdate, true) + .SslPolicy() .LaunchAsync(); ``` -静默更新仍然需要正确配置更新地址、应用密钥、主程序名称、升级程序名称和安装目录。 +生产环境不建议无条件返回 `true`,否则会绕过 HTTPS 的安全保证。 -## 扩展点 +## 下载来源:IDownloadSource -`GeneralUpdateBootstrap` 继承自 `AbstractBootstrap`,可以替换多个内部组件: +`IDownloadSource` 负责返回待下载资源列表。适合接入私有服务、文件服务器、配置中心或自定义云存储。 -| 方法 | 用途 | -| --- | --- | -| `Strategy()` | 指定自定义平台策略。 | -| `Hooks()` | 注入更新前、下载后、更新后、启动前、异常时的生命周期钩子。 | -| `UpdateReporter()` | 自定义更新状态上报。 | -| `SslPolicy()` | 自定义 HTTPS 证书校验策略。 | -| `UpdateAuth()` | 为 HTTP 请求添加认证信息。 | -| `DownloadSource()` | 自定义版本清单和文件来源。 | -| `DownloadPolicy()` | 自定义下载重试/超时策略。 | -| `DownloadExecutor()` | 自定义单文件下载实现。 | -| `DownloadPipeline()` | 自定义下载后处理,例如解密、杀毒、校验。 | -| `DownloadOrchestrator()` | 完全接管批量下载流程。 | - -高级项目可以只替换某一个环节,而不需要 fork Core。 +```csharp +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; -## 与 GeneralUpdate.Tools 的关系 +public sealed class StaticDownloadSource : IDownloadSource +{ + public Task ListAsync(CancellationToken token = default) + { + var assets = new[] + { + new DownloadAsset( + Name: "app-1.0.1.zip", + Url: "https://cdn.example.com/releases/app-1.0.1.zip", + Size: 25_000_000, + SHA256: "expected-sha256", + Version: "1.0.1") + }; + + return Task.FromResult(new DownloadSourceResult + { + Assets = assets, + HasMainUpdate = true, + HasUpgradeUpdate = false + }); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadSource() + .LaunchAsync(); +``` + +## 下载重试策略:IDownloadPolicy + +`IDownloadPolicy` 包装单次下载动作,适合实现重试、超时、熔断或限流。 + +```csharp +using GeneralUpdate.Core.Download.Abstractions; + +public sealed class TwoAttemptDownloadPolicy : IDownloadPolicy +{ + public async Task ExecuteAsync( + Func> action, + CancellationToken token = default) + { + try + { + return await action(token); + } + catch when (!token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(2), token); + return await action(token); + } + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadPolicy() + .LaunchAsync(); +``` + +如果同时注册 `DownloadOrchestrator()`,自定义 orchestrator 会接管完整下载流程,`DownloadPolicy()` 是否生效取决于 orchestrator 自己是否使用该策略。 + +## 单文件下载:IDownloadExecutor + +`IDownloadExecutor` 负责把一个 `DownloadAsset` 下载到目标路径。适合支持 FTP、SFTP、私有协议或自定义 HTTP 客户端。 + +```csharp +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; + +public sealed class MirrorDownloadExecutor : IDownloadExecutor +{ + private readonly HttpClient _client = new(); + + public async Task ExecuteAsync( + DownloadAsset asset, + string destPath, + IProgress? progress = null, + CancellationToken token = default) + { + var started = DateTimeOffset.Now; + await using var input = await _client.GetStreamAsync(asset.Url, token); + await using var output = File.Create(destPath); + await input.CopyToAsync(output, token); + + var fileInfo = new FileInfo(destPath); + return new DownloadResult(asset, destPath, fileInfo.Length, DateTimeOffset.Now - started, 0, true, null); + } +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadExecutor() + .LaunchAsync(); +``` -Core 消费的是更新服务端或 OSS 返回的版本清单和更新包。`GeneralUpdate.Tools` 用来帮助生成和验证这些输入: +## 下载后处理:IDownloadPipeline -- 使用 Patch Package 生成差分更新包。 -- 使用 Extension Package 生成扩展包。 -- 使用 OSS Config 准备云存储分发配置。 -- 使用模拟、报告和 Hash 相关能力提前验证包结构与完整性。 +`IDownloadPipeline` 在文件下载完成后运行。适合做 Hash 校验、解密、病毒扫描、格式转换等。 -推荐流程是:先用 Tools 生成并校验更新包,再把包和版本清单发布到服务端或 OSS,最后由 Core 在客户端执行更新。 +```csharp +using GeneralUpdate.Core.Download.Abstractions; -## 常见问题 +public sealed class AntivirusPipeline : IDownloadPipeline +{ + public Task ProcessAsync(string downloadedPath, CancellationToken token = default) + { + if (!File.Exists(downloadedPath)) + throw new FileNotFoundException("Downloaded file not found.", downloadedPath); -### 升级程序启动后没有执行更新 + Console.WriteLine($"Scanning {downloadedPath}"); + return Task.FromResult(downloadedPath); + } +} -确认它是否由主程序启动。如果直接双击独立升级程序,通常没有 IPC 上下文,`Upgrade` 流程无法知道安装目录和待更新版本。开发调试时可以先从主程序触发完整流程。 +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadPipeline() + .LaunchAsync(); +``` -### 文件替换失败 +Core 在创建下载管道时会优先尝试使用 `string` 构造函数传入期望 Hash;如果没有该构造函数,则使用无参构造函数。 -通常是主程序或 Bowl 进程仍占用文件。确认主程序已退出,并正确配置 `Bowl` 进程名或相关关闭逻辑。 +## 批量下载编排:IDownloadOrchestrator -### 下载成功但校验失败 +`IDownloadOrchestrator` 是下载子系统的最高层扩展点。注册后,它会接管批量下载、并发控制、重试、进度和结果汇总。 -确认服务端或 OSS 上的包没有被重新压缩、截断或替换。若启用了 `Option.VerifyChecksum`,客户端收到的 Hash 必须和清单中的 Hash 一致。 +```csharp +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Executors; +using GeneralUpdate.Core.Download.Models; + +public sealed class SerialDownloadOrchestrator : IDownloadOrchestrator +{ + private readonly IDownloadExecutor _executor = new HttpDownloadExecutor(new HttpClient()); + + public async Task ExecuteAsync( + DownloadPlan plan, + string destDir, + int maxConcurrency = 3, + IProgress? progress = null, + CancellationToken token = default) + { + var results = new List(); + var started = DateTimeOffset.Now; + + foreach (var asset in plan.Assets) + { + var destPath = Path.Combine(destDir, asset.Name); + results.Add(await _executor.ExecuteAsync(asset, destPath, progress, token)); + } + + return new DownloadReport( + results, + results.Where(r => r.Success).Sum(r => r.DownloadedBytes), + DateTimeOffset.Now - started, + results.Count(r => r.Success), + results.Count(r => !r.Success)); + } +} -### 差分包没有生效 +await new GeneralUpdateBootstrap() + .SetConfig(request) + .DownloadOrchestrator() + .LaunchAsync(); +``` -确认服务端返回的是差分包清单,并且 `Option.PatchEnabled` 为 `true`。差分包建议通过 `GeneralUpdate.Tools` 生成,避免手工组织目录导致清单和包内容不一致。 +只有当你需要完整替换下载行为时才建议实现 orchestrator。多数情况下替换 `IDownloadExecutor`、`IDownloadPolicy` 或 `IDownloadPipeline` 就够了。 + +## 平台策略:IStrategy + +`IStrategy` 是最高级别的更新策略接口。Core 内置 `ClientStrategy`、`UpdateStrategy`、`OssStrategy` 以及 Windows/Linux/macOS 平台策略。只有在你需要替换平台级文件操作或启动逻辑时,才应实现它。 + +```csharp +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Strategy; + +public sealed class LoggingStrategy : IStrategy +{ + private UpdateContext? _context; + + public IUpdateHooks Hooks { get; set; } = new NoOpUpdateHooks(); + public IUpdateReporter Reporter { get; set; } = new HttpUpdateReporter(); + + public void Create(UpdateContext parameter) + { + _context = parameter; + } + + 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(); +``` + +## 静默更新选项 + +静默更新通过选项启用,不需要额外接口。启用后,`Client` 角色会启动后台轮询并立即返回。 + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 30) + .SetOption(Option.LaunchClientAfterUpdate, true) + .LaunchAsync(); +``` + +适合把“是否启用、何时提示用户、如何处理退出时升级”等完整策略写到 cookbook,而组件文档只需要说明相关 API。 + +## 与 GeneralUpdate.Tools 的关系 + +Core 消费更新清单和更新包;`GeneralUpdate.Tools` 负责辅助生成和验证这些产物。 + +| Tools 能力 | Core 中对应消费点 | +| --- | --- | +| Patch Package | `Option.PatchEnabled`、`UseDiffPipeline`、差分补丁处理。 | +| Extension Package | 作为更新包内容或扩展包分发,由下载和部署流程消费。 | +| OSS Config | `OssClient` / `OssUpgrade` 角色读取 OSS 配置并下载。 | +| Hash / Simulation / Report | 对应 `Option.VerifyChecksum`、下载后校验和状态上报。 | ## 相关示例 From a72fcd98648f7003178087f94bc11b0b84265e9f Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 01:32:26 +0800 Subject: [PATCH 03/10] Document Core manifest support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 131 +++++++++++++++++- .../current/doc/GeneralUpdate.Core.md | 131 +++++++++++++++++- .../current/doc/GeneralUpdate.Core.md | 131 +++++++++++++++++- 3 files changed, 390 insertions(+), 3 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 9467421..ecf85ec 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -177,7 +177,7 @@ public GeneralUpdateBootstrap SetSource( string? token = null) ``` -`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。 +`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。若需要精确控制清单中的每个身份字段,建议使用后文的 `ManifestInfo.Load().ToUpdateRequest()` 再调用 `SetConfig(request)`。 ```csharp await new GeneralUpdateBootstrap() @@ -283,6 +283,134 @@ var request = new UpdateRequestBuilder() var request = UpdateRequestBuilder.Create().Build(); ``` +## 应用身份清单:generalupdate.manifest.json + +`generalupdate.manifest.json` 是由 `GeneralUpdate.Tools` 生成、由 Core 消费的应用身份清单。它把“主程序叫什么、当前版本是多少、升级程序叫什么、产品标识是什么、升级程序放在哪个目录”等稳定元数据从代码配置中移出来,代码里只保留服务端地址、密钥、令牌等运行时或敏感参数。 + +推荐把它放在应用安装目录,也就是 `UpdateRequest.InstallPath` 指向的目录。默认情况下 `InstallPath` 是 `AppDomain.CurrentDomain.BaseDirectory`,因此普通桌面应用通常把清单放在主程序输出目录根部。 + +```text +MyProduct/ +├─ ClientSample.exe +├─ generalupdate.manifest.json +└─ update/ + └─ UpgradeSample.exe +``` + +### 清单结构 + +Tools 生成的 JSON 使用小驼峰字段名,Core 中对应类型是 `ManifestInfo`。 + +```json +{ + "mainAppName": "ClientSample.exe", + "clientVersion": "1.0.0", + "appType": "Client", + "updateAppName": "UpgradeSample.exe", + "upgradeClientVersion": "1.0.0", + "productId": "sample-product", + "updatePath": "update/" +} +``` + +| 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 负责构建可发布的身份元数据,而密钥仍由应用代码、配置中心或部署环境提供。 + +### 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 生成清单,再在应用代码中补充服务端入口和密钥。 + +### Core 如何读取清单 + +Core 侧提供 `ManifestInfo`: + +```csharp +using GeneralUpdate.Core.Configuration; + +var manifest = ManifestInfo.Load(); +if (manifest == null) + throw new FileNotFoundException("generalupdate.manifest.json was not found."); + +var request = manifest.ToUpdateRequest(); +request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; +request.ReportUrl = "https://update.example.com/api/upgrade/report"; +request.AppSecretKey = "your-app-secret"; +request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +`ManifestInfo.Load()` 默认从 `AppDomain.CurrentDomain.BaseDirectory` 读取清单;`ManifestInfo.Load(path)` 可以从指定文件读取。`ToUpdateRequest()` 会把清单转换成一个最小 `UpdateRequest`,但不会填充服务端地址和密钥。 + +`ClientStrategy` 在标准工作流开始时也会调用 `AppMetadataDiscoverer.Discover(context)`,从 `InstallPath/generalupdate.manifest.json` 补齐空的身份字段。优先级规则很重要:**代码中已经提供的值优先,清单只补齐空字段**。这意味着你可以用清单作为默认身份来源,同时在特殊环境中用代码覆盖某个字段。 + +### 推荐配置方式 + +对业务应用来说,最清晰的方式是“清单提供身份,代码提供服务端和密钥”。 + +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; + +static UpdateRequest CreateRequestFromManifest() +{ + var manifest = ManifestInfo.Load() + ?? throw new InvalidOperationException("Missing generalupdate.manifest.json."); + + var request = manifest.ToUpdateRequest(); + request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; + request.ReportUrl = "https://update.example.com/api/upgrade/report"; + request.AppSecretKey = Environment.GetEnvironmentVariable("GENERALUPDATE_APP_SECRET") + ?? throw new InvalidOperationException("Missing update secret."); + request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; + + return request; +} + +await new GeneralUpdateBootstrap() + .SetConfig(CreateRequestFromManifest()) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +这种写法的好处是: + +| 不再硬编码 | 仍由代码或环境提供 | +| --- | --- | +| `MainAppName`、`ClientVersion`、`UpdateAppName`、`UpgradeClientVersion`、`ProductId`、`UpdatePath` | `UpdateUrl`、`ReportUrl`、`AppSecretKey`、`Scheme`、`Token`、事件、扩展点、运行选项 | + +### 版本回写 + +清单不只是启动时读取。更新成功后,Core 会把新版本写回安装目录下的同一个 `generalupdate.manifest.json`: + +| 场景 | 回写字段 | +| --- | --- | +| 主程序更新完成 | `ClientVersion` | +| 升级程序自身更新完成 | `UpgradeClientVersion` | + +这样下一次轮询或启动时,Core 会从最新版本继续向服务端验证,而不是继续使用打包时的旧版本。这个行为依赖安装目录可写;如果应用安装在受限目录,需要确保升级程序拥有写入清单的权限。 + ## 运行选项:Option Core 使用强类型 `Option` 注册运行时选项,并通过 `SetOption` 设置值。 @@ -822,6 +950,7 @@ 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`、下载后校验和状态上报。 | 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 1aeea45..7927075 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 @@ -175,7 +175,7 @@ public GeneralUpdateBootstrap SetSource( string? token = null) ``` -`SetSource` is a lightweight entry point when identity metadata is discovered elsewhere, such as from `generalupdate.manifest.json`. +`SetSource` is a lightweight entry point when identity metadata is discovered elsewhere, such as from `generalupdate.manifest.json`. When you need precise control over every manifest identity field, prefer `ManifestInfo.Load().ToUpdateRequest()` followed by `SetConfig(request)`. ```csharp await new GeneralUpdateBootstrap() @@ -281,6 +281,134 @@ var request = new UpdateRequestBuilder() 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. It moves stable metadata such as the main executable name, current version, updater executable name, product ID, and updater directory out of code configuration. Application code can then focus on runtime and sensitive values such as server URLs, secrets, and tokens. + +Place the file in the application install directory, the same directory referenced by `UpdateRequest.InstallPath`. By default, `InstallPath` is `AppDomain.CurrentDomain.BaseDirectory`, so desktop applications normally place the manifest in the main app output root. + +```text +MyProduct/ +├─ ClientSample.exe +├─ generalupdate.manifest.json +└─ update/ + └─ UpgradeSample.exe +``` + +### Manifest structure + +The JSON generated by Tools uses camelCase property names. The Core-side type is `ManifestInfo`. + +```json +{ + "mainAppName": "ClientSample.exe", + "clientVersion": "1.0.0", + "appType": "Client", + "updateAppName": "UpgradeSample.exe", + "upgradeClientVersion": "1.0.0", + "productId": "sample-product", + "updatePath": "update/" +} +``` + +| JSON field | Core field | Description | +| --- | --- | --- | +| `mainAppName` | `MainAppName` | Main application executable name. Used to restart the app after update and identify the current product. | +| `clientVersion` | `ClientVersion` | Current main application version. Core sends it to the server to check whether a main-app update exists. | +| `appType` | `AppType` | Current process role, such as `Client`, `Upgrade`, `OssClient`, or `OssUpgrade`. | +| `updateAppName` | `UpdateAppName` | Updater executable name. Defaults to `Update.exe`. | +| `upgradeClientVersion` | `UpgradeClientVersion` | Updater application's own version. Core uses it to decide whether the updater must be updated first. | +| `productId` | `ProductId` | Product identifier for servers that manage multiple products. | +| `updatePath` | `UpdatePath` | Directory containing the updater. It can be relative to `InstallPath`, for example `update/`. | + +The manifest intentionally does not include `UpdateUrl`, `ReportUrl`, `AppSecretKey`, `Scheme`, or `Token`. This keeps Tools responsible for build-time identity metadata while secrets still come from application code, a configuration service, or the deployment environment. + +### 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. + +### How Core reads the manifest + +Core provides `ManifestInfo`: + +```csharp +using GeneralUpdate.Core.Configuration; + +var manifest = ManifestInfo.Load(); +if (manifest == null) + throw new FileNotFoundException("generalupdate.manifest.json was not found."); + +var request = manifest.ToUpdateRequest(); +request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; +request.ReportUrl = "https://update.example.com/api/upgrade/report"; +request.AppSecretKey = "your-app-secret"; +request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +`ManifestInfo.Load()` reads from `AppDomain.CurrentDomain.BaseDirectory` by default. `ManifestInfo.Load(path)` reads a specific file. `ToUpdateRequest()` converts the manifest into a minimal `UpdateRequest`, but it does not populate server URLs or secrets. + +At the beginning of the standard workflow, `ClientStrategy` also calls `AppMetadataDiscoverer.Discover(context)` and fills empty identity fields from `InstallPath/generalupdate.manifest.json`. The precedence rule matters: **values already provided in code win; the manifest only fills empty fields**. This lets you use the manifest as the default identity source while still overriding individual fields in special environments. + +### Recommended configuration pattern + +For application code, the clearest pattern is "manifest for identity, code/environment for server and secrets". + +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; + +static UpdateRequest CreateRequestFromManifest() +{ + var manifest = ManifestInfo.Load() + ?? throw new InvalidOperationException("Missing generalupdate.manifest.json."); + + var request = manifest.ToUpdateRequest(); + request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; + request.ReportUrl = "https://update.example.com/api/upgrade/report"; + request.AppSecretKey = Environment.GetEnvironmentVariable("GENERALUPDATE_APP_SECRET") + ?? throw new InvalidOperationException("Missing update secret."); + request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; + + return request; +} + +await new GeneralUpdateBootstrap() + .SetConfig(CreateRequestFromManifest()) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +This keeps the split explicit: + +| No longer hard-coded | Still provided by code or environment | +| --- | --- | +| `MainAppName`, `ClientVersion`, `UpdateAppName`, `UpgradeClientVersion`, `ProductId`, `UpdatePath` | `UpdateUrl`, `ReportUrl`, `AppSecretKey`, `Scheme`, `Token`, events, extension points, runtime options | + +### Version write-back + +The manifest is not only read at startup. After a successful update, Core writes the new version back to the same `generalupdate.manifest.json` under the install directory: + +| Scenario | Field written back | +| --- | --- | +| Main application update completed | `ClientVersion` | +| Updater self-update completed | `UpgradeClientVersion` | + +On the next polling cycle or process start, Core therefore validates from the latest applied version instead of the build-time version. 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`. @@ -818,6 +946,7 @@ Core consumes manifests and packages. `GeneralUpdate.Tools` helps generate and v | 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. | 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 9467421..ecf85ec 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 @@ -177,7 +177,7 @@ public GeneralUpdateBootstrap SetSource( string? token = null) ``` -`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。 +`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。若需要精确控制清单中的每个身份字段,建议使用后文的 `ManifestInfo.Load().ToUpdateRequest()` 再调用 `SetConfig(request)`。 ```csharp await new GeneralUpdateBootstrap() @@ -283,6 +283,134 @@ var request = new UpdateRequestBuilder() var request = UpdateRequestBuilder.Create().Build(); ``` +## 应用身份清单:generalupdate.manifest.json + +`generalupdate.manifest.json` 是由 `GeneralUpdate.Tools` 生成、由 Core 消费的应用身份清单。它把“主程序叫什么、当前版本是多少、升级程序叫什么、产品标识是什么、升级程序放在哪个目录”等稳定元数据从代码配置中移出来,代码里只保留服务端地址、密钥、令牌等运行时或敏感参数。 + +推荐把它放在应用安装目录,也就是 `UpdateRequest.InstallPath` 指向的目录。默认情况下 `InstallPath` 是 `AppDomain.CurrentDomain.BaseDirectory`,因此普通桌面应用通常把清单放在主程序输出目录根部。 + +```text +MyProduct/ +├─ ClientSample.exe +├─ generalupdate.manifest.json +└─ update/ + └─ UpgradeSample.exe +``` + +### 清单结构 + +Tools 生成的 JSON 使用小驼峰字段名,Core 中对应类型是 `ManifestInfo`。 + +```json +{ + "mainAppName": "ClientSample.exe", + "clientVersion": "1.0.0", + "appType": "Client", + "updateAppName": "UpgradeSample.exe", + "upgradeClientVersion": "1.0.0", + "productId": "sample-product", + "updatePath": "update/" +} +``` + +| 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 负责构建可发布的身份元数据,而密钥仍由应用代码、配置中心或部署环境提供。 + +### 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 生成清单,再在应用代码中补充服务端入口和密钥。 + +### Core 如何读取清单 + +Core 侧提供 `ManifestInfo`: + +```csharp +using GeneralUpdate.Core.Configuration; + +var manifest = ManifestInfo.Load(); +if (manifest == null) + throw new FileNotFoundException("generalupdate.manifest.json was not found."); + +var request = manifest.ToUpdateRequest(); +request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; +request.ReportUrl = "https://update.example.com/api/upgrade/report"; +request.AppSecretKey = "your-app-secret"; +request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +`ManifestInfo.Load()` 默认从 `AppDomain.CurrentDomain.BaseDirectory` 读取清单;`ManifestInfo.Load(path)` 可以从指定文件读取。`ToUpdateRequest()` 会把清单转换成一个最小 `UpdateRequest`,但不会填充服务端地址和密钥。 + +`ClientStrategy` 在标准工作流开始时也会调用 `AppMetadataDiscoverer.Discover(context)`,从 `InstallPath/generalupdate.manifest.json` 补齐空的身份字段。优先级规则很重要:**代码中已经提供的值优先,清单只补齐空字段**。这意味着你可以用清单作为默认身份来源,同时在特殊环境中用代码覆盖某个字段。 + +### 推荐配置方式 + +对业务应用来说,最清晰的方式是“清单提供身份,代码提供服务端和密钥”。 + +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Core.Configuration; + +static UpdateRequest CreateRequestFromManifest() +{ + var manifest = ManifestInfo.Load() + ?? throw new InvalidOperationException("Missing generalupdate.manifest.json."); + + var request = manifest.ToUpdateRequest(); + request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; + request.ReportUrl = "https://update.example.com/api/upgrade/report"; + request.AppSecretKey = Environment.GetEnvironmentVariable("GENERALUPDATE_APP_SECRET") + ?? throw new InvalidOperationException("Missing update secret."); + request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; + + return request; +} + +await new GeneralUpdateBootstrap() + .SetConfig(CreateRequestFromManifest()) + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +这种写法的好处是: + +| 不再硬编码 | 仍由代码或环境提供 | +| --- | --- | +| `MainAppName`、`ClientVersion`、`UpdateAppName`、`UpgradeClientVersion`、`ProductId`、`UpdatePath` | `UpdateUrl`、`ReportUrl`、`AppSecretKey`、`Scheme`、`Token`、事件、扩展点、运行选项 | + +### 版本回写 + +清单不只是启动时读取。更新成功后,Core 会把新版本写回安装目录下的同一个 `generalupdate.manifest.json`: + +| 场景 | 回写字段 | +| --- | --- | +| 主程序更新完成 | `ClientVersion` | +| 升级程序自身更新完成 | `UpgradeClientVersion` | + +这样下一次轮询或启动时,Core 会从最新版本继续向服务端验证,而不是继续使用打包时的旧版本。这个行为依赖安装目录可写;如果应用安装在受限目录,需要确保升级程序拥有写入清单的权限。 + ## 运行选项:Option Core 使用强类型 `Option` 注册运行时选项,并通过 `SetOption` 设置值。 @@ -822,6 +950,7 @@ 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`、下载后校验和状态上报。 | From b783acd8e0ac5afb5cf800ae7c26bbc3c232faa7 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 01:41:34 +0800 Subject: [PATCH 04/10] Refine Core manifest bootstrap guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 58 ++++++------------- .../current/doc/GeneralUpdate.Core.md | 58 ++++++------------- .../current/doc/GeneralUpdate.Core.md | 58 ++++++------------- 3 files changed, 57 insertions(+), 117 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index ecf85ec..107147f 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -177,7 +177,7 @@ public GeneralUpdateBootstrap SetSource( string? token = null) ``` -`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。若需要精确控制清单中的每个身份字段,建议使用后文的 `ManifestInfo.Load().ToUpdateRequest()` 再调用 `SetConfig(request)`。 +`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json`,只在代码中指定服务端入口和密钥。 ```csharp await new GeneralUpdateBootstrap() @@ -338,65 +338,45 @@ Tools 生成的 JSON 使用小驼峰字段名,Core 中对应类型是 `Manifes 配置界面的发布样例流程还会调用 `SamplePublisherService.PublishAsync(...)`,把主程序输出、升级程序输出和清单一起组织到可运行样例目录中。因此新手不需要从零手写完整 `UpdateRequest`,可以先用 Tools 生成清单,再在应用代码中补充服务端入口和密钥。 -### Core 如何读取清单 +### 配合引导类使用 -Core 侧提供 `ManifestInfo`: +使用清单后,业务代码不需要关心 `MainAppName`、`ClientVersion`、`UpdateAppName`、`UpgradeClientVersion`、`ProductId`、`UpdatePath` 这些身份字段,也不需要手动读取 `generalupdate.manifest.json`。引导类启动更新流程时会在内部读取 `InstallPath/generalupdate.manifest.json`,并把清单中的应用身份信息带入后续的版本检查、下载、启动升级程序和版本回写流程。 -```csharp -using GeneralUpdate.Core.Configuration; - -var manifest = ManifestInfo.Load(); -if (manifest == null) - throw new FileNotFoundException("generalupdate.manifest.json was not found."); - -var request = manifest.ToUpdateRequest(); -request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; -request.ReportUrl = "https://update.example.com/api/upgrade/report"; -request.AppSecretKey = "your-app-secret"; -request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; +默认安装目录就是当前应用目录时,只需要把服务端入口和密钥传给 `SetSource`: +```csharp await new GeneralUpdateBootstrap() - .SetConfig(request) + .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(); ``` -`ManifestInfo.Load()` 默认从 `AppDomain.CurrentDomain.BaseDirectory` 读取清单;`ManifestInfo.Load(path)` 可以从指定文件读取。`ToUpdateRequest()` 会把清单转换成一个最小 `UpdateRequest`,但不会填充服务端地址和密钥。 - -`ClientStrategy` 在标准工作流开始时也会调用 `AppMetadataDiscoverer.Discover(context)`,从 `InstallPath/generalupdate.manifest.json` 补齐空的身份字段。优先级规则很重要:**代码中已经提供的值优先,清单只补齐空字段**。这意味着你可以用清单作为默认身份来源,同时在特殊环境中用代码覆盖某个字段。 - -### 推荐配置方式 - -对业务应用来说,最清晰的方式是“清单提供身份,代码提供服务端和密钥”。 +如果应用的实际安装目录不是当前进程基目录,只需要在 `UpdateRequest` 中补充 `InstallPath`,仍然不需要把清单中的身份字段重复写进代码: ```csharp using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; -static UpdateRequest CreateRequestFromManifest() +var request = new UpdateRequest { - var manifest = ManifestInfo.Load() - ?? throw new InvalidOperationException("Missing generalupdate.manifest.json."); - - var request = manifest.ToUpdateRequest(); - request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; - request.ReportUrl = "https://update.example.com/api/upgrade/report"; - request.AppSecretKey = Environment.GetEnvironmentVariable("GENERALUPDATE_APP_SECRET") - ?? throw new InvalidOperationException("Missing update secret."); - request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; - - return request; -} + 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(CreateRequestFromManifest()) + .SetConfig(request) .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -这种写法的好处是: +推荐的职责拆分是: -| 不再硬编码 | 仍由代码或环境提供 | +| 由 manifest 提供 | 由代码或环境提供 | | --- | --- | | `MainAppName`、`ClientVersion`、`UpdateAppName`、`UpgradeClientVersion`、`ProductId`、`UpdatePath` | `UpdateUrl`、`ReportUrl`、`AppSecretKey`、`Scheme`、`Token`、事件、扩展点、运行选项 | 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 7927075..b4de989 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 @@ -175,7 +175,7 @@ public GeneralUpdateBootstrap SetSource( string? token = null) ``` -`SetSource` is a lightweight entry point when identity metadata is discovered elsewhere, such as from `generalupdate.manifest.json`. When you need precise control over every manifest identity field, prefer `ManifestInfo.Load().ToUpdateRequest()` followed by `SetConfig(request)`. +`SetSource` is a lightweight entry point when identity metadata is provided by `generalupdate.manifest.json`, leaving only the server endpoint and secret in application code. ```csharp await new GeneralUpdateBootstrap() @@ -336,65 +336,45 @@ The `GeneralUpdate.Tools` configuration flow parses the main-app and updater `.c 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. -### How Core reads the manifest +### Using the manifest with the bootstrap -Core provides `ManifestInfo`: +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. -```csharp -using GeneralUpdate.Core.Configuration; - -var manifest = ManifestInfo.Load(); -if (manifest == null) - throw new FileNotFoundException("generalupdate.manifest.json was not found."); - -var request = manifest.ToUpdateRequest(); -request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; -request.ReportUrl = "https://update.example.com/api/upgrade/report"; -request.AppSecretKey = "your-app-secret"; -request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; +When the install directory is the current application directory, pass only the server endpoint and secret to `SetSource`: +```csharp await new GeneralUpdateBootstrap() - .SetConfig(request) + .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(); ``` -`ManifestInfo.Load()` reads from `AppDomain.CurrentDomain.BaseDirectory` by default. `ManifestInfo.Load(path)` reads a specific file. `ToUpdateRequest()` converts the manifest into a minimal `UpdateRequest`, but it does not populate server URLs or secrets. - -At the beginning of the standard workflow, `ClientStrategy` also calls `AppMetadataDiscoverer.Discover(context)` and fills empty identity fields from `InstallPath/generalupdate.manifest.json`. The precedence rule matters: **values already provided in code win; the manifest only fills empty fields**. This lets you use the manifest as the default identity source while still overriding individual fields in special environments. - -### Recommended configuration pattern - -For application code, the clearest pattern is "manifest for identity, code/environment for server and secrets". +If the actual install directory is not the current process base directory, provide `InstallPath` through `UpdateRequest` while still keeping manifest identity fields out of code: ```csharp using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; -static UpdateRequest CreateRequestFromManifest() +var request = new UpdateRequest { - var manifest = ManifestInfo.Load() - ?? throw new InvalidOperationException("Missing generalupdate.manifest.json."); - - var request = manifest.ToUpdateRequest(); - request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; - request.ReportUrl = "https://update.example.com/api/upgrade/report"; - request.AppSecretKey = Environment.GetEnvironmentVariable("GENERALUPDATE_APP_SECRET") - ?? throw new InvalidOperationException("Missing update secret."); - request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; - - return request; -} + 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(CreateRequestFromManifest()) + .SetConfig(request) .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -This keeps the split explicit: +The recommended responsibility split is: -| No longer hard-coded | Still provided by code or environment | +| Provided by manifest | Provided by code or environment | | --- | --- | | `MainAppName`, `ClientVersion`, `UpdateAppName`, `UpgradeClientVersion`, `ProductId`, `UpdatePath` | `UpdateUrl`, `ReportUrl`, `AppSecretKey`, `Scheme`, `Token`, events, extension points, runtime options | 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 ecf85ec..107147f 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 @@ -177,7 +177,7 @@ public GeneralUpdateBootstrap SetSource( string? token = null) ``` -`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。若需要精确控制清单中的每个身份字段,建议使用后文的 `ManifestInfo.Load().ToUpdateRequest()` 再调用 `SetConfig(request)`。 +`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json`,只在代码中指定服务端入口和密钥。 ```csharp await new GeneralUpdateBootstrap() @@ -338,65 +338,45 @@ Tools 生成的 JSON 使用小驼峰字段名,Core 中对应类型是 `Manifes 配置界面的发布样例流程还会调用 `SamplePublisherService.PublishAsync(...)`,把主程序输出、升级程序输出和清单一起组织到可运行样例目录中。因此新手不需要从零手写完整 `UpdateRequest`,可以先用 Tools 生成清单,再在应用代码中补充服务端入口和密钥。 -### Core 如何读取清单 +### 配合引导类使用 -Core 侧提供 `ManifestInfo`: +使用清单后,业务代码不需要关心 `MainAppName`、`ClientVersion`、`UpdateAppName`、`UpgradeClientVersion`、`ProductId`、`UpdatePath` 这些身份字段,也不需要手动读取 `generalupdate.manifest.json`。引导类启动更新流程时会在内部读取 `InstallPath/generalupdate.manifest.json`,并把清单中的应用身份信息带入后续的版本检查、下载、启动升级程序和版本回写流程。 -```csharp -using GeneralUpdate.Core.Configuration; - -var manifest = ManifestInfo.Load(); -if (manifest == null) - throw new FileNotFoundException("generalupdate.manifest.json was not found."); - -var request = manifest.ToUpdateRequest(); -request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; -request.ReportUrl = "https://update.example.com/api/upgrade/report"; -request.AppSecretKey = "your-app-secret"; -request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; +默认安装目录就是当前应用目录时,只需要把服务端入口和密钥传给 `SetSource`: +```csharp await new GeneralUpdateBootstrap() - .SetConfig(request) + .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(); ``` -`ManifestInfo.Load()` 默认从 `AppDomain.CurrentDomain.BaseDirectory` 读取清单;`ManifestInfo.Load(path)` 可以从指定文件读取。`ToUpdateRequest()` 会把清单转换成一个最小 `UpdateRequest`,但不会填充服务端地址和密钥。 - -`ClientStrategy` 在标准工作流开始时也会调用 `AppMetadataDiscoverer.Discover(context)`,从 `InstallPath/generalupdate.manifest.json` 补齐空的身份字段。优先级规则很重要:**代码中已经提供的值优先,清单只补齐空字段**。这意味着你可以用清单作为默认身份来源,同时在特殊环境中用代码覆盖某个字段。 - -### 推荐配置方式 - -对业务应用来说,最清晰的方式是“清单提供身份,代码提供服务端和密钥”。 +如果应用的实际安装目录不是当前进程基目录,只需要在 `UpdateRequest` 中补充 `InstallPath`,仍然不需要把清单中的身份字段重复写进代码: ```csharp using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; -static UpdateRequest CreateRequestFromManifest() +var request = new UpdateRequest { - var manifest = ManifestInfo.Load() - ?? throw new InvalidOperationException("Missing generalupdate.manifest.json."); - - var request = manifest.ToUpdateRequest(); - request.UpdateUrl = "https://update.example.com/api/upgrade/verification"; - request.ReportUrl = "https://update.example.com/api/upgrade/report"; - request.AppSecretKey = Environment.GetEnvironmentVariable("GENERALUPDATE_APP_SECRET") - ?? throw new InvalidOperationException("Missing update secret."); - request.InstallPath = AppDomain.CurrentDomain.BaseDirectory; - - return request; -} + 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(CreateRequestFromManifest()) + .SetConfig(request) .SetOption(Option.AppType, AppType.Client) .LaunchAsync(); ``` -这种写法的好处是: +推荐的职责拆分是: -| 不再硬编码 | 仍由代码或环境提供 | +| 由 manifest 提供 | 由代码或环境提供 | | --- | --- | | `MainAppName`、`ClientVersion`、`UpdateAppName`、`UpgradeClientVersion`、`ProductId`、`UpdatePath` | `UpdateUrl`、`ReportUrl`、`AppSecretKey`、`Scheme`、`Token`、事件、扩展点、运行选项 | From 28af4d7a506a185a5e5cab3772246f7a9989469c Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 01:43:40 +0800 Subject: [PATCH 05/10] Document hook permission script usage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 36 ++++++++++++++++++- .../current/doc/GeneralUpdate.Core.md | 36 ++++++++++++++++++- .../current/doc/GeneralUpdate.Core.md | 36 ++++++++++++++++++- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 107147f..fc6fc7f 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -534,7 +534,7 @@ await new GeneralUpdateBootstrap() ## 生命周期钩子:IUpdateHooks -`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。 +`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。它也是一个非常灵活的开放点:在 Linux 或 macOS 上,更新后的可执行文件可能需要重新赋予执行权限,或者需要先执行企业内部的授权脚本、签名校验脚本、权限修复脚本,再启动主程序;这些操作都可以放在 `OnBeforeStartAppAsync` 中完成。 ```csharp using GeneralUpdate.Core.Hooks; @@ -578,6 +578,40 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +Linux/macOS 场景可以直接注册内置的 `UnixPermissionHooks`,让 Core 在启动应用前执行 `chmod +x`: + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + +如果需要执行自己的赋权脚本,可以封装一个无参 hook 适配器,再通过 `Hooks()` 注册: + +```csharp +using GeneralUpdate.Core.Hooks; + +public sealed class ProductPermissionHooks : IUpdateHooks +{ + private readonly CustomPermissionHooks _inner = + new("/opt/my-product/scripts/fix-permissions.sh"); + + public Task OnBeforeStartAppAsync(HookContext ctx) + => _inner.OnBeforeStartAppAsync(ctx); + + public Task OnBeforeUpdateAsync(HookContext ctx) => Task.FromResult(true); + public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; + public Task OnAfterUpdateAsync(HookContext ctx) => Task.CompletedTask; + public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) => Task.CompletedTask; +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + 内置实现包括: | 类型 | 说明 | 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 b4de989..691b921 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 @@ -532,7 +532,7 @@ Registered types must have a parameterless constructor because Core creates them ## Lifecycle hooks: IUpdateHooks -`IUpdateHooks` is best for business logic before update, after download, after update, before app start, and on errors. +`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 using GeneralUpdate.Core.Hooks; @@ -576,6 +576,40 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +On Linux/macOS, you can register the built-in `UnixPermissionHooks` to let Core run `chmod +x` before starting the app: + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + +If you need to run your own permission script, wrap it in a parameterless hook adapter and register that adapter with `Hooks()`: + +```csharp +using GeneralUpdate.Core.Hooks; + +public sealed class ProductPermissionHooks : IUpdateHooks +{ + private readonly CustomPermissionHooks _inner = + new("/opt/my-product/scripts/fix-permissions.sh"); + + public Task OnBeforeStartAppAsync(HookContext ctx) + => _inner.OnBeforeStartAppAsync(ctx); + + public Task OnBeforeUpdateAsync(HookContext ctx) => Task.FromResult(true); + public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; + public Task OnAfterUpdateAsync(HookContext ctx) => Task.CompletedTask; + public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) => Task.CompletedTask; +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + Built-in hook types: | Type | Description | 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 107147f..fc6fc7f 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 @@ -534,7 +534,7 @@ await new GeneralUpdateBootstrap() ## 生命周期钩子:IUpdateHooks -`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。 +`IUpdateHooks` 适合处理“更新前检查、下载完成后处理、更新完成后清理、启动应用前准备、异常处理”等业务逻辑。它也是一个非常灵活的开放点:在 Linux 或 macOS 上,更新后的可执行文件可能需要重新赋予执行权限,或者需要先执行企业内部的授权脚本、签名校验脚本、权限修复脚本,再启动主程序;这些操作都可以放在 `OnBeforeStartAppAsync` 中完成。 ```csharp using GeneralUpdate.Core.Hooks; @@ -578,6 +578,40 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +Linux/macOS 场景可以直接注册内置的 `UnixPermissionHooks`,让 Core 在启动应用前执行 `chmod +x`: + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + +如果需要执行自己的赋权脚本,可以封装一个无参 hook 适配器,再通过 `Hooks()` 注册: + +```csharp +using GeneralUpdate.Core.Hooks; + +public sealed class ProductPermissionHooks : IUpdateHooks +{ + private readonly CustomPermissionHooks _inner = + new("/opt/my-product/scripts/fix-permissions.sh"); + + public Task OnBeforeStartAppAsync(HookContext ctx) + => _inner.OnBeforeStartAppAsync(ctx); + + public Task OnBeforeUpdateAsync(HookContext ctx) => Task.FromResult(true); + public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; + public Task OnAfterUpdateAsync(HookContext ctx) => Task.CompletedTask; + public Task OnUpdateErrorAsync(HookContext ctx, Exception ex) => Task.CompletedTask; +} + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .Hooks() + .LaunchAsync(); +``` + 内置实现包括: | 类型 | 说明 | From 1af32d1ef1a32095b80dfd736ce10d611155ba62 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 01:47:14 +0800 Subject: [PATCH 06/10] Remove driver references from Core docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 1 - .../current/doc/GeneralUpdate.Core.md | 1 - .../current/doc/GeneralUpdate.Core.md | 1 - 3 files changed, 3 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index fc6fc7f..f51946f 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -253,7 +253,6 @@ await new GeneralUpdateBootstrap() | `Files` | 更新时跳过的指定文件。 | | `Formats` | 更新时跳过的扩展名,例如 `.log`。 | | `Directories` | 更新时跳过的目录。 | -| `DriverDirectory` | 驱动目录;驱动安装属于 Drivelution 文档范围。 | ### 使用 UpdateRequestBuilder 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 691b921..8d34a7c 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 @@ -251,7 +251,6 @@ await new GeneralUpdateBootstrap() | `Files` | Specific files to skip during update. | | `Formats` | File extensions to skip, such as `.log`. | | `Directories` | Directories to skip. | -| `DriverDirectory` | Driver directory; driver installation is documented under Drivelution. | ### UpdateRequestBuilder 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 fc6fc7f..f51946f 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 @@ -253,7 +253,6 @@ await new GeneralUpdateBootstrap() | `Files` | 更新时跳过的指定文件。 | | `Formats` | 更新时跳过的扩展名,例如 `.log`。 | | `Directories` | 更新时跳过的目录。 | -| `DriverDirectory` | 驱动目录;驱动安装属于 Drivelution 文档范围。 | ### 使用 UpdateRequestBuilder From 564c8bcd6bc1f6d244963ec54c1073721f4fae43 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 01:49:23 +0800 Subject: [PATCH 07/10] Remove firmware scope row from Core docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 1 - .../current/doc/GeneralUpdate.Core.md | 1 - .../current/doc/GeneralUpdate.Core.md | 1 - 3 files changed, 3 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index f51946f..0a17cd2 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -26,7 +26,6 @@ Core 负责“执行更新”,不负责生成更新包,也不直接管理服 | 校验与应用补丁 | 是 | 支持 Hash 校验、压缩包处理、差分补丁管道。 | | 文件替换与重启应用 | 是 | `Upgrade` / `OssUpgrade` 角色用于独立升级程序。 | | 生成差分包 | 否 | 推荐使用 `GeneralUpdate.Tools`。 | -| 固件升级 | 否 | 固件升级组件不在本页范围内。 | ## 入口类:GeneralUpdateBootstrap 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 8d34a7c..2c985d0 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 @@ -26,7 +26,6 @@ Core executes updates. It does not generate update packages or manage the server | 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`. | -| Firmware update | No | Firmware components are out of scope for this page. | ## Entry point: GeneralUpdateBootstrap 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 f51946f..0a17cd2 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 @@ -26,7 +26,6 @@ Core 负责“执行更新”,不负责生成更新包,也不直接管理服 | 校验与应用补丁 | 是 | 支持 Hash 校验、压缩包处理、差分补丁管道。 | | 文件替换与重启应用 | 是 | `Upgrade` / `OssUpgrade` 角色用于独立升级程序。 | | 生成差分包 | 否 | 推荐使用 `GeneralUpdate.Tools`。 | -| 固件升级 | 否 | 固件升级组件不在本页范围内。 | ## 入口类:GeneralUpdateBootstrap From 4a787a71c22424779d3d4be68837dc3bf0396667 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 01:53:05 +0800 Subject: [PATCH 08/10] Clarify manifest version write-back behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 4 ++-- .../current/doc/GeneralUpdate.Core.md | 4 ++-- .../current/doc/GeneralUpdate.Core.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 0a17cd2..9f22780 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -380,14 +380,14 @@ await new GeneralUpdateBootstrap() ### 版本回写 -清单不只是启动时读取。更新成功后,Core 会把新版本写回安装目录下的同一个 `generalupdate.manifest.json`: +在 `generalupdate.manifest.json` 体系下,清单同时也是本地版本状态文件。开发者只需要在首次发布时通过 Tools 生成清单,不需要在每次更新完成后再写业务代码去修改本地版本号。更新成功后,Core 会把已应用的新版本自动写回安装目录下的同一个 `generalupdate.manifest.json`: | 场景 | 回写字段 | | --- | --- | | 主程序更新完成 | `ClientVersion` | | 升级程序自身更新完成 | `UpgradeClientVersion` | -这样下一次轮询或启动时,Core 会从最新版本继续向服务端验证,而不是继续使用打包时的旧版本。这个行为依赖安装目录可写;如果应用安装在受限目录,需要确保升级程序拥有写入清单的权限。 +这样下一次轮询或启动时,引导类会基于清单中的最新本地版本继续向服务端验证,而不是继续使用打包时的旧版本。回写的意义是把“本地版本号维护”收进 Core 的更新流程里,避免开发者在应用代码中额外维护 `ClientVersion` 或 `UpgradeClientVersion`。这个行为依赖安装目录可写;如果应用安装在受限目录,需要确保升级程序拥有写入清单的权限。 ## 运行选项:Option 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 2c985d0..252ad3c 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 @@ -378,14 +378,14 @@ The recommended responsibility split is: ### Version write-back -The manifest is not only read at startup. After a successful update, Core writes the new version back to the same `generalupdate.manifest.json` under the install directory: +In the `generalupdate.manifest.json` model, the manifest is also the local version state file. Developers generate it with Tools for the first release, but they do not need to write application code that updates the local version after every successful update. After an update succeeds, Core automatically writes the applied version back to the same `generalupdate.manifest.json` under the install directory: | Scenario | Field written back | | --- | --- | | Main application update completed | `ClientVersion` | | Updater self-update completed | `UpgradeClientVersion` | -On the next polling cycle or process start, Core therefore validates from the latest applied version instead of the build-time version. 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. +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 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 0a17cd2..9f22780 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 @@ -380,14 +380,14 @@ await new GeneralUpdateBootstrap() ### 版本回写 -清单不只是启动时读取。更新成功后,Core 会把新版本写回安装目录下的同一个 `generalupdate.manifest.json`: +在 `generalupdate.manifest.json` 体系下,清单同时也是本地版本状态文件。开发者只需要在首次发布时通过 Tools 生成清单,不需要在每次更新完成后再写业务代码去修改本地版本号。更新成功后,Core 会把已应用的新版本自动写回安装目录下的同一个 `generalupdate.manifest.json`: | 场景 | 回写字段 | | --- | --- | | 主程序更新完成 | `ClientVersion` | | 升级程序自身更新完成 | `UpgradeClientVersion` | -这样下一次轮询或启动时,Core 会从最新版本继续向服务端验证,而不是继续使用打包时的旧版本。这个行为依赖安装目录可写;如果应用安装在受限目录,需要确保升级程序拥有写入清单的权限。 +这样下一次轮询或启动时,引导类会基于清单中的最新本地版本继续向服务端验证,而不是继续使用打包时的旧版本。回写的意义是把“本地版本号维护”收进 Core 的更新流程里,避免开发者在应用代码中额外维护 `ClientVersion` 或 `UpgradeClientVersion`。这个行为依赖安装目录可写;如果应用安装在受限目录,需要确保升级程序拥有写入清单的权限。 ## 运行选项:Option From 9ee95505a2d68c2d9ac164cfbc608aabbe24c224 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 02:30:08 +0800 Subject: [PATCH 09/10] Expand GeneralUpdate Core component docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 313 ++++++++++++++--- .../current/doc/GeneralUpdate.Core.md | 314 +++++++++++++++--- .../current/doc/GeneralUpdate.Core.md | 313 ++++++++++++++--- 3 files changed, 823 insertions(+), 117 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 9f22780..5fe8107 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -14,6 +14,24 @@ sidebar_position: 5 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-的关系) | + ## 组件能力边界 Core 负责“执行更新”,不负责生成更新包,也不直接管理服务端后台。 @@ -78,6 +96,104 @@ await new GeneralUpdateBootstrap() > 当升级程序由主程序启动时,Core 会通过加密文件 IPC 自动恢复更新上下文,通常不需要在升级程序里再次调用 `SetConfig`。 +## 执行策略总览 {#execution-strategies} + +Core 内置三类上层执行策略:标准更新策略、OSS 更新策略和静默更新策略。它们不是互相独立的 API,而是由 `LaunchAsync()` 根据 `Option.AppType`、`Option.Silent` 和当前配置自动选择。 + +| 策略 | 触发条件 | 主要角色 | 适用场景 | +| --- | --- | --- | --- | +| [标准更新策略](#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 +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://cdn.example.com/myapp_versions.json", + appSecretKey: "oss-mode-secret") + .SetOption(Option.AppType, AppType.OssClient) + .LaunchAsync(); +``` + +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.OssUpgrade) + .LaunchAsync(); +``` + +### 静默更新策略 {#silent-update-strategy} + +静默更新只在 `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 @@ -196,7 +312,7 @@ await new GeneralUpdateBootstrap() public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) ``` -`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,Core 会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。 +`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,引导类会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。关于算法差异、补丁阶段和并发设置,请看 [差分算法与补丁管道](#differential-pipeline)。 ```csharp using GeneralUpdate.Core.Differential; @@ -223,6 +339,72 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +## 差分算法与补丁管道 {#differential-pipeline} + +Core 的差分能力分两层:`IBinaryDiffer` 负责“单个文件如何生成/应用补丁”,`DiffPipeline` 负责“目录中哪些文件需要补丁、哪些文件是新增/删除、如何并行处理多个文件”。普通使用者只需要打开 `Option.PatchEnabled`;需要调优性能或兼容性时,再通过 `UseDiffPipeline(...)` 调整。 + +### 差分算法类型 + +| 算法/实现 | 默认位置 | 特点 | 适合场景 | +| --- | --- | --- | --- | +| `BsdiffDiffer` | `GeneralUpdateBootstrap` 默认使用 | 经典 BSDIFF 4.0 算法,默认使用 BZip2 压缩,补丁格式兼容性强。 | 追求稳定兼容、已有包体系基于 BSDIFF 的项目。 | +| `StreamingHdiffDiffer` | `DiffPipeline` 直接构造时的默认 differ;也可通过 `UseDiffPipeline` 显式选择 | 使用块级 Hash 索引做候选匹配,典型复杂度更低,默认使用 Deflate,并生成可由 Dirty 阶段读取的 BSDIFF 兼容补丁格式。 | 大文件较多、希望降低生成补丁时内存和 CPU 压力的项目。 | +| 自定义 `IBinaryDiffer` | 通过 `UseDiffPipeline(builder => builder.UseDiffer(...))` 接入 | 完全替换单文件差分算法。 | 企业内部已有补丁格式、加密补丁或专用二进制差分算法。 | + +`BsdiffDiffer` 还支持替换压缩提供器:`BZip2CompressionProvider` 是兼容默认值,`DeflateCompressionProvider` 更偏向速度,`.NET 6+` 可使用 `BrotliCompressionProvider` 在压缩率和解压速度之间取得更好平衡。选择压缩提供器时要确保生成补丁和应用补丁的运行时都能识别对应格式版本。 + +```csharp +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Differential.Abstractions; +using GeneralUpdate.Differential.Differ; + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new BsdiffDiffer(new DeflateCompressionProvider())) + .WithParallelism(4); + }) + .SetOption(Option.PatchEnabled, true) + .LaunchAsync(); +``` + +### Clean 和 Dirty 两个阶段 + +| 阶段 | 方法 | 运行位置 | 作用 | +| --- | --- | --- | --- | +| Clean | `DiffPipeline.CleanAsync(oldDir, newDir, patchDir)` | 发布侧/工具侧 | 对比旧版本和新版本目录,生成 `.patch` 文件,复制新增文件,并写入 `generalupdate.delete.json` 删除清单。 | +| Dirty | `DiffPipeline.DirtyAsync(appDir, patchDir)` | 客户端升级侧 | 读取补丁目录,把 `.patch` 应用到旧文件,复制新增文件,按删除清单移除旧文件。 | + +Core 主要消费 Dirty 阶段;差分包生成建议交给 `GeneralUpdate.Tools` 或发布流水线完成。标准更新中,下载完成后升级程序会先解压包,再在 `PatchEnabled = true` 时通过 `PatchMiddleware` 调用 `DiffPipeline.DirtyAsync(...)` 应用补丁。 + +### 下载并发与差分并行 {#download-diff-concurrency} + +Core 支持两个层面的多线程能力:下载阶段可以并发下载多个更新资源,差分阶段可以并行处理多个文件补丁。内置标准流程是“先完成当前下载计划,再进入解压/差分/替换阶段”;如果你需要把下载和应用做成更细粒度的流水线,可以通过自定义 `IDownloadOrchestrator` 或自定义 `IStrategy` 接管。 + +| 层级 | 控制 API | 默认行为 | 说明 | +| --- | --- | --- | --- | +| 批量下载并发 | `Option.DiffMode` + `Option.MaxConcurrency` | `DiffMode.Serial` 会强制下载并发为 `1`;`DiffMode.Parallel` 使用 `MaxConcurrency`,并被限制在 `1` 到 `Environment.ProcessorCount * 2` 之间。 | 由 `DefaultDownloadOrchestrator` 使用 `SemaphoreSlim` 控制,支持重试、断点续传和校验。 | +| 差分文件并行 | `UseDiffPipeline(...WithParallelism(n))` | 引导类默认 `2`。 | `DiffPipeline` 对多个文件创建任务,并用 `SemaphoreSlim` 限制同时生成/应用补丁的文件数。 | +| 下载后处理 | `DownloadPipeline()` | 默认 SHA256 校验。 | 每个资源下载成功后执行,可替换为解密、扫描、二次校验等。 | + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 6) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4); + }) + .LaunchAsync(); +``` + +并发值不是越大越好。网络慢但磁盘快时可以提高 `Option.MaxConcurrency`;补丁文件很多且磁盘是 SSD 时可以提高 `WithParallelism`;机械硬盘、低配终端或后台静默更新建议降低并发,避免影响主程序响应。 + ## 配置模型:UpdateRequest `UpdateRequest` 是外部调用者最常用的配置对象。它继承 `UpdateConfiguration`,并在 `Validate()` 中检查关键字段。 @@ -283,7 +465,9 @@ var request = UpdateRequestBuilder.Create().Build(); ## 应用身份清单:generalupdate.manifest.json -`generalupdate.manifest.json` 是由 `GeneralUpdate.Tools` 生成、由 Core 消费的应用身份清单。它把“主程序叫什么、当前版本是多少、升级程序叫什么、产品标识是什么、升级程序放在哪个目录”等稳定元数据从代码配置中移出来,代码里只保留服务端地址、密钥、令牌等运行时或敏感参数。 +`generalupdate.manifest.json` 是由 `GeneralUpdate.Tools` 生成、由 Core 消费的应用身份清单。它的核心价值是**帮开发者节约接入和维护时间**:Tools 把“主程序叫什么、当前版本是多少、升级程序叫什么、产品标识是什么、升级程序放在哪个目录”等稳定元数据生成到清单里,Core 在运行时自动消费这些信息,业务代码只需要补充服务端地址、密钥、令牌等运行时或敏感参数。 + +换句话说,使用 manifest 后,接入 GeneralUpdate 不再需要手写一大段完整 `UpdateRequest`。发布时让 Tools 生成 `generalupdate.manifest.json`,运行时再配少量敏感信息,就可以直接启动更新流程。这是 Core 推荐的极简配置方式。 推荐把它放在应用安装目录,也就是 `UpdateRequest.InstallPath` 指向的目录。默认情况下 `InstallPath` 是 `AppDomain.CurrentDomain.BaseDirectory`,因此普通桌面应用通常把清单放在主程序输出目录根部。 @@ -323,6 +507,23 @@ Tools 生成的 JSON 使用小驼峰字段名,Core 中对应类型是 `Manifes 清单刻意不包含 `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`。 @@ -405,7 +606,7 @@ await new GeneralUpdateBootstrap() | 选项 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `Option.AppType` | `AppType` | `Client` | 当前进程角色。 | -| `Option.DiffMode` | `DiffMode` | `Serial` | 差分执行模式。 | +| `Option.DiffMode` | `DiffMode` | `Serial` | 执行模式。`Serial` 会让默认下载编排器串行下载;`Parallel` 允许按 `Option.MaxConcurrency` 并发下载。 | | `Option.Encoding` | `Encoding` | `UTF8` | 压缩包处理编码。 | | `Option.Format` | `Format` | `Zip` | 更新包格式。 | | `Option.DownloadTimeout` | `int?` | `30` | 下载超时时间,单位秒。 | @@ -414,7 +615,7 @@ await new GeneralUpdateBootstrap() | `Option.Silent` | `bool` | `false` | 是否启用静默轮询更新。 | | `Option.SilentPollIntervalMinutes` | `int` | `60` | 静默模式轮询间隔。 | | `Option.LaunchClientAfterUpdate` | `bool` | `true` | 升级后是否启动主程序。 | -| `Option.MaxConcurrency` | `int` | `3` | 下载最大并发数。 | +| `Option.MaxConcurrency` | `int` | `3` | 默认下载编排器最大并发数,实际值会被限制到合理范围。 | | `Option.EnableResume` | `bool` | `true` | 是否启用断点续传。 | | `Option.RetryCount` | `int` | `3` | 下载重试次数。 | | `Option.VerifyChecksum` | `bool` | `true` | 是否校验下载文件 Hash。 | @@ -422,22 +623,30 @@ await new GeneralUpdateBootstrap() 如果传入 `null` 给可空选项,`SetOption` 会移除当前设置,后续读取回到默认值。 -## 事件 API +## 事件 API {#事件-api} 事件适合观察更新过程,不应该承载复杂业务流程。复杂流程建议封装成 `IUpdateHooks` 或 cookbook 中的完整方案。 ### 单个事件回调 -| 方法 | 参数类型 | 触发时机 | -| --- | --- | --- | -| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` | 服务端版本信息返回后。 | -| `AddListenerUpdatePrecheck` | `Func` | 下载开始前,返回 `true` 继续,返回 `false` 中止。 | -| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` | 下载过程中持续触发。 | -| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` | 单个版本下载结束。 | -| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` | 所有下载任务结束。 | -| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` | 下载失败。 | -| `AddListenerProgress` | `ProgressEventArgs` | 下载进度或差分补丁进度变化。 | -| `AddListenerException` | `ExceptionEventArgs` | Core 捕获异常。 | +单个事件回调适合在启动器链式配置中直接订阅某一个通知。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`。 | 做下载前的轻量决策,例如磁盘不足、用户选择稍后、网络类型不允许时返回 `true` 跳过。需要异步、可取消或有副作用的流程请使用 `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` 是“可以跳过”,不是“继续下载”。如果你只是想观察版本信息,不要注册 precheck;如果你要在用户取消、磁盘不足、移动网络等场景阻止非强制更新,才返回 `true`。 ```csharp await new GeneralUpdateBootstrap() @@ -453,23 +662,31 @@ await new GeneralUpdateBootstrap() .Where(d => d.IsReady) .Any(d => d.AvailableFreeSpace > 1024L * 1024 * 1024); - return hasUpdate && enoughDisk; + // 当前实现中返回 true 表示跳过非强制更新,返回 false 表示继续。 + return !hasUpdate || !enoughDisk; }) - .AddListenerMultiDownloadStatistics((_, e) => + .AddListenerMultiDownloadCompleted((_, e) => { - Console.WriteLine($"{e.ProgressPercentage}% {e.Speed} {e.BytesReceived}/{e.TotalBytesToReceive}"); + Console.WriteLine($"{e.Version}: {(e.IsCompleted ? "completed" : "failed")}"); }) - .AddListenerMultiDownloadCompleted((_, e) => + .AddListenerMultiAllDownloadCompleted((_, e) => + { + Console.WriteLine(e.IsAllDownloadCompleted + ? "All downloads completed." + : $"Failed downloads: {e.FailedVersions.Count}"); + }) + .AddListenerMultiDownloadError((_, e) => { - Console.WriteLine(e.IsCompleted ? "Download completed." : "Download failed."); + Console.WriteLine($"Download failed: {e.Version}"); + Console.WriteLine(e.Exception); }) .AddListenerProgress((_, e) => { if (e.Progress != null) - Console.WriteLine($"Download: {e.Progress.Percentage}%"); + 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}"); + Console.WriteLine($"Patch: {e.DiffProgress.Completed}/{e.DiffProgress.Total} {e.DiffProgress.CurrentFile}"); }) .AddListenerException((_, e) => { @@ -494,9 +711,10 @@ public sealed class ConsoleUpdateListener : UpdateEventListenerBase Console.WriteLine($"Update count: {args.Info?.Body?.Count ?? 0}"); } - public override void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) + public override void OnProgress(ProgressEventArgs args) { - Console.WriteLine($"{args.ProgressPercentage}% {args.Speed}"); + if (args.Progress != null) + Console.WriteLine($"{args.Progress.AssetName}: {args.Progress.Percentage:F1}%"); } public override void OnException(ExceptionEventArgs args) @@ -511,6 +729,37 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +## 日志与性能 {#logging-performance} + +Core 内置 `GeneralTracer`,默认开启。它基于 `System.Diagnostics.Trace` 输出日志:Windows 下会写入调试输出窗口,同时写到控制台,并在应用基目录创建 `Logs/generalupdate-trace yyyy-MM-dd.log` 文件。文件监听器使用后台队列写入,但每条日志仍会做开关检查、时间格式化、调用栈定位和入队/输出,因此在性能敏感场景可以关闭。 + +| API | 作用 | +| --- | --- | +| `GeneralTracer.SetTracingEnabled(false)` | 关闭 Core 日志输出。关闭后 `Debug` / `Info` / `Warn` / `Error` / `Fatal` 会快速返回,同时 Trace listener 被过滤。 | +| `GeneralTracer.SetTracingEnabled(true)` | 重新开启日志输出。适合诊断版本、灰度排查或用户反馈问题时启用。 | +| `GeneralTracer.IsTracingEnabled()` | 查询当前日志开关状态。 | +| `GeneralTracer.Dispose()` | 释放文件监听器并清空 Trace listeners,通常只在测试、工具进程退出或你明确接管 Trace listeners 时使用。 | + +建议在应用启动早期设置日志开关,确保更新流程中的下载、校验、差分和替换日志都遵循同一个策略。 + +```csharp +using GeneralUpdate.Core; + +if (performanceMode) +{ + GeneralTracer.SetTracingEnabled(false); +} + +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret") + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +关闭日志适合低功耗设备、I/O 较慢的终端、静默后台轮询、批量自动化更新或对启动耗时极敏感的产品形态。排查线上问题时建议临时开启,因为 Core 的日志会记录策略分派、下载编排、校验、差分、Hook 和异常路径。 + ## 扩展点总览 扩展点由 `AbstractBootstrap` 提供,所有注册方法都返回当前 bootstrap,可链式调用。 @@ -939,21 +1188,7 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` -## 静默更新选项 - -静默更新通过选项启用,不需要额外接口。启用后,`Client` 角色会启动后台轮询并立即返回。 - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.Silent, true) - .SetOption(Option.SilentPollIntervalMinutes, 30) - .SetOption(Option.LaunchClientAfterUpdate, true) - .LaunchAsync(); -``` - -适合把“是否启用、何时提示用户、如何处理退出时升级”等完整策略写到 cookbook,而组件文档只需要说明相关 API。 +> 静默更新不是单独的扩展接口,而是内置执行策略。配置方式和生命周期见 [静默更新策略](#silent-update-strategy)。 ## 与 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 252ad3c..1c6ff6f 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 @@ -14,6 +14,24 @@ sidebar_position: 5 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) | + ## Responsibility boundary Core executes updates. It does not generate update packages or manage the server backend. @@ -76,6 +94,104 @@ await new GeneralUpdateBootstrap() When the updater is launched by the main app, Core restores the update context through encrypted file IPC. In that case the updater normally does not call `SetConfig` again. +## Execution strategy overview {#execution-strategies} + +Core includes three upper-level execution strategies: standard update, OSS update, and silent update. They are not separate public APIs. `LaunchAsync()` selects them from `Option.AppType`, `Option.Silent`, and the current configuration. + +| Strategy | Trigger | Main roles | Best for | +| --- | --- | --- | --- | +| [Standard update strategy](#standard-update-strategy) | `Option.AppType = AppType.Client` or `AppType.Upgrade`, with `Option.Silent = false` | Main app checks/downloads; updater replaces files | Desktop apps with a version-check API, standard package update, and manifest version write-back | +| [OSS update strategy](#oss-update-strategy) | `Option.AppType = AppType.OssClient` or `AppType.OssUpgrade` | Main app downloads OSS version config; updater downloads and extracts assets | Apps whose version config and packages are hosted on object storage/CDN | +| [Silent update strategy](#silent-update-strategy) | `Option.AppType = AppType.Client` and `Option.Silent = true` | Main app polls in the background; updater starts on process exit | Products that want no-interruption download preparation and deferred replacement | + +### Standard update strategy {#standard-update-strategy} + +Standard update is implemented by `ClientStrategy` and `UpdateStrategy`. `ClientStrategy` runs in the main application and discovers the local manifest, requests server versions, builds a download plan, downloads packages, prepares encrypted IPC context, and launches the updater. `UpdateStrategy` runs in the updater process and reads IPC context, extracts packages, applies differential patches, replaces files, writes versions back, and optionally starts the main app. + +The core standard workflow is: + +1. The main app calls `SetConfig(...)` or `SetSource(...)`, then `LaunchAsync()`. +2. Core automatically reads `generalupdate.manifest.json` from the install directory and fills identity fields that were not explicitly provided, including `MainAppName`, `UpdateAppName`, `ClientVersion`, `UpgradeClientVersion`, `ProductId`, and `InstallPath`. +3. `ClientStrategy` uses `DownloadSource` to get server assets and compares main-app and updater versions independently. +4. `DownloadPlanBuilder` filters frozen packages, sorts by version, checks `MinClientVersion`, and builds the download plan. +5. Download uses `IDownloadOrchestrator` to download assets in batch. The default orchestrator supports concurrency, resume, retry, and SHA256 verification. +6. Core splits assets by `AppType` into updater packages and main-app packages, then dispatches by scenario. +7. Updater packages can be applied from the main-app side first and write back `UpgradeClientVersion`; main-app packages are passed to the updater through encrypted IPC. +8. After the updater replaces files, Core automatically writes `ClientVersion` back through the manifest system. Application code does not need to maintain the local version manually. + +| Scenario | Condition | Core behavior | +| --- | --- | --- | +| `None` | Neither main app nor updater needs update | Dispatches the no-update event and exits. | +| `UpgradeOnly` | Only the updater needs update | The main app downloads updater packages, applies them to the updater directory, writes back `UpgradeClientVersion`, and keeps running. | +| `MainOnly` | Only the main app needs update | The main app downloads main-app packages, writes IPC context, and launches the updater to replace main-app files. | +| `Both` | Both main app and updater need update | Core updates the updater first and writes back `UpgradeClientVersion`, then hands main-app packages to the new updater. | + +```csharp +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret", + reportUrl: "https://update.example.com/api/upgrade/report") + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.PatchEnabled, true) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 4) + .LaunchAsync(); +``` + +### OSS update strategy {#oss-update-strategy} + +OSS update uses the same `OssStrategy` with two roles: `OssClient` and `OssUpgrade`. It is intended for hosting the version JSON and update packages on OSS, S3, MinIO, CDN, or a static file server, without depending on the standard version-check API. + +| Role | Local behavior | Key configuration | +| --- | --- | --- | +| `AppType.OssClient` | Downloads the OSS version config from `UpdateUrl` into the install directory, compares the remote latest version with local `ClientVersion`, starts the updater if needed, and exits. | `UpdateUrl` points to the version config file. `MainAppName` / `UpdateAppName` can come from the manifest. | +| `AppType.OssUpgrade` | Reads the local version config or a custom `DownloadSource`, filters assets newer than the local version, downloads them to the install directory, extracts ZIP files, deletes archives, starts the main app, and exits. | The install directory must be writable. Versions in the asset list must be comparable. | + +The OSS version config is saved as `{MainAppName}_versions.json` or `{UpdateAppName}_versions.json`. Registering `DownloadSource()` lets the OSS updater skip the default file-reading path and return assets from your own source. Registering `DownloadOrchestrator()` can fully replace the download process. + +```csharp +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://cdn.example.com/myapp_versions.json", + appSecretKey: "oss-mode-secret") + .SetOption(Option.AppType, AppType.OssClient) + .LaunchAsync(); +``` + +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.OssUpgrade) + .LaunchAsync(); +``` + +### Silent update strategy {#silent-update-strategy} + +Silent update only applies to `AppType.Client`. When enabled, `LaunchAsync()` enters the silent branch, creates the same fully configured `ClientStrategy` as standard update, sets `LaunchAfterPrepare` to `false`, then hands it to `SilentPollOrchestrator` for background polling. + +Silent mode does not reimplement update logic. It moves "check and download" into the background and defers "start updater and replace files" until process exit. The user can keep using the current process while packages are prepared, and actual replacement happens after the app exits. + +| Stage | Standard update | Silent update | +| --- | --- | --- | +| Version check | Runs once after user/code triggers update | Runs in the background every `Option.SilentPollIntervalMinutes` | +| Download | Downloads immediately after update is found | Downloads in the background after update is found | +| Start updater | Starts after the main app prepares update context | Starts from the `ProcessExit` handler | +| User experience | Explicit "check/update now" flow | No-interruption update preparation | + +```csharp +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret") + .SetOption(Option.AppType, AppType.Client) + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 30) + .SetOption(Option.LaunchClientAfterUpdate, true) + .LaunchAsync(); +``` + +Silent update still uses registered `IUpdateHooks`, `IUpdateReporter`, download extensions, SSL policy, auth policy, and differential pipeline. It is "silent download preparation", not "silent in-place file replacement"; main-app files should still be replaced by the updater after the main app exits. + ### Cancel ```csharp @@ -194,7 +310,7 @@ await new GeneralUpdateBootstrap() public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) ``` -`UseDiffPipeline` customizes differential patch processing. Without it, Core builds a default pipeline using `BsdiffDiffer`, `DefaultCleanMatcher`, `DefaultDirtyMatcher`, parallelism `2`, and the Core progress reporter. +`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; @@ -221,6 +337,72 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +## Differential algorithms and patch pipeline {#differential-pipeline} + +Core's differential support has two layers: `IBinaryDiffer` defines how a single file generates/applies a patch, and `DiffPipeline` decides which files in a directory need patches, which files are new/deleted, and how multiple files are processed in parallel. Most users only need `Option.PatchEnabled`; tune `UseDiffPipeline(...)` only when you need performance or compatibility control. + +### Differential algorithm types + +| Algorithm / implementation | Default position | Characteristics | Best for | +| --- | --- | --- | --- | +| `BsdiffDiffer` | Default used by `GeneralUpdateBootstrap` | Classic BSDIFF 4.0 algorithm, BZip2 by default, strong patch-format compatibility. | Projects that prioritize compatibility or already use BSDIFF-based packages. | +| `StreamingHdiffDiffer` | Default differ when constructing `DiffPipeline` directly; can also be selected through `UseDiffPipeline` | Uses block-hash indexing for candidate matching, lower typical complexity, Deflate by default, and emits a BSDIFF-compatible patch format readable by Dirty. | Projects with large files that want to reduce patch-generation memory/CPU pressure. | +| Custom `IBinaryDiffer` | Registered with `UseDiffPipeline(builder => builder.UseDiffer(...))` | Fully replaces the single-file binary differ. | Internal patch formats, encrypted patches, or domain-specific binary diff algorithms. | + +`BsdiffDiffer` can also use a custom compression provider. `BZip2CompressionProvider` is the compatible default, `DeflateCompressionProvider` favors speed, and `.NET 6+` can use `BrotliCompressionProvider` for a better compression/decompression balance. Make sure the runtime that applies patches can recognize the generated format version. + +```csharp +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Differential.Abstractions; +using GeneralUpdate.Differential.Differ; + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new BsdiffDiffer(new DeflateCompressionProvider())) + .WithParallelism(4); + }) + .SetOption(Option.PatchEnabled, true) + .LaunchAsync(); +``` + +### Clean and Dirty phases + +| Phase | Method | Runs on | Purpose | +| --- | --- | --- | --- | +| Clean | `DiffPipeline.CleanAsync(oldDir, newDir, patchDir)` | Publishing/tooling side | Compares old and new directories, creates `.patch` files, copies new files, and writes `generalupdate.delete.json`. | +| Dirty | `DiffPipeline.DirtyAsync(appDir, patchDir)` | Client updater side | Applies `.patch` files to old files, copies new files, and removes old files from the delete manifest. | + +Core mainly consumes the Dirty phase; patch generation should normally be handled by `GeneralUpdate.Tools` or your release pipeline. In standard update, after downloads finish, the updater extracts packages and calls `DiffPipeline.DirtyAsync(...)` through `PatchMiddleware` when `PatchEnabled = true`. + +### Download concurrency and differential parallelism {#download-diff-concurrency} + +Core supports two layers of multithreaded work: the download phase can download multiple assets concurrently, and the differential phase can process multiple file patches in parallel. The built-in standard flow completes the current download plan before entering extract/diff/replace. If you need a finer-grained download/apply pipeline, take over with a custom `IDownloadOrchestrator` or custom `IStrategy`. + +| Layer | Control API | Default behavior | Notes | +| --- | --- | --- | --- | +| Batch download concurrency | `Option.DiffMode` + `Option.MaxConcurrency` | `DiffMode.Serial` forces download concurrency to `1`; `DiffMode.Parallel` uses `MaxConcurrency`, clamped to `1` through `Environment.ProcessorCount * 2`. | `DefaultDownloadOrchestrator` uses `SemaphoreSlim` and supports retry, resume, and verification. | +| Differential file parallelism | `UseDiffPipeline(...WithParallelism(n))` | Bootstrap default is `2`. | `DiffPipeline` creates tasks per file and limits simultaneous patch generation/application with `SemaphoreSlim`. | +| Post-download processing | `DownloadPipeline()` | Default SHA256 verification. | Runs after each successful asset download; can be replaced with decryption, scanning, or extra validation. | + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 6) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4); + }) + .LaunchAsync(); +``` + +Higher concurrency is not always better. Increase `Option.MaxConcurrency` when the network is slow but disk is fast; increase `WithParallelism` when there are many patch files and the device has SSD storage; lower both for HDDs, low-end machines, or silent background updates to avoid hurting main-app responsiveness. + ## Configuration model: UpdateRequest `UpdateRequest` is the main external configuration object. It inherits `UpdateConfiguration` and validates required fields in `Validate()`. @@ -281,7 +463,9 @@ 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. It moves stable metadata such as the main executable name, current version, updater executable name, product ID, and updater directory out of code configuration. Application code can then focus on runtime and sensitive values such as server URLs, secrets, and tokens. +`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. @@ -321,6 +505,23 @@ The JSON generated by Tools uses camelCase property names. The Core-side type is 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`. @@ -403,7 +604,7 @@ await new GeneralUpdateBootstrap() | Option | Type | Default | Description | | --- | --- | --- | --- | | `Option.AppType` | `AppType` | `Client` | Current process role. | -| `Option.DiffMode` | `DiffMode` | `Serial` | Differential execution mode. | +| `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. | @@ -412,7 +613,7 @@ await new GeneralUpdateBootstrap() | `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 concurrent downloads. | +| `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. | @@ -420,22 +621,30 @@ await new GeneralUpdateBootstrap() Passing `null` to a nullable option removes the custom value and falls back to the default. -## Events +## Events {#event-api} Events are for observing update state. Complex business flow should be implemented with `IUpdateHooks` or documented in cookbook workflows. ### Individual callbacks -| Method | Argument type | Trigger | -| --- | --- | --- | -| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` | After server version metadata is returned. | -| `AddListenerUpdatePrecheck` | `Func` | Before download starts; `true` continues, `false` aborts. | -| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` | During download. | -| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` | One version download completes. | -| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` | All download tasks complete. | -| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` | Download failure. | -| `AddListenerProgress` | `ProgressEventArgs` | Download or differential progress changes. | -| `AddListenerException` | `ExceptionEventArgs` | Core catches an exception. | +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. + +| 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 ignore this skip path and continue. | Same input as `UpdateInfoEventArgs`. | Make lightweight pre-download decisions. Return `true` for "skip now" cases such as low disk space, user chose later, or disallowed network type. 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". If you only want to observe version metadata, do not register a precheck callback. Return `true` only when you want to stop a non-forced update because the user cancelled, disk space is low, the current network is not allowed, and so on. ```csharp await new GeneralUpdateBootstrap() @@ -451,23 +660,32 @@ await new GeneralUpdateBootstrap() .Where(d => d.IsReady) .Any(d => d.AvailableFreeSpace > 1024L * 1024 * 1024); - return hasUpdate && enoughDisk; + // In the current implementation, true means "skip non-forced update"; + // false means "continue". + return !hasUpdate || !enoughDisk; }) - .AddListenerMultiDownloadStatistics((_, e) => + .AddListenerMultiDownloadCompleted((_, e) => { - Console.WriteLine($"{e.ProgressPercentage}% {e.Speed} {e.BytesReceived}/{e.TotalBytesToReceive}"); + Console.WriteLine($"{e.Version}: {(e.IsCompleted ? "completed" : "failed")}"); }) - .AddListenerMultiDownloadCompleted((_, e) => + .AddListenerMultiAllDownloadCompleted((_, e) => + { + Console.WriteLine(e.IsAllDownloadCompleted + ? "All downloads completed." + : $"Failed downloads: {e.FailedVersions.Count}"); + }) + .AddListenerMultiDownloadError((_, e) => { - Console.WriteLine(e.IsCompleted ? "Download completed." : "Download failed."); + Console.WriteLine($"Download failed: {e.Version}"); + Console.WriteLine(e.Exception); }) .AddListenerProgress((_, e) => { if (e.Progress != null) - Console.WriteLine($"Download: {e.Progress.Percentage}%"); + 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}"); + Console.WriteLine($"Patch: {e.DiffProgress.Completed}/{e.DiffProgress.Total} {e.DiffProgress.CurrentFile}"); }) .AddListenerException((_, e) => { @@ -492,9 +710,10 @@ public sealed class ConsoleUpdateListener : UpdateEventListenerBase Console.WriteLine($"Update count: {args.Info?.Body?.Count ?? 0}"); } - public override void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) + public override void OnProgress(ProgressEventArgs args) { - Console.WriteLine($"{args.ProgressPercentage}% {args.Speed}"); + if (args.Progress != null) + Console.WriteLine($"{args.Progress.AssetName}: {args.Progress.Percentage:F1}%"); } public override void OnException(ExceptionEventArgs args) @@ -509,6 +728,37 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +## Logging and performance {#logging-performance} + +Core includes `GeneralTracer`, enabled by default. It writes through `System.Diagnostics.Trace`: on Windows it also writes to the debug output window, it writes to the console, and it creates `Logs/generalupdate-trace yyyy-MM-dd.log` under the application base directory. The file listener writes through a background queue, but every log still performs an enable check, timestamp formatting, stack-frame lookup, and queue/output work. Disable tracing in performance-sensitive scenarios. + +| 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 +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 All extension registration methods are provided by `AbstractBootstrap` and can be chained. @@ -935,21 +1185,7 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` -## Silent update options - -Silent update is enabled through options rather than a separate interface. When enabled, the `Client` role starts background polling and returns immediately. - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.Silent, true) - .SetOption(Option.SilentPollIntervalMinutes, 30) - .SetOption(Option.LaunchClientAfterUpdate, true) - .LaunchAsync(); -``` - -The full product decision around prompting users, exit-time upgrade, and rollout policy should be covered in cookbooks. +> Silent update is not a separate extension interface. It is a built-in execution strategy. See [Silent update strategy](#silent-update-strategy) for configuration and lifecycle details. ## Relationship with GeneralUpdate.Tools 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 9f22780..5fe8107 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 @@ -14,6 +14,24 @@ sidebar_position: 5 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-的关系) | + ## 组件能力边界 Core 负责“执行更新”,不负责生成更新包,也不直接管理服务端后台。 @@ -78,6 +96,104 @@ await new GeneralUpdateBootstrap() > 当升级程序由主程序启动时,Core 会通过加密文件 IPC 自动恢复更新上下文,通常不需要在升级程序里再次调用 `SetConfig`。 +## 执行策略总览 {#execution-strategies} + +Core 内置三类上层执行策略:标准更新策略、OSS 更新策略和静默更新策略。它们不是互相独立的 API,而是由 `LaunchAsync()` 根据 `Option.AppType`、`Option.Silent` 和当前配置自动选择。 + +| 策略 | 触发条件 | 主要角色 | 适用场景 | +| --- | --- | --- | --- | +| [标准更新策略](#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 +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://cdn.example.com/myapp_versions.json", + appSecretKey: "oss-mode-secret") + .SetOption(Option.AppType, AppType.OssClient) + .LaunchAsync(); +``` + +```csharp +await new GeneralUpdateBootstrap() + .SetOption(Option.AppType, AppType.OssUpgrade) + .LaunchAsync(); +``` + +### 静默更新策略 {#silent-update-strategy} + +静默更新只在 `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 @@ -196,7 +312,7 @@ await new GeneralUpdateBootstrap() public GeneralUpdateBootstrap UseDiffPipeline(Action? configure) ``` -`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,Core 会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。 +`UseDiffPipeline` 用于替换或调整差分补丁管道。未调用时,引导类会创建默认管道:`BsdiffDiffer`、`DefaultCleanMatcher`、`DefaultDirtyMatcher`、并行度 `2`,并接入 Core 的差分进度事件。关于算法差异、补丁阶段和并发设置,请看 [差分算法与补丁管道](#differential-pipeline)。 ```csharp using GeneralUpdate.Core.Differential; @@ -223,6 +339,72 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +## 差分算法与补丁管道 {#differential-pipeline} + +Core 的差分能力分两层:`IBinaryDiffer` 负责“单个文件如何生成/应用补丁”,`DiffPipeline` 负责“目录中哪些文件需要补丁、哪些文件是新增/删除、如何并行处理多个文件”。普通使用者只需要打开 `Option.PatchEnabled`;需要调优性能或兼容性时,再通过 `UseDiffPipeline(...)` 调整。 + +### 差分算法类型 + +| 算法/实现 | 默认位置 | 特点 | 适合场景 | +| --- | --- | --- | --- | +| `BsdiffDiffer` | `GeneralUpdateBootstrap` 默认使用 | 经典 BSDIFF 4.0 算法,默认使用 BZip2 压缩,补丁格式兼容性强。 | 追求稳定兼容、已有包体系基于 BSDIFF 的项目。 | +| `StreamingHdiffDiffer` | `DiffPipeline` 直接构造时的默认 differ;也可通过 `UseDiffPipeline` 显式选择 | 使用块级 Hash 索引做候选匹配,典型复杂度更低,默认使用 Deflate,并生成可由 Dirty 阶段读取的 BSDIFF 兼容补丁格式。 | 大文件较多、希望降低生成补丁时内存和 CPU 压力的项目。 | +| 自定义 `IBinaryDiffer` | 通过 `UseDiffPipeline(builder => builder.UseDiffer(...))` 接入 | 完全替换单文件差分算法。 | 企业内部已有补丁格式、加密补丁或专用二进制差分算法。 | + +`BsdiffDiffer` 还支持替换压缩提供器:`BZip2CompressionProvider` 是兼容默认值,`DeflateCompressionProvider` 更偏向速度,`.NET 6+` 可使用 `BrotliCompressionProvider` 在压缩率和解压速度之间取得更好平衡。选择压缩提供器时要确保生成补丁和应用补丁的运行时都能识别对应格式版本。 + +```csharp +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Differential.Abstractions; +using GeneralUpdate.Differential.Differ; + +await new GeneralUpdateBootstrap() + .SetConfig(request) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new BsdiffDiffer(new DeflateCompressionProvider())) + .WithParallelism(4); + }) + .SetOption(Option.PatchEnabled, true) + .LaunchAsync(); +``` + +### Clean 和 Dirty 两个阶段 + +| 阶段 | 方法 | 运行位置 | 作用 | +| --- | --- | --- | --- | +| Clean | `DiffPipeline.CleanAsync(oldDir, newDir, patchDir)` | 发布侧/工具侧 | 对比旧版本和新版本目录,生成 `.patch` 文件,复制新增文件,并写入 `generalupdate.delete.json` 删除清单。 | +| Dirty | `DiffPipeline.DirtyAsync(appDir, patchDir)` | 客户端升级侧 | 读取补丁目录,把 `.patch` 应用到旧文件,复制新增文件,按删除清单移除旧文件。 | + +Core 主要消费 Dirty 阶段;差分包生成建议交给 `GeneralUpdate.Tools` 或发布流水线完成。标准更新中,下载完成后升级程序会先解压包,再在 `PatchEnabled = true` 时通过 `PatchMiddleware` 调用 `DiffPipeline.DirtyAsync(...)` 应用补丁。 + +### 下载并发与差分并行 {#download-diff-concurrency} + +Core 支持两个层面的多线程能力:下载阶段可以并发下载多个更新资源,差分阶段可以并行处理多个文件补丁。内置标准流程是“先完成当前下载计划,再进入解压/差分/替换阶段”;如果你需要把下载和应用做成更细粒度的流水线,可以通过自定义 `IDownloadOrchestrator` 或自定义 `IStrategy` 接管。 + +| 层级 | 控制 API | 默认行为 | 说明 | +| --- | --- | --- | --- | +| 批量下载并发 | `Option.DiffMode` + `Option.MaxConcurrency` | `DiffMode.Serial` 会强制下载并发为 `1`;`DiffMode.Parallel` 使用 `MaxConcurrency`,并被限制在 `1` 到 `Environment.ProcessorCount * 2` 之间。 | 由 `DefaultDownloadOrchestrator` 使用 `SemaphoreSlim` 控制,支持重试、断点续传和校验。 | +| 差分文件并行 | `UseDiffPipeline(...WithParallelism(n))` | 引导类默认 `2`。 | `DiffPipeline` 对多个文件创建任务,并用 `SemaphoreSlim` 限制同时生成/应用补丁的文件数。 | +| 下载后处理 | `DownloadPipeline()` | 默认 SHA256 校验。 | 每个资源下载成功后执行,可替换为解密、扫描、二次校验等。 | + +```csharp +await new GeneralUpdateBootstrap() + .SetConfig(request) + .SetOption(Option.DiffMode, DiffMode.Parallel) + .SetOption(Option.MaxConcurrency, 6) + .UseDiffPipeline(builder => + { + builder + .UseDiffer(new StreamingHdiffDiffer()) + .WithParallelism(4); + }) + .LaunchAsync(); +``` + +并发值不是越大越好。网络慢但磁盘快时可以提高 `Option.MaxConcurrency`;补丁文件很多且磁盘是 SSD 时可以提高 `WithParallelism`;机械硬盘、低配终端或后台静默更新建议降低并发,避免影响主程序响应。 + ## 配置模型:UpdateRequest `UpdateRequest` 是外部调用者最常用的配置对象。它继承 `UpdateConfiguration`,并在 `Validate()` 中检查关键字段。 @@ -283,7 +465,9 @@ var request = UpdateRequestBuilder.Create().Build(); ## 应用身份清单:generalupdate.manifest.json -`generalupdate.manifest.json` 是由 `GeneralUpdate.Tools` 生成、由 Core 消费的应用身份清单。它把“主程序叫什么、当前版本是多少、升级程序叫什么、产品标识是什么、升级程序放在哪个目录”等稳定元数据从代码配置中移出来,代码里只保留服务端地址、密钥、令牌等运行时或敏感参数。 +`generalupdate.manifest.json` 是由 `GeneralUpdate.Tools` 生成、由 Core 消费的应用身份清单。它的核心价值是**帮开发者节约接入和维护时间**:Tools 把“主程序叫什么、当前版本是多少、升级程序叫什么、产品标识是什么、升级程序放在哪个目录”等稳定元数据生成到清单里,Core 在运行时自动消费这些信息,业务代码只需要补充服务端地址、密钥、令牌等运行时或敏感参数。 + +换句话说,使用 manifest 后,接入 GeneralUpdate 不再需要手写一大段完整 `UpdateRequest`。发布时让 Tools 生成 `generalupdate.manifest.json`,运行时再配少量敏感信息,就可以直接启动更新流程。这是 Core 推荐的极简配置方式。 推荐把它放在应用安装目录,也就是 `UpdateRequest.InstallPath` 指向的目录。默认情况下 `InstallPath` 是 `AppDomain.CurrentDomain.BaseDirectory`,因此普通桌面应用通常把清单放在主程序输出目录根部。 @@ -323,6 +507,23 @@ Tools 生成的 JSON 使用小驼峰字段名,Core 中对应类型是 `Manifes 清单刻意不包含 `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`。 @@ -405,7 +606,7 @@ await new GeneralUpdateBootstrap() | 选项 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `Option.AppType` | `AppType` | `Client` | 当前进程角色。 | -| `Option.DiffMode` | `DiffMode` | `Serial` | 差分执行模式。 | +| `Option.DiffMode` | `DiffMode` | `Serial` | 执行模式。`Serial` 会让默认下载编排器串行下载;`Parallel` 允许按 `Option.MaxConcurrency` 并发下载。 | | `Option.Encoding` | `Encoding` | `UTF8` | 压缩包处理编码。 | | `Option.Format` | `Format` | `Zip` | 更新包格式。 | | `Option.DownloadTimeout` | `int?` | `30` | 下载超时时间,单位秒。 | @@ -414,7 +615,7 @@ await new GeneralUpdateBootstrap() | `Option.Silent` | `bool` | `false` | 是否启用静默轮询更新。 | | `Option.SilentPollIntervalMinutes` | `int` | `60` | 静默模式轮询间隔。 | | `Option.LaunchClientAfterUpdate` | `bool` | `true` | 升级后是否启动主程序。 | -| `Option.MaxConcurrency` | `int` | `3` | 下载最大并发数。 | +| `Option.MaxConcurrency` | `int` | `3` | 默认下载编排器最大并发数,实际值会被限制到合理范围。 | | `Option.EnableResume` | `bool` | `true` | 是否启用断点续传。 | | `Option.RetryCount` | `int` | `3` | 下载重试次数。 | | `Option.VerifyChecksum` | `bool` | `true` | 是否校验下载文件 Hash。 | @@ -422,22 +623,30 @@ await new GeneralUpdateBootstrap() 如果传入 `null` 给可空选项,`SetOption` 会移除当前设置,后续读取回到默认值。 -## 事件 API +## 事件 API {#事件-api} 事件适合观察更新过程,不应该承载复杂业务流程。复杂流程建议封装成 `IUpdateHooks` 或 cookbook 中的完整方案。 ### 单个事件回调 -| 方法 | 参数类型 | 触发时机 | -| --- | --- | --- | -| `AddListenerUpdateInfo` | `UpdateInfoEventArgs` | 服务端版本信息返回后。 | -| `AddListenerUpdatePrecheck` | `Func` | 下载开始前,返回 `true` 继续,返回 `false` 中止。 | -| `AddListenerMultiDownloadStatistics` | `MultiDownloadStatisticsEventArgs` | 下载过程中持续触发。 | -| `AddListenerMultiDownloadCompleted` | `MultiDownloadCompletedEventArgs` | 单个版本下载结束。 | -| `AddListenerMultiAllDownloadCompleted` | `MultiAllDownloadCompletedEventArgs` | 所有下载任务结束。 | -| `AddListenerMultiDownloadError` | `MultiDownloadErrorEventArgs` | 下载失败。 | -| `AddListenerProgress` | `ProgressEventArgs` | 下载进度或差分补丁进度变化。 | -| `AddListenerException` | `ExceptionEventArgs` | Core 捕获异常。 | +单个事件回调适合在启动器链式配置中直接订阅某一个通知。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`。 | 做下载前的轻量决策,例如磁盘不足、用户选择稍后、网络类型不允许时返回 `true` 跳过。需要异步、可取消或有副作用的流程请使用 `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` 是“可以跳过”,不是“继续下载”。如果你只是想观察版本信息,不要注册 precheck;如果你要在用户取消、磁盘不足、移动网络等场景阻止非强制更新,才返回 `true`。 ```csharp await new GeneralUpdateBootstrap() @@ -453,23 +662,31 @@ await new GeneralUpdateBootstrap() .Where(d => d.IsReady) .Any(d => d.AvailableFreeSpace > 1024L * 1024 * 1024); - return hasUpdate && enoughDisk; + // 当前实现中返回 true 表示跳过非强制更新,返回 false 表示继续。 + return !hasUpdate || !enoughDisk; }) - .AddListenerMultiDownloadStatistics((_, e) => + .AddListenerMultiDownloadCompleted((_, e) => { - Console.WriteLine($"{e.ProgressPercentage}% {e.Speed} {e.BytesReceived}/{e.TotalBytesToReceive}"); + Console.WriteLine($"{e.Version}: {(e.IsCompleted ? "completed" : "failed")}"); }) - .AddListenerMultiDownloadCompleted((_, e) => + .AddListenerMultiAllDownloadCompleted((_, e) => + { + Console.WriteLine(e.IsAllDownloadCompleted + ? "All downloads completed." + : $"Failed downloads: {e.FailedVersions.Count}"); + }) + .AddListenerMultiDownloadError((_, e) => { - Console.WriteLine(e.IsCompleted ? "Download completed." : "Download failed."); + Console.WriteLine($"Download failed: {e.Version}"); + Console.WriteLine(e.Exception); }) .AddListenerProgress((_, e) => { if (e.Progress != null) - Console.WriteLine($"Download: {e.Progress.Percentage}%"); + 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}"); + Console.WriteLine($"Patch: {e.DiffProgress.Completed}/{e.DiffProgress.Total} {e.DiffProgress.CurrentFile}"); }) .AddListenerException((_, e) => { @@ -494,9 +711,10 @@ public sealed class ConsoleUpdateListener : UpdateEventListenerBase Console.WriteLine($"Update count: {args.Info?.Body?.Count ?? 0}"); } - public override void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) + public override void OnProgress(ProgressEventArgs args) { - Console.WriteLine($"{args.ProgressPercentage}% {args.Speed}"); + if (args.Progress != null) + Console.WriteLine($"{args.Progress.AssetName}: {args.Progress.Percentage:F1}%"); } public override void OnException(ExceptionEventArgs args) @@ -511,6 +729,37 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` +## 日志与性能 {#logging-performance} + +Core 内置 `GeneralTracer`,默认开启。它基于 `System.Diagnostics.Trace` 输出日志:Windows 下会写入调试输出窗口,同时写到控制台,并在应用基目录创建 `Logs/generalupdate-trace yyyy-MM-dd.log` 文件。文件监听器使用后台队列写入,但每条日志仍会做开关检查、时间格式化、调用栈定位和入队/输出,因此在性能敏感场景可以关闭。 + +| API | 作用 | +| --- | --- | +| `GeneralTracer.SetTracingEnabled(false)` | 关闭 Core 日志输出。关闭后 `Debug` / `Info` / `Warn` / `Error` / `Fatal` 会快速返回,同时 Trace listener 被过滤。 | +| `GeneralTracer.SetTracingEnabled(true)` | 重新开启日志输出。适合诊断版本、灰度排查或用户反馈问题时启用。 | +| `GeneralTracer.IsTracingEnabled()` | 查询当前日志开关状态。 | +| `GeneralTracer.Dispose()` | 释放文件监听器并清空 Trace listeners,通常只在测试、工具进程退出或你明确接管 Trace listeners 时使用。 | + +建议在应用启动早期设置日志开关,确保更新流程中的下载、校验、差分和替换日志都遵循同一个策略。 + +```csharp +using GeneralUpdate.Core; + +if (performanceMode) +{ + GeneralTracer.SetTracingEnabled(false); +} + +await new GeneralUpdateBootstrap() + .SetSource( + updateUrl: "https://update.example.com/api/upgrade/verification", + appSecretKey: "your-app-secret") + .SetOption(Option.AppType, AppType.Client) + .LaunchAsync(); +``` + +关闭日志适合低功耗设备、I/O 较慢的终端、静默后台轮询、批量自动化更新或对启动耗时极敏感的产品形态。排查线上问题时建议临时开启,因为 Core 的日志会记录策略分派、下载编排、校验、差分、Hook 和异常路径。 + ## 扩展点总览 扩展点由 `AbstractBootstrap` 提供,所有注册方法都返回当前 bootstrap,可链式调用。 @@ -939,21 +1188,7 @@ await new GeneralUpdateBootstrap() .LaunchAsync(); ``` -## 静默更新选项 - -静默更新通过选项启用,不需要额外接口。启用后,`Client` 角色会启动后台轮询并立即返回。 - -```csharp -await new GeneralUpdateBootstrap() - .SetConfig(request) - .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.Silent, true) - .SetOption(Option.SilentPollIntervalMinutes, 30) - .SetOption(Option.LaunchClientAfterUpdate, true) - .LaunchAsync(); -``` - -适合把“是否启用、何时提示用户、如何处理退出时升级”等完整策略写到 cookbook,而组件文档只需要说明相关 API。 +> 静默更新不是单独的扩展接口,而是内置执行策略。配置方式和生命周期见 [静默更新策略](#silent-update-strategy)。 ## 与 GeneralUpdate.Tools 的关系 From 214789257666208af5fe685aa2c2d342c6c7445a Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 1 Jun 2026 02:36:16 +0800 Subject: [PATCH 10/10] Clarify update precheck event usage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- website/docs/doc/GeneralUpdate.Core.md | 10 ++++++---- .../current/doc/GeneralUpdate.Core.md | 10 ++++++---- .../current/doc/GeneralUpdate.Core.md | 10 ++++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/website/docs/doc/GeneralUpdate.Core.md b/website/docs/doc/GeneralUpdate.Core.md index 5fe8107..25f9311 100644 --- a/website/docs/doc/GeneralUpdate.Core.md +++ b/website/docs/doc/GeneralUpdate.Core.md @@ -636,7 +636,7 @@ await new GeneralUpdateBootstrap() | 方法 | 参数类型 | 当前代码中的触发时机 | 关键字段 | 推荐用途 | | --- | --- | --- | --- | --- | | `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`。 | 做下载前的轻量决策,例如磁盘不足、用户选择稍后、网络类型不允许时返回 `true` 跳过。需要异步、可取消或有副作用的流程请使用 `IUpdateHooks.OnBeforeUpdateAsync`。 | +| `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、输出失败汇总、决定是否展示重试入口。 | @@ -646,7 +646,7 @@ await new GeneralUpdateBootstrap() `UpdateInfoEventArgs.Info.Body` 中的元素是 Core 经过版本对比、应用类型筛选和下载计划构建后需要处理的版本包,不是简单的原始 HTTP 响应透传。需要关注下载 URL、Hash、强制更新、跨版本差分范围时,可以直接读取 `VersionEntry` 上的属性。 -`AddListenerUpdatePrecheck` 的返回值容易误解:以当前代码为准,返回 `true` 是“可以跳过”,不是“继续下载”。如果你只是想观察版本信息,不要注册 precheck;如果你要在用户取消、磁盘不足、移动网络等场景阻止非强制更新,才返回 `true`。 +`AddListenerUpdatePrecheck` 的返回值容易误解:以当前代码为准,返回 `true` 是“可以跳过”,不是“继续下载”。它适合放在“下载前确认”这个场景里:先从 `UpdateInfoEventArgs.Info.Body` 整理本次更新涉及的版本号、更新日志、包大小、升级类型等内容,弹窗给用户阅读;用户确认更新时返回 `false` 继续,用户选择稍后、磁盘不足或当前网络不允许时返回 `true` 跳过非强制更新。如果只是展示服务端版本信息、不需要决定是否跳过,可以只监听 `AddListenerUpdateInfo`。 ```csharp await new GeneralUpdateBootstrap() @@ -657,13 +657,15 @@ await new GeneralUpdateBootstrap() }) .AddListenerUpdatePrecheck(e => { - var hasUpdate = (e.Info?.Body?.Count ?? 0) > 0; + 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; + return !hasUpdate || !enoughDisk || userRejected; }) .AddListenerMultiDownloadCompleted((_, e) => { 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 1c6ff6f..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 @@ -634,7 +634,7 @@ Callbacks may be raised from the update workflow thread, download task threads, | 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 ignore this skip path and continue. | Same input as `UpdateInfoEventArgs`. | Make lightweight pre-download decisions. Return `true` for "skip now" cases such as low disk space, user chose later, or disallowed network type. Use `IUpdateHooks.OnBeforeUpdateAsync` for asynchronous, cancelable, or side-effecting workflows. | +| `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. | @@ -644,7 +644,7 @@ Callbacks may be raised from the update workflow thread, download task threads, 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". If you only want to observe version metadata, do not register a precheck callback. Return `true` only when you want to stop a non-forced update because the user cancelled, disk space is low, the current network is not allowed, and so on. +`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() @@ -655,14 +655,16 @@ await new GeneralUpdateBootstrap() }) .AddListenerUpdatePrecheck(e => { - var hasUpdate = (e.Info?.Body?.Count ?? 0) > 0; + 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; + return !hasUpdate || !enoughDisk || userRejected; }) .AddListenerMultiDownloadCompleted((_, e) => { 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 5fe8107..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 @@ -636,7 +636,7 @@ await new GeneralUpdateBootstrap() | 方法 | 参数类型 | 当前代码中的触发时机 | 关键字段 | 推荐用途 | | --- | --- | --- | --- | --- | | `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`。 | 做下载前的轻量决策,例如磁盘不足、用户选择稍后、网络类型不允许时返回 `true` 跳过。需要异步、可取消或有副作用的流程请使用 `IUpdateHooks.OnBeforeUpdateAsync`。 | +| `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、输出失败汇总、决定是否展示重试入口。 | @@ -646,7 +646,7 @@ await new GeneralUpdateBootstrap() `UpdateInfoEventArgs.Info.Body` 中的元素是 Core 经过版本对比、应用类型筛选和下载计划构建后需要处理的版本包,不是简单的原始 HTTP 响应透传。需要关注下载 URL、Hash、强制更新、跨版本差分范围时,可以直接读取 `VersionEntry` 上的属性。 -`AddListenerUpdatePrecheck` 的返回值容易误解:以当前代码为准,返回 `true` 是“可以跳过”,不是“继续下载”。如果你只是想观察版本信息,不要注册 precheck;如果你要在用户取消、磁盘不足、移动网络等场景阻止非强制更新,才返回 `true`。 +`AddListenerUpdatePrecheck` 的返回值容易误解:以当前代码为准,返回 `true` 是“可以跳过”,不是“继续下载”。它适合放在“下载前确认”这个场景里:先从 `UpdateInfoEventArgs.Info.Body` 整理本次更新涉及的版本号、更新日志、包大小、升级类型等内容,弹窗给用户阅读;用户确认更新时返回 `false` 继续,用户选择稍后、磁盘不足或当前网络不允许时返回 `true` 跳过非强制更新。如果只是展示服务端版本信息、不需要决定是否跳过,可以只监听 `AddListenerUpdateInfo`。 ```csharp await new GeneralUpdateBootstrap() @@ -657,13 +657,15 @@ await new GeneralUpdateBootstrap() }) .AddListenerUpdatePrecheck(e => { - var hasUpdate = (e.Info?.Body?.Count ?? 0) > 0; + 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; + return !hasUpdate || !enoughDisk || userRejected; }) .AddListenerMultiDownloadCompleted((_, e) => {