refactor(util): share process stop policy (#45725)

This commit is contained in:
Kit Langton
2026-08-27 23:12:57 -04:00
committed by GitHub
parent 6bf342a861
commit 07facd2086
2 changed files with 20 additions and 24 deletions
@@ -244,11 +244,11 @@ describe("cross-spawn spawner", () => {
)
fx.effect(
"kills a child when scope exits",
"uses the configured kill signal when scope exits",
Effect.gen(function* () {
const pid = yield* Effect.scoped(
Effect.gen(function* () {
const handle = yield* js("setInterval(() => {}, 10_000)")
const handle = yield* js("setInterval(() => {}, 10_000)", { killSignal: "SIGKILL" })
return Number(handle.pid)
}),
)
+18 -22
View File
@@ -321,6 +321,22 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
return Effect.fail(toPlatformError("kill", new Error("Failed to kill child process"), command))
})
const stop = (
command: ChildProcess.StandardCommand,
proc: NodeChildProcess.ChildProcess,
exit: ExitSignal,
opts: ChildProcess.KillOptions | undefined,
) => {
const send = (signal: NodeJS.Signals) =>
Effect.catch(killGroup(command, proc, signal), () => killOne(command, proc, signal))
const attempt = send(opts?.killSignal ?? "SIGTERM").pipe(Effect.andThen(Deferred.await(exit)), Effect.asVoid)
if (!opts?.forceKillAfter) return attempt
return Effect.timeoutOrElse(attempt, {
duration: opts.forceKillAfter,
orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(exit)), Effect.asVoid),
})
}
const timeout =
(
proc: NodeChildProcess.ChildProcess,
@@ -388,17 +404,7 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
if (code !== 0 && Predicate.isNotNull(code)) return yield* Effect.ignore(kill(killGroup))
return yield* Effect.void
}
const send = (s: NodeJS.Signals) =>
Effect.catch(killGroup(command, proc, s), () => killOne(command, proc, s))
const sig = command.options.killSignal ?? "SIGTERM"
const attempt = send(sig).pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid)
const escalated = command.options.forceKillAfter
? Effect.timeoutOrElse(attempt, {
duration: command.options.forceKillAfter,
orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid),
})
: attempt
return yield* Effect.ignore(escalated)
return yield* Effect.ignore(stop(command, proc, signal, command.options))
}),
)
@@ -424,17 +430,7 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
),
)
}),
kill: (opts?: ChildProcess.KillOptions) => {
const sig = opts?.killSignal ?? "SIGTERM"
const send = (s: NodeJS.Signals) =>
Effect.catch(killGroup(command, proc, s), () => killOne(command, proc, s))
const attempt = send(sig).pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid)
if (!opts?.forceKillAfter) return attempt
return Effect.timeoutOrElse(attempt, {
duration: opts.forceKillAfter,
orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid),
})
},
kill: (opts?: ChildProcess.KillOptions) => stop(command, proc, signal, opts),
unref: Effect.sync(() => {
if (ref) {
proc.unref()