



























@@ -3,6 +3,7 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process";
3344const TEARDOWN_GRACE_MS = 2_000;
55const TEARDOWN_KILL_GRACE_MS = 1_000;
6+const EXIT_POLL_MS = 10;
6778export type ChildExit = {
89exitCode: number | null;
@@ -23,43 +24,95 @@ export async function stopChild(
2324child: ChildProcessWithoutNullStreams,
2425options: { killGraceMs?: number; teardownGraceMs?: number } = {},
2526): Promise<StopChildResult> {
26-const currentExit = (): ChildExit | null =>
27-child.exitCode != null || child.signalCode != null
27+const teardownGraceMs = options.teardownGraceMs ?? TEARDOWN_GRACE_MS;
28+const killGraceMs = options.killGraceMs ?? TEARDOWN_KILL_GRACE_MS;
29+let observedExit: ChildExit | null = null;
30+const directExit = (): ChildExit | null =>
31+observedExit ??
32+(child.exitCode != null || child.signalCode != null
2833 ? { exitCode: child.exitCode, signal: child.signalCode }
29- : null;
34+ : null);
35+const currentExit = (): ChildExit | null => {
36+const exit = directExit();
37+if (exit == null || isProcessTreeAlive(child)) {
38+return null;
39+}
40+return exit;
41+};
42+const waitForProcessTreeExit = async (ms: number): Promise<boolean> => {
43+const deadlineAt = Date.now() + ms;
44+while (Date.now() < deadlineAt) {
45+if (!isProcessTreeAlive(child)) {
46+return true;
47+}
48+await delay(Math.min(EXIT_POLL_MS, deadlineAt - Date.now()));
49+}
50+return !isProcessTreeAlive(child);
51+};
52+const cleanupExitedProcessTree = async (
53+exit: ChildExit,
54+exitedBeforeTeardown: boolean,
55+): Promise<StopChildResult> => {
56+if (!isProcessTreeAlive(child)) {
57+return { ...exit, exitedBeforeTeardown };
58+}
59+const sentTeardownSignal = killProcessTree(child, "SIGTERM");
60+if (sentTeardownSignal) {
61+await waitForProcessTreeExit(teardownGraceMs);
62+}
63+if (sentTeardownSignal && isProcessTreeAlive(child)) {
64+killProcessTree(child, "SIGKILL");
65+await waitForProcessTreeExit(killGraceMs);
66+}
67+if (!sentTeardownSignal) {
68+releaseUnsettledChild(child);
69+}
70+return { ...exit, exitedBeforeTeardown };
71+};
307231-const existingExit = currentExit();
73+const existingExit = directExit();
3274if (existingExit != null) {
33-return { ...existingExit, exitedBeforeTeardown: true };
75+return await cleanupExitedProcessTree(existingExit, true);
3476}
357736-let observedExit: ChildExit | null = null;
3778const exited = new Promise<ChildExit>((resolve) => {
3879child.once("exit", (exitCode, signal) => {
3980observedExit = { exitCode, signal };
4081resolve(observedExit);
4182});
4283});
43-const waitForExit = async (ms: number): Promise<ChildExit | null> =>
44-await Promise.race([exited, delay(ms).then(() => null)]);
84+const waitForExit = async (ms: number): Promise<ChildExit | null> => {
85+const deadlineAt = Date.now() + ms;
86+while (Date.now() < deadlineAt) {
87+const waitMs = Math.min(EXIT_POLL_MS, deadlineAt - Date.now());
88+if (directExit() == null) {
89+await Promise.race([exited, delay(waitMs)]);
90+} else {
91+await delay(waitMs);
92+}
93+const exit = currentExit();
94+if (exit != null) {
95+return exit;
96+}
97+}
98+return currentExit();
99+};
4510046101await new Promise<void>((resolve) => {
47102setImmediate(resolve);
48103});
49-const queuedExit = observedExit ?? currentExit();
104+const queuedExit = directExit();
50105if (queuedExit != null) {
51-return { ...queuedExit, exitedBeforeTeardown: true };
106+return await cleanupExitedProcessTree(queuedExit, true);
52107}
5310854-const teardownGraceMs = options.teardownGraceMs ?? TEARDOWN_GRACE_MS;
55-const killGraceMs = options.killGraceMs ?? TEARDOWN_KILL_GRACE_MS;
56109const sentTeardownSignal = killProcessTree(child, "SIGTERM");
57110const gracefulExit = await waitForExit(teardownGraceMs);
58111if (gracefulExit != null) {
59112return { ...gracefulExit, exitedBeforeTeardown: !sentTeardownSignal };
60113}
6111462-const postGraceExit = currentExit() ?? observedExit;
115+const postGraceExit = currentExit();
63116if (postGraceExit != null) {
64117return { ...postGraceExit, exitedBeforeTeardown: !sentTeardownSignal };
65118}
@@ -70,7 +123,7 @@ export async function stopChild(
7012371124killProcessTree(child, "SIGKILL");
72125const killedExit = await waitForExit(killGraceMs);
73-const finalExit = killedExit ?? currentExit() ?? observedExit;
126+const finalExit = killedExit ?? currentExit();
74127if (finalExit != null) {
75128return { ...finalExit, exitedBeforeTeardown: false };
76129}
@@ -86,6 +139,23 @@ function releaseUnsettledChild(child: ChildProcessWithoutNullStreams): void {
86139child.unref();
87140}
88141142+function isProcessTreeAlive(child: ChildProcessWithoutNullStreams): boolean {
143+if (process.platform === "win32" || child.pid === undefined) {
144+return false;
145+}
146+try {
147+process.kill(-child.pid, 0);
148+return true;
149+} catch (error) {
150+return isProcessStillExistsError(error);
151+}
152+}
153+154+function isProcessStillExistsError(error: unknown): boolean {
155+const code = (error as { code?: unknown }).code;
156+return code === "EPERM";
157+}
158+89159function killProcessTree(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): boolean {
90160if (process.platform !== "win32" && child.pid !== undefined) {
91161try {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。