Wait for Pi qualification shutdown before removing its temporary home
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
14163ef057
commit
802dfe5e9e
|
|
@ -73,7 +73,7 @@
|
|||
"typecheck:rust": "cargo fmt --manifest-path runner/Cargo.toml --all -- --check && cargo check --manifest-path runner/Cargo.toml --locked --workspace",
|
||||
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
|
||||
"test": "pnpm run test:typescript && pnpm run test:rust",
|
||||
"test:typescript": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/materialize-pi-binary.test.mjs scripts/portable-provider-shim.test.mjs scripts/provider-pack-layout.test.mjs scripts/build-provider-pack.test.mjs && vitest run",
|
||||
"test:typescript": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/materialize-pi-binary.test.mjs scripts/portable-provider-shim.test.mjs scripts/provider-pack-layout.test.mjs scripts/build-provider-pack.test.mjs scripts/verify-pi-provider-launch.test.mjs && vitest run",
|
||||
"test:rust": "cargo test --release --manifest-path runner/Cargo.toml --locked --workspace",
|
||||
"test:codex": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider",
|
||||
"test:durable": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core durable::",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ const child = lease.spawn([], { cwd: root, detached: true, env: {
|
|||
// Makes the static OpenRouter model catalog selectable; never sends a prompt.
|
||||
OPENROUTER_API_KEY: "qualification-no-model-requests",
|
||||
} });
|
||||
// Register before requesting a session so even an early close is observed.
|
||||
let childDidClose = false;
|
||||
const childClosed = new Promise((resolveClose) => child.once("close", () => { childDidClose = true; resolveClose(); }));
|
||||
let buffer = "", stderr = "";
|
||||
const pending = new Map();
|
||||
child.stderr.on("data", (data) => { stderr = (stderr + data).slice(-4000); });
|
||||
|
|
@ -47,7 +50,24 @@ try {
|
|||
assert.equal(typeof session.result?.sessionId, "string");
|
||||
console.log("Verified Pi ACP and pinned RPC runtime started successfully");
|
||||
} finally {
|
||||
try { process.kill(-child.pid, "SIGTERM"); } catch {}
|
||||
await lease.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
// Pi can still write its private home while handling SIGTERM. Keep the
|
||||
// temporary directory until its pipes close, with a bounded group teardown.
|
||||
let forceTimer, deadlineTimer;
|
||||
const shutdownDeadline = new Promise((_, reject) => {
|
||||
forceTimer = setTimeout(() => {
|
||||
try { if (!childDidClose) process.kill(-child.pid, "SIGKILL"); }
|
||||
catch (error) { if (error.code !== "ESRCH") reject(error); }
|
||||
}, 5_000);
|
||||
deadlineTimer = setTimeout(() => reject(new Error("Pi qualification process did not close after SIGKILL")), 10_000);
|
||||
});
|
||||
try {
|
||||
try { if (!childDidClose) process.kill(-child.pid, "SIGTERM"); }
|
||||
catch (error) { if (error.code !== "ESRCH") throw error; }
|
||||
await Promise.race([childClosed, shutdownDeadline]);
|
||||
} finally {
|
||||
clearTimeout(forceTimer);
|
||||
clearTimeout(deadlineTimer);
|
||||
await lease.close();
|
||||
}
|
||||
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import {test} from 'node:test';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {mkdtemp,mkdir,writeFile,readFile,rm} from 'node:fs/promises';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join,basename,dirname} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const verifier=fileURLToPath(new URL('./verify-pi-provider-launch.mjs',import.meta.url));
|
||||
for(const ignoreTerm of [false,true]) test('Pi qualification waits for process closure before removing HOME (ignore SIGTERM='+ignoreTerm+')',async(t)=>{
|
||||
if(process.platform==='win32'){t.skip('The image verifier uses POSIX process groups');return;}
|
||||
const root=await mkdtemp(join(tmpdir(),'pi-qualification-cleanup-test-'));
|
||||
try{
|
||||
const pack=join(root,'pack'),modules=join(pack,'dist/drivers/acpx');await mkdir(modules,{recursive:true});
|
||||
await writeFile(join(pack,'package.json'),JSON.stringify({type:'module'}));
|
||||
await writeFile(join(modules,'qualified-profiles.js'),'export const resolveQualifiedAcpxProfile=()=>({});');
|
||||
await writeFile(join(modules,'installation-integrity.js'),`
|
||||
import assert from 'node:assert/strict';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {writeFile} from 'node:fs/promises';
|
||||
export const createAcpxPackageJsonResolver=()=>({});
|
||||
export async function verifyQualifiedAcpxInstallation(){return {openCommand:async()=>{
|
||||
let child,closed=false;
|
||||
return {spawn:(_args,options)=>{
|
||||
child=spawn(${JSON.stringify(process.execPath)},[${JSON.stringify(join(root,'provider.mjs'))}],{...options,stdio:['pipe','pipe','pipe']});
|
||||
child.once('close',()=>{closed=true;});
|
||||
return child;
|
||||
},close:async()=>{assert(closed,'lease was closed before the provider completed its shutdown writes');await writeFile(${JSON.stringify(join(root,'lease-closed'))},String(child.signalCode??child.exitCode));}};
|
||||
}};}
|
||||
`);
|
||||
await writeFile(join(root,'provider.mjs'),`
|
||||
import fs from 'node:fs';import readline from 'node:readline';import path from 'node:path';
|
||||
fs.writeFileSync(${JSON.stringify(join(root,'home-path'))},process.env.HOME);
|
||||
readline.createInterface({input:process.stdin}).on('line',line=>{const request=JSON.parse(line);process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,result:request.method==='initialize'?{}:{sessionId:'fixture-session'}})+'\\n');});
|
||||
process.on('SIGTERM',()=>{if(${ignoreTerm})return;setTimeout(()=>{fs.mkdirSync(path.join(process.env.HOME,'.pi'),{recursive:true});fs.writeFileSync(path.join(process.env.HOME,'.pi','last-write'),'shutdown completed');fs.writeFileSync(${JSON.stringify(join(root,'shutdown-finished'))},'finished');process.exit(0);},150);});
|
||||
`);
|
||||
const child=spawn(process.execPath,[verifier,pack],{stdio:['ignore','pipe','pipe']});
|
||||
let stdout='',stderr='';child.stdout.on('data',b=>stdout+=b);child.stderr.on('data',b=>stderr+=b);
|
||||
const timer=setTimeout(()=>child.kill('SIGKILL'),15000);
|
||||
let code;try{code=await new Promise((resolve,reject)=>{child.once('error',reject);child.once('close',resolve);});}finally{clearTimeout(timer);}
|
||||
assert.equal(code,0,stderr);assert.match(stdout,/Verified Pi ACP/);
|
||||
if(!ignoreTerm)assert.equal(await readFile(join(root,'shutdown-finished'),'utf8'),'finished');
|
||||
assert.equal(await readFile(join(root,'lease-closed'),'utf8'),ignoreTerm?'SIGKILL':'0');
|
||||
const home=await readFile(join(root,'home-path'),'utf8');await assert.rejects(readFile(join(home,'.pi','last-write')), {code:'ENOENT'});
|
||||
}finally{
|
||||
const home=await readFile(join(root,'home-path'),'utf8').catch(()=>null);
|
||||
if(home){assert.equal(dirname(home),tmpdir());assert(basename(home).startsWith('pi-qualified-launch-'));await rm(home,{recursive:true,force:true});}
|
||||
await rm(root,{recursive:true,force:true});
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue