@@ -23,6 +23,19 @@ import { Teardown } from "./teardown.js"
2323/** Closing a child's scope escalates to SIGKILL after this, unless the command sets its own `forceKillAfter`. */
2424export const RELEASE_FORCE_KILL_AFTER = "3 seconds"
2525
26+ // Process groups of running children. Exiting without stopping them (a shutdown deadline, a second signal, a
27+ // crash) would orphan them, so a synchronous exit hook kills whatever is left.
28+ const groups = new Set < number > ( )
29+ let hooked = false
30+ const killGroups = ( ) =>
31+ groups . forEach ( ( pid ) => {
32+ try {
33+ globalThis . process . kill ( - pid , "SIGKILL" )
34+ } catch {
35+ // Already gone.
36+ }
37+ } )
38+
2639const toError = ( err : unknown ) : Error => ( err instanceof globalThis . Error ? err : new globalThis . Error ( String ( err ) ) )
2740
2841const toTag = ( err : NodeJS . ErrnoException ) : PlatformError . SystemErrorTag => {
@@ -297,6 +310,7 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
297310 resume ( Effect . fail ( toPlatformError ( "spawn" , err , command ) ) )
298311 } )
299312 proc . on ( "exit" , ( ...args ) => {
313+ groups . delete ( proc . pid ! )
300314 exit = args
301315 Deferred . doneUnsafe ( exited , Exit . succeed ( args ) )
302316 } )
@@ -308,6 +322,11 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
308322 Deferred . doneUnsafe ( closed , Exit . succeed ( exit ?? args ) )
309323 } )
310324 proc . on ( "spawn" , ( ) => {
325+ if ( opts . detached && globalThis . process . platform !== "win32" ) {
326+ if ( ! hooked ) globalThis . process . once ( "exit" , killGroups )
327+ hooked = true
328+ groups . add ( proc . pid ! )
329+ }
311330 settle = Teardown . track (
312331 `child ${ proc . pid } running: ${ [ command . command , ...command . args ] . join ( " " ) . slice ( 0 , 200 ) } ` ,
313332 )
0 commit comments