mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:15:45 +08:00
fix: run ARMv6 archive workers without V8 JIT and verify offline installation
This commit is contained in:
@@ -6,6 +6,7 @@ const { spawn } = require('node:child_process');
|
||||
const http = require('node:http');
|
||||
const { setTimeout: delay } = require('node:timers/promises');
|
||||
const artifacts = require('./upgradeArtifacts.cjs');
|
||||
const { nodeRuntimeArgs } = require('./platformBundle.cjs');
|
||||
|
||||
const active = new Set(['starting', 'running', 'restarting']);
|
||||
const stateFile = (tmp) => path.join(tmp, 'panel-upgrade.json');
|
||||
@@ -443,11 +444,15 @@ async function launchWorker(id) {
|
||||
// detached creates a session but keeps the original parent. PM2 kills the
|
||||
// backend's entire descendant tree, so an intermediate launcher must exit
|
||||
// before the worker is allowed to stop that backend.
|
||||
const child = spawn(process.execPath, [__filename, 'worker', id], {
|
||||
env: { ...process.env, QL_UPGRADE_LAUNCHER_PID: String(process.pid) },
|
||||
detached: true,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[...nodeRuntimeArgs(), __filename, 'worker', id],
|
||||
{
|
||||
env: { ...process.env, QL_UPGRADE_LAUNCHER_PID: String(process.pid) },
|
||||
detached: true,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
await new Promise((resolve, reject) => {
|
||||
child.once('spawn', resolve);
|
||||
child.once('error', reject);
|
||||
|
||||
@@ -7,6 +7,12 @@ const { execFile } = require('node:child_process');
|
||||
const { promisify } = require('node:util');
|
||||
const tar = require('tar');
|
||||
const execute = promisify(execFile);
|
||||
// Heavy archive parsing can hit unsupported V8 JIT paths on ARM1176. Keep
|
||||
// upgrade workers interpreted without changing the installed Node or server.
|
||||
const nodeRuntimeArgs = (runtime = process) =>
|
||||
runtime.arch === 'arm' && Number(runtime.config.variables.arm_version) === 6
|
||||
? ['--jitless']
|
||||
: [];
|
||||
const sha256 = (bytes) =>
|
||||
crypto.createHash('sha256').update(bytes).digest('hex');
|
||||
const inside = (root, filename) => {
|
||||
@@ -198,7 +204,7 @@ async function smokeDependencies(source, signal) {
|
||||
db.serialize(()=>{db.run('CREATE TABLE verify_bundle(value INTEGER)');db.run('INSERT INTO verify_bundle VALUES(42)');
|
||||
db.get('SELECT value FROM verify_bundle',(err,row)=>{if(err||row.value!==42){console.error(err||'SQLite readback failed');process.exitCode=1;}db.close();});});
|
||||
`;
|
||||
await execute(process.execPath, ['-e', probe], {
|
||||
await execute(process.execPath, [...nodeRuntimeArgs(), '-e', probe], {
|
||||
cwd: source,
|
||||
env: { ...process.env, NODE_PATH: '', NODE_OPTIONS: '' },
|
||||
timeout: 30000,
|
||||
@@ -457,6 +463,7 @@ async function verifyStagedBundle(selected) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nodeRuntimeArgs,
|
||||
atLeast,
|
||||
runtimeTarget,
|
||||
targetKey,
|
||||
|
||||
@@ -7,7 +7,7 @@ ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
USER root
|
||||
# The generic "arm" sqlite release is ARMv7. ARMv6 must compile its own addon.
|
||||
RUN if [ "$TARGETARCH/$TARGETVARIANT" = arm/v6 ]; then \
|
||||
RUN ulimit -c 0; if [ "$TARGETARCH/$TARGETVARIANT" = arm/v6 ]; then \
|
||||
apk add --no-cache --virtual .ql-bundle-build build-base binutils python3 nodejs-dev && \
|
||||
cd "$(readlink -f /ql/node_modules/sqlite3)" && \
|
||||
CFLAGS='-march=armv6 -mfpu=vfp -mfloat-abi=hard' \
|
||||
@@ -18,7 +18,13 @@ RUN if [ "$TARGETARCH/$TARGETVARIANT" = arm/v6 ]; then \
|
||||
rm -rf build && \
|
||||
apk del .ql-bundle-build; \
|
||||
fi
|
||||
RUN if [ "$TARGETARCH/$TARGETVARIANT" = arm/v6 ]; then export QEMU_CPU=arm1176; fi; \
|
||||
node /ql/scripts/build-upgrade-bundle.cjs /ql /out
|
||||
RUN ulimit -c 0; if [ "$TARGETARCH/$TARGETVARIANT" = arm/v6 ]; then \
|
||||
export QEMU_CPU=arm1176; \
|
||||
node --jitless /ql/scripts/build-upgrade-bundle.cjs /ql /out && \
|
||||
for archive in /out/*.tar.gz; do \
|
||||
node /ql/shell/install-bundle.cjs --root /tmp/ql-bundle-offline-smoke --file "$archive" --install && \
|
||||
rm -rf /tmp/ql-bundle-offline-smoke || exit 1; \
|
||||
done; \
|
||||
else node /ql/scripts/build-upgrade-bundle.cjs /ql /out; fi
|
||||
FROM scratch
|
||||
COPY --from=bundle /out/ /
|
||||
|
||||
@@ -42,6 +42,7 @@ QL_DIR=/opt/qinglong bash /opt/qinglong/shell/start-simplify.sh
|
||||
- 以 Node 实际运行架构、ARM 指令集和 libc 类型/版本选择包;不依据 Docker 架构别名猜测。
|
||||
- glibc、musl 分包,并记录构建系统 libc 下限。系统库仍以目标设备加载测试作为最后检查。
|
||||
- ARMv6 与 ARMv7 使用不同包名。ARMv6 在目标镜像中从源码编译 SQLite,禁止复用 generic `arm` 的 ARMv7 预编译件;构建需通过加载和内存库读写,失败则不发布渠道。
|
||||
- ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,正常面板进程的启动参数不变。依赖加载和 SQLite 读写仍由现有 Node 的独立进程验证。
|
||||
- tar 中允许 pnpm 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。
|
||||
|
||||
自托管镜像可设置 `QL_UPGRADE_INDEX_URL` 指向兼容索引;要求 HTTPS,测试时仅放行 loopback HTTP。索引和所有平台归档应部署到同一可访问源,避免依赖额外的 raw 域名。
|
||||
|
||||
@@ -84,6 +84,7 @@ async function buildBundle(root, output) {
|
||||
path.join(payload, 'bundle.json'),
|
||||
JSON.stringify(manifest),
|
||||
);
|
||||
console.log('Bundle inventory verified; writing archive');
|
||||
const filename = `qinglong-${build.sourceCommit}-${bundle.targetKey(
|
||||
target,
|
||||
)}.tar.gz`;
|
||||
@@ -100,6 +101,7 @@ async function buildBundle(root, output) {
|
||||
// Verify the bytes users receive, not just the pre-archive build directory.
|
||||
const roundtrip = path.join(workspace, 'roundtrip');
|
||||
await fs.mkdir(roundtrip);
|
||||
console.log('Verifying archived paths, permissions and checksums');
|
||||
await bundle.extractArchive(path.join(output, filename), roundtrip);
|
||||
const extracted = {
|
||||
source: path.join(roundtrip, 'qinglong/source'),
|
||||
@@ -108,6 +110,7 @@ async function buildBundle(root, output) {
|
||||
await bundle.verifyPayload(extracted, manifest, target);
|
||||
await verifyUpgrade(extracted.source, extracted.static);
|
||||
await bundle.smokeDependencies(extracted.source);
|
||||
console.log('Archived production dependencies and SQLite verified');
|
||||
const bytes = await fs.readFile(path.join(output, filename));
|
||||
const record = {
|
||||
format: 2,
|
||||
|
||||
@@ -9,6 +9,36 @@ const execute = promisify(require('node:child_process').execFile);
|
||||
const { parseArgs } = require('node:util');
|
||||
const artifacts = require('../cli/src/internal/maintenance/upgradeArtifacts.cjs');
|
||||
const jobs = require('../cli/src/internal/maintenance/panelUpgrade.cjs');
|
||||
const {
|
||||
nodeRuntimeArgs,
|
||||
} = require('../cli/src/internal/maintenance/platformBundle.cjs');
|
||||
|
||||
async function runInstallerMain(argv = process.argv.slice(2)) {
|
||||
const flags = nodeRuntimeArgs();
|
||||
if (!flags.length || process.execArgv.includes('--jitless'))
|
||||
return installBundle(argv);
|
||||
const child = require('node:child_process').spawn(
|
||||
process.execPath,
|
||||
[...flags, __filename, ...argv],
|
||||
{ stdio: 'inherit', env: process.env },
|
||||
);
|
||||
const interrupt = () => child.kill('SIGINT');
|
||||
const terminate = () => child.kill('SIGTERM');
|
||||
process.once('SIGINT', interrupt);
|
||||
process.once('SIGTERM', terminate);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
child.once('error', reject);
|
||||
child.once('exit', (code, signal) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`Offline installer exited: ${signal || code}`));
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
process.removeListener('SIGINT', interrupt);
|
||||
process.removeListener('SIGTERM', terminate);
|
||||
}
|
||||
}
|
||||
|
||||
async function installBundle(argv = process.argv.slice(2), env = process.env) {
|
||||
const { values } = parseArgs({
|
||||
@@ -274,9 +304,9 @@ async function installBundle(argv = process.argv.slice(2), env = process.env) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { installBundle };
|
||||
module.exports = { installBundle, runInstallerMain };
|
||||
if (require.main === module)
|
||||
installBundle().catch((error) => {
|
||||
runInstallerMain().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -6,6 +6,39 @@ const os = require('node:os');
|
||||
const tar = require('tar');
|
||||
const bundle = require('../../cli/src/internal/maintenance/platformBundle.cjs');
|
||||
const artifacts = require('../../shell/upgrade.cjs');
|
||||
|
||||
test('ARMv6 offline entry re-executes with an interpreted Node while preserving CLI errors', async (t) => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-armv6-entry-'));
|
||||
t.after(() => fs.rm(directory, { recursive: true, force: true }));
|
||||
const preload = path.join(directory, 'armv6.cjs');
|
||||
const observed = path.join(directory, 'child.json');
|
||||
await fs.writeFile(
|
||||
preload,
|
||||
`if(process.execArgv.includes('--jitless'))require('node:fs').writeFileSync(${JSON.stringify(
|
||||
observed,
|
||||
)},JSON.stringify({args:process.execArgv,execPath:process.execPath}));else{Object.defineProperty(process,'arch',{value:'arm'});Object.defineProperty(process,'config',{value:{variables:{arm_version:6}}});}`,
|
||||
);
|
||||
const { execFile } = require('node:child_process');
|
||||
const result = await new Promise((resolve) =>
|
||||
execFile(
|
||||
process.execPath,
|
||||
[path.resolve(__dirname, '../../shell/install-bundle.cjs')],
|
||||
{
|
||||
timeout: 10000,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_OPTIONS: '--require ' + JSON.stringify(preload),
|
||||
},
|
||||
},
|
||||
(error, stdout, stderr) => resolve({ error, stdout, stderr }),
|
||||
),
|
||||
);
|
||||
assert.equal(result.error.code, 1);
|
||||
const child = JSON.parse(await fs.readFile(observed));
|
||||
assert.ok(child.args.includes('--jitless'));
|
||||
assert.equal(child.execPath, process.execPath);
|
||||
assert.match(result.stderr, /Usage: node install-bundle.cjs/);
|
||||
});
|
||||
const {
|
||||
writeManifest,
|
||||
sourceCommit,
|
||||
|
||||
Reference in New Issue
Block a user