@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
138 lines (137 loc) • 5.1 kB
JavaScript
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();