|
1 | | -import { spawn } from "node:child_process"; |
| 1 | +import { spawn, type ChildProcess } from "node:child_process"; |
2 | 2 | import { randomUUID } from "node:crypto"; |
3 | 3 | import { createWriteStream, existsSync } from "node:fs"; |
4 | 4 | import fs from "node:fs/promises"; |
@@ -339,8 +339,57 @@ export const __testing = { |
339 | 339 | resolveQaBundledPluginsSourceRoot, |
340 | 340 | resolveQaRuntimeHostVersion, |
341 | 341 | createQaBundledPluginsDir, |
| 342 | + stopQaGatewayChildProcessTree, |
342 | 343 | }; |
343 | 344 |
|
| 345 | +function hasChildExited(child: ChildProcess) { |
| 346 | + return child.exitCode !== null || child.signalCode !== null; |
| 347 | +} |
| 348 | + |
| 349 | +function signalQaGatewayChildProcessTree(child: ChildProcess, signal: NodeJS.Signals) { |
| 350 | + if (!child.pid) { |
| 351 | + return; |
| 352 | + } |
| 353 | + try { |
| 354 | + if (process.platform === "win32") { |
| 355 | + child.kill(signal); |
| 356 | + return; |
| 357 | + } |
| 358 | + process.kill(-child.pid, signal); |
| 359 | + } catch { |
| 360 | + try { |
| 361 | + child.kill(signal); |
| 362 | + } catch { |
| 363 | + // The child already exited. |
| 364 | + } |
| 365 | + } |
| 366 | +} |
| 367 | + |
| 368 | +async function waitForQaGatewayChildExit(child: ChildProcess, timeoutMs: number) { |
| 369 | + if (hasChildExited(child)) { |
| 370 | + return true; |
| 371 | + } |
| 372 | + return await Promise.race([ |
| 373 | + new Promise<boolean>((resolve) => child.once("exit", () => resolve(true))), |
| 374 | + sleep(timeoutMs).then(() => false), |
| 375 | + ]); |
| 376 | +} |
| 377 | + |
| 378 | +async function stopQaGatewayChildProcessTree( |
| 379 | + child: ChildProcess, |
| 380 | + opts?: { gracefulTimeoutMs?: number; forceTimeoutMs?: number }, |
| 381 | +) { |
| 382 | + if (hasChildExited(child)) { |
| 383 | + return; |
| 384 | + } |
| 385 | + signalQaGatewayChildProcessTree(child, "SIGTERM"); |
| 386 | + if (await waitForQaGatewayChildExit(child, opts?.gracefulTimeoutMs ?? 5_000)) { |
| 387 | + return; |
| 388 | + } |
| 389 | + signalQaGatewayChildProcessTree(child, "SIGKILL"); |
| 390 | + await waitForQaGatewayChildExit(child, opts?.forceTimeoutMs ?? 2_000); |
| 391 | +} |
| 392 | + |
344 | 393 | function resolveQaBundledPluginsSourceRoot(repoRoot: string) { |
345 | 394 | const candidates = [ |
346 | 395 | path.join(repoRoot, "dist", "extensions"), |
@@ -811,6 +860,7 @@ export async function startQaGatewayChild(params: { |
811 | 860 | { |
812 | 861 | cwd: runtimeCwd, |
813 | 862 | env, |
| 863 | + detached: process.platform !== "win32", |
814 | 864 | stdio: ["ignore", "pipe", "pipe"], |
815 | 865 | }, |
816 | 866 | ); |
@@ -868,7 +918,7 @@ export async function startQaGatewayChild(params: { |
868 | 918 | } catch (error) { |
869 | 919 | stdoutLog.end(); |
870 | 920 | stderrLog.end(); |
871 | | - child.kill("SIGTERM"); |
| 921 | + await stopQaGatewayChildProcessTree(child, { gracefulTimeoutMs: 1_000 }).catch(() => {}); |
872 | 922 | if (!keepTemp && stagedBundledPluginsRoot) { |
873 | 923 | await fs.rm(stagedBundledPluginsRoot, { recursive: true, force: true }).catch(() => {}); |
874 | 924 | } |
@@ -925,17 +975,7 @@ export async function startQaGatewayChild(params: { |
925 | 975 | await rpcClient.stop().catch(() => {}); |
926 | 976 | stdoutLog.end(); |
927 | 977 | stderrLog.end(); |
928 | | - if (!child.killed) { |
929 | | - child.kill("SIGTERM"); |
930 | | - await Promise.race([ |
931 | | - new Promise<void>((resolve) => child.once("exit", () => resolve())), |
932 | | - sleep(5_000).then(() => { |
933 | | - if (!child.killed) { |
934 | | - child.kill("SIGKILL"); |
935 | | - } |
936 | | - }), |
937 | | - ]); |
938 | | - } |
| 978 | + await stopQaGatewayChildProcessTree(child); |
939 | 979 | if (!(opts?.keepTemp ?? keepTemp)) { |
940 | 980 | await fs.rm(tempRoot, { recursive: true, force: true }); |
941 | 981 | if (stagedBundledPluginsRoot) { |
|
0 commit comments