Skip to content

Commit 643da62

Browse files
authored
Merge pull request #668 from 2chanhaeng/fix/test-init-process-cleanup
Fix dev server process tree not terminated in `test:init`
2 parents 12c6288 + a6f0fa2 commit 643da62

File tree

1 file changed

+28
-14
lines changed

1 file changed

+28
-14
lines changed

packages/init/src/test/server.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import $ from "@david/dax";
1+
import { spawn } from "node:child_process";
22
import { createWriteStream, type WriteStream } from "node:fs";
33
import { join as joinPath } from "node:path";
4+
import process from "node:process";
5+
import { Readable } from "node:stream";
46
import { printErrorMessage } from "../utils.ts";
57
import { ensurePortReleased, killProcessOnPort } from "./port.ts";
68

@@ -45,20 +47,22 @@ export async function serverClosure<T>(
4547
await releasePort?.();
4648

4749
const devCommand = cmd.split(" ");
48-
const serverProcess = $`${devCommand}`
49-
.cwd(dir)
50-
.env("PORT", String(defaultPort))
51-
.stdin("null")
52-
.stdout("piped")
53-
.stderr("piped")
54-
.noThrow()
55-
.spawn();
50+
const child = spawn(devCommand[0], devCommand.slice(1), {
51+
cwd: dir,
52+
env: { ...process.env, PORT: String(defaultPort) },
53+
stdio: ["ignore", "pipe", "pipe"],
54+
detached: true, // creates a new process group so we can kill the tree
55+
});
56+
57+
// Prevent unhandled exception when the process is killed
58+
child.on("error", () => {});
5659

57-
// Prevent unhandled rejection when the process is killed
58-
serverProcess.catch(() => {});
60+
// Convert Node.js readable streams to Web ReadableStreams for tee()
61+
const stdoutWeb = Readable.toWeb(child.stdout!) as ReadableStream<Uint8Array>;
62+
const stderrWeb = Readable.toWeb(child.stderr!) as ReadableStream<Uint8Array>;
5963

60-
const [stdoutForFile, stdoutForPort] = serverProcess.stdout().tee();
61-
const [stderrForFile, stderrForPort] = serverProcess.stderr().tee();
64+
const [stdoutForFile, stdoutForPort] = stdoutWeb.tee();
65+
const [stderrForFile, stderrForPort] = stderrWeb.tee();
6266

6367
// Shared signal to cancel all background stream readers on cleanup
6468
const cleanup = new AbortController();
@@ -82,8 +86,18 @@ export async function serverClosure<T>(
8286
});
8387
return await callback(port);
8488
} finally {
89+
// Kill the entire process group (tsx watch + all its children)
90+
try {
91+
if (child.pid != null) {
92+
process.kill(-child.pid, "SIGKILL");
93+
}
94+
} catch {
95+
// Process group already exited
96+
}
97+
98+
// Also kill the child directly in case it wasn't in the group
8599
try {
86-
serverProcess.kill("SIGKILL");
100+
child.kill("SIGKILL");
87101
} catch {
88102
// Process already exited
89103
}

0 commit comments

Comments
 (0)