فهرست منبع

Use SIGKILL for command execution timeouts in the "execa" variant (#6071)

Chris Estreich 5 ماه پیش
والد
کامیت
9956cc1f48

+ 1 - 1
apps/web-evals/src/actions/runs.ts

@@ -56,7 +56,7 @@ export async function createRun({ suite, exercises = [], systemPrompt, timeout,
 
 		const dockerArgs = [
 			`--name evals-controller-${run.id}`,
-			// "--rm",
+			"--rm",
 			"--network evals_default",
 			"-v /var/run/docker.sock:/var/run/docker.sock",
 			"-v /tmp/evals:/var/log/evals",

+ 20 - 2
packages/evals/src/cli/runTask.ts

@@ -5,7 +5,14 @@ import * as os from "node:os"
 import pWaitFor from "p-wait-for"
 import { execa } from "execa"
 
-import { type TaskEvent, TaskCommandName, RooCodeEventName, IpcMessageType, EVALS_SETTINGS } from "@roo-code/types"
+import {
+	type TaskEvent,
+	type ClineSay,
+	TaskCommandName,
+	RooCodeEventName,
+	IpcMessageType,
+	EVALS_SETTINGS,
+} from "@roo-code/types"
 import { IpcClient } from "@roo-code/ipc"
 
 import {
@@ -203,6 +210,15 @@ export const runTask = async ({ run, task, publish, logger }: RunTaskOptions) =>
 		log: [RooCodeEventName.TaskTokenUsageUpdated, RooCodeEventName.TaskAskResponded],
 	}
 
+	const loggableSays: ClineSay[] = [
+		"error",
+		"command_output",
+		"rooignore_error",
+		"diff_error",
+		"condense_context",
+		"condense_context_error",
+	]
+
 	client.on(IpcMessageType.TaskEvent, async (taskEvent) => {
 		const { eventName, payload } = taskEvent
 
@@ -215,7 +231,9 @@ export const runTask = async ({ run, task, publish, logger }: RunTaskOptions) =>
 		// For message events we only log non-partial messages.
 		if (
 			!ignoreEvents.log.includes(eventName) &&
-			(eventName !== RooCodeEventName.Message || payload[0].message.partial !== true)
+			(eventName !== RooCodeEventName.Message ||
+				(payload[0].message.say && loggableSays.includes(payload[0].message.say)) ||
+				payload[0].message.partial !== true)
 		) {
 			logger.info(`${eventName} ->`, payload)
 		}

+ 2 - 2
packages/types/src/global-settings.ts

@@ -222,7 +222,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
 	alwaysAllowUpdateTodoList: true,
 	followupAutoApproveTimeoutMs: 0,
 	allowedCommands: ["*"],
-	commandExecutionTimeout: 30_000,
+	commandExecutionTimeout: 20,
 	commandTimeoutAllowlist: [],
 	preventCompletionWithOpenTodos: false,
 
@@ -266,7 +266,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
 
 	mcpEnabled: false,
 
-	mode: "code",
+	mode: "code", // "architect",
 
 	customModes: [],
 }

+ 43 - 50
src/core/tools/executeCommandTool.ts

@@ -21,7 +21,7 @@ import { t } from "../../i18n"
 class ShellIntegrationError extends Error {}
 
 export async function executeCommandTool(
-	cline: Task,
+	task: Task,
 	block: ToolUse,
 	askApproval: AskApproval,
 	handleError: HandleError,
@@ -33,25 +33,25 @@ export async function executeCommandTool(
 
 	try {
 		if (block.partial) {
-			await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
+			await task.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
 			return
 		} else {
 			if (!command) {
-				cline.consecutiveMistakeCount++
-				cline.recordToolError("execute_command")
-				pushToolResult(await cline.sayAndCreateMissingParamError("execute_command", "command"))
+				task.consecutiveMistakeCount++
+				task.recordToolError("execute_command")
+				pushToolResult(await task.sayAndCreateMissingParamError("execute_command", "command"))
 				return
 			}
 
-			const ignoredFileAttemptedToAccess = cline.rooIgnoreController?.validateCommand(command)
+			const ignoredFileAttemptedToAccess = task.rooIgnoreController?.validateCommand(command)
 
 			if (ignoredFileAttemptedToAccess) {
-				await cline.say("rooignore_error", ignoredFileAttemptedToAccess)
+				await task.say("rooignore_error", ignoredFileAttemptedToAccess)
 				pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess)))
 				return
 			}
 
-			cline.consecutiveMistakeCount = 0
+			task.consecutiveMistakeCount = 0
 
 			command = unescapeHtmlEntities(command) // Unescape HTML entities.
 			const didApprove = await askApproval("command", command)
@@ -60,14 +60,15 @@ export async function executeCommandTool(
 				return
 			}
 
-			const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString()
-			const clineProvider = await cline.providerRef.deref()
-			const clineProviderState = await clineProvider?.getState()
+			const executionId = task.lastMessageTs?.toString() ?? Date.now().toString()
+			const provider = await task.providerRef.deref()
+			const providerState = await provider?.getState()
+
 			const {
 				terminalOutputLineLimit = 500,
 				terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
 				terminalShellIntegrationDisabled = false,
-			} = clineProviderState ?? {}
+			} = providerState ?? {}
 
 			// Get command execution timeout from VSCode configuration (in seconds)
 			const commandExecutionTimeoutSeconds = vscode.workspace
@@ -96,26 +97,26 @@ export async function executeCommandTool(
 			}
 
 			try {
-				const [rejected, result] = await executeCommand(cline, options)
+				const [rejected, result] = await executeCommand(task, options)
 
 				if (rejected) {
-					cline.didRejectTool = true
+					task.didRejectTool = true
 				}
 
 				pushToolResult(result)
 			} catch (error: unknown) {
 				const status: CommandExecutionStatus = { executionId, status: "fallback" }
-				clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
-				await cline.say("shell_integration_warning")
+				provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
+				await task.say("shell_integration_warning")
 
 				if (error instanceof ShellIntegrationError) {
-					const [rejected, result] = await executeCommand(cline, {
+					const [rejected, result] = await executeCommand(task, {
 						...options,
 						terminalShellIntegrationDisabled: true,
 					})
 
 					if (rejected) {
-						cline.didRejectTool = true
+						task.didRejectTool = true
 					}
 
 					pushToolResult(result)
@@ -143,7 +144,7 @@ export type ExecuteCommandOptions = {
 }
 
 export async function executeCommand(
-	cline: Task,
+	task: Task,
 	{
 		executionId,
 		command,
@@ -154,16 +155,16 @@ export async function executeCommand(
 		commandExecutionTimeout = 0,
 	}: ExecuteCommandOptions,
 ): Promise<[boolean, ToolResponse]> {
-	// Convert milliseconds back to seconds for display purposes
+	// Convert milliseconds back to seconds for display purposes.
 	const commandExecutionTimeoutSeconds = commandExecutionTimeout / 1000
 	let workingDir: string
 
 	if (!customCwd) {
-		workingDir = cline.cwd
+		workingDir = task.cwd
 	} else if (path.isAbsolute(customCwd)) {
 		workingDir = customCwd
 	} else {
-		workingDir = path.resolve(cline.cwd, customCwd)
+		workingDir = path.resolve(task.cwd, customCwd)
 	}
 
 	try {
@@ -180,7 +181,7 @@ export async function executeCommand(
 	let shellIntegrationError: string | undefined
 
 	const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode"
-	const clineProvider = await cline.providerRef.deref()
+	const provider = await task.providerRef.deref()
 
 	let accumulatedOutput = ""
 	const callbacks: RooTerminalCallbacks = {
@@ -192,14 +193,14 @@ export async function executeCommand(
 				terminalOutputCharacterLimit,
 			)
 			const status: CommandExecutionStatus = { executionId, status: "output", output: compressedOutput }
-			clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
+			provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
 
 			if (runInBackground) {
 				return
 			}
 
 			try {
-				const { response, text, images } = await cline.ask("command_output", "")
+				const { response, text, images } = await task.ask("command_output", "")
 				runInBackground = true
 
 				if (response === "messageResponse") {
@@ -214,29 +215,30 @@ export async function executeCommand(
 				terminalOutputLineLimit,
 				terminalOutputCharacterLimit,
 			)
-			cline.say("command_output", result)
+
+			task.say("command_output", result)
 			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) })
+			provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
 		},
 		onShellExecutionComplete: (details: ExitCodeDetails) => {
 			const status: CommandExecutionStatus = { executionId, status: "exited", exitCode: details.exitCode }
-			clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
+			provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
 			exitDetails = details
 		},
 	}
 
 	if (terminalProvider === "vscode") {
 		callbacks.onNoShellIntegration = async (error: string) => {
-			TelemetryService.instance.captureShellIntegrationError(cline.taskId)
+			TelemetryService.instance.captureShellIntegrationError(task.taskId)
 			shellIntegrationError = error
 		}
 	}
 
-	const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, cline.taskId, terminalProvider)
+	const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, task.taskId, terminalProvider)
 
 	if (terminal instanceof Terminal) {
 		terminal.terminal.show(true)
@@ -248,9 +250,9 @@ export async function executeCommand(
 	}
 
 	const process = terminal.runCommand(command, callbacks)
-	cline.terminalProcess = process
+	task.terminalProcess = process
 
-	// Implement command execution timeout (skip if timeout is 0)
+	// Implement command execution timeout (skip if timeout is 0).
 	if (commandExecutionTimeout > 0) {
 		let timeoutId: NodeJS.Timeout | undefined
 		let isTimedOut = false
@@ -258,10 +260,7 @@ export async function executeCommand(
 		const timeoutPromise = new Promise<void>((_, reject) => {
 			timeoutId = setTimeout(() => {
 				isTimedOut = true
-				// Try to abort the process
-				if (cline.terminalProcess) {
-					cline.terminalProcess.abort()
-				}
+				task.terminalProcess?.abort()
 				reject(new Error(`Command execution timed out after ${commandExecutionTimeout}ms`))
 			}, commandExecutionTimeout)
 		})
@@ -270,17 +269,10 @@ export async function executeCommand(
 			await Promise.race([process, timeoutPromise])
 		} catch (error) {
 			if (isTimedOut) {
-				// Handle timeout case
 				const status: CommandExecutionStatus = { executionId, status: "timeout" }
-				clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
-
-				// Add visual feedback for timeout
-				await cline.say(
-					"error",
-					t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds }),
-				)
-
-				cline.terminalProcess = undefined
+				provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
+				await task.say("error", t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds }))
+				task.terminalProcess = undefined
 
 				return [
 					false,
@@ -292,14 +284,15 @@ export async function executeCommand(
 			if (timeoutId) {
 				clearTimeout(timeoutId)
 			}
-			cline.terminalProcess = undefined
+
+			task.terminalProcess = undefined
 		}
 	} else {
-		// No timeout - just wait for the process to complete
+		// No timeout - just wait for the process to complete.
 		try {
 			await process
 		} finally {
-			cline.terminalProcess = undefined
+			task.terminalProcess = undefined
 		}
 	}
 
@@ -316,7 +309,7 @@ export async function executeCommand(
 
 	if (message) {
 		const { text, images } = message
-		await cline.say("user_feedback", text, images)
+		await task.say("user_feedback", text, images)
 
 		return [
 			true,

+ 13 - 9
src/integrations/terminal/ExecaTerminalProcess.ts

@@ -73,6 +73,8 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
 				let timeoutId: NodeJS.Timeout | undefined
 
 				const kill = new Promise<void>((resolve) => {
+					console.log(`[ExecaTerminalProcess#run] SIGKILL -> ${this.pid}`)
+
 					timeoutId = setTimeout(() => {
 						try {
 							subprocess.kill("SIGKILL")
@@ -86,7 +88,7 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
 					await Promise.race([subprocess, kill])
 				} catch (error) {
 					console.log(
-						`[ExecaTerminalProcess] subprocess termination error: ${error instanceof Error ? error.message : String(error)}`,
+						`[ExecaTerminalProcess#run] subprocess termination error: ${error instanceof Error ? error.message : String(error)}`,
 					)
 				}
 
@@ -98,12 +100,13 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
 			this.emit("shell_execution_complete", { exitCode: 0 })
 		} catch (error) {
 			if (error instanceof ExecaError) {
-				console.error(`[ExecaTerminalProcess] shell execution error: ${error.message}`)
+				console.error(`[ExecaTerminalProcess#run] shell execution error: ${error.message}`)
 				this.emit("shell_execution_complete", { exitCode: error.exitCode ?? 0, signalName: error.signal })
 			} else {
 				console.error(
-					`[ExecaTerminalProcess] shell execution error: ${error instanceof Error ? error.message : String(error)}`,
+					`[ExecaTerminalProcess#run] shell execution error: ${error instanceof Error ? error.message : String(error)}`,
 				)
+
 				this.emit("shell_execution_complete", { exitCode: 1 })
 			}
 		}
@@ -128,29 +131,30 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
 			psTree(this.pid, async (err, children) => {
 				if (!err) {
 					const pids = children.map((p) => parseInt(p.PID))
+					console.error(`[ExecaTerminalProcess#abort] SIGKILL children -> ${pids.join(", ")}`)
 
 					for (const pid of pids) {
 						try {
-							process.kill(pid, "SIGINT")
+							process.kill(pid, "SIGKILL")
 						} catch (e) {
 							console.warn(
-								`[ExecaTerminalProcess] Failed to send SIGINT to child PID ${pid}: ${e instanceof Error ? e.message : String(e)}`,
+								`[ExecaTerminalProcess#abort] Failed to send SIGKILL 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}`,
+						`[ExecaTerminalProcess#abort] Failed to get process tree for PID ${this.pid}: ${err.message}`,
 					)
 				}
 			})
 
 			try {
-				process.kill(this.pid, "SIGINT")
+				console.error(`[ExecaTerminalProcess#abort] SIGKILL parent -> ${this.pid}`)
+				process.kill(this.pid, "SIGKILL")
 			} catch (e) {
 				console.warn(
-					`[ExecaTerminalProcess] Failed to send SIGINT to main PID ${this.pid}: ${e instanceof Error ? e.message : String(e)}`,
+					`[ExecaTerminalProcess#abort] Failed to send SIGKILL to main PID ${this.pid}: ${e instanceof Error ? e.message : String(e)}`,
 				)
 			}
 		}