@@ -140,70 +140,89 @@ describe("managed-child-process", () => {
140140 }
141141 } ) ;
142142
143- posixIt ( "kills the managed child process group when the runner is terminated" , async ( ) => {
144- const dir = createTempDir ( "openclaw-managed-child-" ) ;
145- const childPath = path . join ( dir , "child.mjs" ) ;
146- const runnerPath = path . join ( dir , "runner.mjs" ) ;
147- const childPidPath = path . join ( dir , "child.pid" ) ;
148- const runnerReadyPath = path . join ( dir , "runner.ready" ) ;
149- const helperUrl = pathToFileURL ( path . resolve ( "scripts/lib/managed-child-process.mjs" ) ) . href ;
150-
151- fs . writeFileSync (
152- childPath ,
153- `
154- import fs from "node:fs";
155-
156- fs.writeFileSync(process.argv[2], String(process.pid));
157- for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) {
158- process.on(signal, () => process.exit(0));
159- }
143+ posixIt (
144+ "kills managed child process group descendants when the runner is terminated" ,
145+ async ( ) => {
146+ const dir = createTempDir ( "openclaw-managed-child-" ) ;
147+ const childPath = path . join ( dir , "child.mjs" ) ;
148+ const runnerPath = path . join ( dir , "runner.mjs" ) ;
149+ const childPidPath = path . join ( dir , "child.pid" ) ;
150+ const descendantPidPath = path . join ( dir , "descendant.pid" ) ;
151+ const runnerReadyPath = path . join ( dir , "runner.ready" ) ;
152+ const helperUrl = pathToFileURL ( path . resolve ( "scripts/lib/managed-child-process.mjs" ) ) . href ;
153+
154+ fs . writeFileSync (
155+ childPath ,
156+ `
157+ import { spawn } from "node:child_process";
158+ import fs from "node:fs";
159+
160+ const descendant = spawn(process.execPath, [
161+ "-e",
162+ "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);",
163+ ], { stdio: "ignore" });
164+ fs.writeFileSync(process.argv[2], String(process.pid));
165+ fs.writeFileSync(process.argv[3], String(descendant.pid));
166+ for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) {
167+ process.on(signal, () => process.exit(0));
168+ }
160169setInterval(() => {}, 1_000);
161170` ,
162- "utf8" ,
163- ) ;
164- fs . writeFileSync (
165- runnerPath ,
166- `
171+ "utf8" ,
172+ ) ;
173+ fs . writeFileSync (
174+ runnerPath ,
175+ `
167176import fs from "node:fs";
168177import { runManagedCommand } from ${ JSON . stringify ( helperUrl ) } ;
169178
170- process.exitCode = await runManagedCommand({
171- bin: process.execPath,
172- args: [${ JSON . stringify ( childPath ) } , ${ JSON . stringify ( childPidPath ) } ],
173- stdio: "ignore",
174- onReady: () => fs.writeFileSync(${ JSON . stringify ( runnerReadyPath ) } , "1"),
175- });
179+ process.exitCode = await runManagedCommand({
180+ bin: process.execPath,
181+ args: [${ JSON . stringify ( childPath ) } , ${ JSON . stringify ( childPidPath ) } , ${ JSON . stringify ( descendantPidPath ) } ],
182+ stdio: "ignore",
183+ onReady: () => fs.writeFileSync(${ JSON . stringify ( runnerReadyPath ) } , "1"),
184+ });
176185` ,
177- "utf8" ,
178- ) ;
179-
180- const runner = spawn ( process . execPath , [ runnerPath ] , {
181- stdio : "ignore" ,
182- } ) ;
183- const runnerPid = expectProcessPid ( runner . pid ) ;
184- let childPid = 0 ;
186+ "utf8" ,
187+ ) ;
185188
186- try {
187- await waitFor ( ( ) => fs . existsSync ( runnerReadyPath ) ) ;
188- await waitFor ( ( ) => fs . existsSync ( childPidPath ) ) ;
189- childPid = Number ( fs . readFileSync ( childPidPath , "utf8" ) ) ;
190- expect ( Number . isInteger ( childPid ) ) . toBe ( true ) ;
191- expect ( isProcessAlive ( childPid ) ) . toBe ( true ) ;
192-
193- process . kill ( runnerPid , "SIGTERM" ) ;
194- const result = await waitForClose ( runner ) ;
195-
196- expect ( result ) . toEqual ( { code : 143 , signal : null } ) ;
197- await waitFor ( ( ) => ! isProcessAlive ( childPid ) , 1_500 ) ;
198- } finally {
199- if ( isProcessAlive ( runnerPid ) ) {
200- process . kill ( runnerPid , "SIGKILL" ) ;
201- }
202- if ( childPid && isProcessAlive ( childPid ) ) {
203- process . kill ( childPid , "SIGKILL" ) ;
189+ const runner = spawn ( process . execPath , [ runnerPath ] , {
190+ stdio : "ignore" ,
191+ } ) ;
192+ const runnerPid = expectProcessPid ( runner . pid ) ;
193+ let childPid = 0 ;
194+ let descendantPid = 0 ;
195+
196+ try {
197+ await waitFor ( ( ) => fs . existsSync ( runnerReadyPath ) ) ;
198+ await waitFor ( ( ) => fs . existsSync ( childPidPath ) ) ;
199+ await waitFor ( ( ) => fs . existsSync ( descendantPidPath ) ) ;
200+ childPid = Number ( fs . readFileSync ( childPidPath , "utf8" ) ) ;
201+ descendantPid = Number ( fs . readFileSync ( descendantPidPath , "utf8" ) ) ;
202+ expect ( Number . isInteger ( childPid ) ) . toBe ( true ) ;
203+ expect ( Number . isInteger ( descendantPid ) ) . toBe ( true ) ;
204+ expect ( isProcessAlive ( childPid ) ) . toBe ( true ) ;
205+ expect ( isProcessAlive ( descendantPid ) ) . toBe ( true ) ;
206+
207+ process . kill ( runnerPid , "SIGTERM" ) ;
208+ const result = await waitForClose ( runner ) ;
209+
210+ expect ( result ) . toEqual ( { code : 143 , signal : null } ) ;
211+ await waitFor ( ( ) => ! isProcessAlive ( childPid ) , 1_500 ) ;
212+ await waitFor ( ( ) => ! isProcessAlive ( descendantPid ) , 1_500 ) ;
213+ } finally {
214+ if ( isProcessAlive ( runnerPid ) ) {
215+ process . kill ( runnerPid , "SIGKILL" ) ;
216+ }
217+ if ( childPid && isProcessAlive ( childPid ) ) {
218+ process . kill ( childPid , "SIGKILL" ) ;
219+ }
220+ if ( descendantPid && isProcessAlive ( descendantPid ) ) {
221+ process . kill ( descendantPid , "SIGKILL" ) ;
222+ }
204223 }
205- }
206- } ) ;
224+ } ,
225+ ) ;
207226} ) ;
208227
209228async function waitFor ( condition : ( ) => boolean , timeoutMs = 3_000 ) {
0 commit comments