Skip to content

Commit ccd8ab9

Browse files
authored
Fix: Kill button for execute_command tool (#6457)
1 parent 38c6c7a commit ccd8ab9

File tree

1 file changed

+72
-15
lines changed

1 file changed

+72
-15
lines changed

src/integrations/terminal/ExecaTerminalProcess.ts

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
99
private terminalRef: WeakRef<RooTerminal>
1010
private aborted = false
1111
private pid?: number
12+
private subprocess?: ReturnType<typeof execa>
13+
private pidUpdatePromise?: Promise<void>
1214

1315
constructor(terminal: RooTerminal) {
1416
super()
@@ -36,7 +38,7 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
3638
try {
3739
this.isHot = true
3840

39-
const subprocess = execa({
41+
this.subprocess = execa({
4042
shell: true,
4143
cwd: this.terminal.getCurrentWorkingDirectory(),
4244
all: true,
@@ -48,9 +50,37 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
4850
},
4951
})`${command}`
5052

51-
this.pid = subprocess.pid
52-
const stream = subprocess.iterable({ from: "all", preserveNewlines: true })
53-
this.terminal.setActiveStream(stream, subprocess.pid)
53+
this.pid = this.subprocess.pid
54+
55+
// When using shell: true, the PID is for the shell, not the actual command
56+
// Find the actual command PID after a small delay
57+
if (this.pid) {
58+
this.pidUpdatePromise = new Promise<void>((resolve) => {
59+
setTimeout(() => {
60+
psTree(this.pid!, (err, children) => {
61+
if (!err && children.length > 0) {
62+
// Update PID to the first child (the actual command)
63+
const actualPid = parseInt(children[0].PID)
64+
if (!isNaN(actualPid)) {
65+
this.pid = actualPid
66+
}
67+
}
68+
resolve()
69+
})
70+
}, 100)
71+
})
72+
}
73+
74+
const rawStream = this.subprocess.iterable({ from: "all", preserveNewlines: true })
75+
76+
// Wrap the stream to ensure all chunks are strings (execa can return Uint8Array)
77+
const stream = (async function* () {
78+
for await (const chunk of rawStream) {
79+
yield typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk)
80+
}
81+
})()
82+
83+
this.terminal.setActiveStream(stream, this.pid)
5484

5585
for await (const line of stream) {
5686
if (this.aborted) {
@@ -77,15 +107,15 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
77107

78108
timeoutId = setTimeout(() => {
79109
try {
80-
subprocess.kill("SIGKILL")
110+
this.subprocess?.kill("SIGKILL")
81111
} catch (e) {}
82112

83113
resolve()
84114
}, 5_000)
85115
})
86116

87117
try {
88-
await Promise.race([subprocess, kill])
118+
await Promise.race([this.subprocess, kill])
89119
} catch (error) {
90120
console.log(
91121
`[ExecaTerminalProcess#run] subprocess termination error: ${error instanceof Error ? error.message : String(error)}`,
@@ -109,13 +139,15 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
109139

110140
this.emit("shell_execution_complete", { exitCode: 1 })
111141
}
142+
this.subprocess = undefined
112143
}
113144

114145
this.terminal.setActiveStream(undefined)
115146
this.emitRemainingBufferIfListening()
116147
this.stopHotTimer()
117148
this.emit("completed", this.fullOutput)
118149
this.emit("continue")
150+
this.subprocess = undefined
119151
}
120152

121153
public override continue() {
@@ -127,7 +159,41 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
127159
public override abort() {
128160
this.aborted = true
129161

162+
// Function to perform the kill operations
163+
const performKill = () => {
164+
// Try to kill using the subprocess object
165+
if (this.subprocess) {
166+
try {
167+
this.subprocess.kill("SIGKILL")
168+
} catch (e) {
169+
console.warn(
170+
`[ExecaTerminalProcess#abort] Failed to kill subprocess: ${e instanceof Error ? e.message : String(e)}`,
171+
)
172+
}
173+
}
174+
175+
// Kill the stored PID (which should be the actual command after our update)
176+
if (this.pid) {
177+
try {
178+
process.kill(this.pid, "SIGKILL")
179+
} catch (e) {
180+
console.warn(
181+
`[ExecaTerminalProcess#abort] Failed to kill process ${this.pid}: ${e instanceof Error ? e.message : String(e)}`,
182+
)
183+
}
184+
}
185+
}
186+
187+
// If PID update is in progress, wait for it before killing
188+
if (this.pidUpdatePromise) {
189+
this.pidUpdatePromise.then(performKill).catch(() => performKill())
190+
} else {
191+
performKill()
192+
}
193+
194+
// Continue with the rest of the abort logic
130195
if (this.pid) {
196+
// Also check for any child processes
131197
psTree(this.pid, async (err, children) => {
132198
if (!err) {
133199
const pids = children.map((p) => parseInt(p.PID))
@@ -148,15 +214,6 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
148214
)
149215
}
150216
})
151-
152-
try {
153-
console.error(`[ExecaTerminalProcess#abort] SIGKILL parent -> ${this.pid}`)
154-
process.kill(this.pid, "SIGKILL")
155-
} catch (e) {
156-
console.warn(
157-
`[ExecaTerminalProcess#abort] Failed to send SIGKILL to main PID ${this.pid}: ${e instanceof Error ? e.message : String(e)}`,
158-
)
159-
}
160217
}
161218
}
162219

0 commit comments

Comments
 (0)