fix: clean up orphaned child processes (#77481)

* fix: forward launcher respawn signals

* docs: explain respawn signal exit timer

* fix: centralize launcher respawn supervision

* fix: include respawn helper in duplicate scan

* fix: keep launcher respawn bridge local
This commit is contained in:
Kevin Lin
2026-05-04 15:28:49 -07:00
committed by GitHub
parent 9f2c8a6ab6
commit 592998ae0e
6 changed files with 527 additions and 111 deletions

View File

@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
- Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc.
- Plugins/update: treat OpenClaw CalVer correction versions like `2026.5.3-1` as satisfying base plugin API ranges, so correction builds can install plugins that require the base runtime API. Fixes #77293. (#77450) Thanks @p3nchan.
- Discord/Gateway startup: retry Discord READY waits with backoff, defer startup `sessions.list` and native approval readiness failures until sidecars recover, and preserve component-only Discord payloads when final reply scrubbing removes all text. (#77478) Thanks @NikolaFC.
- CLI/launcher: forward termination signals to compile-cache respawn children, so killing a wrapper process no longer leaves the security audit worker orphaned. Fixes #77458. Thanks @jaikharbanda.
- fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987.
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
- Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { spawn } from "node:child_process";
import { existsSync, readFileSync, statSync } from "node:fs";
import { access } from "node:fs/promises";
import module from "node:module";
@@ -84,6 +84,102 @@ const resolvePackagedCompileCacheDirectory = () => {
);
};
const respawnSignals =
process.platform === "win32"
? ["SIGTERM", "SIGINT", "SIGBREAK"]
: ["SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT"];
const respawnSignalExitGraceMs = 1_000;
const respawnSignalForceKillGraceMs = 1_000;
const runRespawnedChild = (command, args, env) => {
const child = spawn(command, args, {
stdio: "inherit",
env,
});
const listeners = new Map();
// This intentionally overlaps with src/entry.compile-cache.ts; keep the
// respawn supervision behavior in sync until the launcher can share TS code.
// Give the child a moment to honor forwarded signals, then exit the wrapper so
// a child that ignores SIGTERM cannot keep the launcher alive indefinitely.
let signalExitTimer = null;
let signalForceKillTimer = null;
const detach = () => {
for (const [signal, listener] of listeners) {
process.off(signal, listener);
}
listeners.clear();
if (signalExitTimer) {
clearTimeout(signalExitTimer);
signalExitTimer = null;
}
if (signalForceKillTimer) {
clearTimeout(signalForceKillTimer);
signalForceKillTimer = null;
}
};
const forceKillChild = () => {
try {
child.kill(process.platform === "win32" ? "SIGTERM" : "SIGKILL");
} catch {
// Best-effort shutdown fallback.
}
};
const requestChildTermination = () => {
try {
child.kill("SIGTERM");
} catch {
// Best-effort shutdown fallback.
}
signalForceKillTimer = setTimeout(() => {
forceKillChild();
process.exit(1);
}, respawnSignalForceKillGraceMs);
signalForceKillTimer.unref?.();
};
const scheduleParentExit = () => {
if (signalExitTimer) {
return;
}
signalExitTimer = setTimeout(() => {
requestChildTermination();
}, respawnSignalExitGraceMs);
signalExitTimer.unref?.();
};
for (const signal of respawnSignals) {
const listener = () => {
try {
child.kill(signal);
} catch {
// Best-effort signal forwarding.
}
scheduleParentExit();
};
try {
process.on(signal, listener);
listeners.set(signal, listener);
} catch {
// Unsupported signal on this platform.
}
}
child.once("exit", (code, signal) => {
detach();
if (signal) {
process.exit(1);
}
process.exit(code ?? 1);
});
child.once("error", (error) => {
detach();
process.stderr.write(
`[openclaw] Failed to respawn launcher: ${
error instanceof Error ? (error.stack ?? error.message) : String(error)
}\n`,
);
process.exit(1);
});
return true;
};
const respawnWithoutCompileCacheIfNeeded = () => {
if (!isSourceCheckoutLauncher()) {
return false;
@@ -100,22 +196,13 @@ const respawnWithoutCompileCacheIfNeeded = () => {
OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED: "1",
};
delete env.NODE_COMPILE_CACHE;
const result = spawnSync(
return runRespawnedChild(
process.execPath,
[...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
{
stdio: "inherit",
env,
},
env,
);
if (result.error) {
throw result.error;
}
process.exit(result.status ?? 1);
};
respawnWithoutCompileCacheIfNeeded();
const respawnWithPackagedCompileCacheIfNeeded = () => {
if (isSourceCheckoutLauncher() || isNodeCompileCacheDisabled()) {
return false;
@@ -136,24 +223,23 @@ const respawnWithPackagedCompileCacheIfNeeded = () => {
NODE_COMPILE_CACHE: desiredDirectory,
OPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNED: "1",
};
const result = spawnSync(
return runRespawnedChild(
process.execPath,
[...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
{
stdio: "inherit",
env,
},
env,
);
if (result.error) {
throw result.error;
}
process.exit(result.status ?? 1);
};
respawnWithPackagedCompileCacheIfNeeded();
const waitingForCompileCacheRespawn =
respawnWithoutCompileCacheIfNeeded() || respawnWithPackagedCompileCacheIfNeeded();
// https://nodejs.org/api/module.html#module-compile-cache
if (module.enableCompileCache && !isNodeCompileCacheDisabled() && !isSourceCheckoutLauncher()) {
if (
!waitingForCompileCacheRespawn &&
module.enableCompileCache &&
!isNodeCompileCacheDisabled() &&
!isSourceCheckoutLauncher()
) {
try {
module.enableCompileCache(resolvePackagedCompileCacheDirectory());
} catch {
@@ -297,17 +383,19 @@ const tryOutputBrowserHelp = () => {
return true;
};
if (!isHelpFastPathDisabled() && (await tryOutputBareRootHelp())) {
// OK
} else if (!isHelpFastPathDisabled() && tryOutputBrowserHelp()) {
// OK
} else {
await installProcessWarningFilter();
if (await tryImport("./dist/entry.js")) {
if (!waitingForCompileCacheRespawn) {
if (!isHelpFastPathDisabled() && (await tryOutputBareRootHelp())) {
// OK
} else if (await tryImport("./dist/entry.mjs")) {
} else if (!isHelpFastPathDisabled() && tryOutputBrowserHelp()) {
// OK
} else {
throw new Error(await buildMissingEntryErrorMessage());
await installProcessWarningFilter();
if (await tryImport("./dist/entry.js")) {
// OK
} else if (await tryImport("./dist/entry.mjs")) {
// OK
} else {
throw new Error(await buildMissingEntryErrorMessage());
}
}
}

View File

@@ -1,12 +1,15 @@
import type { ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanupTempDirs, makeTempDir } from "../test/helpers/temp-dir.js";
import {
buildOpenClawCompileCacheRespawnPlan,
isSourceCheckoutInstallRoot,
resolveOpenClawCompileCacheDirectory,
resolveEntryInstallRoot,
runOpenClawCompileCacheRespawnPlan,
shouldEnableOpenClawCompileCache,
} from "./entry.compile-cache.js";
@@ -122,4 +125,109 @@ describe("entry compile cache", () => {
}),
).toBeUndefined();
});
it("runs compile-cache respawn plans with the child-process bridge", () => {
const child = new EventEmitter() as ChildProcess;
const spawn = vi.fn(() => child);
const attachChildProcessBridge = vi.fn();
const exit = vi.fn();
const writeError = vi.fn();
runOpenClawCompileCacheRespawnPlan(
{
command: "/usr/bin/node",
args: ["/repo/openclaw/dist/entry.js", "status"],
env: { NODE_DISABLE_COMPILE_CACHE: "1" },
},
{
spawn: spawn as unknown as typeof import("node:child_process").spawn,
attachChildProcessBridge,
exit: exit as unknown as (code?: number) => never,
writeError,
},
);
expect(spawn).toHaveBeenCalledWith(
"/usr/bin/node",
["/repo/openclaw/dist/entry.js", "status"],
{
stdio: "inherit",
env: { NODE_DISABLE_COMPILE_CACHE: "1" },
},
);
expect(attachChildProcessBridge).toHaveBeenCalledWith(child, {
onSignal: expect.any(Function),
});
child.emit("exit", 0, null);
expect(exit).toHaveBeenCalledWith(0);
expect(writeError).not.toHaveBeenCalled();
});
it("marks signal-terminated compile-cache respawn children as failed without forcing another exit", () => {
const child = new EventEmitter() as ChildProcess;
const spawn = vi.fn(() => child);
const exit = vi.fn();
runOpenClawCompileCacheRespawnPlan(
{
command: "/usr/bin/node",
args: ["/repo/openclaw/dist/entry.js"],
env: {},
},
{
spawn: spawn as unknown as typeof import("node:child_process").spawn,
attachChildProcessBridge: vi.fn(),
exit: exit as unknown as (code?: number) => never,
writeError: vi.fn(),
},
);
child.emit("exit", null, "SIGTERM");
expect(exit).toHaveBeenCalledWith(1);
});
it("terminates before force-killing a signaled compile-cache respawn child", () => {
vi.useFakeTimers();
const child = new EventEmitter() as ChildProcess;
const kill = vi.fn<(signal?: NodeJS.Signals) => boolean>(() => true);
child.kill = kill as ChildProcess["kill"];
const spawn = vi.fn(() => child);
const exit = vi.fn();
let onSignal: ((signal: NodeJS.Signals) => void) | undefined;
try {
runOpenClawCompileCacheRespawnPlan(
{
command: "/usr/bin/node",
args: ["/repo/openclaw/dist/entry.js"],
env: {},
},
{
spawn: spawn as unknown as typeof import("node:child_process").spawn,
attachChildProcessBridge: vi.fn((_child, options) => {
onSignal = options?.onSignal;
return { detach: vi.fn() };
}),
exit: exit as unknown as (code?: number) => never,
writeError: vi.fn(),
},
);
onSignal?.("SIGTERM");
vi.advanceTimersByTime(1_000);
expect(kill).toHaveBeenCalledWith("SIGTERM");
expect(exit).not.toHaveBeenCalled();
vi.advanceTimersByTime(1_000);
expect(kill).toHaveBeenCalledWith(process.platform === "win32" ? "SIGTERM" : "SIGKILL");
expect(exit).toHaveBeenCalledWith(1);
} finally {
vi.useRealTimers();
}
});
});

View File

@@ -1,8 +1,12 @@
import { spawnSync } from "node:child_process";
import { spawn, type ChildProcess } from "node:child_process";
import { existsSync, readFileSync, statSync } from "node:fs";
import { enableCompileCache, getCompileCacheDir } from "node:module";
import os from "node:os";
import path from "node:path";
import { attachChildProcessBridge } from "./process/child-process-bridge.js";
const COMPILE_CACHE_RESPAWN_SIGNAL_EXIT_GRACE_MS = 1_000;
const COMPILE_CACHE_RESPAWN_SIGNAL_FORCE_KILL_GRACE_MS = 1_000;
export function resolveEntryInstallRoot(entryFile: string): string {
const entryDir = path.dirname(entryFile);
@@ -84,12 +88,19 @@ export function resolveOpenClawCompileCacheDirectory(params: {
);
}
type OpenClawCompileCacheRespawnPlan = {
export type OpenClawCompileCacheRespawnPlan = {
command: string;
args: string[];
env: NodeJS.ProcessEnv;
};
type OpenClawCompileCacheRespawnRuntime = {
spawn: typeof spawn;
attachChildProcessBridge: typeof attachChildProcessBridge;
exit: (code?: number) => never;
writeError: (message: string) => void;
};
export function buildOpenClawCompileCacheRespawnPlan(params: {
currentFile: string;
env?: NodeJS.ProcessEnv;
@@ -138,15 +149,89 @@ export function respawnWithoutOpenClawCompileCacheIfNeeded(params: {
if (!plan) {
return false;
}
const result = spawnSync(plan.command, plan.args, {
runOpenClawCompileCacheRespawnPlan(plan);
return true;
}
export function runOpenClawCompileCacheRespawnPlan(
plan: OpenClawCompileCacheRespawnPlan,
runtime: OpenClawCompileCacheRespawnRuntime = {
spawn,
attachChildProcessBridge,
exit: process.exit.bind(process) as (code?: number) => never,
writeError: (message: string) => process.stderr.write(message),
},
): ChildProcess {
const child = runtime.spawn(plan.command, plan.args, {
stdio: "inherit",
env: plan.env,
});
if (result.error) {
throw result.error;
}
process.exit(result.status ?? 1);
return true;
// Give the child a moment to honor forwarded signals, then exit the parent so
// a child that ignores SIGTERM cannot keep the compile-cache wrapper alive indefinitely.
let signalExitTimer: NodeJS.Timeout | undefined;
let signalForceKillTimer: NodeJS.Timeout | undefined;
const clearSignalExitTimer = (): void => {
if (signalExitTimer) {
clearTimeout(signalExitTimer);
signalExitTimer = undefined;
}
if (signalForceKillTimer) {
clearTimeout(signalForceKillTimer);
signalForceKillTimer = undefined;
}
};
const forceKillChild = (): void => {
try {
child.kill(process.platform === "win32" ? "SIGTERM" : "SIGKILL");
} catch {
// Best-effort shutdown fallback.
}
};
const requestChildTermination = (): void => {
try {
child.kill("SIGTERM");
} catch {
// Best-effort shutdown fallback.
}
signalForceKillTimer = setTimeout(() => {
forceKillChild();
runtime.exit(1);
}, COMPILE_CACHE_RESPAWN_SIGNAL_FORCE_KILL_GRACE_MS);
signalForceKillTimer.unref?.();
};
const scheduleParentExit = (): void => {
if (signalExitTimer) {
return;
}
signalExitTimer = setTimeout(() => {
requestChildTermination();
}, COMPILE_CACHE_RESPAWN_SIGNAL_EXIT_GRACE_MS);
signalExitTimer.unref?.();
};
runtime.attachChildProcessBridge(child, {
onSignal: scheduleParentExit,
});
child.once("exit", (code, signal) => {
clearSignalExitTimer();
if (signal) {
runtime.exit(1);
}
runtime.exit(code ?? 1);
});
child.once("error", (error) => {
clearSignalExitTimer();
runtime.writeError(
`[openclaw] Failed to respawn CLI without compile cache: ${
error instanceof Error ? (error.stack ?? error.message) : String(error)
}\n`,
);
runtime.exit(1);
});
return child;
}
export function enableOpenClawCompileCache(params: {

View File

@@ -84,92 +84,94 @@ if (
} else {
const entryFile = fileURLToPath(import.meta.url);
const installRoot = resolveEntryInstallRoot(entryFile);
respawnWithoutOpenClawCompileCacheIfNeeded({
const waitingForCompileCacheRespawn = respawnWithoutOpenClawCompileCacheIfNeeded({
currentFile: entryFile,
installRoot,
});
process.title = "openclaw";
ensureOpenClawExecMarkerOnProcess();
installProcessWarningFilter();
normalizeEnv();
enableOpenClawCompileCache({
installRoot,
});
gatewayEntryStartupTrace.mark("bootstrap");
if (!waitingForCompileCacheRespawn) {
process.title = "openclaw";
ensureOpenClawExecMarkerOnProcess();
installProcessWarningFilter();
normalizeEnv();
enableOpenClawCompileCache({
installRoot,
});
gatewayEntryStartupTrace.mark("bootstrap");
if (shouldForceReadOnlyAuthStore(process.argv)) {
process.env.OPENCLAW_AUTH_STORE_READONLY = "1";
}
if (process.argv.includes("--no-color")) {
process.env.NO_COLOR = "1";
process.env.FORCE_COLOR = "0";
}
function ensureCliRespawnReady(): boolean {
const plan = buildCliRespawnPlan();
if (!plan) {
return false;
if (shouldForceReadOnlyAuthStore(process.argv)) {
process.env.OPENCLAW_AUTH_STORE_READONLY = "1";
}
const child = spawn(plan.command, plan.argv, {
stdio: "inherit",
env: plan.env,
});
if (process.argv.includes("--no-color")) {
process.env.NO_COLOR = "1";
process.env.FORCE_COLOR = "0";
}
attachChildProcessBridge(child);
child.once("exit", (code, signal) => {
if (signal) {
process.exitCode = 1;
return;
function ensureCliRespawnReady(): boolean {
const plan = buildCliRespawnPlan();
if (!plan) {
return false;
}
process.exit(code ?? 1);
});
child.once("error", (error) => {
console.error(
"[openclaw] Failed to respawn CLI:",
error instanceof Error ? (error.stack ?? error.message) : error,
);
process.exit(1);
});
const child = spawn(plan.command, plan.argv, {
stdio: "inherit",
env: plan.env,
});
// Parent must not continue running the CLI.
return true;
}
attachChildProcessBridge(child);
process.argv = normalizeWindowsArgv(process.argv);
child.once("exit", (code, signal) => {
if (signal) {
process.exitCode = 1;
return;
}
process.exit(code ?? 1);
});
if (!ensureCliRespawnReady()) {
const parsedContainer = parseCliContainerArgs(process.argv);
if (!parsedContainer.ok) {
console.error(`[openclaw] ${parsedContainer.error}`);
process.exit(2);
child.once("error", (error) => {
console.error(
"[openclaw] Failed to respawn CLI:",
error instanceof Error ? (error.stack ?? error.message) : error,
);
process.exit(1);
});
// Parent must not continue running the CLI.
return true;
}
const parsed = parseCliProfileArgs(parsedContainer.argv);
if (!parsed.ok) {
// Keep it simple; Commander will handle rich help/errors after we strip flags.
console.error(`[openclaw] ${parsed.error}`);
process.exit(2);
}
process.argv = normalizeWindowsArgv(process.argv);
const containerTargetName = resolveCliContainerTarget(process.argv);
if (containerTargetName && parsed.profile) {
console.error("[openclaw] --container cannot be combined with --profile/--dev");
process.exit(2);
}
if (!ensureCliRespawnReady()) {
const parsedContainer = parseCliContainerArgs(process.argv);
if (!parsedContainer.ok) {
console.error(`[openclaw] ${parsedContainer.error}`);
process.exit(2);
}
if (parsed.profile) {
applyCliProfileEnv({ profile: parsed.profile });
// Keep Commander and ad-hoc argv checks consistent.
process.argv = parsed.argv;
}
gatewayEntryStartupTrace.mark("argv");
const parsed = parseCliProfileArgs(parsedContainer.argv);
if (!parsed.ok) {
// Keep it simple; Commander will handle rich help/errors after we strip flags.
console.error(`[openclaw] ${parsed.error}`);
process.exit(2);
}
if (!tryHandleRootVersionFastPath(process.argv)) {
await runMainOrRootHelp(process.argv);
const containerTargetName = resolveCliContainerTarget(process.argv);
if (containerTargetName && parsed.profile) {
console.error("[openclaw] --container cannot be combined with --profile/--dev");
process.exit(2);
}
if (parsed.profile) {
applyCliProfileEnv({ profile: parsed.profile });
// Keep Commander and ad-hoc argv checks consistent.
process.argv = parsed.argv;
}
gatewayEntryStartupTrace.mark("argv");
if (!tryHandleRootVersionFastPath(process.argv)) {
await runMainOrRootHelp(process.argv);
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { spawnSync } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
@@ -36,6 +36,41 @@ async function addCompileCacheProbe(fixtureRoot: string): Promise<void> {
);
}
async function waitForFile(filePath: string, timeoutMs: number): Promise<string> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
return await fs.readFile(filePath, "utf8");
} catch {
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
throw new Error(`timed out waiting for ${filePath}`);
}
async function waitUntil(check: () => boolean, timeoutMs: number): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (check()) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
return check();
}
function isProcessAlive(pid: number | undefined): boolean {
if (!pid) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function launcherEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
const env = { ...process.env, ...extra };
delete env.NODE_COMPILE_CACHE;
@@ -138,6 +173,103 @@ describe("openclaw launcher", () => {
expect(result.stdout).toBe("cache:disabled;respawn:1");
});
it.runIf(process.platform !== "win32")(
"forwards SIGTERM to source-checkout compile-cache respawn children",
async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addGitMarker(fixtureRoot);
const childInfoPath = path.join(fixtureRoot, "child-info.json");
await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"),
[
'import { writeFileSync } from "node:fs";',
`writeFileSync(${JSON.stringify(childInfoPath)}, JSON.stringify({ pid: process.pid }) + "\\n");`,
'process.title = "openclaw-launcher-sigterm-test-child";',
"setInterval(() => {}, 1000);",
"",
].join("\n"),
"utf8",
);
const launcher = spawn(process.execPath, [path.join(fixtureRoot, "openclaw.mjs")], {
cwd: fixtureRoot,
env: launcherEnv({
NODE_COMPILE_CACHE: path.join(fixtureRoot, ".node-compile-cache"),
}),
stdio: "ignore",
});
let respawnChildPid: number | undefined;
try {
const childInfo = JSON.parse(await waitForFile(childInfoPath, 5000)) as { pid: number };
respawnChildPid = childInfo.pid;
launcher.kill("SIGTERM");
await waitUntil(() => !isProcessAlive(respawnChildPid), 5000);
expect(isProcessAlive(respawnChildPid)).toBe(false);
} finally {
if (isProcessAlive(respawnChildPid)) {
process.kill(respawnChildPid!, "SIGKILL");
}
if (isProcessAlive(launcher.pid)) {
process.kill(launcher.pid!, "SIGKILL");
}
}
},
);
it.runIf(process.platform !== "win32")(
"exits after SIGTERM when the respawn child ignores the forwarded signal",
async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addGitMarker(fixtureRoot);
const childInfoPath = path.join(fixtureRoot, "child-info.json");
await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"),
[
'import { writeFileSync } from "node:fs";',
`writeFileSync(${JSON.stringify(childInfoPath)}, JSON.stringify({ pid: process.pid }) + "\\n");`,
'process.title = "openclaw-launcher-sigterm-ignore-test-child";',
'process.on("SIGTERM", () => {});',
"setInterval(() => {}, 1000);",
"",
].join("\n"),
"utf8",
);
const launcher = spawn(process.execPath, [path.join(fixtureRoot, "openclaw.mjs")], {
cwd: fixtureRoot,
env: launcherEnv({
NODE_COMPILE_CACHE: path.join(fixtureRoot, ".node-compile-cache"),
}),
stdio: "ignore",
});
let respawnChildPid: number | undefined;
try {
const childInfo = JSON.parse(await waitForFile(childInfoPath, 5000)) as { pid: number };
respawnChildPid = childInfo.pid;
launcher.kill("SIGTERM");
await waitUntil(
() => !isProcessAlive(launcher.pid) && !isProcessAlive(respawnChildPid),
5000,
);
expect(isProcessAlive(launcher.pid)).toBe(false);
expect(isProcessAlive(respawnChildPid)).toBe(false);
} finally {
if (isProcessAlive(respawnChildPid)) {
process.kill(respawnChildPid!, "SIGKILL");
}
if (isProcessAlive(launcher.pid)) {
process.kill(launcher.pid!, "SIGKILL");
}
}
},
);
it.runIf(process.platform !== "win32")(
"respawns symlinked source-checkout launchers without inherited NODE_COMPILE_CACHE",
async () => {