mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
437 lines
16 KiB
JavaScript
437 lines
16 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { quotaEnvironment } = require('./helpers/quotaEnvironment.cjs');
|
|
const { test } = require('node:test');
|
|
const { CompletionReceiptFileStore } = require('../dist');
|
|
const { LocalProcessLaunchError, LocalProcessLauncher } = require('../dist');
|
|
|
|
const RUN_ID = '019f70e0-0000-7000-8000-000000000001';
|
|
const ATTEMPT_ID = '019f70e0-0000-7000-8000-000000000002';
|
|
const TOKEN = '0123456789abcdef0123456789abcdef0123456789abcdef';
|
|
|
|
function fixture(t) {
|
|
const directory = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-local-process-'),
|
|
);
|
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
return {
|
|
directory,
|
|
receiptRoot: path.join(directory, 'receipts'),
|
|
};
|
|
}
|
|
|
|
function identityProvider(onCapture = () => undefined) {
|
|
return {
|
|
async capture(pid) {
|
|
onCapture(pid);
|
|
return {
|
|
platform: 'linux',
|
|
bootId: '11111111-2222-3333-4444-555555555555',
|
|
pid,
|
|
processGroupId: pid,
|
|
startTimeTicks: '1',
|
|
};
|
|
},
|
|
async inspect(identity) {
|
|
return { status: 'running', identityPid: identity.pid };
|
|
},
|
|
};
|
|
}
|
|
|
|
async function waitForReceipt(store) {
|
|
for (let index = 0; index < 100; index += 1) {
|
|
const receipt = await store.read(ATTEMPT_ID);
|
|
if (receipt) return receipt;
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
throw new Error('receipt was not published');
|
|
}
|
|
|
|
test('persists the journal barrier before spawn and publishes a trusted receipt', async (t) => {
|
|
const { receiptRoot } = fixture(t);
|
|
let registered = false;
|
|
const journal = {
|
|
async register(command) {
|
|
assert.deepEqual(command, {
|
|
runId: RUN_ID,
|
|
attemptId: ATTEMPT_ID,
|
|
registeredAtMs: 100,
|
|
});
|
|
registered = true;
|
|
},
|
|
};
|
|
const launcher = new LocalProcessLauncher(journal, {
|
|
receiptRoot,
|
|
clock: { now: () => 100 },
|
|
createHandleId: () => 'handle-1',
|
|
identityProvider: identityProvider(() => assert.equal(registered, true)),
|
|
});
|
|
const handle = await launcher.start({
|
|
runId: RUN_ID,
|
|
attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1,
|
|
callbackToken: TOKEN,
|
|
command: {
|
|
kind: 'shell',
|
|
command:
|
|
'test -z "${QL3_RECEIPT_CALLBACK_TOKEN-}" && test -z "${QL3_RECEIPT_TARGET-}"',
|
|
},
|
|
environment: { USER_VALUE: 'present' },
|
|
});
|
|
assert.equal(handle.startedAtMs, 100);
|
|
assert.equal(handle.pid > 0, true);
|
|
assert.match(handle.durableHandle, /^ql3lp1\./);
|
|
assert.deepEqual(await handle.completion, { exitCode: 0, signal: null });
|
|
const receipt = await waitForReceipt(
|
|
new CompletionReceiptFileStore(receiptRoot),
|
|
);
|
|
assert.deepEqual(
|
|
{ ...receipt, finishedAtMs: 100 },
|
|
{
|
|
schemaVersion: 1,
|
|
runId: RUN_ID,
|
|
attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1,
|
|
token: TOKEN,
|
|
startedAtMs: 100,
|
|
finishedAtMs: 100,
|
|
exitCode: 0,
|
|
},
|
|
);
|
|
assert.equal(receipt.finishedAtMs >= 100, true);
|
|
});
|
|
|
|
test('journal failure prevents spawn and filesystem side effects from user code', async (t) => {
|
|
const { directory, receiptRoot } = fixture(t);
|
|
const marker = path.join(directory, 'spawned');
|
|
const launcher = new LocalProcessLauncher(
|
|
{
|
|
async register() {
|
|
throw new Error('database unavailable');
|
|
},
|
|
},
|
|
{ receiptRoot, identityProvider: identityProvider() },
|
|
);
|
|
await assert.rejects(
|
|
launcher.start({
|
|
runId: RUN_ID,
|
|
attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1,
|
|
callbackToken: TOKEN,
|
|
command: { kind: 'argv', file: '/usr/bin/touch', args: [marker] },
|
|
}),
|
|
LocalProcessLaunchError,
|
|
);
|
|
assert.equal(fs.existsSync(marker), false);
|
|
});
|
|
|
|
test('a modified launcher is rejected before durable registration', async (t) => {
|
|
const { directory, receiptRoot } = fixture(t);
|
|
const launcherPath = path.join(directory, 'launcher.sh');
|
|
fs.writeFileSync(launcherPath, '#!/bin/sh\nexit 0\n', { mode: 0o700 });
|
|
let registered = false;
|
|
const launcher = new LocalProcessLauncher(
|
|
{
|
|
async register() {
|
|
registered = true;
|
|
},
|
|
},
|
|
{ receiptRoot, launcherPath, identityProvider: identityProvider() },
|
|
);
|
|
await assert.rejects(
|
|
launcher.start({
|
|
runId: RUN_ID,
|
|
attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1,
|
|
callbackToken: TOKEN,
|
|
command: { kind: 'argv', file: '/usr/bin/true', args: [] },
|
|
}),
|
|
/digest does not match review/,
|
|
);
|
|
assert.equal(registered, false);
|
|
});
|
|
|
|
test('executes the verified launcher fd when its path is replaced after registration', async (t) => {
|
|
const { directory, receiptRoot } = fixture(t);
|
|
const launcherPath = path.join(directory, 'launcher.sh');
|
|
fs.copyFileSync(
|
|
path.resolve(__dirname, '../assets/ql3-launcher.sh'),
|
|
launcherPath,
|
|
);
|
|
const replacementPath = path.join(directory, 'replacement.sh');
|
|
const launcher = new LocalProcessLauncher(
|
|
{
|
|
async register() {
|
|
fs.writeFileSync(replacementPath, '#!/bin/sh\nexit 99\n', {
|
|
mode: 0o700,
|
|
});
|
|
fs.renameSync(replacementPath, launcherPath);
|
|
},
|
|
},
|
|
{
|
|
receiptRoot,
|
|
launcherPath,
|
|
clock: { now: () => 100 },
|
|
identityProvider: identityProvider(),
|
|
},
|
|
);
|
|
|
|
const handle = await launcher.start({
|
|
runId: RUN_ID,
|
|
attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1,
|
|
callbackToken: TOKEN,
|
|
command: { kind: 'argv', file: '/usr/bin/true', args: [] },
|
|
});
|
|
|
|
assert.deepEqual(await handle.completion, { exitCode: 0, signal: null });
|
|
assert.equal(
|
|
(await waitForReceipt(new CompletionReceiptFileStore(receiptRoot)))
|
|
.exitCode,
|
|
0,
|
|
);
|
|
});
|
|
|
|
test('hard-caps durable output and publishes an immutable truncation fact', async (t) => {
|
|
const { directory, receiptRoot } = fixture(t);
|
|
const logArtifactId = `local-${'b'.repeat(30)}`;
|
|
const outputDirectory = path.join(directory, 'artifacts', 'bb');
|
|
const outputFilePath = path.join(outputDirectory, `${logArtifactId}.log`);
|
|
const maximumBytes = 64 * 1024;
|
|
const launcher = new LocalProcessLauncher(
|
|
{ register: async () => undefined },
|
|
{
|
|
receiptRoot,
|
|
clock: { now: () => 100 },
|
|
identityProvider: identityProvider(),
|
|
},
|
|
);
|
|
const handle = await launcher.start({
|
|
runId: RUN_ID,
|
|
attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1,
|
|
callbackToken: TOKEN,
|
|
environment: quotaEnvironment(directory),
|
|
command: {
|
|
kind: 'argv',
|
|
file: process.execPath,
|
|
args: [
|
|
'-e',
|
|
[
|
|
'if (process.env.QL3_OUTPUT_QUOTA_FIFO) process.exit(91);',
|
|
'if (process.env.QL3_OUTPUT_TRUNCATION_TARGET) process.exit(92);',
|
|
'process.stdout.write(Buffer.alloc(256 * 1024, 0x61), () => process.exit(0));',
|
|
].join(''),
|
|
],
|
|
},
|
|
output: {
|
|
filePath: outputFilePath,
|
|
maximumBytes,
|
|
logArtifactId,
|
|
},
|
|
});
|
|
assert.deepEqual(await handle.completion, { exitCode: 0, signal: null });
|
|
assert.equal(fs.statSync(outputFilePath).size, maximumBytes);
|
|
const factPath = path.join(
|
|
outputDirectory,
|
|
`.${logArtifactId}.log.truncated.json`,
|
|
);
|
|
const fact = JSON.parse(fs.readFileSync(factPath, 'utf8'));
|
|
assert.deepEqual(
|
|
{
|
|
schemaVersion: fact.schemaVersion,
|
|
runId: fact.runId,
|
|
attemptId: fact.attemptId,
|
|
logArtifactId: fact.logArtifactId,
|
|
maximumBytes: fact.maximumBytes,
|
|
quotaReached: fact.quotaReached,
|
|
},
|
|
{
|
|
schemaVersion: 1,
|
|
runId: RUN_ID,
|
|
attemptId: ATTEMPT_ID,
|
|
logArtifactId,
|
|
maximumBytes,
|
|
quotaReached: true,
|
|
},
|
|
);
|
|
assert.equal(Number.isSafeInteger(fact.observedAtMs), true);
|
|
assert.deepEqual(
|
|
fs.readdirSync(outputDirectory).filter((name) => name.endsWith('.fifo')),
|
|
[],
|
|
);
|
|
});
|
|
|
|
for (const kind of ['argv', 'shell']) {
|
|
test(`publishes sparse binary ${kind} output before the process exits`, async (t) => {
|
|
const { directory, receiptRoot } = fixture(t);
|
|
const logArtifactId = `local-${'c'.repeat(30)}`;
|
|
const outputFilePath = path.join(directory, `${logArtifactId}.log`);
|
|
const release = path.join(directory, 'release');
|
|
const marker = Buffer.from([0x00, 0xff, 0x71, 0x6c, 0x33]);
|
|
const script = path.join(directory, 'producer.cjs');
|
|
fs.writeFileSync(script, `
|
|
const fs = require('node:fs');
|
|
process.stdout.write(Buffer.from([0x00, 0xff, 0x71, 0x6c, 0x33]));
|
|
const deadline = setTimeout(() => process.exit(92), 15000);
|
|
const timer = setInterval(() => {
|
|
if (!fs.existsSync(process.argv[2])) return;
|
|
clearInterval(timer);
|
|
clearTimeout(deadline);
|
|
process.stderr.write('tail', () => process.exit(7));
|
|
}, 10);
|
|
`, { mode: 0o600 });
|
|
const launcher = new LocalProcessLauncher(
|
|
{ register: async () => undefined },
|
|
{ receiptRoot, identityProvider: identityProvider() },
|
|
);
|
|
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
const handle = await launcher.start({
|
|
runId: RUN_ID, attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1, callbackToken: TOKEN,
|
|
environment: quotaEnvironment(directory),
|
|
command: kind === 'argv'
|
|
? { kind, file: process.execPath, args: [script, release] }
|
|
: { kind, command: [process.execPath, script, release].map(quote).join(' ') },
|
|
output: { filePath: outputFilePath, maximumBytes: 65536, logArtifactId },
|
|
});
|
|
try {
|
|
const deadline = Date.now() + 5000;
|
|
while (fs.statSync(outputFilePath).size < marker.length && Date.now() < deadline) {
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
assert.deepEqual(fs.readFileSync(outputFilePath), marker,
|
|
'sparse bytes must be readable while the producer is waiting for release');
|
|
assert.equal(await new CompletionReceiptFileStore(receiptRoot).read(ATTEMPT_ID), undefined);
|
|
fs.writeFileSync(release, '', { mode: 0o600 });
|
|
assert.deepEqual(await handle.completion, { exitCode: 7, signal: null });
|
|
assert.deepEqual(fs.readFileSync(outputFilePath), Buffer.concat([marker, Buffer.from('tail')]));
|
|
const fact = JSON.parse(fs.readFileSync(path.join(directory, `.${logArtifactId}.log.truncated.json`)));
|
|
assert.equal(fact.quotaReached, false);
|
|
assert.equal((await waitForReceipt(new CompletionReceiptFileStore(receiptRoot))).exitCode, 7);
|
|
} finally {
|
|
fs.writeFileSync(release, '', { mode: 0o600 });
|
|
await handle.completion;
|
|
}
|
|
});
|
|
}
|
|
|
|
for (const [label, initialBytes, producedBytes] of [
|
|
['empty output', 0, 0],
|
|
['exact quota', 0, 65536],
|
|
['one-byte overflow', 0, 65537],
|
|
['partial final block', 65513, 31],
|
|
['exhausted quota', 65536, 23],
|
|
['exhausted quota without overflow', 65536, 0],
|
|
]) {
|
|
test(`keeps byte-exact capture and truncation for ${label}`, async (t) => {
|
|
const { directory, receiptRoot } = fixture(t);
|
|
const logArtifactId = `local-${'d'.repeat(30)}`;
|
|
const filePath = path.join(directory, `${logArtifactId}.log`);
|
|
const initial = Buffer.alloc(initialBytes, 0x5a);
|
|
fs.writeFileSync(filePath, initial, { mode: 0o600 });
|
|
const produced = Buffer.from(Array.from({ length: producedBytes }, (_, i) => i % 256));
|
|
const launcher = new LocalProcessLauncher(
|
|
{ register: async () => undefined },
|
|
{ receiptRoot, identityProvider: identityProvider() },
|
|
);
|
|
const handle = await launcher.start({
|
|
runId: RUN_ID, attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1, callbackToken: TOKEN,
|
|
environment: quotaEnvironment(directory),
|
|
command: { kind: 'argv', file: process.execPath, args: ['-e', `
|
|
const data = Buffer.from(Array.from({length: ${producedBytes}}, (_, i) => i % 256));
|
|
let offset = 0;
|
|
function write() {
|
|
if (offset === data.length) return process.exit(9);
|
|
const end = Math.min(data.length, offset + 997);
|
|
const chunk = data.subarray(offset, end);
|
|
offset = end;
|
|
process.stdout.write(chunk, () => setImmediate(write));
|
|
}
|
|
write();
|
|
`] },
|
|
output: { filePath, maximumBytes: 65536, logArtifactId },
|
|
});
|
|
assert.deepEqual(await handle.completion, { exitCode: 9, signal: null });
|
|
assert.deepEqual(fs.readFileSync(filePath), Buffer.concat([initial, produced]).subarray(0, 65536));
|
|
const fact = JSON.parse(fs.readFileSync(path.join(directory, `.${logArtifactId}.log.truncated.json`)));
|
|
assert.equal(fact.quotaReached, initialBytes + producedBytes > 65536);
|
|
assert.equal((await waitForReceipt(new CompletionReceiptFileStore(receiptRoot))).exitCode, 9);
|
|
});
|
|
}
|
|
|
|
for (const availability of ['missing', 'not-executable', 'rejecting']) {
|
|
test(`rejects unsupported capture utilities (${availability}) before running user code`, async (t) => {
|
|
const { directory, receiptRoot } = fixture(t);
|
|
const bin = path.join(directory, 'unsupported-bin');
|
|
fs.mkdirSync(bin, { mode: 0o700 });
|
|
if (availability !== 'missing') {
|
|
for (const name of ['busybox', 'head', 'stdbuf']) {
|
|
fs.writeFileSync(path.join(bin, name), '#!/bin/sh\nexit 1\n', {
|
|
mode: availability === 'not-executable' ? 0o600 : 0o700,
|
|
});
|
|
}
|
|
}
|
|
const marker = path.join(directory, 'must-not-run');
|
|
const logArtifactId = `local-${'e'.repeat(30)}`;
|
|
const filePath = path.join(directory, `${logArtifactId}.log`);
|
|
const launcher = new LocalProcessLauncher(
|
|
{ register: async () => undefined },
|
|
{ receiptRoot, identityProvider: identityProvider() },
|
|
);
|
|
const handle = await launcher.start({
|
|
runId: RUN_ID, attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1, callbackToken: TOKEN,
|
|
// Keep this negative fixture isolated even on a noexec tmpfs: a shell
|
|
// must not fall through an unexecutable stub to real system utilities.
|
|
environment: { PATH: bin },
|
|
// Use the actual installed Node binary so an accidental launch always
|
|
// writes the marker, rather than failing because /usr/bin/touch is absent.
|
|
command: { kind: 'argv', file: process.execPath, args: [
|
|
'-e', "require('node:fs').writeFileSync(process.argv[1], 'ran');", marker,
|
|
] },
|
|
output: { filePath, maximumBytes: 65536, logArtifactId },
|
|
});
|
|
assert.deepEqual(await handle.completion, { exitCode: 125, signal: null });
|
|
assert.equal(fs.existsSync(marker), false);
|
|
assert.equal(fs.statSync(filePath).size, 0);
|
|
assert.equal(await new CompletionReceiptFileStore(receiptRoot).read(ATTEMPT_ID), undefined);
|
|
assert.equal(fs.readdirSync(directory).some((name) => name.endsWith('.fifo')), false);
|
|
});
|
|
}
|
|
|
|
test('capture failure drains output without forging a truncation fact or changing user exit', async (t) => {
|
|
const { directory, receiptRoot } = fixture(t);
|
|
const bin = path.join(directory, 'failing-bin');
|
|
fs.mkdirSync(bin, { mode: 0o700 });
|
|
fs.writeFileSync(path.join(bin, 'busybox'), `#!/bin/sh
|
|
case " $* " in
|
|
*' count=0 '*) exit 0 ;;
|
|
esac
|
|
printf 'capture failure must not bypass the log quota' >&2
|
|
exit 1
|
|
`, { mode: 0o700 });
|
|
const logArtifactId = `local-${'f'.repeat(30)}`;
|
|
const filePath = path.join(directory, `${logArtifactId}.log`);
|
|
const launcher = new LocalProcessLauncher(
|
|
{ register: async () => undefined },
|
|
{ receiptRoot, identityProvider: identityProvider() },
|
|
);
|
|
const handle = await launcher.start({
|
|
runId: RUN_ID, attemptId: ATTEMPT_ID,
|
|
callbackSequence: 1, callbackToken: TOKEN,
|
|
environment: { PATH: `${bin}:/usr/bin:/bin` },
|
|
command: { kind: 'argv', file: process.execPath, args: [
|
|
'-e', 'process.stdout.write(Buffer.alloc(256 * 1024), () => process.exit(9));',
|
|
] },
|
|
output: { filePath, maximumBytes: 65536, logArtifactId },
|
|
});
|
|
assert.deepEqual(await handle.completion, { exitCode: 9, signal: null });
|
|
assert.equal(fs.statSync(filePath).size, 0);
|
|
assert.equal(fs.existsSync(path.join(directory, `.${logArtifactId}.log.truncated.json`)), false);
|
|
assert.equal((await waitForReceipt(new CompletionReceiptFileStore(receiptRoot))).exitCode, 9);
|
|
});
|