diff --git a/src/core/Cline.ts b/src/core/Cline.ts index ecc22a1fd9..3bf87119d5 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -27,7 +27,8 @@ import { everyLineHasLineNumbers, truncateOutput, } from "../integrations/misc/extract-text" -import { TerminalManager, ExitCodeDetails } from "../integrations/terminal/TerminalManager" +import { TerminalManager } from "../integrations/terminal/TerminalManager" +import { ExitCodeDetails } from "../integrations/terminal/TerminalProcess" import { UrlContentFetcher } from "../services/browser/UrlContentFetcher" import { listFiles } from "../services/glob/list-files" import { regexSearchFiles } from "../services/ripgrep" diff --git a/src/integrations/terminal/TerminalManager.ts b/src/integrations/terminal/TerminalManager.ts index 5bf87784ae..f693303849 100644 --- a/src/integrations/terminal/TerminalManager.ts +++ b/src/integrations/terminal/TerminalManager.ts @@ -1,7 +1,7 @@ import pWaitFor from "p-wait-for" import * as vscode from "vscode" import { arePathsEqual } from "../../utils/path" -import { mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess" +import { ExitCodeDetails, mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess" import { Terminal } from "./Terminal" import { TerminalRegistry } from "./TerminalRegistry" @@ -95,112 +95,11 @@ declare module "vscode" { } } -export interface ExitCodeDetails { - exitCode: number | undefined - signal?: number | undefined - signalName?: string - coreDumpPossible?: boolean -} - export class TerminalManager { private terminalIds: Set = new Set() private processes: Map = new Map() private disposables: vscode.Disposable[] = [] - private interpretExitCode(exitCode: number | undefined): ExitCodeDetails { - if (exitCode === undefined) { - return { exitCode } - } - - if (exitCode <= 128) { - return { exitCode } - } - - const signal = exitCode - 128 - const signals: Record = { - // Standard signals - 1: "SIGHUP", - 2: "SIGINT", - 3: "SIGQUIT", - 4: "SIGILL", - 5: "SIGTRAP", - 6: "SIGABRT", - 7: "SIGBUS", - 8: "SIGFPE", - 9: "SIGKILL", - 10: "SIGUSR1", - 11: "SIGSEGV", - 12: "SIGUSR2", - 13: "SIGPIPE", - 14: "SIGALRM", - 15: "SIGTERM", - 16: "SIGSTKFLT", - 17: "SIGCHLD", - 18: "SIGCONT", - 19: "SIGSTOP", - 20: "SIGTSTP", - 21: "SIGTTIN", - 22: "SIGTTOU", - 23: "SIGURG", - 24: "SIGXCPU", - 25: "SIGXFSZ", - 26: "SIGVTALRM", - 27: "SIGPROF", - 28: "SIGWINCH", - 29: "SIGIO", - 30: "SIGPWR", - 31: "SIGSYS", - - // Real-time signals base - 34: "SIGRTMIN", - - // SIGRTMIN+n signals - 35: "SIGRTMIN+1", - 36: "SIGRTMIN+2", - 37: "SIGRTMIN+3", - 38: "SIGRTMIN+4", - 39: "SIGRTMIN+5", - 40: "SIGRTMIN+6", - 41: "SIGRTMIN+7", - 42: "SIGRTMIN+8", - 43: "SIGRTMIN+9", - 44: "SIGRTMIN+10", - 45: "SIGRTMIN+11", - 46: "SIGRTMIN+12", - 47: "SIGRTMIN+13", - 48: "SIGRTMIN+14", - 49: "SIGRTMIN+15", - - // SIGRTMAX-n signals - 50: "SIGRTMAX-14", - 51: "SIGRTMAX-13", - 52: "SIGRTMAX-12", - 53: "SIGRTMAX-11", - 54: "SIGRTMAX-10", - 55: "SIGRTMAX-9", - 56: "SIGRTMAX-8", - 57: "SIGRTMAX-7", - 58: "SIGRTMAX-6", - 59: "SIGRTMAX-5", - 60: "SIGRTMAX-4", - 61: "SIGRTMAX-3", - 62: "SIGRTMAX-2", - 63: "SIGRTMAX-1", - 64: "SIGRTMAX", - } - - // These signals may produce core dumps: - // SIGQUIT, SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV - const coreDumpPossible = new Set([3, 4, 6, 7, 8, 11]) - - return { - exitCode, - signal, - signalName: signals[signal] || `Unknown Signal (${signal})`, - coreDumpPossible: coreDumpPossible.has(signal), - } - } - constructor() { let startDisposable: vscode.Disposable | undefined let endDisposable: vscode.Disposable | undefined @@ -231,7 +130,10 @@ export class TerminalManager { // onDidEndTerminalShellExecution endDisposable = (vscode.window as vscode.Window).onDidEndTerminalShellExecution?.(async (e) => { - const exitDetails = this.interpretExitCode(e?.exitCode) + // Find the terminal ID by the VSCode terminal instance + const terminalId = this.findTerminalIdByVscodeTerminal(e.terminal) + const process = terminalId !== undefined ? this.processes.get(terminalId) : undefined + const exitDetails = process ? process.interpretExitCode(e?.exitCode) : { exitCode: e?.exitCode } console.info("[TerminalManager] Shell execution ended:", { ...exitDetails, }) @@ -356,6 +258,21 @@ export class TerminalManager { return process ? process.getUnretrievedOutput() : "" } + /** + * Finds the terminal ID by the VSCode terminal instance + * @param terminal The VSCode terminal instance + * @returns The terminal ID or undefined if not found + */ + private findTerminalIdByVscodeTerminal(terminal: vscode.Terminal): number | undefined { + for (const id of this.terminalIds) { + const info = TerminalRegistry.getTerminal(id) + if (info && info.terminal === terminal) { + return id + } + } + return undefined + } + isProcessHot(terminalId: number): boolean { const process = this.processes.get(terminalId) return process ? process.isHot : false diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts index fa578d890f..104dbc0379 100644 --- a/src/integrations/terminal/TerminalProcess.ts +++ b/src/integrations/terminal/TerminalProcess.ts @@ -3,7 +3,12 @@ import stripAnsi from "strip-ansi" import * as vscode from "vscode" import { inspect } from "util" -import { ExitCodeDetails } from "./TerminalManager" +export interface ExitCodeDetails { + exitCode: number | undefined + signal?: number | undefined + signalName?: string + coreDumpPossible?: boolean +} import { Terminal } from "./Terminal" import { TerminalRegistry } from "./TerminalRegistry" @@ -34,6 +39,100 @@ export class TerminalProcess extends EventEmitter { private fullOutput: string = "" private lastRetrievedIndex: number = 0 isHot: boolean = false + + interpretExitCode(exitCode: number | undefined): ExitCodeDetails { + if (exitCode === undefined) { + return { exitCode } + } + + if (exitCode <= 128) { + return { exitCode } + } + + const signal = exitCode - 128 + const signals: Record = { + // Standard signals + 1: "SIGHUP", + 2: "SIGINT", + 3: "SIGQUIT", + 4: "SIGILL", + 5: "SIGTRAP", + 6: "SIGABRT", + 7: "SIGBUS", + 8: "SIGFPE", + 9: "SIGKILL", + 10: "SIGUSR1", + 11: "SIGSEGV", + 12: "SIGUSR2", + 13: "SIGPIPE", + 14: "SIGALRM", + 15: "SIGTERM", + 16: "SIGSTKFLT", + 17: "SIGCHLD", + 18: "SIGCONT", + 19: "SIGSTOP", + 20: "SIGTSTP", + 21: "SIGTTIN", + 22: "SIGTTOU", + 23: "SIGURG", + 24: "SIGXCPU", + 25: "SIGXFSZ", + 26: "SIGVTALRM", + 27: "SIGPROF", + 28: "SIGWINCH", + 29: "SIGIO", + 30: "SIGPWR", + 31: "SIGSYS", + + // Real-time signals base + 34: "SIGRTMIN", + + // SIGRTMIN+n signals + 35: "SIGRTMIN+1", + 36: "SIGRTMIN+2", + 37: "SIGRTMIN+3", + 38: "SIGRTMIN+4", + 39: "SIGRTMIN+5", + 40: "SIGRTMIN+6", + 41: "SIGRTMIN+7", + 42: "SIGRTMIN+8", + 43: "SIGRTMIN+9", + 44: "SIGRTMIN+10", + 45: "SIGRTMIN+11", + 46: "SIGRTMIN+12", + 47: "SIGRTMIN+13", + 48: "SIGRTMIN+14", + 49: "SIGRTMIN+15", + + // SIGRTMAX-n signals + 50: "SIGRTMAX-14", + 51: "SIGRTMAX-13", + 52: "SIGRTMAX-12", + 53: "SIGRTMAX-11", + 54: "SIGRTMAX-10", + 55: "SIGRTMAX-9", + 56: "SIGRTMAX-8", + 57: "SIGRTMAX-7", + 58: "SIGRTMAX-6", + 59: "SIGRTMAX-5", + 60: "SIGRTMAX-4", + 61: "SIGRTMAX-3", + 62: "SIGRTMAX-2", + 63: "SIGRTMAX-1", + 64: "SIGRTMAX", + } + + // These signals may produce core dumps: + // SIGQUIT, SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV + const coreDumpPossible = new Set([3, 4, 6, 7, 8, 11]) + + return { + exitCode, + signal, + signalName: signals[signal] || `Unknown Signal (${signal})`, + coreDumpPossible: coreDumpPossible.has(signal), + } + } private hotTimer: NodeJS.Timeout | null = null async run(terminal: vscode.Terminal, command: string) { diff --git a/src/integrations/terminal/__tests__/TerminalProcessInterpretExitCode.test.ts b/src/integrations/terminal/__tests__/TerminalProcessInterpretExitCode.test.ts new file mode 100644 index 0000000000..3b8e0a2fcd --- /dev/null +++ b/src/integrations/terminal/__tests__/TerminalProcessInterpretExitCode.test.ts @@ -0,0 +1,159 @@ +import { TerminalProcess } from "../TerminalProcess" +import { execSync } from "child_process" + +describe("TerminalProcess.interpretExitCode", () => { + let terminalProcess: TerminalProcess + + beforeEach(() => { + terminalProcess = new TerminalProcess() + }) + + it("should handle undefined exit code", () => { + const result = terminalProcess.interpretExitCode(undefined) + expect(result).toEqual({ exitCode: undefined }) + }) + + it("should handle normal exit codes (0-127)", () => { + // Test success exit code (0) + let result = terminalProcess.interpretExitCode(0) + expect(result).toEqual({ exitCode: 0 }) + + // Test error exit code (1) + result = terminalProcess.interpretExitCode(1) + expect(result).toEqual({ exitCode: 1 }) + + // Test arbitrary exit code within normal range + result = terminalProcess.interpretExitCode(42) + expect(result).toEqual({ exitCode: 42 }) + + // Test boundary exit code + result = terminalProcess.interpretExitCode(127) + expect(result).toEqual({ exitCode: 127 }) + }) + + it("should handle signal exit codes (128+)", () => { + // Test SIGINT (Ctrl+C) - 128 + 2 = 130 + const result = terminalProcess.interpretExitCode(130) + expect(result).toEqual({ + exitCode: 130, + signal: 2, + signalName: "SIGINT", + coreDumpPossible: false, + }) + + // Test SIGTERM - 128 + 15 = 143 + const resultTerm = terminalProcess.interpretExitCode(143) + expect(resultTerm).toEqual({ + exitCode: 143, + signal: 15, + signalName: "SIGTERM", + coreDumpPossible: false, + }) + + // Test SIGSEGV (segmentation fault) - 128 + 11 = 139 + const resultSegv = terminalProcess.interpretExitCode(139) + expect(resultSegv).toEqual({ + exitCode: 139, + signal: 11, + signalName: "SIGSEGV", + coreDumpPossible: true, + }) + }) + + it("should identify signals that can produce core dumps", () => { + // Core dump possible signals: SIGQUIT(3), SIGILL(4), SIGABRT(6), SIGBUS(7), SIGFPE(8), SIGSEGV(11) + const coreDumpSignals = [3, 4, 6, 7, 8, 11] + + for (const signal of coreDumpSignals) { + const exitCode = 128 + signal + const result = terminalProcess.interpretExitCode(exitCode) + expect(result.coreDumpPossible).toBe(true) + } + + // Test a non-core-dump signal + const nonCoreDumpResult = terminalProcess.interpretExitCode(128 + 1) // SIGHUP + expect(nonCoreDumpResult.coreDumpPossible).toBe(false) + }) + + it("should handle unknown signals", () => { + // Test an exit code for a signal that's not in our mapping + const result = terminalProcess.interpretExitCode(128 + 99) + expect(result).toEqual({ + exitCode: 128 + 99, + signal: 99, + signalName: "Unknown Signal (99)", + coreDumpPossible: false, + }) + }) +}) + +describe("TerminalProcess.interpretExitCode with real commands", () => { + let terminalProcess: TerminalProcess + + beforeEach(() => { + terminalProcess = new TerminalProcess() + }) + + it("should correctly interpret exit code 0 from successful command", () => { + try { + // Run a command that should succeed + execSync("echo test", { stdio: "ignore" }) + // If we get here, the command succeeded with exit code 0 + const result = terminalProcess.interpretExitCode(0) + expect(result).toEqual({ exitCode: 0 }) + } catch (error: any) { + // This should not happen for a successful command + fail("Command should have succeeded: " + error.message) + } + }) + + it("should correctly interpret exit code 1 from failed command", () => { + try { + // Run a command that should fail with exit code 1 or 2 + execSync("ls /nonexistent_directory", { stdio: "ignore" }) + fail("Command should have failed") + } catch (error: any) { + // Verify the exit code is what we expect (can be 1 or 2 depending on the system) + expect(error.status).toBeGreaterThan(0) + expect(error.status).toBeLessThan(128) // Not a signal + const result = terminalProcess.interpretExitCode(error.status) + expect(result).toEqual({ exitCode: error.status }) + } + }) + + it("should correctly interpret exit code from command with custom exit code", () => { + try { + // Run a command that exits with a specific code + execSync("exit 42", { stdio: "ignore" }) + fail("Command should have exited with code 42") + } catch (error: any) { + expect(error.status).toBe(42) + const result = terminalProcess.interpretExitCode(error.status) + expect(result).toEqual({ exitCode: 42 }) + } + }) + + // Test signal interpretation directly without relying on actual process termination + it("should correctly interpret signal termination codes", () => { + // Test SIGTERM (signal 15) + const sigtermExitCode = 128 + 15 + const sigtermResult = terminalProcess.interpretExitCode(sigtermExitCode) + expect(sigtermResult.signal).toBe(15) + expect(sigtermResult.signalName).toBe("SIGTERM") + expect(sigtermResult.coreDumpPossible).toBe(false) + + // Test SIGSEGV (signal 11) + const sigsegvExitCode = 128 + 11 + const sigsegvResult = terminalProcess.interpretExitCode(sigsegvExitCode) + expect(sigsegvResult.signal).toBe(11) + expect(sigsegvResult.signalName).toBe("SIGSEGV") + expect(sigsegvResult.coreDumpPossible).toBe(true) + + // Test SIGINT (signal 2) + const sigintExitCode = 128 + 2 + const sigintResult = terminalProcess.interpretExitCode(sigintExitCode) + expect(sigintResult.signal).toBe(2) + expect(sigintResult.signalName).toBe("SIGINT") + expect(sigintResult.coreDumpPossible).toBe(false) + }) +})