terminalManager.js•4.8 kB
/**
* Terminal Manager for MalwareAnalyzerMCP
* Manages terminal processes, executes commands, and reads output
*
* @author: abdessamad el amrani ,
*/
import { spawn } from 'child_process';
const DEF_TIMEOUT = 40000;
export class TerminalManager {
constructor() {
// Map of active terminal sessions by PID
this.sessions = new Map();
// Map of completed sessions by PID
this.completedSessions = new Map();
}
/**
* Execute a terminal command with optional timeout
* @param {string} command - The command to execute
* @param {number} timeoutMs - Timeout
* @returns {Promise<object>} Result with pid, output, and blocked status
*/
async shellCommand(command, timeoutMs = DEF_TIMEOUT) {
const process = spawn(command, [], { shell: true });
let output = '';
// Ensure process.pid is defined before proceeding
if (!process.pid) {
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
};
}
// Create a session object to track this process
const session = {
pid: process.pid,
process,
lastOutput: '',
isBlocked: false,
startTime: new Date()
};
this.sessions.set(process.pid, session);
return new Promise((resolve) => {
// Handle standard output
process.stdout.on('data', (data) => {
const text = data.toString();
output += text;
session.lastOutput += text;
});
// Handle error output
process.stderr.on('data', (data) => {
const text = data.toString();
output += text;
session.lastOutput += text;
});
// Set timeout to mark process as blocked if it exceeds timeoutMs
setTimeout(() => {
session.isBlocked = true;
resolve({
pid: process.pid,
output,
isBlocked: true
});
}, timeoutMs);
// Handle process completion
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 50 completed sessions
if (this.completedSessions.size > 50) {
const oldestKey = Array.from(this.completedSessions.keys())[0];
this.completedSessions.delete(oldestKey);
}
this.sessions.delete(process.pid);
}
resolve({
pid: process.pid,
output,
isBlocked: false
});
});
});
}
/**
* Read output from a running or completed process
* @param {number} pid - Process ID to read output from
* @returns {object} Result with output string or null
*/
readOutput(pid) {
// First check active sessions
const session = this.sessions.get(pid);
if (session) {
const output = session.lastOutput;
session.lastOutput = ''; // Clear the buffer after reading
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;
const outputStr = `Process completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime.toFixed(2)}s\nFinal output:\n${completedSession.output}`;
// Remove from completed sessions as we've delivered the final output
this.completedSessions.delete(pid);
return { output: outputStr };
}
// Return null if PID not found
return { output: null };
}
/**
* Get status of terminal manager
* @returns {object} Count of active and completed sessions
*/
getStatus() {
return {
activeSessions: this.sessions.size,
completedSessions: this.completedSessions.size
};
}
/**
* Shut down the terminal manager and terminate all processes
*/
shutdown() {
// Terminate all active sessions
for (const session of this.sessions.values()) {
try {
session.process.kill('SIGINT');
setTimeout(() => {
session.process.kill('SIGKILL');
}, 500);
} catch (error) {
console.error(`Failed to terminate process ${session.pid}:`, error);
}
}
this.sessions.clear();
this.completedSessions.clear();
}
}