Repository Wiki
isHarryh/Ark-Pets

应用更新与自更新流程

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 将"应用更新"设计为一个检查 → 协商 → 下载 → 安装 → 退出的完整闭环,而非简单的"提示用户去官网下载"。这带来三个关键收益:

  1. 静默与手动统一:启动时的后台检查与用户点击"检查更新"复用同一个任务类 CheckAppUpdateTask,仅通过 GuiTaskStyle(HIDDEN / COMMON)区分是否展示进度与弹窗,避免了两套逻辑的漂移。
  2. 多级下载源容灾:考虑到 ArkPets 的中文用户占比与 GitHub 在中国大陆的可达性问题,下载源被设计为"CDK 认证的 MirrorChyan 主源 + GitHub 直连 + GHProxy 代理"的多级结构,由 SourceStrategy.getStrategy("AppDownload") 统一调度。
  3. 自更新安全性:安装新版本前必须先通过 HostTray 向所有运行中的桌宠核心实例发送 LOGOUT 指令(Socket 通信),确保旧版本文件不被占用,安装成功后再淡出退出启动器,避免文件锁导致的半更新状态。

关键概念与术语:

术语含义
stableVersion官方 API 返回的最新正式版(AppQueryVersion.getStableVersion())
appVersion当前客户端版本常量(Const.appVersion),参与比较
Const.isUpdateAvailable全局更新可用标志,驱动通知栏 UI
CDKMirrorChyan(镜晶)平台的兑换码,用于认证获取加速下载主源
GuiTaskStyleGUI 任务展示风格:HIDDEN(隐藏)、COMMON(通用)、STRICT(严格)
自更新(auto update)仅安装版可用;绿色版/受限环境回退为引导手动下载

Architecture

Loading diagram...

上图展示了三层结构:触发入口层三个入口汇聚到同一个检查任务;任务层四个任务串成检查—校验—下载—安装链;网络层中 SourceStrategy(策略名固定为 "AppDownload")同时接收来自检查任务的备源注册与来自 CDK 校验的主源注册,形成"主源优先、备源兜底"的下载源池。Const.isUpdateAvailable 作为唯一的全局状态桥接异步任务与 UI 通知栏。

之所以让 CheckAppUpdateTask 而非下载任务负责注册 GitHub/GHProxy 备源(见 setAppBackupSource),是因为备源 URL 中携带具体版本号 v%s,只有在版本检查完成、得知目标版本后才能确定下载地址——这是一个"检查即锁定下载目标"的设计。

核心流程:版本检查 CheckAppUpdateTask

任务定义与请求构造

CheckAppUpdateTask 继承自通用数据获取任务 FetchAsDataTask,构造时接收一个 sourceStr 参数用于向服务端区分检查来源("auto" 为启动时静默检查,"manual" 为用户手动检查)——这便于服务端做遥测与统计。

java
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)是模板方法模式的典型应用:基类负责网络与解析的固定流程,调用方通过匿名子类按需覆写回调,从而让启动时的静默检查(不弹任何窗)与手动检查(弹窗询问)复用同一套控制流。

响应解析与版本比较

java
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

java
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

Loading diagram...

第一步:安装版资格判定

java
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

java
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(主源)生效。关键步骤:

  1. 清空主源:clearPrimarySource() 确保每次协商从头开始,避免残留上次会话的过期 URL(备源在版本检查阶段已注册且随 stableVersion 确定,无需清理)。
  2. CDK 存在则严格校验:McCheckAppUpdateTask 以 STRICT 风格启动,回调中 value.raiseForCode() 将服务端状态码转换为 McQueryVersion.McException 抛出——成功则把 value.data.url 设为 "AppDownload" 策略的主源;失败则视为"CDK 无效"。
  3. 递归式双重确认:doDoubleCheck 标志实现了"询问一次"的语义。首次调用 assertDownloadSource(true, ...),CDK 失败或未配置时弹出 downloadDialog 让用户输入/修正 CDK;用户确认后以 assertDownloadSource(false, onDone) 递归重试,且只重试一次,二次失败直接降级走备源(GitHub/GHProxy),绝不无限循环。
  4. 回调透传:无论走哪条路径,最终都会执行 onDone.run(),把控制权交还更新主流程——协商失败不阻断更新,只是降级。

