应用更新与自更新流程
ArkPets 桌面启动器的应用版本检查、下载源协商与自更新执行机制:由 CheckAppUpdateTask 向官方 API 查询最新正式版本并驱动 Const.isUpdateAvailable 全局标志,再经 assertDownloadSource() 协商 MirrorChyan/GitHub/GHProxy 多级下载源,最终通过 DownloadAppTask → AppInstallTask 完成安装并安全退出启动器。
Purpose and Scope
本页面覆盖 ArkPets 桌面端(启动器 GUI)中应用本体的更新能力端到端机制,具体包括:
- 版本检查任务
CheckAppUpdateTask的请求构造、响应解析与版本比较逻辑 - 三类触发入口:启动时静默检查(
RootModule)、"关于"页手动检查(SettingsModule.initAbout())、版本通知栏(NoticeBar) - 下载源协商与 MirrorChyan CDK 校验(
assertDownloadSource()+McCheckAppUpdateTask) - 自更新执行链路
executeAppUpdate():安装版判定 → 下载安装包 → 登出全部桌宠核心实例 → 静默安装 → 退出启动器
以下相关主题有意留由兄弟页面阐述,本页仅引用不展开:
- 通用 GUI 异步任务框架(
GuiTask、FetchAsDataTask、GuiTaskStyle的完整生命周期)——见 GUI 任务框架页 - 通用网络下载与镜像源调度策略(
SourceStrategy的注册与回退机制)——见网络与镜像源页 - 桌宠模型资源的检查与更新(
McCheckModelsUpdateTask、资源下载流程)——见模型资源更新页 - 启动器整体模块化结构(
RootModule、SettingsModule的 UI 组织)——见启动器架构页
Overview
ArkPets 将"应用更新"设计为一个检查 → 协商 → 下载 → 安装 → 退出的完整闭环,而非简单的"提示用户去官网下载"。这带来三个关键收益:
- 静默与手动统一:启动时的后台检查与用户点击"检查更新"复用同一个任务类
CheckAppUpdateTask,仅通过GuiTaskStyle(HIDDEN/COMMON)区分是否展示进度与弹窗,避免了两套逻辑的漂移。 - 多级下载源容灾:考虑到 ArkPets 的中文用户占比与 GitHub 在中国大陆的可达性问题,下载源被设计为"CDK 认证的 MirrorChyan 主源 + GitHub 直连 + GHProxy 代理"的多级结构,由
SourceStrategy.getStrategy("AppDownload")统一调度。 - 自更新安全性:安装新版本前必须先通过
HostTray向所有运行中的桌宠核心实例发送LOGOUT指令(Socket 通信),确保旧版本文件不被占用,安装成功后再淡出退出启动器,避免文件锁导致的半更新状态。
关键概念与术语:
| 术语 | 含义 |
|---|---|
stableVersion | 官方 API 返回的最新正式版(AppQueryVersion.getStableVersion()) |
appVersion | 当前客户端版本常量(Const.appVersion),参与比较 |
Const.isUpdateAvailable | 全局更新可用标志,驱动通知栏 UI |
| CDK | MirrorChyan(镜晶)平台的兑换码,用于认证获取加速下载主源 |
GuiTaskStyle | GUI 任务展示风格:HIDDEN(隐藏)、COMMON(通用)、STRICT(严格) |
| 自更新(auto update) | 仅安装版可用;绿色版/受限环境回退为引导手动下载 |
Architecture
上图展示了三层结构:触发入口层三个入口汇聚到同一个检查任务;任务层四个任务串成检查—校验—下载—安装链;网络层中 SourceStrategy(策略名固定为 "AppDownload")同时接收来自检查任务的备源注册与来自 CDK 校验的主源注册,形成"主源优先、备源兜底"的下载源池。Const.isUpdateAvailable 作为唯一的全局状态桥接异步任务与 UI 通知栏。
之所以让 CheckAppUpdateTask 而非下载任务负责注册 GitHub/GHProxy 备源(见 setAppBackupSource),是因为备源 URL 中携带具体版本号 v%s,只有在版本检查完成、得知目标版本后才能确定下载地址——这是一个"检查即锁定下载目标"的设计。
核心流程:版本检查 CheckAppUpdateTask
任务定义与请求构造
CheckAppUpdateTask 继承自通用数据获取任务 FetchAsDataTask,构造时接收一个 sourceStr 参数用于向服务端区分检查来源("auto" 为启动时静默检查,"manual" 为用户手动检查)——这便于服务端做遥测与统计。
1public class CheckAppUpdateTask extends FetchAsDataTask {
2 private final String sourceStr;
3
4 public CheckAppUpdateTask(StackPane parent, GuiTaskStyle style, String sourceStr) {
5 super(parent, style);
6 this.sourceStr = sourceStr;
7 }
8
9 protected void onHasNewStableVersion(Version stableVersion) {
10 }
11
12 protected void onUpToDated(Version stableVersion) {
13 }
14
15 protected void onAPIFailed() {
16 }
17
18 @Override
19 protected URL getTargetURL() {
20 return new StringUtils.URLStringBuilder(urlOfficialApi)
21 .addPath("version")
22 .addQuery("cliVer", appVersion.toString())
23 .addQuery("source", sourceStr)
24 .toURL();
25 }
26}Source: CheckAppUpdateTask.java
三个空实现的钩子方法(onHasNewStableVersion / onUpToDated / onAPIFailed)是模板方法模式的典型应用:基类负责网络与解析的固定流程,调用方通过匿名子类按需覆写回调,从而让启动时的静默检查(不弹任何窗)与手动检查(弹窗询问)复用同一套控制流。
响应解析与版本比较
1@Override
2protected void onReceivedData(JSONObject json) {
3 try {
4 AppQueryVersion value = json.toJavaObject(AppQueryVersion.class);
5 if (value.code == 0) {
6 // On API succeeded:
7 Version stableVersion = Objects.requireNonNull(value.getStableVersion());
8 Logger.info("Checker", "Application version check finished, newest: " + stableVersion);
9 setAppBackupSource(stableVersion);
10
11 if (appVersion.lessThan(stableVersion)) {
12 Const.isUpdateAvailable = true;
13 onHasNewStableVersion(stableVersion);
14 } else {
15 Const.isUpdateAvailable = false;
16 onUpToDated(stableVersion);
17 }
18 } else {
19 // On API failed:
20 Logger.warn("Checker", "Application version check failed (api failed)");
21 onAPIFailed();
22 }
23 } catch (Exception e) {
24 // On parsing failed:
25 Logger.error("Checker", "Application version check failed unexpectedly, details see below.", e);
26 if (style != GuiTaskStyle.HIDDEN)
27 GuiPrefabs.Dialogs.createErrorDialog(parent, e).show();
28 }
29}Source: CheckAppUpdateTask.java
控制流分为四条明确分支,异常处理上做了三重防护:
| 分支 | 判定条件 | 行为 |
|---|---|---|
| API 成功 + 有新版 | value.code == 0 且 appVersion.lessThan(stableVersion) | 置 Const.isUpdateAvailable = true,触发 onHasNewStableVersion |
| API 成功 + 已最新 | value.code == 0 且不小于 | 置 Const.isUpdateAvailable = false,触发 onUpToDated |
| API 失败 | value.code != 0 | 触发 onAPIFailed(服务端语义层面的失败) |
| 解析异常 | 任何 Exception | 记录错误日志;仅当任务不是 HIDDEN 风格时才弹出错误对话框 |
值得注意的设计细节:解析异常分支中 if (style != GuiTaskStyle.HIDDEN) 的守卫——启动时的静默检查即使解析失败也绝不打扰用户,错误只进日志;只有用户主动发起的检查才值得用对话框打断。而 Objects.requireNonNull(value.getStableVersion()) 则在版本字段缺失时立即抛 NPE 进入异常分支,避免后续比较产生更隐蔽的错误。
下载备源注册:setAppBackupSource
1protected static void setAppBackupSource(Version version) {
2 // Set update source according to the given version
3 // Should be replaced with actual OS detection logic in higher version of ArkPets
4 // Windows only
5 SourceStrategy.getStrategy("AppDownload")
6 .clearBackupSource()
7 .addBackupSource("GitHub", "https://github.com/isHarryh/Ark-Pets/releases/download/v%s/ArkPets-v%s-Setup.exe".formatted(version, version))
8 .addBackupSource("GHProxy", "https://ghproxy.harryh.cn/https://github.com/isHarryh/Ark-Pets/releases/download/v%s/ArkPets-v%s-Setup.exe".formatted(version, version));
9}Source: CheckAppUpdateTask.java
该方法在拿到 stableVersion 后立即向 SourceStrategy 的 "AppDownload" 策略注册两个备源:GitHub 直连(对海外用户最优)与 GHProxy 镜像代理(对大陆用户可达性更好)。clearBackupSource() 保证重复检查不会累积过期源。源码注释明确说明当前仅支持 Windows 的 Setup.exe 安装包命名,未来高版本将替换为真实的 OS 检测逻辑——这是当前实现的一个已知边界。
核心流程:自更新执行 executeAppUpdate
第一步:安装版资格判定
1private void executeAppUpdate() {
2 if (!StartupConfig.getInstance().isAutoUpdateAvailable()) {
3 // Maybe the user is not an installer user
4 Logger.warn("Updater", "Maybe auto update is not available");
5 GuiPrefabs.Dialogs.createConfirmDialog(app.body,
6 GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_INFO_ALT, GuiPrefabs.COLOR_INFO),
7 "自动更新不可用",
8 "请您手动下载新版程序文件并进行安装",
9 "您使用的似乎不是安装包版本的 ArkPets,或者您的系统环境存在限制,所以无法自动更新哦~ 是否前往 ArkPets 官网下载?",
10 () -> app.popBrowser(PathConfig.urlOfficialDownloadPage)).show();
11 } else {
12 ...
13 }
14}Source: SettingsModule.java
自更新的第一道门槛是 StartupConfig.getInstance().isAutoUpdateAvailable():只有通过安装器(Setup.exe)安装、且系统环境允许执行安装器写入的用户才具备自更新资格。绿色版用户会收到友好降级——弹窗引导跳转官网下载页,而不是让流程静默失败。这是"能力探测 + 优雅降级"的典型处理。
第二步:下载源协商 assertDownloadSource
1private void assertDownloadSource(boolean doDoubleCheck, Runnable onDone) {
2 SourceStrategy.getStrategy("AppDownload").clearPrimarySource();
3 String cdk;
4 if ((cdk = app.config.getMcCdk()) != null) {
5 Logger.debug("Updater", "Attempting using CDK to fetch resource");
6 new McCheckAppUpdateTask(app.body, GuiTask.GuiTaskStyle.STRICT, cdk) {
7 @Override
8 protected void onReceivedData(JSONObject json) {
9 McQueryVersion value = json.toJavaObject(McQueryVersion.class);
10 try {
11 value.raiseForCode();
12 SourceStrategy.getStrategy("AppDownload").setPrimarySource("MirrorChyan", value.data.url);
13
14 Logger.info("Updater", "CDK assertion passed");
15 if (onDone != null)
16 onDone.run();
17 } catch (McQueryVersion.McException e) {
18 Logger.warn("Updater", "CDK assertion not passed, " + e.getMessage());
19 if (doDoubleCheck)
20 app.dialogs.popDialog("downloadDialog", (Runnable) () -> assertDownloadSource(false, onDone));
21 else if (onDone != null)
22 onDone.run();
23 }
24 }
25 }.start();
26 } else {
27 Logger.info("Updater", "CDK assertion not passed due to not set");
28 if (doDoubleCheck)
29 app.dialogs.popDialog("downloadDialog", (Runnable) () -> assertDownloadSource(false, onDone));
30 else if (onDone != null)
31 onDone.run();
32 }
33}Source: SettingsModule.java
协商逻辑的意图是:在真正下载前,尽可能让 MirrorChyan(主源)生效。关键步骤:
- 清空主源:
clearPrimarySource()确保每次协商从头开始,避免残留上次会话的过期 URL(备源在版本检查阶段已注册且随stableVersion确定,无需清理)。 - CDK 存在则严格校验:
McCheckAppUpdateTask以STRICT风格启动,回调中value.raiseForCode()将服务端状态码转换为McQueryVersion.McException抛出——成功则把value.data.url设为"AppDownload"策略的主源;失败则视为"CDK 无效"。 - 递归式双重确认:
doDoubleCheck标志实现了"询问一次"的语义。首次调用assertDownloadSource(true, ...),CDK 失败或未配置时弹出downloadDialog让用户输入/修正 CDK;用户确认后以assertDownloadSource(false, onDone)递归重试,且只重试一次,二次失败直接降级走备源(GitHub/GHProxy),绝不无限循环。 - 回调透传:无论走哪条路径,最终都会执行
onDone.run(),把控制权交还更新主流程——协商失败不阻断更新,只是降级。
注意这里 McCheckAppUpdateTask 的角色并非"检查版本",而是用 CDK 换取带鉴权的下载直链(value.data.url),类名中的 "Check" 指的是校验 CDK 的有效性。
第三步:下载 → 登出实例 → 安装 → 退出
1} else {
2 assertDownloadSource(true, () -> new DownloadAppTask(app.body, GuiTask.GuiTaskStyle.COMMON) {
3 @Override
4 protected void onDownloadedFile(File file) {
5 Logger.info("Updater", "Closing all core instances");
6 HostTray.getInstance().forEachMemberTray(memberTray -> memberTray.sendOperation(SocketData.Operation.LOGOUT));
7
8 Logger.info("Updater", "Starting update task");
9 new AppInstallTask(app.body, GuiTaskStyle.COMMON, file) {
10 @Override
11 protected void onSucceeded(boolean result) {
12 Logger.info("Launcher", "Updater close request");
13 GuiPrefabs.fadeOutWindow(app.stage, durationNormal, e -> Platform.exit());
14 }
15 }.start();
16 }
17 }.start());
18}Source: SettingsModule.java
下载完成回调 onDownloadedFile(File file) 中有三个不可省略的动作,顺序有讲究:
HostTray.getInstance().forEachMemberTray(... sendOperation(SocketData.Operation.LOGOUT))—— 向每一个成员托盘(即每一个正在运行的桌宠核心进程)发送LOGOUTSocket 指令。这是 Windows 上避免"文件被占用导致安装失败"的关键:ArkPets 的桌宠核心是独立进程,若不先登出,安装器覆写被锁文件会直接失败。new AppInstallTask(..., file).start()—— 拿着刚下载的安装包文件执行静默安装。安装本身作为独立的 GUI 任务执行,带有COMMON风格的进度反馈。onSucceeded中GuiPrefabs.fadeOutWindow(..., e -> Platform.exit())—— 安装成功后以淡出动画关闭启动器窗口并Platform.exit()退出 JVM。至此流程结束,新版本由安装器落盘,用户下次启动即为新版。
触发入口详解
入口一:启动时静默检查(RootModule)
new CheckAppUpdateTask(app.body, GuiTask.GuiTaskStyle.HIDDEN, "auto").start();Source: RootModule.java
启动器初始化完成后立即以 HIDDEN 风格发起检查,sourceStr = "auto" 告知服务端这是自动检查。HIDDEN 风格下所有对话框回调均被守卫拦截(见上文 onHasNewStableVersion 内的 if (style == GuiTaskStyle.HIDDEN) return;),用户无感知;结果仅落日志与 Const.isUpdateAvailable 标志,等待通知栏周期性读取。
入口二:手动检查(SettingsModule.initAbout)
"关于"区域的 aboutQueryUpdate 点击后以 COMMON 风格启动检查,三种回调分别呈现三种对话框:
- 有新版:确认对话框提示"当前版本 X 可更新到 Y",附带一个额外的"访问官网"按钮(
GuiPrefabs.Dialogs.attachAction(dialog, gotoButton, 0)在位置 0 挂载),确认即executeAppUpdate()。 - 已最新:普通对话框告知已是最新,同时提供"强制重装"按钮——即用户可在无新版时也重新走一遍完整更新流程(用于修复损坏安装)。
- API 失败:危险图标对话框说明"服务器返回了无效的消息",并建议访问官网或 GitHub 仓库,因为此时本地无法判断版本状态。
Source: SettingsModule.java
入口三:版本更新通知栏(NoticeBar)
1appVersionNotice = new NoticeBar(noticeBox) {
2 @Override
3 protected boolean isToActivate() {
4 return isUpdateAvailable;
5 }
6 ...
7 @Override
8 protected void onClick(MouseEvent event) {
9 executeAppUpdate();
10 }
11};Source: SettingsModule.java
通知栏是 Const.isUpdateAvailable 全局标志的消费者:isToActivate() 返回该标志决定横幅是否显示"ArkPets 有新版本可用!点击此处进行更新~"。由于标志由异步任务写入,initScheduledListener() 中的 ScheduledService 每 5000ms 调用一次 appVersionNotice.refresh() 轮询刷新(含 setDelay/setPeriod 均为 5000ms、setRestartOnFailure(true)),实现"静默检查完成后数秒内横幅自动出现"。点击横幅直接 executeAppUpdate() 进入主更新流程。
这一"任务写标志 + 定时器读标志"的解耦设计,使异步网络检查与 JavaFX UI 线程渲染无需直接握手,也允许多次检查复用同一条横幅。
数据模型与网络契约
版本查询请求与响应
| 项目 | 值 | 说明 |
|---|---|---|
| 请求 URL | {urlOfficialApi}/version?cliVer={appVersion}&source={sourceStr} | 由 StringUtils.URLStringBuilder 拼接 |
| 响应载体 | AppQueryVersion(fastjson2 反序列化) | code + getStableVersion() |
| 成功判定 | value.code == 0 | 非 0 走 onAPIFailed |
| 版本比较 | appVersion.lessThan(stableVersion) | Version 类型提供语义化比较 |
下载源数据流
- 主源(MirrorChyan):唯一需要鉴权的源,URL 来自
McQueryVersion.data.url,只在 CDK 校验通过时注册。 - 备源(GitHub / GHProxy):无需鉴权,URL 由
stableVersion格式化生成(v%s模板),在版本检查阶段即注册。
状态标志生命周期
注意:API 失败与解析异常不会修改 Const.isUpdateAvailable,即检查失败保持原有状态(首次启动则为默认值),这避免了网络抖动导致横幅误消失或误出现。
配置选项
| 配置项 | 类型/取值 | 默认 | 说明 |
|---|---|---|---|
CDK(app.config.getMcCdk()) | String | null(未配置) | MirrorChyan 兑换码;为 null 时跳过主源协商直接走备源 |
GuiTaskStyle(检查任务) | HIDDEN / COMMON | — | 启动静默检查用 HIDDEN,手动检查用 COMMON |
sourceStr | "auto" / "manual" | — | 上报给服务端的检查来源,用于遥测 |
StartupConfig.isAutoUpdateAvailable() | boolean | 安装版为 true | 自更新资格;false 时降级为引导手动下载 |
| 通知栏轮询周期 | Duration | 5000ms | initScheduledListener() 中 setDelay/setPeriod |
urlOfficialApi | URL 常量 | Const.PathConfig | 官方版本查询 API 根地址 |
urlOfficialDownloadPage | URL 常量 | Const.PathConfig | 降级引导跳转的官网下载页 |
API Reference
CheckAppUpdateTask(StackPane parent, GuiTaskStyle style, String sourceStr)
构造版本检查任务。
Parameters:
parent(StackPane):任务 UI 的挂载容器(启动器主体)style(GuiTaskStyle):HIDDEN隐藏 /COMMON通用 /STRICT严格sourceStr(String):检查来源,"auto"(启动自动)或"manual"(手动)
onHasNewStableVersion(Version stableVersion) / onUpToDated(Version stableVersion) / onAPIFailed()
受保护的回调钩子,默认空实现,由匿名子类覆写。分别对应"有新正式版""已是最新""API 业务失败"。调用方回调内部需自行做 style == GuiTaskStyle.HIDDEN 守卫以决定是否弹窗。
getTargetURL(): URL
构造版本查询地址:{urlOfficialApi}/version?cliVer={appVersion}&source={sourceStr}。
setAppBackupSource(Version version): void(static)
向 SourceStrategy 的 "AppDownload" 策略注册 GitHub 与 GHProxy 两个备源。当前仅支持 Windows 的 Setup.exe(源码注释标明应由更高版本的 OS 检测逻辑替换)。
executeAppUpdate(): void(SettingsModule 私有)
自更新主入口。先以 StartupConfig.isAutoUpdateAvailable() 判定资格;不可用则弹窗引导官网;可用则 assertDownloadSource(true, ...) 协商源后启动 DownloadAppTask,并在下载完成回调中登出全部桌宠实例、执行 AppInstallTask、成功后淡出退出启动器。
assertDownloadSource(boolean doDoubleCheck, Runnable onDone): void(SettingsModule 私有)
下载源协商。清空主源 → 有 CDK 则经 McCheckAppUpdateTask 严格校验并注册 MirrorChyan 主源;CDK 无效或未配置时按 doDoubleCheck 决定是否弹 downloadDialog 后重试一次;任何路径最终执行 onDone。
失败模式、边界情况与并发
失败模式一览
| 失败点 | 检测方式 | 处理策略 |
|---|---|---|
版本 API 返回 code != 0 | onReceivedData 分支 | onAPIFailed 弹窗(仅非 HIDDEN);不改全局标志 |
| 响应解析异常 / stableVersion 缺失 | try-catch + Objects.requireNonNull | 记录错误日志;仅非 HIDDEN 弹错误对话框 |
| 网络请求失败 | FetchAsDataTask 基类处理 | 任务失败,不触碰全局标志 |
CDK 校验失败(McException) | raiseForCode() 抛出 | 弹 downloadDialog 供修正,重试一次后降级备源 |
| CDK 未配置 | getMcCdk() == null | 可弹 downloadDialog,确认后降级备源 |
| 非安装版自更新 | isAutoUpdateAvailable() == false | 弹窗引导跳转官网下载页 |
| 全部下载源不可达 | DownloadAppTask / SourceStrategy 回退 | 主源失败依序回退备源(GitHub → GHProxy) |
| 桌宠实例占用文件 | 下载后先 LOGOUT 所有实例 | 主动登出核心进程,规避文件锁 |
并发与一致性
- 异步任务与 UI 线程:所有网络任务均为
GuiTask异步执行;通知栏通过 5 秒轮询而非事件推送消费Const.isUpdateAvailable,天然规避了跨线程写 UI 的问题,代价是横幅出现最多延迟一个轮询周期。 - 重复检查:手动检查与自动检查可能并发发生;两者都会调用
setAppBackupSource,其中clearBackupSource()先清后加,保证源池不累积过期项;主源协商中clearPrimarySource()同理。 - 重复触达 executeAppUpdate:用户同时点击横幅与对话框确认可能并发启动多个
DownloadAppTask——实现层面依赖GuiTaskStyle.COMMON的进度遮罩串行化用户体验(具体互斥细节在 GUI 任务框架页阐述)。
已知边界
- 平台限制:备源 URL 硬编码 Windows
Setup.exe命名(源码注释自述待替换为 OS 检测),macOS/Linux 无自更新备源路径。 - 时序窗口:下载源 URL 与
stableVersion绑定,若检查后服务端再发新版,本次仍下载检查时刻锁定的版本——这对安装包场景是可接受的一致性选择。
运维与扩展点
运维要点
- 日志标签:版本检查使用
"Checker",更新执行链使用"Updater",安装成功关闭使用"Launcher"——按标签可快速过滤排障。 - 服务端遥测:
source=auto/manual与cliVer上报便于统计自动/手动检查比例与版本分布。 - CDK 运营:MirrorChyan 主源本质是第三方加速服务的付费/兑换码机制;未配置 CDK 的用户自动落到 GitHub/GHProxy 备源,不影响功能完整性。
扩展点
- 新增下载源:在
setAppBackupSource中追加addBackupSource(name, url)即可纳入回退链,无需改动下载任务。 - 跨平台自更新:替换
setAppBackupSource中硬编码的 Windows 命名,按 OS 分派不同安装包 URL(源码已预留注释位)。 - 自定义检查策略:
CheckAppUpdateTask的三个回调钩子即为扩展面;新入口(如菜单项)只需以相应GuiTaskStyle启动任务并覆写回调。 - 检查频率控制:当前自动检查仅在启动时执行一次;如需周期检查,可在
initScheduledListener的ScheduledService中追加检查任务。
Related Links
- CheckAppUpdateTask.java — 版本检查任务核心实现
- McCheckAppUpdateTask.java — MirrorChyan CDK 校验任务
- SettingsModule.java — 手动检查入口、通知栏与 executeAppUpdate
- RootModule.java — 启动时静默检查入口
- DownloadDialog.java — CDK 输入对话框(
downloadDialog)