From cb87c619bfb0af9a33bbc43523d44a10c62ccb37 Mon Sep 17 00:00:00 2001 From: whyour Date: Sun, 18 Oct 2026 04:21:15 +0800 Subject: [PATCH] fix: allow ARMv6 upgrades enough time to become healthy --- cli/src/internal/maintenance/panelUpgrade.cjs | 2 +- .../internal/maintenance/upgradeArtifacts.cjs | 14 ++++- docs/development/platform-upgrade-bundles.md | 2 +- shell/install-bundle.cjs | 2 +- test/back/upgrade-guards.test.cjs | 51 +++++++++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/cli/src/internal/maintenance/panelUpgrade.cjs b/cli/src/internal/maintenance/panelUpgrade.cjs index 3ad52d05..34ba1ca7 100644 --- a/cli/src/internal/maintenance/panelUpgrade.cjs +++ b/cli/src/internal/maintenance/panelUpgrade.cjs @@ -391,7 +391,7 @@ async function runJob({ root, tmp, env = process.env }, id) { ); await artifacts.waitForHealth( env, - 60000, + undefined, recover ? undefined : controller.signal, ); if (!recover) diff --git a/cli/src/internal/maintenance/upgradeArtifacts.cjs b/cli/src/internal/maintenance/upgradeArtifacts.cjs index 9fc9b172..18d2f658 100644 --- a/cli/src/internal/maintenance/upgradeArtifacts.cjs +++ b/cli/src/internal/maintenance/upgradeArtifacts.cjs @@ -668,7 +668,16 @@ async function reloadSystem({ .catch(() => {}); } -async function waitForHealth(env, timeout = 60000, signal) { +function startupHealthTimeout(runtime = process) { + // ARMv6 starts the database, scheduler and HTTP workers sequentially. Give + // slow devices time to become healthy before deciding to restore a backup. + return runtime.arch === 'arm' && + Number(runtime.config.variables.arm_version) === 6 + ? 300000 + : 60000; +} + +async function waitForHealth(env, timeout = startupHealthTimeout(), signal) { const port = Number(env.QlPort || 5700); let base = env.QlBaseUrl || ''; if (base && !base.startsWith('/')) base = `/${base}`; @@ -856,7 +865,7 @@ async function main() { await hook('start', [], recover ? undefined : controller.signal); await waitForHealth( env, - 60000, + undefined, recover ? undefined : controller.signal, ); }, @@ -903,6 +912,7 @@ module.exports = { selectedUpgrade, replaceAndReload, reloadSystem, + startupHealthTimeout, waitForHealth, runProcess, withLifecycleHooks, diff --git a/docs/development/platform-upgrade-bundles.md b/docs/development/platform-upgrade-bundles.md index 606fd9df..f7703178 100644 --- a/docs/development/platform-upgrade-bundles.md +++ b/docs/development/platform-upgrade-bundles.md @@ -45,7 +45,7 @@ ARMv6 glibc 小设备(例如 Raspberry Pi 1/Zero 的 Raspbian/Raspberry Pi OS - glibc、musl 分包,并记录构建系统 libc 下限。系统库仍以目标设备加载测试作为最后检查。 - ARMv6 glibc 使用签名校验的 Raspbian bullseye(glibc 2.31)构建环境,以固定 SHA256 的 ARMv6 Node 20.18.1 头文件编译 SQLite,强制 `-march=armv6 -mfpu=vfp -mfloat-abi=hard`。CI 检查 ELF 的 ARMv6 和硬浮点标记,并在 QEMU ARM1176 下完成归档复验、SQLite 读写和空目录离线安装;验证辅助镜像按不可变摘要记录,不进入用户安装包。Debian ARMv7 镜像仅提供同次构建的应用文件,不复用其 Node、libc 或 SQLite 二进制。 - ARMv6 与 ARMv7 使用不同包名。ARMv6 在目标镜像中从源码编译 SQLite,禁止复用 generic `arm` 的 ARMv7 预编译件;构建需通过加载和内存库读写,失败则不发布渠道。 -- ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,正常面板进程的启动参数不变。依赖加载和 SQLite 读写仍由现有 Node 的正常模式独立进程验证,使 Node 20.18.1 的内置 HTTP/WebAssembly 组件保持可用。 +- ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,正常面板进程的启动参数不变。ARMv6 升级后的健康检查最多等待 5 分钟,覆盖慢设备按顺序启动数据库、调度与 HTTP 工作进程的耗时;其他平台仍为 60 秒,超时或版本校验失败仍执行回退。依赖加载和 SQLite 读写仍由现有 Node 的正常模式独立进程验证,使 Node 20.18.1 的内置 HTTP/WebAssembly 组件保持可用。 - tar 中允许 pnpm 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。 自托管镜像可设置 `QL_UPGRADE_INDEX_URL` 指向兼容索引;要求 HTTPS,测试时仅放行 loopback HTTP。索引和所有平台归档应部署到同一可访问源,避免依赖额外的 raw 域名。 diff --git a/shell/install-bundle.cjs b/shell/install-bundle.cjs index eeb94bb7..a95e938c 100644 --- a/shell/install-bundle.cjs +++ b/shell/install-bundle.cjs @@ -256,7 +256,7 @@ async function installBundle(argv = process.argv.slice(2), env = process.env) { ); await artifacts.waitForHealth( environment, - 60000, + undefined, recover ? undefined : controller.signal, ); if (!recover) diff --git a/test/back/upgrade-guards.test.cjs b/test/back/upgrade-guards.test.cjs index 741e5bc9..25579cfd 100644 --- a/test/back/upgrade-guards.test.cjs +++ b/test/back/upgrade-guards.test.cjs @@ -283,3 +283,54 @@ test('unhealthy, malformed and oversized health responses fail within the deadli await assert.rejects(waitForHealth({ QlPort: port }, 20), /health check|后端启动检查失败/); } }); + +test('ARMv6 startup can become healthy after the standard one-minute deadline', async (t) => { + const { EventEmitter } = require('node:events'); + const descriptors = ['arch', 'config'].map((key) => [ + key, + Object.getOwnPropertyDescriptor(process, key), + ]); + t.after(() => { + for (const [key, descriptor] of descriptors) + Object.defineProperty(process, key, descriptor); + }); + Object.defineProperty(process, 'arch', { value: 'arm', configurable: true }); + Object.defineProperty(process, 'config', { + value: { variables: { arm_version: 6 } }, + configurable: true, + }); + let now = 0; + let probes = 0; + t.mock.method(Date, 'now', () => now); + t.mock.method(http, 'get', (_options, callback) => { + const request = new EventEmitter(); + request.destroy = (error) => request.emit('error', error); + queueMicrotask(() => { + const response = new EventEmitter(); + response.statusCode = ++probes === 1 ? 503 : 200; + callback(response); + now = probes * 70000; + response.emit( + 'data', + JSON.stringify({ + code: response.statusCode, + data: { status: probes > 1 ? 'ok' : 'starting' }, + }), + ); + response.emit('end'); + }); + return request; + }); + await waitForHealth({}); + assert.equal(probes, 2); + assert.equal(now, 140000); + const { startupHealthTimeout } = require('../../shell/upgrade.cjs'); + assert.equal( + startupHealthTimeout({ + arch: 'arm', + config: { variables: { arm_version: 7 } }, + }), + 60000, + ); + assert.equal(startupHealthTimeout({ arch: 'arm64' }), 60000); +});