UNPKG

@probelabs/probe

Version:

Node.js wrapper for the probe code search tool

319 lines (281 loc) 8.58 kB
/** * Bash command executor with security and timeout controls * @module agent/bashExecutor */ import { spawn } from 'child_process'; import { resolve, join } from 'path'; import { existsSync } from 'fs'; import { parseCommandForExecution } from './bashCommandUtils.js'; /** * Execute a bash command with security controls * @param {string} command - Command to execute * @param {Object} options - Execution options * @param {string} [options.workingDirectory] - Working directory for command execution * @param {number} [options.timeout=120000] - Timeout in milliseconds * @param {Object} [options.env={}] - Additional environment variables * @param {number} [options.maxBuffer=10485760] - Maximum buffer size (10MB) * @param {boolean} [options.debug=false] - Enable debug logging * @returns {Promise<Object>} Execution result */ export async function executeBashCommand(command, options = {}) { const { workingDirectory = process.cwd(), timeout = 120000, // 2 minutes default env = {}, maxBuffer = 10 * 1024 * 1024, // 10MB debug = false } = options; // Validate working directory let cwd = workingDirectory; try { cwd = resolve(cwd); if (!existsSync(cwd)) { throw new Error(`Working directory does not exist: ${cwd}`); } } catch (error) { return { success: false, error: `Invalid working directory: ${error.message}`, stdout: '', stderr: '', exitCode: 1, command, workingDirectory: cwd, duration: 0 }; } const startTime = Date.now(); if (debug) { console.log(`[BashExecutor] Executing command: "${command}"`); console.log(`[BashExecutor] Working directory: "${cwd}"`); console.log(`[BashExecutor] Timeout: ${timeout}ms`); } return new Promise((resolve, reject) => { // Create environment const processEnv = { ...process.env, ...env }; // Parse command for shell execution // We use shell: false for security, so we need to parse manually const args = parseCommandForExecution(command); if (!args || args.length === 0) { resolve({ success: false, error: 'Failed to parse command', stdout: '', stderr: '', exitCode: 1, command, workingDirectory: cwd, duration: Date.now() - startTime }); return; } const [cmd, ...cmdArgs] = args; // Spawn the process const child = spawn(cmd, cmdArgs, { cwd, env: processEnv, stdio: ['ignore', 'pipe', 'pipe'], // stdin ignored, capture stdout/stderr shell: false, // For security windowsHide: true }); let stdout = ''; let stderr = ''; let killed = false; let timeoutHandle; // Set timeout if (timeout > 0) { timeoutHandle = setTimeout(() => { if (!killed) { killed = true; child.kill('SIGTERM'); // Force kill after 5 seconds if still running setTimeout(() => { if (child.exitCode === null) { child.kill('SIGKILL'); } }, 5000); } }, timeout); } // Handle stdout child.stdout.on('data', (data) => { const chunk = data.toString(); if (stdout.length + chunk.length <= maxBuffer) { stdout += chunk; } else { // Buffer overflow if (!killed) { killed = true; child.kill('SIGTERM'); } } }); // Handle stderr child.stderr.on('data', (data) => { const chunk = data.toString(); if (stderr.length + chunk.length <= maxBuffer) { stderr += chunk; } else { // Buffer overflow if (!killed) { killed = true; child.kill('SIGTERM'); } } }); // Handle process exit child.on('close', (code, signal) => { if (timeoutHandle) { clearTimeout(timeoutHandle); } const duration = Date.now() - startTime; if (debug) { console.log(`[BashExecutor] Command completed - Code: ${code}, Signal: ${signal}, Duration: ${duration}ms`); console.log(`[BashExecutor] Stdout length: ${stdout.length}, Stderr length: ${stderr.length}`); } let success = true; let error = ''; if (killed) { success = false; if (stdout.length + stderr.length > maxBuffer) { error = `Command output exceeded maximum buffer size (${maxBuffer} bytes)`; } else { error = `Command timed out after ${timeout}ms`; } } else if (code !== 0) { success = false; error = `Command exited with code ${code}`; } resolve({ success, error, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code, signal, command, workingDirectory: cwd, duration, killed }); }); // Handle spawn errors child.on('error', (error) => { if (timeoutHandle) { clearTimeout(timeoutHandle); } if (debug) { console.log(`[BashExecutor] Spawn error:`, error); } resolve({ success: false, error: `Failed to execute command: ${error.message}`, stdout: '', stderr: '', exitCode: 1, command, workingDirectory: cwd, duration: Date.now() - startTime }); }); }); } /** * Format execution result for display * @param {Object} result - Execution result * @param {boolean} [includeMetadata=false] - Include metadata in output * @returns {string} Formatted result */ export function formatExecutionResult(result, includeMetadata = false) { if (!result) { return 'No result available'; } let output = ''; // Add command info if metadata requested if (includeMetadata) { output += `Command: ${result.command}\n`; output += `Working directory: ${result.workingDirectory}\n`; output += `Duration: ${result.duration}ms\n`; output += `Exit Code: ${result.exitCode}\n`; if (result.signal) { output += `Signal: ${result.signal}\n`; } output += '\n'; } // Add stdout if present if (result.stdout) { if (includeMetadata) { output += '--- STDOUT ---\n'; } output += result.stdout; if (includeMetadata && result.stderr) { output += '\n'; } } // Add stderr if present if (result.stderr) { if (includeMetadata) { if (result.stdout) output += '\n'; output += '--- STDERR ---\n'; } else if (result.stdout) { output += '\n--- STDERR ---\n'; } output += result.stderr; } // Add error message if failed and no stderr if (!result.success && result.error && !result.stderr) { if (output) output += '\n'; output += `Error: ${result.error}`; } // Add exit code for failed commands if (!result.success && result.exitCode !== undefined && result.exitCode !== 0) { if (output) output += '\n'; output += `Exit code: ${result.exitCode}`; } return output || (result.success ? 'Command completed successfully (no output)' : 'Command failed (no output)'); } /** * Validate execution options * @param {Object} options - Options to validate * @returns {Object} Validation result */ export function validateExecutionOptions(options = {}) { const errors = []; const warnings = []; // Check timeout if (options.timeout !== undefined) { if (typeof options.timeout !== 'number' || options.timeout < 0) { errors.push('timeout must be a non-negative number'); } else if (options.timeout > 600000) { // 10 minutes warnings.push('timeout is very high (>10 minutes)'); } } // Check maxBuffer if (options.maxBuffer !== undefined) { if (typeof options.maxBuffer !== 'number' || options.maxBuffer < 1024) { errors.push('maxBuffer must be at least 1024 bytes'); } else if (options.maxBuffer > 100 * 1024 * 1024) { // 100MB warnings.push('maxBuffer is very high (>100MB)'); } } // Check working directory if (options.workingDirectory) { if (typeof options.workingDirectory !== 'string') { errors.push('workingDirectory must be a string'); } else if (!existsSync(options.workingDirectory)) { errors.push(`workingDirectory does not exist: ${options.workingDirectory}`); } } // Check environment if (options.env && typeof options.env !== 'object') { errors.push('env must be an object'); } return { valid: errors.length === 0, errors, warnings }; }