fix(coding-agent): kill tracked detached bash children on shutdown
This commit is contained in:
@@ -10,7 +10,13 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
|
|||||||
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
|
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
|
||||||
import { theme } from "../../modes/interactive/theme/theme.js";
|
import { theme } from "../../modes/interactive/theme/theme.js";
|
||||||
import { waitForChildProcess } from "../../utils/child-process.js";
|
import { waitForChildProcess } from "../../utils/child-process.js";
|
||||||
import { getShellConfig, getShellEnv, killProcessTree } from "../../utils/shell.js";
|
import {
|
||||||
|
getShellConfig,
|
||||||
|
getShellEnv,
|
||||||
|
killProcessTree,
|
||||||
|
trackDetachedChildPid,
|
||||||
|
untrackDetachedChildPid,
|
||||||
|
} from "../../utils/shell.js";
|
||||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||||
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
||||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||||
@@ -81,6 +87,7 @@ export function createLocalBashOperations(): BashOperations {
|
|||||||
env: env ?? getShellEnv(),
|
env: env ?? getShellEnv(),
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
if (child.pid) trackDetachedChildPid(child.pid);
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
let timeoutHandle: NodeJS.Timeout | undefined;
|
let timeoutHandle: NodeJS.Timeout | undefined;
|
||||||
// Set timeout if provided.
|
// Set timeout if provided.
|
||||||
@@ -105,6 +112,7 @@ export function createLocalBashOperations(): BashOperations {
|
|||||||
// on inherited stdio handles held by detached descendants.
|
// on inherited stdio handles held by detached descendants.
|
||||||
waitForChildProcess(child)
|
waitForChildProcess(child)
|
||||||
.then((code) => {
|
.then((code) => {
|
||||||
|
if (child.pid) untrackDetachedChildPid(child.pid);
|
||||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||||
if (signal) signal.removeEventListener("abort", onAbort);
|
if (signal) signal.removeEventListener("abort", onAbort);
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
@@ -118,6 +126,7 @@ export function createLocalBashOperations(): BashOperations {
|
|||||||
resolve({ exitCode: code });
|
resolve({ exitCode: code });
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
if (child.pid) untrackDetachedChildPid(child.pid);
|
||||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||||
if (signal) signal.removeEventListener("abort", onAbort);
|
if (signal) signal.removeEventListener("abort", onAbort);
|
||||||
reject(err);
|
reject(err);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/cha
|
|||||||
import { copyToClipboard } from "../../utils/clipboard.js";
|
import { copyToClipboard } from "../../utils/clipboard.js";
|
||||||
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
||||||
import { parseGitUrl } from "../../utils/git.js";
|
import { parseGitUrl } from "../../utils/git.js";
|
||||||
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
||||||
import { ensureTool } from "../../utils/tools-manager.js";
|
import { ensureTool } from "../../utils/tools-manager.js";
|
||||||
import { ArminComponent } from "./components/armin.js";
|
import { ArminComponent } from "./components/armin.js";
|
||||||
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
||||||
@@ -2941,6 +2942,7 @@ export class InteractiveMode {
|
|||||||
|
|
||||||
for (const signal of signals) {
|
for (const signal of signals) {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
|
killTrackedDetachedChildren();
|
||||||
void this.shutdown();
|
void this.shutdown();
|
||||||
};
|
};
|
||||||
process.on(signal, handler);
|
process.on(signal, handler);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import type { AssistantMessage, ImageContent } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, ImageContent } from "@mariozechner/pi-ai";
|
||||||
import type { AgentSessionRuntime } from "../core/agent-session-runtime.js";
|
import type { AgentSessionRuntime } from "../core/agent-session-runtime.js";
|
||||||
import { flushRawStdout, writeRawStdout } from "../core/output-guard.js";
|
import { flushRawStdout, writeRawStdout } from "../core/output-guard.js";
|
||||||
|
import { killTrackedDetachedChildren } from "../utils/shell.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for print mode.
|
* Options for print mode.
|
||||||
@@ -51,6 +52,7 @@ export async function runPrintMode(runtimeHost: AgentSessionRuntime, options: Pr
|
|||||||
|
|
||||||
for (const signal of signals) {
|
for (const signal of signals) {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
|
killTrackedDetachedChildren();
|
||||||
void disposeRuntime().finally(() => {
|
void disposeRuntime().finally(() => {
|
||||||
process.exit(signal === "SIGHUP" ? 129 : 143);
|
process.exit(signal === "SIGHUP" ? 129 : 143);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
ExtensionWidgetOptions,
|
ExtensionWidgetOptions,
|
||||||
} from "../../core/extensions/index.js";
|
} from "../../core/extensions/index.js";
|
||||||
import { takeOverStdout, writeRawStdout } from "../../core/output-guard.js";
|
import { takeOverStdout, writeRawStdout } from "../../core/output-guard.js";
|
||||||
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
||||||
import { type Theme, theme } from "../interactive/theme/theme.js";
|
import { type Theme, theme } from "../interactive/theme/theme.js";
|
||||||
import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
|
import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -346,6 +347,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
|
|||||||
|
|
||||||
for (const signal of signals) {
|
for (const signal of signals) {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
|
killTrackedDetachedChildren();
|
||||||
void shutdown(signal === "SIGHUP" ? 129 : 143);
|
void shutdown(signal === "SIGHUP" ? 129 : 143);
|
||||||
};
|
};
|
||||||
process.on(signal, handler);
|
process.on(signal, handler);
|
||||||
|
|||||||
@@ -172,6 +172,27 @@ export function sanitizeBinaryOutput(str: string): string {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detached child processes must be tracked so they can be killed on parent
|
||||||
|
* shutdown signals (SIGHUP/SIGTERM).
|
||||||
|
*/
|
||||||
|
const trackedDetachedChildPids = new Set<number>();
|
||||||
|
|
||||||
|
export function trackDetachedChildPid(pid: number): void {
|
||||||
|
trackedDetachedChildPids.add(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function untrackDetachedChildPid(pid: number): void {
|
||||||
|
trackedDetachedChildPids.delete(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function killTrackedDetachedChildren(): void {
|
||||||
|
for (const pid of trackedDetachedChildPids) {
|
||||||
|
killProcessTree(pid);
|
||||||
|
}
|
||||||
|
trackedDetachedChildPids.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kill a process and all its children (cross-platform)
|
* Kill a process and all its children (cross-platform)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user