Skip to content

Commit 9fd145a

Browse files
authored
Merge pull request RooCodeInc#1435 from RooVetGit/cte/terminal-reliability
Theoretical handling of stream non-availability or delayed availability
2 parents f86396b + eee7bbe commit 9fd145a

File tree

2 files changed

+79
-28
lines changed

2 files changed

+79
-28
lines changed

src/integrations/terminal/TerminalManager.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ declare module "vscode" {
8282
// https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L10794
8383
interface Window {
8484
onDidStartTerminalShellExecution?: (
85-
listener: (e: any) => any,
85+
listener: (e: {
86+
terminal: vscode.Terminal
87+
execution: { read(): AsyncIterable<string>; commandLine: { value: string } }
88+
}) => any,
8689
thisArgs?: any,
8790
disposables?: vscode.Disposable[],
8891
) => vscode.Disposable
@@ -203,57 +206,77 @@ export class TerminalManager {
203206
constructor() {
204207
let startDisposable: vscode.Disposable | undefined
205208
let endDisposable: vscode.Disposable | undefined
209+
206210
try {
207211
// onDidStartTerminalShellExecution
208212
startDisposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
209213
// Get a handle to the stream as early as possible:
210214
const stream = e?.execution.read()
211215
const terminalInfo = TerminalRegistry.getTerminalInfoByTerminal(e.terminal)
212-
if (stream && terminalInfo) {
213-
const process = this.processes.get(terminalInfo.id)
214-
if (process) {
215-
terminalInfo.stream = stream
216-
terminalInfo.running = true
217-
terminalInfo.streamClosed = false
218-
process.emit("stream_available", terminalInfo.id, stream)
219-
}
220-
} else {
221-
console.error("[TerminalManager] Stream failed, not registered for terminal")
222-
}
223216

224-
console.info("[TerminalManager] Shell execution started:", {
217+
console.info("[TerminalManager] shell execution started", {
225218
hasExecution: !!e?.execution,
219+
hasStream: !!stream,
226220
command: e?.execution?.commandLine?.value,
227221
terminalId: terminalInfo?.id,
228222
})
223+
224+
if (terminalInfo) {
225+
const process = this.processes.get(terminalInfo.id)
226+
227+
if (process) {
228+
if (stream) {
229+
terminalInfo.stream = stream
230+
terminalInfo.running = true
231+
terminalInfo.streamClosed = false
232+
console.log(`[TerminalManager] stream_available -> ${terminalInfo.id}`)
233+
process.emit("stream_available", terminalInfo.id, stream)
234+
} else {
235+
process.emit("stream_unavailable", terminalInfo.id)
236+
console.error(`[TerminalManager] stream_unavailable -> ${terminalInfo.id}`)
237+
}
238+
}
239+
} else {
240+
console.error("[TerminalManager] terminalInfo not available")
241+
}
229242
})
230243

231244
// onDidEndTerminalShellExecution
232245
endDisposable = (vscode.window as vscode.Window).onDidEndTerminalShellExecution?.(async (e) => {
233246
const exitDetails = this.interpretExitCode(e?.exitCode)
234-
console.info("[TerminalManager] Shell execution ended:", {
235-
...exitDetails,
236-
})
247+
console.info("[TerminalManager] Shell execution ended:", { ...exitDetails })
248+
let emitted = false
237249

238-
// Signal completion to any waiting processes
250+
// Signal completion to any waiting processes.
239251
for (const id of this.terminalIds) {
240252
const info = TerminalRegistry.getTerminal(id)
253+
241254
if (info && info.terminal === e.terminal) {
242255
info.running = false
243256
const process = this.processes.get(id)
257+
244258
if (process) {
259+
console.log(`[TerminalManager] emitting shell_execution_complete -> ${id}`)
260+
emitted = true
245261
process.emit("shell_execution_complete", id, exitDetails)
246262
}
263+
247264
break
248265
}
249266
}
267+
268+
if (!emitted) {
269+
console.log(`[TerminalManager#onDidStartTerminalShellExecution] no terminal found`)
270+
}
250271
})
251272
} catch (error) {
252-
console.error("[TerminalManager] Error setting up shell execution handlers:", error)
273+
console.error("[TerminalManager] failed to configure shell execution handlers", error)
253274
}
275+
254276
if (startDisposable) {
255277
this.disposables.push(startDisposable)
256278
}
279+
257280
if (endDisposable) {
258281
this.disposables.push(endDisposable)
259282
}
@@ -366,9 +389,6 @@ export class TerminalManager {
366389
}
367390

368391
disposeAll() {
369-
// for (const info of this.terminals) {
370-
// //info.terminal.dispose() // dont want to dispose terminals when task is aborted
371-
// }
372392
this.terminalIds.clear()
373393
this.processes.clear()
374394
this.disposables.forEach((disposable) => disposable.dispose())

src/integrations/terminal/TerminalProcess.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface TerminalProcessEvents {
4747
*/
4848
shell_execution_complete: [id: number, exitDetails: ExitCodeDetails]
4949
stream_available: [id: number, stream: AsyncIterable<string>]
50+
stream_unavailable: [id: number]
5051
/**
5152
* Emitted when an execution fails to emit a "line" event for a given period of time.
5253
* @param id The terminal ID
@@ -85,15 +86,24 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
8586
const terminalInfo = TerminalRegistry.getTerminalInfoByTerminal(terminal)
8687

8788
if (!terminalInfo) {
88-
console.error("[TerminalProcess] Terminal not found in registry")
89+
console.error("[TerminalProcess#run] terminal not found in registry")
8990
this.emit("no_shell_integration")
9091
this.emit("completed")
9192
this.emit("continue")
9293
return
9394
}
9495

95-
// When executeCommand() is called, onDidStartTerminalShellExecution will fire in TerminalManager
96-
// which creates a new stream via execution.read() and emits 'stream_available'
96+
this.once("stream_unavailable", (id: number) => {
97+
if (id === terminalInfo.id) {
98+
console.error(`[TerminalProcess#run] stream_unavailable`)
99+
this.emit("completed")
100+
this.emit("continue")
101+
}
102+
})
103+
104+
// When `executeCommand()` is called, `onDidStartTerminalShellExecution`
105+
// will fire in `TerminalManager` which creates a new stream via
106+
// `execution.read()` and emits `stream_available`.
97107
const streamAvailable = new Promise<AsyncIterable<string>>((resolve) => {
98108
this.once("stream_available", (id: number, stream: AsyncIterable<string>) => {
99109
if (id === terminalInfo.id) {
@@ -111,15 +121,35 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
111121
})
112122
})
113123

114-
// readLine needs to know if streamClosed, so store this for later
124+
// `readLine()` needs to know if streamClosed, so store this for later.
125+
// NOTE: This doesn't seem to be used anywhere.
115126
this.terminalInfo = terminalInfo
116127

117-
// Execute command
128+
// Execute command.
118129
terminal.shellIntegration.executeCommand(command)
119130
this.isHot = true
120131

121-
// Wait for stream to be available
122-
const stream = await streamAvailable
132+
// Wait for stream to be available.
133+
// const stream = await streamAvailable
134+
135+
// Wait for stream to be available.
136+
let stream: AsyncIterable<string>
137+
138+
try {
139+
stream = await Promise.race([
140+
streamAvailable,
141+
new Promise<never>((_, reject) => {
142+
setTimeout(
143+
() => reject(new Error("Timeout waiting for terminal stream to become available")),
144+
10_000,
145+
)
146+
}),
147+
])
148+
} catch (error) {
149+
console.error(`[TerminalProcess#run] timed out waiting for stream`)
150+
this.emit("stream_stalled", terminalInfo.id)
151+
stream = await streamAvailable
152+
}
123153

124154
let preOutput = ""
125155
let commandOutputStarted = false
@@ -256,6 +286,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
256286
}
257287

258288
public continue() {
289+
console.log(`[TerminalProcess#continue] flushing all`)
259290
this.flushAll()
260291
this.isListening = false
261292
this.removeAllListeners("line")

0 commit comments

Comments
 (0)