Skip to content

Commit e06469a

Browse files
committed
fix: handle daemon processes without blocking UI
- Add daemon process detection utility with common service patterns - Modify executeCommandTool to detect and handle daemons as background processes - Add configuration options for custom daemon patterns - Add comprehensive tests for daemon detection logic Fixes #8636
1 parent 6b8c21f commit e06469a

File tree

5 files changed

+524
-34
lines changed

5 files changed

+524
-34
lines changed

src/core/tools/executeCommandTool.ts

Lines changed: 116 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
1717
import { Terminal } from "../../integrations/terminal/Terminal"
1818
import { Package } from "../../shared/package"
1919
import { t } from "../../i18n"
20+
import {
21+
isDaemonCommand,
22+
getDaemonMessage,
23+
getServiceType,
24+
addDaemonPatterns,
25+
clearUserDaemonPatterns,
26+
} from "../../utils/daemon-detector"
2027

2128
class ShellIntegrationError extends Error {}
2229

@@ -173,6 +180,24 @@ export async function executeCommand(
173180
return [false, `Working directory '${workingDir}' does not exist.`]
174181
}
175182

183+
// Check configuration for daemon detection
184+
const disableDaemonDetection = vscode.workspace
185+
.getConfiguration(Package.name)
186+
.get<boolean>("disableDaemonDetection", false)
187+
188+
// Load user-defined daemon patterns from configuration
189+
if (!disableDaemonDetection) {
190+
clearUserDaemonPatterns()
191+
const userDaemonPatterns = vscode.workspace.getConfiguration(Package.name).get<string[]>("daemonCommands", [])
192+
if (userDaemonPatterns.length > 0) {
193+
addDaemonPatterns(userDaemonPatterns)
194+
}
195+
}
196+
197+
// Check if this is a daemon/long-running process
198+
const isDaemon = !disableDaemonDetection && isDaemonCommand(command)
199+
const serviceType = isDaemon ? getServiceType(command) : null
200+
176201
let message: { text?: string; images?: string[] } | undefined
177202
let runInBackground = false
178203
let completed = false
@@ -252,47 +277,104 @@ export async function executeCommand(
252277
const process = terminal.runCommand(command, callbacks)
253278
task.terminalProcess = process
254279

255-
// Implement command execution timeout (skip if timeout is 0).
256-
if (commandExecutionTimeout > 0) {
257-
let timeoutId: NodeJS.Timeout | undefined
258-
let isTimedOut = false
259-
260-
const timeoutPromise = new Promise<void>((_, reject) => {
261-
timeoutId = setTimeout(() => {
262-
isTimedOut = true
263-
task.terminalProcess?.abort()
264-
reject(new Error(`Command execution timed out after ${commandExecutionTimeout}ms`))
265-
}, commandExecutionTimeout)
280+
// If this is a daemon process, handle it differently
281+
if (isDaemon) {
282+
// Wait for a short time to capture initial output and check for startup errors
283+
const initialOutputTimeout = 3000 // 3 seconds to capture initial output
284+
let initialOutputCaptured = false
285+
286+
const initialOutputPromise = new Promise<void>((resolve) => {
287+
setTimeout(() => {
288+
initialOutputCaptured = true
289+
resolve()
290+
}, initialOutputTimeout)
266291
})
267292

268-
try {
269-
await Promise.race([process, timeoutPromise])
270-
} catch (error) {
271-
if (isTimedOut) {
272-
const status: CommandExecutionStatus = { executionId, status: "timeout" }
273-
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
274-
await task.say("error", t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds }))
275-
task.terminalProcess = undefined
276-
277-
return [
278-
false,
279-
`The command was terminated after exceeding a user-configured ${commandExecutionTimeoutSeconds}s timeout. Do not try to re-run the command.`,
280-
]
281-
}
282-
throw error
283-
} finally {
284-
if (timeoutId) {
285-
clearTimeout(timeoutId)
293+
// Also resolve if the process completes early (e.g., startup error)
294+
const processCompletePromise = new Promise<void>((resolve) => {
295+
const checkComplete = () => {
296+
if (completed) {
297+
resolve()
298+
}
286299
}
300+
// Check periodically if process completed
301+
const interval = setInterval(() => {
302+
checkComplete()
303+
if (completed || initialOutputCaptured) {
304+
clearInterval(interval)
305+
}
306+
}, 100)
307+
})
308+
309+
// Wait for either initial output timeout or process completion
310+
await Promise.race([initialOutputPromise, processCompletePromise])
287311

312+
// If the process completed quickly, it likely failed to start
313+
if (completed) {
314+
// Handle as normal - the process failed
288315
task.terminalProcess = undefined
316+
} else {
317+
// Process is still running - it's a daemon
318+
task.terminalProcess = undefined // Clear reference but don't abort
319+
320+
const daemonMessage = getDaemonMessage(command)
321+
await task.say("text", daemonMessage)
322+
323+
return [
324+
false,
325+
`Started ${serviceType} in the background from '${terminal.getCurrentWorkingDirectory().toPosix()}'.\n` +
326+
`The service is running and you can proceed with other tasks.\n` +
327+
`Current output:\n${result}\n` +
328+
`The terminal will continue to show output from this service.`,
329+
]
289330
}
290331
} else {
291-
// No timeout - just wait for the process to complete.
292-
try {
293-
await process
294-
} finally {
295-
task.terminalProcess = undefined
332+
// Normal command execution with timeout handling
333+
// Implement command execution timeout (skip if timeout is 0).
334+
if (commandExecutionTimeout > 0) {
335+
let timeoutId: NodeJS.Timeout | undefined
336+
let isTimedOut = false
337+
338+
const timeoutPromise = new Promise<void>((_, reject) => {
339+
timeoutId = setTimeout(() => {
340+
isTimedOut = true
341+
task.terminalProcess?.abort()
342+
reject(new Error(`Command execution timed out after ${commandExecutionTimeout}ms`))
343+
}, commandExecutionTimeout)
344+
})
345+
346+
try {
347+
await Promise.race([process, timeoutPromise])
348+
} catch (error) {
349+
if (isTimedOut) {
350+
const status: CommandExecutionStatus = { executionId, status: "timeout" }
351+
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
352+
await task.say(
353+
"error",
354+
t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds }),
355+
)
356+
task.terminalProcess = undefined
357+
358+
return [
359+
false,
360+
`The command was terminated after exceeding a user-configured ${commandExecutionTimeoutSeconds}s timeout. Do not try to re-run the command.`,
361+
]
362+
}
363+
throw error
364+
} finally {
365+
if (timeoutId) {
366+
clearTimeout(timeoutId)
367+
}
368+
369+
task.terminalProcess = undefined
370+
}
371+
} else {
372+
// No timeout - just wait for the process to complete.
373+
try {
374+
await process
375+
} finally {
376+
task.terminalProcess = undefined
377+
}
296378
}
297379
}
298380

src/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,19 @@
377377
"default": false,
378378
"description": "%commands.preventCompletionWithOpenTodos.description%"
379379
},
380+
"roo-cline.daemonCommands": {
381+
"type": "array",
382+
"items": {
383+
"type": "string"
384+
},
385+
"default": [],
386+
"description": "%commands.daemonCommands.description%"
387+
},
388+
"roo-cline.disableDaemonDetection": {
389+
"type": "boolean",
390+
"default": false,
391+
"description": "%commands.disableDaemonDetection.description%"
392+
},
380393
"roo-cline.vsCodeLmModelSelector": {
381394
"type": "object",
382395
"properties": {

src/package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"commands.commandExecutionTimeout.description": "Maximum time in seconds to wait for command execution to complete before timing out (0 = no timeout, 1-600s, default: 0s)",
3434
"commands.commandTimeoutAllowlist.description": "Command prefixes that are excluded from the command execution timeout. Commands matching these prefixes will run without timeout restrictions.",
3535
"commands.preventCompletionWithOpenTodos.description": "Prevent task completion when there are incomplete todos in the todo list",
36+
"commands.daemonCommands.description": "Additional command patterns to identify as daemon/long-running processes. These commands will be handled as background services that continue running without blocking the workflow.",
37+
"commands.disableDaemonDetection.description": "Disable automatic detection of daemon/long-running processes. When disabled, all commands will wait for completion before proceeding.",
3638
"settings.vsCodeLmModelSelector.description": "Settings for VSCode Language Model API",
3739
"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
3840
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",

0 commit comments

Comments
 (0)