From 12513b73c39dcdc778d4aef5e281f474a1d57ff7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 19:54:09 +0000 Subject: [PATCH] Runner: Docker-Zeitlimit liefert sicher 124 nach rm -f Co-Authored-By: Claude Sonnet 5.5 --- runner/src/docker.js | 11 +++++++++-- runner/test/docker.test.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/runner/src/docker.js b/runner/src/docker.js index 0a12801..7591b5d 100644 --- a/runner/src/docker.js +++ b/runner/src/docker.js @@ -139,6 +139,7 @@ export function createDocker({ exec = spawnDocker, baseEnv = process.env, envDir child.stdout?.on('data', (d) => split.push(String(d))); child.stderr?.on('data', (d) => split2.push(String(d))); let done = false; + let timedOut = false; let timer; const finish = (res) => { if (done) return; @@ -148,10 +149,16 @@ export function createDocker({ exec = spawnDocker, baseEnv = process.env, envDir split.flush(); split2.flush(); resolve(res); }; - child.on('error', (e) => { if (!done) { done = true; clearTimeout(timer); envFile?.cleanup(); reject(e); } }); - child.on('close', (code) => finish({ code: code ?? 1 })); + child.on('error', (e) => { + if (done) return; + if (timedOut) return; // Zeitlimit-Pfad löst selbst auf (124) + done = true; clearTimeout(timer); envFile?.cleanup(); reject(e); + }); + child.on('close', (code) => { if (!timedOut) finish({ code: code ?? 1 }); }); if (timeoutMs > 0) { timer = setTimeout(async () => { + if (done) return; + timedOut = true; try { child.kill('SIGKILL'); } catch { /* schon beendet */ } const name = optionName(args); if (name) await forceRemove(name); diff --git a/runner/test/docker.test.js b/runner/test/docker.test.js index 0af14b2..bcd0522 100644 --- a/runner/test/docker.test.js +++ b/runner/test/docker.test.js @@ -169,3 +169,41 @@ test('rm/stop: ungültiger Name wird abgelehnt', async () => { await assert.rejects(docker.rm('--all')); await assert.rejects(docker.stop('a b')); }); + +test('Zeitlimit: close direkt nach kill ändert Ergebnis nicht, rm -f wird vor Auflösung abgewartet', async () => { + const events = []; + const child = fakeChild(); + child.kill = (s) => { child.killed.push(s); setImmediate(() => child.emit('close', null)); }; + const rmChild = fakeChild(); + let calls = 0; + const docker = createDocker({ rmCapMs: 1000, exec: (args) => { + if (calls++ === 0) return child; + events.push('rm-start'); + setTimeout(() => { events.push('rm-done'); rmChild.emit('close', 0); }, 40); + return rmChild; + } }); + const r = await docker.run(['run', '--name', 'job-2', 'node:20', 'x'], { timeoutMs: 10 }); + events.push('resolved'); + assert.deepEqual(r, { code: 124 }); + assert.deepEqual(events, ['rm-start', 'rm-done', 'resolved']); +}); + +test('spawn-Fehler ⇒ env-file wird entfernt, run lehnt ab', async () => { + let filePath; + const child = fakeChild(); + const docker = createDocker({ exec: (args) => { filePath = args[args.indexOf('--env-file') + 1]; return child; } }); + const p = docker.run(['run', 'node:20'], { env: { K: 'v' } }); + assert.equal(existsSync(filePath), true); + child.emit('error', new Error('ENOENT')); + await assert.rejects(p, /ENOENT/); + assert.equal(existsSync(filePath), false); +}); + +test('Zeitlimit mit env-file: Datei wird entfernt', async () => { + let filePath; + const child = fakeChild(); + child.kill = () => setImmediate(() => child.emit('close', null)); + const docker = createDocker({ rmCapMs: 10, exec: (args) => { if (args[0] === 'run') { filePath = args[args.indexOf('--env-file') + 1]; return child; } return fakeChild(); } }); + assert.deepEqual(await docker.run(['run', '--name', 'j', 'node:20'], { env: { K: 'v' }, timeoutMs: 10 }), { code: 124 }); + assert.equal(existsSync(filePath), false); +});