Files
qinglong/cli/test/internal/backendProcesses.test.cjs
T

282 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const path = require('node:path');
const os = require('node:os');
const {
findDirectBackendPids,
} = require('../../dist/internal/runtime/backendProcesses');
test('backend discovery accepts only owned compatibility flags and the exact installation entry', async (t) => {
const root = await fs.realpath(
await fs.mkdtemp(path.join(os.tmpdir(), 'ql-flagged-proc-')),
);
t.after(() => fs.rm(root, { recursive: true, force: true }));
const proc = path.join(root, 'proc'),
panel = path.join(root, 'panel with spaces');
const other = path.join(root, 'other/static/build/app.js');
const entry = path.join(panel, 'static/build/app.js');
for (const file of [entry, other]) {
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, '');
}
const flags = ['--no-opt', '--no-sparkplug', '--no-maglev'];
const cases = [
[['/usr/bin/node', ...flags, entry], true],
[['node24', ...flags, 'static/build/app.js'], true],
[['node', ...flags, other], false],
[['node', '-e', entry], false],
[['node', '-r', entry], false],
[['node', ...flags, entry, 'argument'], false],
[['node', '--no-opt', entry], false],
[['bash', ...flags, entry], false],
[['node', ...flags, '-e', entry], false],
];
const expected = [];
for (const [i, [args, matches]] of cases.entries()) {
const pid = 910001 + i,
directory = path.join(proc, String(pid));
await fs.mkdir(directory, { recursive: true });
await fs.writeFile(path.join(directory, 'cmdline'), args.join('\0') + '\0');
await fs.symlink(panel, path.join(directory, 'cwd'));
if (matches) expected.push(pid);
}
assert.deepEqual((await findDirectBackendPids(entry, proc)).sort(), expected);
});
test('Linux backend discovery scopes legacy relative and absolute commands to one installation', async (t) => {
const root = await fs.realpath(
await fs.mkdtemp(path.join(os.tmpdir(), 'ql-proc-')),
);
t.after(() => fs.rm(root, { recursive: true, force: true }));
const proc = path.join(root, 'proc');
const panel = path.join(root, 'panel with spaces'),
other = path.join(root, 'other');
const entry = path.join(panel, 'static/build/app.js');
for (const base of [panel, other]) {
await fs.mkdir(path.join(base, 'static/build'), { recursive: true });
await fs.writeFile(path.join(base, 'static/build/app.js'), '');
}
await fs.mkdir(proc);
const alias = path.join(root, 'alias');
await fs.symlink(panel, alias);
async function processFixture(pid, args, cwd = panel) {
const directory = path.join(proc, String(pid));
await fs.mkdir(directory);
await fs.writeFile(path.join(directory, 'cmdline'), args.join('\0') + '\0');
await fs.symlink(cwd, path.join(directory, 'cwd'));
}
// Use synthetic PIDs far outside ordinary host ranges; no signals are sent.
await processFixture(900001, ['node', 'static/build/app.js']);
await processFixture(900002, ['/usr/bin/node', entry], other);
await processFixture(900003, ['node', './static/build/app.js'], alias);
await processFixture(900004, ['node', 'static/build/app.js'], other);
await processFixture(900005, ['node', '-e', 'static/build/app.js']);
await processFixture(900006, ['bash', entry]);
await processFixture(900007, ['node', entry, 'extra']);
await processFixture(900008, ['node', 'missing.js']);
await processFixture(
900009,
['node', 'static/build/app.js'],
path.join(root, 'gone'),
);
await processFixture(process.pid, ['node', entry]);
await fs.mkdir(path.join(proc, '900010')); // Exited before cmdline could be read.
await fs.mkdir(path.join(proc, 'self'));
assert.deepEqual(
(await findDirectBackendPids(entry, proc)).sort(),
[900001, 900002, 900003],
);
assert.deepEqual(
await findDirectBackendPids(path.join(root, 'missing/app.js'), proc),
[],
);
});
test(
'real Linux stop and direct startup isolate panel installations',
{
skip: process.platform !== 'linux',
timeout: 15000,
},
async (t) => {
const { spawn } = require('node:child_process');
const { once } = require('node:events');
const { createContext } = require('../../dist/internal/runtime/context');
const {
stopPanel,
startPanel,
} = require('../../dist/internal/maintenance/operator');
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-live-proc-'));
const children = [];
let fallbackPid;
t.after(async () => {
for (const child of children) {
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGKILL');
await once(child, 'close');
}
}
if (fallbackPid) {
try {
process.kill(fallbackPid, 'SIGKILL');
} catch {}
}
await fs.rm(root, { recursive: true, force: true });
});
const contexts = [];
for (const name of ['panel with spaces', 'other-panel']) {
await fs.mkdir(path.join(root, name));
const context = createContext(
{ root: path.join(root, name) },
{ PATH: '' },
);
contexts.push(context);
const entry = path.join(context.paths.dir_static, 'build/app.js');
await fs.mkdir(path.dirname(entry), { recursive: true });
await fs.writeFile(
entry,
'process.on("SIGTERM",()=>{console.log("terminated");process.exit(0)});console.log("ready");setInterval(()=>{},1000)',
);
const child = spawn(process.execPath, ['static/build/app.js'], {
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe'],
});
children.push(child);
await once(child.stdout, 'data');
assert.equal(child.exitCode, null);
}
const [target, other] = contexts;
const entry = path.join(target.paths.dir_static, 'build/app.js');
assert.deepEqual(await findDirectBackendPids(entry), [children[0].pid]);
const stopped = once(children[0], 'close');
await stopPanel(target);
assert.deepEqual(await stopped, [0, null]);
assert.equal(children[1].exitCode, null);
assert.equal(children[1].signalCode, null);
assert.deepEqual(
await findDirectBackendPids(
path.join(other.paths.dir_static, 'build/app.js'),
),
[children[1].pid],
);
const started = await startPanel(target);
fallbackPid = started.pid;
assert.equal(started.manager, 'node');
assert.ok(Number.isSafeInteger(fallbackPid));
assert.deepEqual(await findDirectBackendPids(entry), [fallbackPid]);
assert.match(
await fs.readFile(
path.join(target.paths.dir_log, 'qinglong.log'),
'utf8',
),
/ready/,
);
await stopPanel(target);
const deadline = Date.now() + 2000;
while ((await findDirectBackendPids(entry)).length && Date.now() < deadline)
await new Promise((resolve) => setTimeout(resolve, 20));
assert.deepEqual(await findDirectBackendPids(entry), []);
fallbackPid = undefined;
assert.equal(children[1].exitCode, null);
},
);
test(
'Linux direct restart waits for port release and refuses an unresponsive old service',
{
skip: process.platform !== 'linux',
timeout: 20000,
},
async (t) => {
const { spawn } = require('node:child_process');
const { once } = require('node:events');
const { createContext } = require('../../dist/internal/runtime/context');
const { startPanel } = require('../../dist/internal/maintenance/operator');
for (const mode of ['delayed', 'ignore']) {
await t.test(mode, async () => {
const root = await fs.mkdtemp(
path.join(os.tmpdir(), 'ql-port-handoff-'),
);
const context = createContext(
{ root },
{ PATH: '', MODE: mode, QL_LANG: 'en' },
);
const entry = path.join(context.paths.dir_static, 'build/app.js');
const marker = path.join(root, 'starts');
context.env.STARTS = marker;
await fs.mkdir(path.dirname(entry), { recursive: true });
await fs.writeFile(
entry,
'require("fs").appendFileSync(process.env.STARTS,"start\\n");' +
'const server=require("http").createServer((q,s)=>s.end(String(process.pid)));' +
'process.on("SIGTERM",()=>{if(process.env.MODE!=="ignore")setTimeout(()=>server.close(()=>process.exit(0)),400)});' +
'server.listen(Number(process.env.PORT||0),"127.0.0.1",()=>console.log(server.address().port));',
);
const old = spawn(process.execPath, ['static/build/app.js'], {
cwd: root,
env: context.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let pid;
try {
const [chunk] = await once(old.stdout, 'data');
context.env.PORT = chunk.toString().trim();
assert.match(context.env.PORT, /^\d+$/);
if (mode === 'ignore') {
await assert.rejects(startPanel(context), /did not stop/);
assert.equal(await fs.readFile(marker, 'utf8'), 'start\n');
assert.equal(old.exitCode, null);
} else {
const result = await startPanel(context);
pid = result.pid;
assert.equal(result.manager, 'node');
assert.equal(old.exitCode, 0);
const response = await fetch(
`http://127.0.0.1:${context.env.PORT}`,
);
assert.equal(await response.text(), String(pid));
assert.equal(await fs.readFile(marker, 'utf8'), 'start\nstart\n');
}
} finally {
if (old.exitCode === null && old.signalCode === null) {
old.kill('SIGKILL');
await once(old, 'close');
}
if (pid) {
try {
process.kill(pid, 'SIGKILL');
} catch {}
}
await fs.rm(root, { recursive: true, force: true });
}
});
}
},
);
for (const language of ['zh', 'en', 'unsupported']) {
test(
`direct backend startup failure retains localized exit detail: ${language}`,
{ skip: process.platform !== 'linux' },
async (t) => {
const root = await fs.mkdtemp(
path.join(os.tmpdir(), 'ql-start-language-'),
);
t.after(() => fs.rm(root, { recursive: true, force: true }));
const { createContext } = require('../../dist/internal/runtime/context');
const {
startPanel,
} = require('../../dist/internal/maintenance/operator');
const context = createContext({ root }, { PATH: '', QL_LANG: language });
const entry = path.join(context.paths.dir_static, 'build/app.js');
await fs.mkdir(path.dirname(entry), { recursive: true });
await fs.writeFile(entry, 'process.exitCode=9;');
await assert.rejects(
startPanel(context),
language === 'en' ? /startup \(9\)/ : /启动期间退出(9)/,
);
},
);
}