From bfafc430b2bad3c06535316098355aeb2319498c Mon Sep 17 00:00:00 2001 From: cte Date: Fri, 2 May 2025 17:14:24 -0700 Subject: [PATCH] More robust process killing --- .changeset/great-sheep-speak.md | 5 ++ package-lock.json | 89 +++++++++++++++++++ package.json | 2 + src/core/tools/executeCommandTool.ts | 1 + .../terminal/ExecaTerminalProcess.ts | 39 +++++++- 5 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 .changeset/great-sheep-speak.md diff --git a/.changeset/great-sheep-speak.md b/.changeset/great-sheep-speak.md new file mode 100644 index 0000000000..727e72c8ee --- /dev/null +++ b/.changeset/great-sheep-speak.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +More robust process killing diff --git a/package-lock.json b/package-lock.json index 528e962b89..396d799518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "pkce-challenge": "^4.1.0", "posthog-node": "^4.7.0", "pretty-bytes": "^6.1.1", + "ps-tree": "^1.2.0", "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", "reconnecting-eventsource": "^1.6.4", @@ -79,6 +80,7 @@ "@types/node": "20.x", "@types/node-cache": "^4.1.3", "@types/node-ipc": "^9.2.3", + "@types/ps-tree": "^1.1.6", "@types/string-similarity": "^4.0.2", "@typescript-eslint/eslint-plugin": "^7.14.1", "@typescript-eslint/parser": "^7.11.0", @@ -9029,6 +9031,13 @@ "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.4.tgz", "integrity": "sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==" }, + "node_modules/@types/ps-tree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@types/ps-tree/-/ps-tree-1.1.6.tgz", + "integrity": "sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -11792,6 +11801,12 @@ "node": ">= 0.4" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -12458,6 +12473,21 @@ "node": ">=12.0.0" } }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -13137,6 +13167,12 @@ "node": ">= 0.8" } }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "license": "MIT" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -16515,6 +16551,11 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -17803,6 +17844,18 @@ "node": ">=16" } }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, "node_modules/pdf-parse": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz", @@ -18245,6 +18298,21 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "license": "MIT", + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -19389,6 +19457,18 @@ "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -19448,6 +19528,15 @@ "npm": ">=6" } }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1" + } + }, "node_modules/streamx": { "version": "2.21.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.0.tgz", diff --git a/package.json b/package.json index 44f516ffd2..d60089fee9 100644 --- a/package.json +++ b/package.json @@ -417,6 +417,7 @@ "pkce-challenge": "^4.1.0", "posthog-node": "^4.7.0", "pretty-bytes": "^6.1.1", + "ps-tree": "^1.2.0", "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", "reconnecting-eventsource": "^1.6.4", @@ -449,6 +450,7 @@ "@types/node": "20.x", "@types/node-cache": "^4.1.3", "@types/node-ipc": "^9.2.3", + "@types/ps-tree": "^1.1.6", "@types/string-similarity": "^4.0.2", "@typescript-eslint/eslint-plugin": "^7.14.1", "@typescript-eslint/parser": "^7.11.0", diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index f3f655b036..4bd303904f 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -174,6 +174,7 @@ export async function executeCommand( completed = true }, onShellExecutionStarted: (pid: number | undefined) => { + console.log(`[executeCommand] onShellExecutionStarted: ${pid}`) const status: CommandExecutionStatus = { executionId, status: "started", pid, command } clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) }, diff --git a/src/integrations/terminal/ExecaTerminalProcess.ts b/src/integrations/terminal/ExecaTerminalProcess.ts index 63b772e5db..7764ecadbe 100644 --- a/src/integrations/terminal/ExecaTerminalProcess.ts +++ b/src/integrations/terminal/ExecaTerminalProcess.ts @@ -1,12 +1,14 @@ import { execa, ExecaError } from "execa" +import psTree from "ps-tree" +import process from "process" import type { RooTerminal } from "./types" import { BaseTerminalProcess } from "./BaseTerminalProcess" export class ExecaTerminalProcess extends BaseTerminalProcess { private terminalRef: WeakRef - private controller?: AbortController private aborted = false + private pid?: number constructor(terminal: RooTerminal) { super() @@ -30,7 +32,6 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { public override async run(command: string) { this.command = command - this.controller = new AbortController() try { this.isHot = true @@ -38,10 +39,10 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { const subprocess = execa({ shell: true, cwd: this.terminal.getCurrentWorkingDirectory(), - cancelSignal: this.controller.signal, all: true, })`${command}` + this.pid = subprocess.pid const stream = subprocess.iterable({ from: "all", preserveNewlines: true }) this.terminal.setActiveStream(stream, subprocess.pid) @@ -116,7 +117,37 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { public override abort() { this.aborted = true - this.controller?.abort() + + if (this.pid) { + psTree(this.pid, async (err, children) => { + if (!err) { + const pids = children.map((p) => parseInt(p.PID)) + + for (const pid of pids) { + try { + process.kill(pid, "SIGINT") + } catch (e) { + console.warn( + `[ExecaTerminalProcess] Failed to send SIGINT to child PID ${pid}: ${e instanceof Error ? e.message : String(e)}`, + ) + // Optionally try SIGTERM or SIGKILL on failure, depending on desired behavior. + } + } + } else { + console.error( + `[ExecaTerminalProcess] Failed to get process tree for PID ${this.pid}: ${err.message}`, + ) + } + }) + + try { + process.kill(this.pid, "SIGINT") + } catch (e) { + console.warn( + `[ExecaTerminalProcess] Failed to send SIGINT to main PID ${this.pid}: ${e instanceof Error ? e.message : String(e)}`, + ) + } + } } public override hasUnretrievedOutput() {