mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:33:06 +08:00
fix: verify staged upgrades in the background before restarting
This commit is contained in:
@@ -221,12 +221,14 @@ async function startJob({ root, tmp, env = process.env }, action) {
|
|||||||
let selected;
|
let selected;
|
||||||
let metadata;
|
let metadata;
|
||||||
if (action === 'apply') {
|
if (action === 'apply') {
|
||||||
// Fail before launching a process or stopping the working panel.
|
// Keep requests short on slow devices. The detached worker performs
|
||||||
|
// complete file/dependency verification before it can stop the panel.
|
||||||
try {
|
try {
|
||||||
selected = await artifacts.selectedUpgrade(
|
selected = await artifacts.selectedUpgrade(
|
||||||
tmp,
|
tmp,
|
||||||
artifacts.releaseBranch(env),
|
artifacts.releaseBranch(env),
|
||||||
env,
|
env,
|
||||||
|
{ metadataOnly: true },
|
||||||
);
|
);
|
||||||
metadata = await stagedMetadata(selected);
|
metadata = await stagedMetadata(selected);
|
||||||
if (previous.status === 'ready')
|
if (previous.status === 'ready')
|
||||||
|
|||||||
@@ -495,7 +495,12 @@ async function stageUpgrade({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectedUpgrade(tmp, branch, env = process.env) {
|
async function selectedUpgrade(
|
||||||
|
tmp,
|
||||||
|
branch,
|
||||||
|
env = process.env,
|
||||||
|
{ metadataOnly = false } = {},
|
||||||
|
) {
|
||||||
const pointer = JSON.parse(
|
const pointer = JSON.parse(
|
||||||
await fs.readFile(path.join(tmp, `upgrade-ready-${branch}.json`), 'utf8'),
|
await fs.readFile(path.join(tmp, `upgrade-ready-${branch}.json`), 'utf8'),
|
||||||
);
|
);
|
||||||
@@ -519,10 +524,12 @@ async function selectedUpgrade(tmp, branch, env = process.env) {
|
|||||||
);
|
);
|
||||||
if (ready.source !== selected.source || ready.static !== selected.static)
|
if (ready.source !== selected.source || ready.static !== selected.static)
|
||||||
throw new Error(upgradeMessage('暂存升级就绪记录不匹配。', env));
|
throw new Error(upgradeMessage('暂存升级就绪记录不匹配。', env));
|
||||||
const manifest = await verifyUpgrade(selected.source, selected.static, env);
|
const manifest = metadataOnly
|
||||||
|
? await manifestAt(selected.static, env)
|
||||||
|
: await verifyUpgrade(selected.source, selected.static, env);
|
||||||
if (ready.sourceCommit !== manifest.sourceCommit)
|
if (ready.sourceCommit !== manifest.sourceCommit)
|
||||||
throw new Error(upgradeMessage('暂存升级就绪记录不匹配。', env));
|
throw new Error(upgradeMessage('暂存升级就绪记录不匹配。', env));
|
||||||
if (ready.bundle)
|
if (ready.bundle && !metadataOnly)
|
||||||
await require('./platformBundle.cjs').verifyStagedBundle(selected);
|
await require('./platformBundle.cjs').verifyStagedBundle(selected);
|
||||||
if (ready.dependenciesChanged)
|
if (ready.dependenciesChanged)
|
||||||
await fs.access(path.join(selected.source, 'node_modules'));
|
await fs.access(path.join(selected.source, 'node_modules'));
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ CI 从同次构建的不可变镜像摘要导出完整 tar.gz,包含源码中
|
|||||||
|
|
||||||
`package.json` 中 `qinglongUpgradeFormat: 2` 表示使用完整包。旧版升级器仍可通过原归档路径完成首次迁移;完整包版本不会在平台包缺失、损坏或不兼容时退回设备端 npm 安装。
|
`package.json` 中 `qinglongUpgradeFormat: 2` 表示使用完整包。旧版升级器仍可通过原归档路径完成首次迁移;完整包版本不会在平台包缺失、损坏或不兼容时退回设备端 npm 安装。
|
||||||
|
|
||||||
面板下载任务与本地离线入口共用 `platformBundle.cjs`:平台匹配 → 归档安全检查 → 全文件/链接/权限和锁文件校验 → SQLite/Sequelize/cron-parser/gRPC/undici 加载及 SQLite 内存库读写 → 发布就绪指针。重启前重新验证完整载荷。离线升级同步面板任务状态,并与在线任务互斥;中断准备时保留之前的可用就绪包。原有服务停止、文件备份、启动健康检查及失败恢复仍由共享升级引擎处理。
|
面板下载任务与本地离线入口共用 `platformBundle.cjs`:平台匹配 → 归档安全检查 → 全文件/链接/权限和锁文件校验 → SQLite/Sequelize/cron-parser/gRPC/undici 加载及 SQLite 内存库读写 → 发布就绪指针。点击重启时,接口只检查就绪记录并提交后台任务;完整文件与依赖复验由独立工作进程执行,复验通过后才停止旧服务,避免小设备的预检耗时超过 HTTP 请求期限。离线升级同步面板任务状态,并与在线任务互斥;中断准备时保留之前的可用就绪包。原有服务停止、文件备份、启动健康检查及失败恢复仍由共享升级引擎处理。
|
||||||
|
|
||||||
渠道索引为 GitHub prerelease `upgrade-develop` 或 `upgrade-master` 下的 `latest.json`。索引记录准确字节数、SHA256 和不可变 `upgrade-<commit>` release 资产 URL。13 个平台均完成归档、解包后复验及依赖试运行才发布渠道;过期工作流不能覆盖更新提交的渠道。
|
渠道索引为 GitHub prerelease `upgrade-develop` 或 `upgrade-master` 下的 `latest.json`。索引记录准确字节数、SHA256 和不可变 `upgrade-<commit>` release 资产 URL。13 个平台均完成归档、解包后复验及依赖试运行才发布渠道;过期工作流不能覆盖更新提交的渠道。
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ test('detached download survives its initiating request and is idempotent; apply
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('invalid staged payload is rejected before spawning a restart', async (t) => {
|
test('invalid staged payload fails asynchronously before stopping the panel', async (t) => {
|
||||||
const f = await fixture(t);
|
const f = await fixture(t);
|
||||||
await startJob(f, 'download');
|
await startJob(f, 'download');
|
||||||
assert.equal((await finished(f.tmp)).status, 'ready');
|
assert.equal((await finished(f.tmp)).status, 'ready');
|
||||||
@@ -197,9 +197,16 @@ test('invalid staged payload is rejected before spawning a restart', async (t) =
|
|||||||
path.join(f.tmp, pointer.directory, 'qinglong-static-master/build/app.js'),
|
path.join(f.tmp, pointer.directory, 'qinglong-static-master/build/app.js'),
|
||||||
'corrupted',
|
'corrupted',
|
||||||
);
|
);
|
||||||
await assert.rejects(startJob(f, 'apply'), /checksum|损坏/);
|
const queued = await startJob(f, 'apply');
|
||||||
|
assert.equal(queued.status, 'starting');
|
||||||
|
const failed = await finished(f.tmp);
|
||||||
|
assert.equal(failed.status, 'failed');
|
||||||
|
assert.match(failed.error, /checksum|损坏/);
|
||||||
assert.equal(syncFs.existsSync(path.join(f.root, 'stops')), false);
|
assert.equal(syncFs.existsSync(path.join(f.root, 'stops')), false);
|
||||||
assert.equal((await getStatus(f.tmp)).status, 'failed');
|
assert.equal(
|
||||||
|
await fs.readFile(path.join(f.root, 'version.yaml'), 'utf8'),
|
||||||
|
'version: 2.23.0',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('failed download persists its failure without stopping the panel and can be retried', async (t) => {
|
test('failed download persists its failure without stopping the panel and can be retried', async (t) => {
|
||||||
@@ -467,3 +474,15 @@ for (const knownSize of [true, false])
|
|||||||
assert.equal(reports.at(-1).downloadedBytes, 100);
|
assert.equal(reports.at(-1).downloadedBytes, 100);
|
||||||
assert.equal(reports.at(-1).percent, knownSize ? 100 : null);
|
assert.equal(reports.at(-1).percent, knownSize ? 100 : null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('asynchronous apply still rejects unsafe stage pointers before dispatch', async (t) => {
|
||||||
|
const f = await fixture(t);
|
||||||
|
await startJob(f, 'download');
|
||||||
|
assert.equal((await finished(f.tmp)).status, 'ready');
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(f.tmp, 'upgrade-ready-master.json'),
|
||||||
|
JSON.stringify({ directory: '../outside' }),
|
||||||
|
);
|
||||||
|
await assert.rejects(startJob(f, 'apply'), /指针|pointer/);
|
||||||
|
assert.equal(syncFs.existsSync(path.join(f.root, 'stops')), false);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user