Skip to content

Commit 872102c

Browse files
chore(manifest): ensure e2e teardown
1 parent 7b7d4b3 commit 872102c

File tree

1 file changed

+90
-5
lines changed

1 file changed

+90
-5
lines changed

tools/scripts/run-manifest-e2e.mjs

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ async function runScenario(name) {
7878

7979
const serve = spawn(scenario.serveCmd[0], scenario.serveCmd.slice(1), {
8080
stdio: 'inherit',
81+
detached: true,
8182
});
8283

8384
let serveExitInfo;
@@ -150,13 +151,9 @@ async function runScenario(name) {
150151
} finally {
151152
shutdownRequested = true;
152153

153-
if (serve.exitCode === null && serve.signalCode === null) {
154-
serve.kill('SIGINT');
155-
}
156-
157154
let serveExitError = null;
158155
try {
159-
await serveExitPromise;
156+
await shutdownServe(serve, serveExitPromise);
160157
} catch (error) {
161158
console.error('[manifest-e2e] Serve command emitted error:', error);
162159
serveExitError = error;
@@ -214,6 +211,94 @@ function spawnWithPromise(cmd, args, options = {}) {
214211
return { child, promise };
215212
}
216213

214+
async function shutdownServe(proc, exitPromise) {
215+
if (proc.exitCode !== null || proc.signalCode !== null) {
216+
return exitPromise;
217+
}
218+
219+
const sequence = [
220+
{ signal: 'SIGINT', timeoutMs: 8000 },
221+
{ signal: 'SIGTERM', timeoutMs: 5000 },
222+
{ signal: 'SIGKILL', timeoutMs: 3000 },
223+
];
224+
225+
for (const { signal, timeoutMs } of sequence) {
226+
if (proc.exitCode !== null || proc.signalCode !== null) {
227+
break;
228+
}
229+
230+
sendSignal(proc, signal);
231+
232+
try {
233+
await waitWithTimeout(exitPromise, timeoutMs);
234+
break;
235+
} catch (error) {
236+
if (error?.name !== 'TimeoutError') {
237+
throw error;
238+
}
239+
// escalate to next signal on timeout
240+
}
241+
}
242+
243+
return exitPromise;
244+
}
245+
246+
function sendSignal(proc, signal) {
247+
if (proc.exitCode !== null || proc.signalCode !== null) {
248+
return;
249+
}
250+
251+
try {
252+
process.kill(-proc.pid, signal);
253+
} catch (error) {
254+
if (error.code !== 'ESRCH' && error.code !== 'EPERM') {
255+
throw error;
256+
}
257+
try {
258+
proc.kill(signal);
259+
} catch (innerError) {
260+
if (innerError.code !== 'ESRCH') {
261+
throw innerError;
262+
}
263+
}
264+
}
265+
}
266+
267+
function waitWithTimeout(promise, timeoutMs) {
268+
return new Promise((resolve, reject) => {
269+
let settled = false;
270+
271+
const timer = setTimeout(() => {
272+
if (settled) {
273+
return;
274+
}
275+
settled = true;
276+
const timeoutError = new Error(`Timed out after ${timeoutMs}ms`);
277+
timeoutError.name = 'TimeoutError';
278+
reject(timeoutError);
279+
}, timeoutMs);
280+
281+
promise.then(
282+
(value) => {
283+
if (settled) {
284+
return;
285+
}
286+
settled = true;
287+
clearTimeout(timer);
288+
resolve(value);
289+
},
290+
(error) => {
291+
if (settled) {
292+
return;
293+
}
294+
settled = true;
295+
clearTimeout(timer);
296+
reject(error);
297+
},
298+
);
299+
});
300+
}
301+
217302
function isExpectedServeExit(info) {
218303
if (!info) {
219304
return false;

0 commit comments

Comments
 (0)