注意这里 McCheckAppUpdateTask 的角色并非"检查版本",而是用 CDK 换取带鉴权的下载直链(value.data.url),类名中的 "Check" 指的是校验 CDK 的有效性。

第三步:下载 → 登出实例 → 安装 → 退出

java
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) 中有三个不可省略的动作,顺序有讲究:

  1. HostTray.getInstance().forEachMemberTray(... sendOperation(SocketData.Operation.LOGOUT)) —— 向每一个成员托盘(即每一个正在运行的桌宠核心进程)发送 LOGOUT Socket 指令。这是 Windows 上避免"文件被占用导致安装失败"的关键:ArkPets 的桌宠核心是独立进程,若不先登出,安装器覆写被锁文件会直接失败。
  2. new AppInstallTask(..., file).start() —— 拿着刚下载的安装包文件执行静默安装。安装本身作为独立的 GUI 任务执行,带有 COMMON 风格的进度反馈。
  3. onSucceeded 中 GuiPrefabs.fadeOutWindow(..., e -> Platform.exit()) —— 安装成功后以淡出动画关闭启动器窗口并 Platform.exit() 退出 JVM。至此流程结束,新版本由安装器落盘,用户下次启动即为新版。

触发入口详解

入口一:启动时静默检查(RootModule)

java
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)

java
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 类型提供语义化比较

下载源数据流

Loading diagram...
  • 主源(MirrorChyan):唯一需要鉴权的源,URL 来自 McQueryVersion.data.url,只在 CDK 校验通过时注册。
  • 备源(GitHub / GHProxy):无需鉴权,URL 由 stableVersion 格式化生成(v%s 模板),在版本检查阶段即注册。

状态标志生命周期

Loading diagram...

注意:API 失败与解析异常不会修改 Const.isUpdateAvailable,即检查失败保持原有状态(首次启动则为默认值),这避免了网络抖动导致横幅误消失或误出现。

配置选项

配置项类型/取值默认说明
CDK(app.config.getMcCdk())Stringnull(未配置)MirrorChyan 兑换码;为 null 时跳过主源协商直接走备源
GuiTaskStyle(检查任务)HIDDEN / COMMON—启动静默检查用 HIDDEN,手动检查用 COMMON
sourceStr"auto" / "manual"—上报给服务端的检查来源,用于遥测
StartupConfig.isAutoUpdateAvailable()boolean安装版为 true自更新资格;false 时降级为引导手动下载
通知栏轮询周期Duration5000msinitScheduledListener() 中 setDelay/setPeriod
urlOfficialApiURL 常量Const.PathConfig官方版本查询 API 根地址
urlOfficialDownloadPageURL 常量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 != 0onReceivedData 分支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 备源,不影响功能完整性。

扩展点

  1. 新增下载源:在 setAppBackupSource 中追加 addBackupSource(name, url) 即可纳入回退链,无需改动下载任务。
  2. 跨平台自更新:替换 setAppBackupSource 中硬编码的 Windows 命名,按 OS 分派不同安装包 URL(源码已预留注释位)。
  3. 自定义检查策略:CheckAppUpdateTask 的三个回调钩子即为扩展面;新入口(如菜单项)只需以相应 GuiTaskStyle 启动任务并覆写回调。
  4. 检查频率控制:当前自动检查仅在启动时执行一次;如需周期检查,可在 initScheduledListener 的 ScheduledService 中追加检查任务。

Sources

(2 files)
desktop/src/cn/harryh/arkpets/controllers
desktop/src/cn/harryh/arkpets/guitasks/requests