Vernext 3.0.0
dotnet add package Vernext --version 3.0.0
NuGet\Install-Package Vernext -Version 3.0.0
<PackageReference Include="Vernext" Version="3.0.0" />
<PackageVersion Include="Vernext" Version="3.0.0" />
<PackageReference Include="Vernext" />
paket add Vernext --version 3.0.0
#r "nuget: Vernext, 3.0.0"
#:package Vernext@3.0.0
#addin nuget:?package=Vernext&version=3.0.0
#tool nuget:?package=Vernext&version=3.0.0
Vernext SDK
最低 .NET 8,一个 Vernext NuGet,提供检查、下载校验、准备和更新交接。默认 Rust helper 按 RID 随应用交付;helper 无需外部 .NET Runtime,应用不配置 helper 密钥或选择语言。SDK 可独立使用,不要求配套 Server、Publisher、UI 框架或 DI 容器。
安装与平台
本说明对应 3.0.0,从 NuGet.org 安装:
dotnet add package Vernext --version 3.0.0
包目标 RID 为 win-x64、linux-x64、osx-arm64、osx-x64;本机生成的测试候选只含当前 RID。正式发布流程在 Windows、Linux、macOS arm64/x64 上执行原生安装测试,再用同一个四 RID 包验证 .NET 8 JIT 单文件与 Native AOT 消费,通过后才上传。UI 组合、最低 OS、Developer ID 和公证分别以实测为准,历史发行证据不替代当前版本验收。
3.0.0 增加只读 IUpdateManager.CurrentVersion,展示与检查共用同一版本;自行实现该接口的消费者需要补齐属性。默认检查器在解析时使用 DI 最终配置,单独注册的 UpdateOptions 实例或工厂可放在 AddVernext() 前后。提交使用 CommitAsync(bool restart, CancellationToken),多实例保护按需调用 AcquireApplicationLock();失败回滚和事务清理由组件内部承担,无启动恢复组合入口或公开历史结果查询。
helper 与签名保持为应用旁的独立文件;许可证和第三方声明也会自动复制到构建与发布目录。分发应用时保留这些文件,无需额外配置复制步骤。
macOS 直接发布到 .app/Contents/MacOS/ 时,SDK 自动把 .sig 和 Vernext 许可放入 Contents/Resources/,避免数据进入代码目录而破坏 bundle 签名。helper 不应在生成 Vernext .sig 后重签或改写;应用自身的 bundle 签名和公证仍属于应用发行流程。
当前包的 macOS helper 尚未完成 Apple Developer ID 签名和公证,本地 ad-hoc 签名验收不代表 Gatekeeper 放行。从互联网下载的消费应用可能被拦截或需要手动允许。需要正式公证时,不能只签外层 .app:应先取得已完成 Developer ID 签名、再生成匹配 Vernext .sig 的 helper 资产并随 SDK 组包,然后签名和公证完整应用。源码维护者可以自行签名组包,但不能直接重签现有包内的 helper;普通消费应用无需配置 helper 密钥。
Vernext 采用 MIT 许可,全文随包提供。项目地址为 github.com/zeekmeta/Vernext;源码仓库目前为私有,计划后续开源。安装和使用 NuGet 包不需要仓库访问权限。
不使用容器
普通 JSON 文件和返回相同 JSON 的 API 使用同一入口,不根据 URL 后缀判断协议:
using Vernext;
using var updater = UpdateManager.Create(
new Uri("https://downloads.example.com/my-app/win-x64.json"));
Console.WriteLine(updater.CurrentVersion);
如果使用配套 Server,从管理台复制应用配置。SDK 组合渠道和当前 RID 的地址,并验证发布签名和目标身份:
using var updater = UpdateManager.Create(new UpdateOptions
{
ServiceAddress = new Uri("https://updates.example.com/"),
ApplicationId = "my-app",
PublicKeyPem = publicKeyPem
});
updater.CurrentVersion 是检查更新实际使用的当前版本,界面可直接读取。优先使用 UpdateOptions.CurrentVersion,否则读取入口程序集的 AssemblyInformationalVersion,缺失时读取程序集版本;无法确定时明确失败。版本在访问属性或检查时读取,保留预发布标签与构建元数据,不读取 SDK 自身版本。使用 SDK 的应用无需重复维护一个版本变量;特殊宿主才覆盖。默认比较接受 2~4 段数字及 SemVer 预发布规则,构建元数据不影响优先级。
使用容器
using Microsoft.Extensions.DependencyInjection;
using Vernext;
services.AddVernext(updateUrl);
services.AddSingleton<MainViewModel>();
public sealed class MainViewModel(IUpdateManager updater)
{
public string CurrentVersion => updater.CurrentVersion;
public Task<UpdateCandidate?> CheckAsync(CancellationToken ct)
=> updater.CheckAsync(ct);
}
Server 配置使用 services.AddVernext(new UpdateOptions { ... })。已有企业更新接口时:
services.AddVernext();
services.AddSingleton<IUpdateChecker, CompanyUpdateChecker>();
services.AddSingleton<IUpdateFileProvider, CompanyFileProvider>();
services.AddSingleton<MainViewModel>();
默认组件使用 TryAddSingleton;自定义注册放在 AddVernext 前会被保留,放在后面则按标准容器的单服务最后注册规则生效。管理器只注入单个接口,不枚举全部实现;IEnumerable<T> 仍包含所有描述。不使用 AddVernext<TChecker>、服务定位器或内部容器。
配置也遵循同一规则。通常直接把 UpdateOptions 传给 AddVernext;已有容器配置时,也可单独注册 UpdateOptions 实例或单例工厂,放在 AddVernext() 前后均可。默认检查器在首次解析时读取容器最终选定的配置,不在注册时判断服务地址是否存在。自定义检查器生效时,不创建或校验未使用的默认检查器。
AddVernext 不合并配置,也不提供热更新;重复配置同一服务仍遵循标准 DI 规则。显式清单地址由 AddVernext(updateUrl) 提供,不能同时在最终配置中指定 ServiceAddress。
IUpdateManager 对应一个 UpdateManager 单例。构造函数必需检查器、文件获取器、验证器、执行器,直接接收这四个接口。没有地址且未注册检查器时,解析管理器明确失败。注册和构造不联网、不启动 helper;纯检查不要求安装目录可用。
只使用 Microsoft.Extensions.DependencyInjection.Abstractions 提供薄注册扩展,不要求 Host。不使用 DI 的应用仍只引用同一个 Vernext 包。
两种接入共用的调用流程
以下使用默认目录执行器。确认、保存、错误展示和退出方法代表应用已有逻辑;pendingUpdate 是窗口或退出协调器持有的 IUpdateHandoff? 字段:
if (pendingUpdate is null)
{
var candidate = await updater.CheckAsync(ct);
if (candidate is null || !await ConfirmUpdateAsync(candidate))
return;
pendingUpdate = await updater.DownloadAndPrepareAsync(candidate, progress, ct);
}
if (!await PrepareToExitAsync(ct))
return;
// 确认通过后,才将所有权交给本次提交作用域。
await using (var handoff = pendingUpdate)
{
pendingUpdate = null;
var result = await handoff.CommitAsync(restart: true, cancellationToken: ct);
if (result.Status == UpdateResultStatus.Started)
ExitApplication();
else
ShowUpdateError(result);
}
下载和准备期间不替换应用文件。准备结果可以持续持有,用户拒绝关闭或保存失败时保留,之后直接重试退出与提交,不重新检查或下载。明确放弃或退出而不安装时异步释放;需要保留的准备结果不能在关闭确认前进入一次按钮事件的 await using 作用域。示例在确认通过后转交所有权,提交结束释放本次句柄;失败后重新准备,不继续持有已经使用过的句柄。
提交时传 restart: true 表示安装成功后重启,false 表示安装后保持退出。同一份准备结果只提交一次;入口和参数来自本地配置,在准备时验证并固定,提交不修改计划。未配置有效入口却请求重启会在接管前明确拒绝,准备结果仍可选择不重启。
保存和所有可否决的关闭确认必须在提交前完成。Started 表示 helper 已接手,等待当前进程退出,不能当作安装成功。提交成功后应用正常退出,helper 独立完成安装;提交后的取消或释放不会撤销已经交接的任务。
没有必调通用 InitializeAsync、公开历史结果查询或离线导入主入口。下载文件是 SDK 内部工作材料,应用无需保存或传递其路径。可以用标准 IProgress<UpdateProgress> 报告下载、校验和准备进度;取消使用 CancellationToken。
生命周期与框架接入
直接创建和标准 DI 使用相同的管理器、准备结果及所有权。AddVernext 只组合组件;它不会启动 Host、注册退出事件或取得应用运行锁。Vernext 不调用窗口关闭、StopApplication 或 Environment.Exit。
应用或框架在现有退出流程的一个位置接入更新:完成所有可否决确认与安装前必须成功的业务收尾,再在进程仍存活、能够等待异步完成的位置提交。收到 Started 后正常结束进程,不再否决退出。UI 消息循环和 Host 可以在此前或此后结束,SDK 不要求固定顺序;框架自行处理原生同步入口,不能在已停止的 UI 上下文中阻塞等待提交。仅收到停止通知不等于业务收尾已完成。
| 所属 | 负责内容 |
|---|---|
| Vernext | 检查、下载、验证、准备及安装事务;管理自己创建的技术资源 |
| 具体框架适配 | 用框架已有能力协调操作收尾、准备结果、退出时安装和所需运行锁 |
| 应用业务 | 更新界面、关闭判断、业务保存和安装意图 |
框架适配维护在对应框架项目中;例如 AddVernextUpdater 属于 Zeek 的适配入口,不是 Vernext API。本仓库不实现 Zeek 注册或退出动作,框架也无需另建一套更新流程。版本、RID、目标和重启入口优先沿用 SDK 默认规则;版本展示读取管理器的 CurrentVersion。ApplicationId 是更新来源中的身份,不能无条件填入本地框架的应用名称;只有确认二者使用同一标识约定时,框架才可补充默认值。
准备结果由一个明确的持有者管理,确认通过后可转交最终提交方;容器、管理器和执行器先释放不会使它失效。接口不要求引用计数或双重持有。取消“退出时安装”的安排只改变适配层的意图,仍可保留 handoff;调用 CancelAsync 或释放未提交 handoff 则表示放弃准备材料。Vernext 不保存跨重启的安装安排。
自定义与鉴权
| 契约 | 默认实现 | 职责 |
|---|---|---|
IUpdateChecker |
JsonUpdateChecker |
获取、认证发布信息,选择最终候选 |
IUpdateFileProvider |
UpdateFileDownloader |
获取完整归档,成功后转交文件释放责任 |
IUpdateFileVerifier |
Sha256Verifier |
校验归档 SHA-256 与可选长度 |
IUpdateExecutor |
DirectoryUpdateExecutor |
准备独立的 IUpdateHandoff,维护安装与恢复语义 |
检查器签名为 CheckAsync(string currentVersion, CancellationToken);无更新返回空,失败抛出异常。自定义检查器已经完成选择,管理器不会再次比较版本或改变候选。静态 JSON 的普通格式如下,size、notes 可省略:
{"version":"2.0.0","url":"MyApp.zip","sha256":"<SHA-256>","size":123456,"notes":"更新说明"}
相对下载地址以清单地址解析。配置公钥时要求签名清单;无公钥时读取普通 JSON,可信来源由 HTTPS 或已有认证保证,SHA-256 本身不证明发布者身份。其他 API 格式通过 IUpdateChecker 接入,不要求转成 Server 协议。只更换版本顺序时可给 JsonUpdateChecker 传入标准 IComparer<string>。
默认来源的配置边界如下,直接创建与 DI 一致:
| 来源 | 必要配置与校验 |
|---|---|
| Vernext Server | ServiceAddress、ApplicationId、PublicKeyPem;渠道默认 stable,RID 默认当前进程,校验完整签名身份 |
| 普通 JSON/同格式 API | 显式地址;不填写签名身份配置 |
| 签名清单地址 | 显式地址与 PublicKeyPem;另填 ApplicationId 时,同时校验渠道和 RID,使用对应默认值或覆盖值 |
| 自定义检查协议 | 注册 IUpdateChecker,由它负责来源认证及最终候选选择;后续仍遵守所选验证器与执行器的输入契约 |
手动替换一步,其余步骤仍使用默认实现:
using var downloader = new UpdateFileDownloader(downloadHttpClient);
using var updater = UpdateManager.Create(
companyChecker, fileProvider: downloader);
检查或下载只需要鉴权、代理或 Token 刷新时,直接给对应默认实现传入已配置的 HttpClient,不重写文件传输:
using var checker = new JsonUpdateChecker(updateUrl, checkHttpClient);
using var downloader = new UpdateFileDownloader(downloadHttpClient);
using var updater = UpdateManager.Create(checker, fileProvider: downloader);
DI 使用相同实现:services.AddSingleton<IUpdateFileProvider>(_ => new UpdateFileDownloader(downloadHttpClient));。若用 IHttpClientFactory,在注册工厂中取得命名客户端即可;SDK 不额外增加 HTTP 接口。默认检查和下载各自拥有独立客户端,检查凭据不会自动发送到包地址。
完整手动构造也可使用 new UpdateManager(checker, fileProvider, fileVerifier, executor, options)。组件接口可独立使用,替换步骤无需复制默认编排。UpdateOptions 只保存参数,不保存行为委托、客户端或组件。
配置与文件规则
| 配置 | 何时需要 |
|---|---|
Channel |
Server 的非 stable 渠道 |
PreservePaths |
保留包内同名本地文件,如 ["appsettings.local.json", "data/"] |
RestartArguments |
重启需要业务参数;默认不转发当前命令行 |
TargetDirectory / RestartEntryPoint |
特殊布局或无法推导的入口 |
CurrentVersion / RuntimeIdentifier |
特殊版本或 Server 目标平台覆盖 |
DownloadDirectory / MaxDownloadBytes |
默认下载器的临时目录或 4 GiB 上限覆盖 |
Create / AddVernext 将配置交给默认组件;显式传入的自定义组件使用自己的配置。直接创建或传入注册入口的配置会复制集合;单独注册到 DI 的配置在组件解析时取得,由需要持有集合的组件复制。配置不在更新期间动态变化。
普通目录按包内文件新增或替换,包外内容保留;PreservePaths 保护同名本地内容,缺失时允许从包中初始化。废弃文件在归档根目录的可选 vernext-update.json 声明,应用无需写删除逻辑:
{"deletePaths":["Legacy.Component.dll","plugins/RemovedPlugin/"]}
macOS 完整替换 .app,业务数据放在 bundle 外,不使用包内保留规则。
多实例保护与资源归属
默认 helper 等待发起进程退出并对安装目标互斥。需要保护其他运行实例及更新期间新启动的实例时,每个实例在业务开始前取得运行租约,并保持到正常退出:
var executor = new DirectoryUpdateExecutor(options);
using var running = executor.AcquireApplicationLock();
using var updater = UpdateManager.Create(updateUrl, options, executor: executor);
// 进入应用主循环,退出后才释放 running。
使用默认目录执行器时,DI 提供同一个具体实例与 IUpdateExecutor;在应用启动入口解析并取得租约:
var executor = serviceProvider.GetRequiredService<DirectoryUpdateExecutor>();
using var running = executor.AcquireApplicationLock();
// 启动应用主循环,业务类仍只注入 IUpdateManager。
这是按需的多实例协调,不是更新初始化。取得租约不运行 helper、不恢复或清理文件;目录仍有未完成替换时拒绝取得租约。普通应用无需增加启动步骤,需要多实例保证时才在业务开始前取得租约,并由入口或框架适配持有到业务运行及必要清理结束;DI 注册不会自动取得或持有它。替换 IUpdateExecutor 后按所选实现的生命周期契约接入,不额外取得无关的目录运行锁。
helper 在安装失败时尝试回滚;准备新更新前,执行器内部使用保留并验签的匹配 helper 处理遗留事务,不能完成恢复时拒绝新操作。committed / rolledBack 残留仅在共享安装锁与独占操作锁下核实归属、清理材料,不等待仍在运行的旧父进程,也不修改安装目录,因此可与当前应用的运行租约并存。未完成文件替换仍需独占安装锁,不能误当终态删除;应用无需释放运行租约来绕过冲突。
准备时直接检查各事务并回收已结束操作的归档与 helper,不查询或排序历史。结果文件和日志由组件维护,用于避免重复安装、确认清理条件和排障;没有公开历史查询接口,应用只处理本次交接结果。
断电或 helper 被终止后不会自动唤醒。主程序损坏无法启动时,从外部调用保留 helper 恢复;不依赖主程序下次初始化,不默认引入启动器或常驻服务。回滚成功不等于自动重启旧版,当前 helper 仅在安装成功且本次选择重启时启动应用。
资源规则:Create 创建的默认组件由管理器释放;构造函数或 Create 传入的外部组件由原 owner 释放;DI 创建的服务由容器释放。借用 HttpClient 不随组件释放。下载器失败或取消时自行清理,成功返回的 UpdateFile 由管理器在准备完成后释放;执行器返回的 handoff 必须独立拥有后续资源。
维护与验证
dotnet build sdk/Vernext.slnx -c Release -warnaserror
dotnet run --project sdk/probes/Vernext.DirectoryExecutorProbe -c Release -- --check-only
./eng/Verify-Helper.ps1
./eng/Verify-Packages.ps1 -PackagePath '<最终候选 nupkg>'
--check-only 覆盖来源、版本、手动与 DI 接入、资源和鉴权,不要求 helper。源码验证可直接运行 eng/Verify-Packages.ps1,使用隔离测试密钥及本机 helper;-SourceFixtures 只验证需要测试密钥的 helper 场景。
手动工作流从同一源码构建四 RID helper、签名并组包一次,再完成同包 JIT/AOT 验收,默认不发布。helper 与签名自动排除在应用 single-file 外;托管 SDK 由 AOT 应用编入自身。具体发行证据、helper 构建、NuGet 发布和 Avalonia 示例位于源码仓库的 docs/ 与 samples/,访问这些维护资料需要仓库权限。
源码保留 Rust 和 C# Native AOT 两种实现。自行打包者在 eng/Build-DirectoryHelper.ps1 末尾固定调用 Build-RustHelper(默认)或 Build-CSharpHelper,使用自己的 helper 签名密钥重新构建 SDK 与 NuGet。包内只保留选定实现;切换不增加应用代码,也不能用另一种 helper 替换未完成事务的恢复副本。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
新增 IUpdateManager.CurrentVersion,使界面展示与检查使用同一版本;自行实现 IUpdateManager 的消费者须补齐该只读属性。修复默认检查器在 DI 解析时读取最终 UpdateOptions,支持在 AddVernext 前后独立注册实例或单例工厂。明确更新来源身份与本地应用标识的区别,保留直接创建与 DI 一致的来源校验。默认交付 Rust helper;Developer ID、公证和 Gatekeeper 分发尚未验收。