Chris Estreich 8 месяцев назад
Родитель
Сommit
dd3cd7fd7d

+ 5 - 0
.changeset/great-sheep-speak.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+More robust process killing

+ 89 - 0
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",

+ 2 - 0
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",

+ 1 - 0
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) })
 		},

+ 35 - 4
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<RooTerminal>
-	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() {