



























@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
22import { mkdir, readFile, writeFile } from "node:fs/promises";
33import { createRequire } from "node:module";
44import path from "node:path";
5+import { pathToFileURL } from "node:url";
5667type Options = {
78altScreen: boolean;
@@ -20,6 +21,24 @@ const MODE_TEST_FILES = {
2021const MIRROR_TERMINAL_QUERIES = ["\x1b[?u", "\x1b[16t"];
2122const DEFAULT_PTY_COLS = 100;
2223const DEFAULT_PTY_ROWS = 30;
24+const CHILD_SIGTERM_GRACE_MS = 500;
25+const CHILD_SIGKILL_GRACE_MS = 5_000;
26+27+type KillableChild = {
28+pid?: number;
29+kill(signal: NodeJS.Signals): boolean;
30+};
31+32+type ChildStopper = {
33+cancel: () => void;
34+stop: () => void;
35+};
36+37+type SignalChild = (child: KillableChild, signal: NodeJS.Signals) => void;
38+39+function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
40+(timer as { unref?: () => void }).unref?.();
41+}
23422443function readOption(args: string[], name: string): string | undefined {
2544const idx = args.indexOf(name);
@@ -72,6 +91,64 @@ function currentTerminalDimension(value: number | undefined, fallback: number):
7291return String(value && value > 0 ? value : fallback);
7392}
749394+function signalChildProcessTree(child: KillableChild, signal: NodeJS.Signals): void {
95+if (process.platform !== "win32" && typeof child.pid === "number") {
96+try {
97+process.kill(-child.pid, signal);
98+return;
99+} catch {
100+// Non-detached fallback or already-exited group; direct child signaling is
101+// still useful on platforms without process groups.
102+}
103+}
104+child.kill(signal);
105+}
106+107+function createChildStopper(
108+child: KillableChild,
109+options: {
110+signalChild?: SignalChild;
111+sigtermGraceMs?: number;
112+sigkillGraceMs?: number;
113+} = {},
114+): ChildStopper {
115+const signalChild = options.signalChild ?? signalChildProcessTree;
116+const sigtermGraceMs = options.sigtermGraceMs ?? CHILD_SIGTERM_GRACE_MS;
117+const sigkillGraceMs = options.sigkillGraceMs ?? CHILD_SIGKILL_GRACE_MS;
118+let stopping = false;
119+let termTimer: ReturnType<typeof setTimeout> | undefined;
120+let killTimer: ReturnType<typeof setTimeout> | undefined;
121+122+const cancel = () => {
123+if (termTimer) {
124+clearTimeout(termTimer);
125+termTimer = undefined;
126+}
127+if (killTimer) {
128+clearTimeout(killTimer);
129+killTimer = undefined;
130+}
131+};
132+133+const stop = () => {
134+if (stopping) {
135+return;
136+}
137+stopping = true;
138+signalChild(child, "SIGINT");
139+termTimer = setTimeout(() => {
140+signalChild(child, "SIGTERM");
141+killTimer = setTimeout(() => {
142+signalChild(child, "SIGKILL");
143+}, sigkillGraceMs);
144+unrefTimer(killTimer);
145+}, sigtermGraceMs);
146+unrefTimer(termTimer);
147+};
148+149+return { cancel, stop };
150+}
151+75152async function createMirrorFile(mirrorPath: string): Promise<void> {
76153await mkdir(path.dirname(mirrorPath), { recursive: true });
77154await writeFile(mirrorPath, "", "utf8");
@@ -108,6 +185,7 @@ async function main(): Promise<void> {
108185],
109186{
110187cwd: process.cwd(),
188+detached: process.platform !== "win32",
111189env: {
112190 ...process.env,
113191OPENCLAW_TUI_PTY_MIRROR_PATH: options.mirrorPath,
@@ -172,10 +250,8 @@ async function main(): Promise<void> {
172250}
173251};
174252175-const stopChild = () => {
176-child.kill("SIGINT");
177-setTimeout(() => child.kill("SIGTERM"), 500).unref();
178-};
253+const childStopper = createChildStopper(child);
254+const stopChild = childStopper.stop;
179255180256const ignoredInput = (chunk: Buffer) => {
181257if (chunk.includes(0x03)) {
@@ -238,12 +314,20 @@ async function main(): Promise<void> {
238314childStderr += chunk.toString("utf8");
239315});
240316241-let childExit: { code: number | null; signal: NodeJS.Signals | null } | null = null;
242-child.on("exit", (code, signal) => {
243-childExit = { code, signal };
317+type ChildExit = { code: number | null; signal: NodeJS.Signals | null };
318+let childExit: ChildExit | null = null;
319+const childFinished = new Promise<ChildExit>((resolve) => {
320+child.once("exit", (code, signal) => {
321+childExit = { code, signal };
322+childStopper.cancel();
323+resolve(childExit);
324+});
244325});
245326246-process.once("SIGINT", stopChild);
327+const parentSignals: NodeJS.Signals[] = ["SIGINT", "SIGTERM", "SIGHUP"];
328+for (const signal of parentSignals) {
329+process.once(signal, stopChild);
330+}
247331248332try {
249333for (;;) {
@@ -265,6 +349,12 @@ async function main(): Promise<void> {
265349writeMirrorChunk(result.chunk);
266350}
267351} finally {
352+if (!childExit) {
353+stopChild();
354+}
355+for (const signal of parentSignals) {
356+process.off(signal, stopChild);
357+}
268358await drainParentInput();
269359restoreInput();
270360if (useAltScreen) {
@@ -273,6 +363,10 @@ async function main(): Promise<void> {
273363restoreScreen();
274364}
275365366+if (!childExit) {
367+childExit = await childFinished;
368+}
369+276370if (childStdout) {
277371process.stdout.write(childStdout);
278372}
@@ -288,9 +382,16 @@ async function main(): Promise<void> {
288382}
289383}
290384291-main().catch((error: unknown) => {
292-process.stderr.write(
293-`${error instanceof Error ? error.stack || error.message : String(error)}\n`,
294-);
295-process.exit(1);
296-});
385+if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
386+main().catch((error: unknown) => {
387+process.stderr.write(
388+`${error instanceof Error ? error.stack || error.message : String(error)}\n`,
389+);
390+process.exit(1);
391+});
392+}
393+394+export const testing = {
395+ createChildStopper,
396+ signalChildProcessTree,
397+};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。