UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

138 lines (137 loc) 5.1 kB
import { spawn } from 'child_process'; import { DEFAULT_COMMAND_TIMEOUT } from './config.js'; import { configManager } from './config-manager.js'; import { capture } from "./utils.js"; export class TerminalManager { constructor() { this.sessions = new Map(); this.completedSessions = new Map(); } async executeCommand(command, timeoutMs = DEFAULT_COMMAND_TIMEOUT, shell) { // Get the shell from config if not specified let shellToUse = shell; if (!shellToUse) { try { const config = await configManager.getConfig(); shellToUse = config.shell || true; } catch (error) { // If there's an error getting the config, fall back to default shellToUse = true; } } const spawnOptions = { shell: shellToUse }; const process = spawn(command, [], spawnOptions); let output = ''; // Ensure process.pid is defined before proceeding if (!process.pid) { // Return a consistent error object instead of throwing return { pid: -1, // Use -1 to indicate an error state output: 'Error: Failed to get process ID. The command could not be executed.', isBlocked: false }; } const session = { pid: process.pid, process, lastOutput: '', isBlocked: false, startTime: new Date() }; this.sessions.set(process.pid, session); return new Promise((resolve) => { process.stdout.on('data', (data) => { const text = data.toString(); output += text; session.lastOutput += text; }); process.stderr.on('data', (data) => { const text = data.toString(); output += text; session.lastOutput += text; }); setTimeout(() => { session.isBlocked = true; resolve({ pid: process.pid, output, isBlocked: true }); }, timeoutMs); process.on('exit', (code) => { if (process.pid) { // Store completed session before removing active session this.completedSessions.set(process.pid, { pid: process.pid, output: output + session.lastOutput, // Combine all output exitCode: code, startTime: session.startTime, endTime: new Date() }); // Keep only last 100 completed sessions if (this.completedSessions.size > 100) { const oldestKey = Array.from(this.completedSessions.keys())[0]; this.completedSessions.delete(oldestKey); } this.sessions.delete(process.pid); } resolve({ pid: process.pid, output, isBlocked: false }); }); }); } getNewOutput(pid) { // First check active sessions const session = this.sessions.get(pid); if (session) { const output = session.lastOutput; session.lastOutput = ''; return output; } // Then check completed sessions const completedSession = this.completedSessions.get(pid); if (completedSession) { // Format completion message with exit code and runtime const runtime = (completedSession.endTime.getTime() - completedSession.startTime.getTime()) / 1000; return `Process completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s\nFinal output:\n${completedSession.output}`; } return null; } forceTerminate(pid) { const session = this.sessions.get(pid); if (!session) { return false; } try { session.process.kill('SIGINT'); setTimeout(() => { if (this.sessions.has(pid)) { session.process.kill('SIGKILL'); } }, 1000); return true; } catch (error) { capture('server_request_error', { error: error, message: `Failed to terminate process ${pid}:` }); return false; } } listActiveSessions() { const now = new Date(); return Array.from(this.sessions.values()).map(session => ({ pid: session.pid, isBlocked: session.isBlocked, runtime: now.getTime() - session.startTime.getTime() })); } listCompletedSessions() { return Array.from(this.completedSessions.values()); } } export const terminalManager = new TerminalManager();