From f57c086a7233f0a17eb3ffe1d9cab6a818dcbd08 Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:30:13 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Bound=20Windows=20process=20shut?= =?UTF-8?q?down?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- process/CHANGELOG.md | 4 ++++ process/package.json | 2 +- process/src/exec/win32.ts | 26 ++++++++++++++++-------- process/test/exec.test.ts | 23 +++++++++++++++++++++ process/test/fixtures/ignore-shutdown.ts | 24 ++++++++++++++++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 process/test/fixtures/ignore-shutdown.ts diff --git a/process/CHANGELOG.md b/process/CHANGELOG.md index ebdd4ee8..6ac2b67f 100644 --- a/process/CHANGELOG.md +++ b/process/CHANGELOG.md @@ -1,5 +1,9 @@ # @effection/process +## \[0.8.2] + +- Bound graceful Windows process shutdown before force-killing the process tree. + ## \[2.1.4] ### Dependencies diff --git a/process/package.json b/process/package.json index 4b14a468..9bde599f 100644 --- a/process/package.json +++ b/process/package.json @@ -1,7 +1,7 @@ { "name": "@effectionx/process", "description": "Spawn and manage child processes with structured concurrency", - "version": "0.8.1", + "version": "0.8.2", "keywords": ["process"], "type": "module", "main": "./dist/mod.js", diff --git a/process/src/exec/win32.ts b/process/src/exec/win32.ts index 3a2c78b2..e8f3a1ee 100644 --- a/process/src/exec/win32.ts +++ b/process/src/exec/win32.ts @@ -13,6 +13,8 @@ import { all, createSignal, ensure, + race, + sleep, spawn, withResolvers, } from "effection"; @@ -29,6 +31,8 @@ import { unbox, useEvalScope } from "@effectionx/scope-eval"; type ProcessResultValue = [number?, string?]; +const gracefulShutdownTimeout = 1000; + function* killTree(pid: number) { try { const killer = spawnProcess( @@ -177,16 +181,22 @@ export function* createWin32Process( stdin.end(); } - // depending on how we shutdown, this may already be closed and - // will pass immediately over the operations - yield* all([io.stdoutDone.operation, io.stderrDone.operation]); - - if (pid && childProcess.exitCode === null) { - // If the process is still around after we've waited - // for stdout and stderr to close, - // then force kill the tree. + const closed = yield* race([ + (function* () { + yield* processResult.operation; + return true; + })(), + (function* () { + yield* sleep(gracefulShutdownTimeout); + return false; + })(), + ]); + + if (pid && !closed) { yield* killTree(pid); } + + yield* all([io.stdoutDone.operation, io.stderrDone.operation]); }); return { diff --git a/process/test/exec.test.ts b/process/test/exec.test.ts index 3b6908fa..e8849172 100644 --- a/process/test/exec.test.ts +++ b/process/test/exec.test.ts @@ -162,6 +162,29 @@ describe("exec", () => { expect(status).toBeUndefined(); }); }); + + if (process.platform === "win32") { + it("force kills the process tree when graceful shutdown does not close stdio", function* () { + const ready = withResolvers(); + const task = yield* spawn(function* () { + const proc = yield* exec("node", { + arguments: [ + "--experimental-strip-types", + "fixtures/ignore-shutdown.ts", + ], + cwd: import.meta.dirname, + }); + ready.resolve(proc); + yield* proc.join(); + }); + + const proc = yield* ready.operation; + const started = yield* expectMatch(/ready/, lines()(proc.stdout)); + expect(started).toBe(true); + + expect(yield* task.halt()).toBeUndefined(); + }); + } }); describe("successfully", () => { diff --git a/process/test/fixtures/ignore-shutdown.ts b/process/test/fixtures/ignore-shutdown.ts new file mode 100644 index 00000000..f681acab --- /dev/null +++ b/process/test/fixtures/ignore-shutdown.ts @@ -0,0 +1,24 @@ +import { spawn } from "node:child_process"; +import process from "node:process"; + +const descendant = spawn( + process.execPath, + ["-e", 'process.on("SIGINT", () => {}); setInterval(() => {}, 1000);'], + { + stdio: ["ignore", "inherit", "inherit"], + windowsHide: true, + }, +); + +descendant.once("spawn", () => { + console.log("ready"); +}); + +descendant.once("error", (error) => { + console.error(error); + process.exitCode = 1; +}); + +process.on("SIGINT", () => {}); +process.stdin.resume(); +setInterval(() => {}, 1000);