From 684fd7ffa00f0995e9389cb7428af4b9c8692f67 Mon Sep 17 00:00:00 2001 From: serg33v Date: Fri, 23 May 2025 00:23:42 +0100 Subject: [PATCH 1/3] fix: added timeout_ms to execute_command --- package-lock.json | 4 ++-- setup-claude-server.js | 12 ++++++++++++ src/tools/schemas.ts | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fae273..ab34f67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@wonderwhy-er/desktop-commander", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@wonderwhy-er/desktop-commander", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", diff --git a/setup-claude-server.js b/setup-claude-server.js index 1df13e0..9143ed9 100755 --- a/setup-claude-server.js +++ b/setup-claude-server.js @@ -518,6 +518,16 @@ export default async function setup() { const setupStep = addSetupStep('main_setup'); const debugMode = isDebugMode(); + // Print ASCII art for DESKTOP COMMANDER + console.log('\n'); + console.log('██████╗ ███████╗███████╗██╗ ██╗████████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗ █████╗ ███╗ ██╗██████╗ ███████╗██████╗ '); + console.log('██╔══██╗██╔════╝██╔════╝██║ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗ ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗ ██║██╔══██╗██╔════╝██╔══██╗'); + console.log('██║ ██║█████╗ ███████╗█████╔╝ ██║ ██║ ██║██████╔╝ ██║ ██║ ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║ ██║█████╗ ██████╔╝'); + console.log('██║ ██║██╔══╝ ╚════██║██╔═██╗ ██║ ██║ ██║██╔═══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗'); + console.log('██████╔╝███████╗███████║██║ ██╗ ██║ ╚██████╔╝██║ ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝███████╗██║ ██║'); + console.log('╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝'); + console.log('\n'); + if (debugMode) { logToFile('Debug mode enabled. Will configure with Node.js inspector options.'); await trackEvent('npx_setup_debug_mode', { enabled: true }); @@ -726,6 +736,8 @@ export default async function setup() { total_time_ms: Date.now() - setupStartTime }); + + return true; } catch (error) { updateSetupStep(setupStep, 'failed', error); diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index c2e1c64..d3384e9 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -16,7 +16,7 @@ export const ListProcessesArgsSchema = z.object({}); // Terminal tools schemas export const ExecuteCommandArgsSchema = z.object({ command: z.string(), - timeout_ms: z.number().optional(), + timeout_ms: z.number(), shell: z.string().optional(), }); From 37206bca0c4b0806a90c141fe77971429d25522d Mon Sep 17 00:00:00 2001 From: serg33v Date: Fri, 23 May 2025 00:33:26 +0100 Subject: [PATCH 2/3] feat: read output timeout --- src/server.ts | 1 + src/terminal-manager.ts | 9 ++++++ src/tools/execute.ts | 67 ++++++++++++++++++++++++++++++++++------- src/tools/schemas.ts | 1 + 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/server.ts b/src/server.ts index 2c1fe5f..b306db2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -335,6 +335,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "read_output", description: ` Read new output from a running terminal session. + Set timeout_ms for long running commands. ${CMD_PREFIX_DESCRIPTION}`, inputSchema: zodToJsonSchema(ReadOutputArgsSchema), diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index e6c164a..ebb599d 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -126,6 +126,15 @@ export class TerminalManager { return null; } + /** + * Get a session by PID + * @param pid Process ID + * @returns The session or undefined if not found + */ + getSession(pid: number): TerminalSession | undefined { + return this.sessions.get(pid); + } + forceTerminate(pid: number): boolean { const session = this.sessions.get(pid); if (!session) { diff --git a/src/tools/execute.ts b/src/tools/execute.ts index 5d05e14..eab6eac 100644 --- a/src/tools/execute.ts +++ b/src/tools/execute.ts @@ -62,21 +62,66 @@ export async function executeCommand(args: unknown): Promise { } export async function readOutput(args: unknown): Promise { - const parsed = ReadOutputArgsSchema.safeParse(args); - if (!parsed.success) { - return { - content: [{ type: "text", text: `Error: Invalid arguments for read_output: ${parsed.error}` }], - isError: true, - }; - } + const parsed = ReadOutputArgsSchema.safeParse(args); + if (!parsed.success) { + return { + content: [{ type: "text", text: `Error: Invalid arguments for read_output: ${parsed.error}` }], + isError: true, + }; + } + + const { pid, timeout_ms = 5000 } = parsed.data; + + // Check if the process exists + const session = terminalManager.getSession(pid); + if (!session) { + return { + content: [{ type: "text", text: `No session found for PID ${pid}` }], + isError: true, + }; + } + // Wait for output with timeout + let output = ""; + let timeoutReached = false; + try { + // Create a promise that resolves when new output is available or when timeout is reached + const outputPromise:Promise = new Promise((resolve) => { + // Check for initial output + const initialOutput = terminalManager.getNewOutput(pid); + if (initialOutput && initialOutput.length > 0) { + resolve(initialOutput); + return; + } + + // Setup an interval to poll for output + const interval = setInterval(() => { + const newOutput = terminalManager.getNewOutput(pid); + if (newOutput && newOutput.length > 0) { + clearInterval(interval); + resolve(newOutput); + } + }, 300); // Check every 300ms + + // Set a timeout to stop waiting + setTimeout(() => { + clearInterval(interval); + timeoutReached = true; + resolve(terminalManager.getNewOutput(pid) || ""); + }, timeout_ms); + }); + + output = await outputPromise; + } catch (error) { + return { + content: [{ type: "text", text: `Error reading output: ${error}` }], + isError: true, + }; + } - const output = terminalManager.getNewOutput(parsed.data.pid); return { content: [{ type: "text", - text: output === null - ? `No session found for PID ${parsed.data.pid}` - : output || 'No new output available' + text: output || 'No new output available' + (timeoutReached ? ' (timeout reached)' : '') }], }; } diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index d3384e9..3c03cbd 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -22,6 +22,7 @@ export const ExecuteCommandArgsSchema = z.object({ export const ReadOutputArgsSchema = z.object({ pid: z.number(), + timeout_ms: z.number().optional(), }); export const ForceTerminateArgsSchema = z.object({ From 53656367c879fea6ad049d7fdac79133d43bc80a Mon Sep 17 00:00:00 2001 From: serg33v Date: Fri, 23 May 2025 09:48:24 +0100 Subject: [PATCH 3/3] fix: fixed racing condition --- src/tools/execute.ts | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/tools/execute.ts b/src/tools/execute.ts index eab6eac..7e5d462 100644 --- a/src/tools/execute.ts +++ b/src/tools/execute.ts @@ -85,7 +85,7 @@ export async function readOutput(args: unknown): Promise { let timeoutReached = false; try { // Create a promise that resolves when new output is available or when timeout is reached - const outputPromise:Promise = new Promise((resolve) => { + const outputPromise: Promise = new Promise((resolve) => { // Check for initial output const initialOutput = terminalManager.getNewOutput(pid); if (initialOutput && initialOutput.length > 0) { @@ -93,20 +93,41 @@ export async function readOutput(args: unknown): Promise { return; } + let resolved = false; + let interval: NodeJS.Timeout | null = null; + let timeout: NodeJS.Timeout | null = null; + + const cleanup = () => { + if (interval) { + clearInterval(interval); + interval = null; + } + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + }; + + const resolveOnce = (value: string, isTimeout = false) => { + if (resolved) return; + resolved = true; + cleanup(); + if (isTimeout) timeoutReached = true; + resolve(value); + }; + // Setup an interval to poll for output - const interval = setInterval(() => { + interval = setInterval(() => { const newOutput = terminalManager.getNewOutput(pid); if (newOutput && newOutput.length > 0) { - clearInterval(interval); - resolve(newOutput); + resolveOnce(newOutput); } }, 300); // Check every 300ms - // Set a timeout to stop waiting - setTimeout(() => { - clearInterval(interval); - timeoutReached = true; - resolve(terminalManager.getNewOutput(pid) || ""); + // Set a timeout to stop waiting + timeout = setTimeout(() => { + const finalOutput = terminalManager.getNewOutput(pid) || ""; + resolveOnce(finalOutput, true); }, timeout_ms); });