mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 15:56:59 +08:00
282 lines
11 KiB
JavaScript
282 lines
11 KiB
JavaScript
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)/,
|
||
);
|
||
},
|
||
);
|
||
}
|