diff --git a/scripts/lib/workspace-bootstrap-smoke.mjs b/scripts/lib/workspace-bootstrap-smoke.mjs index 5a1efd8382c0..56739bc100fc 100644 --- a/scripts/lib/workspace-bootstrap-smoke.mjs +++ b/scripts/lib/workspace-bootstrap-smoke.mjs @@ -311,24 +311,54 @@ async function waitForGatewayReadiness(params) { throw new Error(`extracted gateway did not become ready:\n${params.readOutput()}`); } +async function waitForGatewayProcessGroupExit(processGroupId, timeoutMs) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + process.kill(processGroupId, 0); + } catch (error) { + if (error && typeof error === "object" && error.code === "ESRCH") { + return true; + } + throw error; + } + await new Promise((resolvePromise) => { + setTimeout(resolvePromise, 50); + }); + } + return false; +} + async function stopGatewaySmoke(child) { - if (child.exitCode !== null) { + if (process.platform === "win32" || !child.pid) { + if (child.exitCode === null) { + child.kill("SIGTERM"); + await Promise.race([ + new Promise((resolvePromise) => { + child.once("exit", resolvePromise); + }), + new Promise((resolvePromise) => { + setTimeout(resolvePromise, 5_000); + }), + ]); + if (child.exitCode === null) { + child.kill("SIGKILL"); + await new Promise((resolvePromise) => { + child.once("exit", resolvePromise); + }); + } + } return; } - child.kill("SIGTERM"); - await Promise.race([ - new Promise((resolvePromise) => { - child.once("exit", resolvePromise); - }), - new Promise((resolvePromise) => { - setTimeout(resolvePromise, 5_000); - }), - ]); - if (child.exitCode === null) { - child.kill("SIGKILL"); - await new Promise((resolvePromise) => { - child.once("exit", resolvePromise); - }); + + const processGroupId = -child.pid; + process.kill(processGroupId, "SIGTERM"); + if (await waitForGatewayProcessGroupExit(processGroupId, 5_000)) { + return; + } + process.kill(processGroupId, "SIGKILL"); + if (!(await waitForGatewayProcessGroupExit(processGroupId, 5_000))) { + throw new Error("failed to stop the extracted gateway process group"); } } @@ -415,7 +445,12 @@ export async function buildAndSmokeDistRuntimeArtifact(params) { "--port", String(port), ], - { cwd, env: smokeEnv, stdio: ["ignore", "pipe", "pipe"] }, + { + cwd, + detached: process.platform !== "win32", + env: smokeEnv, + stdio: ["ignore", "pipe", "pipe"], + }, ); const appendGatewayOutput = (chunk) => { gatewayOutput += chunk.toString(); diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 3a8dcca8252b..4574f308ae73 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -4695,6 +4695,8 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" expect(artifactBuilder).not.toContain("--import"); expect(artifactBuilder).not.toContain("dist-runtime-artifact-resolver-hook"); expect(artifactBuilder).toContain("node_modules/@openclaw"); + expect(artifactBuilder).toContain('detached: process.platform !== "win32"'); + expect(artifactBuilder).toContain('process.kill(processGroupId, "SIGKILL")'); expect(artifactBuilder).toContain('"acp", "--help"'); expect(artifactBuilder).toContain("/readyz"); expect(artifactBuilder).toContain("dist-runtime/extensions/");