39 lines
2.4 KiB
JavaScript
39 lines
2.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {spawn} from 'node:child_process';
|
|
import {watch,mkdtempSync,readFileSync,existsSync,rmSync} from 'node:fs';
|
|
import {join} from 'node:path';
|
|
import {tmpdir} from 'node:os';
|
|
import {once} from 'node:events';
|
|
|
|
// Reproduce the gate's external TERM, not Node's cooperative test-abort hook.
|
|
test('external gate termination closes owned detached fixture work before exit', {timeout:10000}, async()=>{
|
|
const root=mkdtempSync(join(tmpdir(),'copy-outer-abort-')),ready=join(root,'ready');
|
|
const helper=new URL('./fixtures/owned-child.mjs',import.meta.url).href;
|
|
let descendant,outer;
|
|
const watcher=watch(root);
|
|
try{
|
|
const announced=new Promise(resolve=>{watcher.on('change',()=>{if(existsSync(ready))resolve()})});
|
|
const childCode=`const fs=require('node:fs');fs.writeFileSync(${JSON.stringify(ready+'.tmp')},String(process.pid));fs.renameSync(${JSON.stringify(ready+'.tmp')},${JSON.stringify(ready)});setInterval(()=>{},1000);`;
|
|
const code=`import {createOwnedRunner} from ${JSON.stringify(helper)};const t={after(){},signal:new AbortController().signal};const r=createOwnedRunner(t,()=>process.env);await r.run(process.execPath,['-e',${JSON.stringify(childCode)}]);`;
|
|
outer=spawn(process.execPath,['--input-type=module','-e',code],{stdio:['ignore','pipe','pipe']});
|
|
const exited=once(outer,'close');
|
|
await Promise.race([announced,exited.then(()=>{throw Error('outer exited before readiness')})]);
|
|
descendant=Number(readFileSync(ready,'utf8'));assert.ok(Number.isSafeInteger(descendant)&&descendant>1);
|
|
assert.ok(readFileSync(`/proc/${descendant}/cmdline`,'utf8').includes(ready),'same owned descendant');
|
|
outer.kill('SIGTERM');
|
|
const [status,signal]=await exited;
|
|
assert.equal(existsSync(`/proc/${descendant}`),false,'owned detached child is reaped before outer exit');
|
|
assert.equal(status,143);assert.equal(signal,null);
|
|
}finally{
|
|
watcher.close();
|
|
if(descendant&&existsSync(`/proc/${descendant}`)){
|
|
// Only this captured owned child group; no process/registry census.
|
|
assert.ok(readFileSync(`/proc/${descendant}/cmdline`,'utf8').includes(ready));
|
|
try{process.kill(-descendant,'SIGKILL')}catch(e){if(e.code!=='ESRCH')throw e}
|
|
}
|
|
if(outer&&outer.exitCode===null&&outer.signalCode===null){const done=once(outer,'close');outer.kill('SIGKILL');await done}
|
|
rmSync(root,{recursive:true,force:true});
|
|
}
|
|
});
|