const { spawn } = require('child_process'); const fs = require('fs').promises; const path = require('path'); class SimpleSandboxManager { constructor() { this.sandboxes = new Map(); this.basePath = path.join('/tmp', 'sandboxes'); this.portCounter = 9000; this.streamClients = new Map(); this.commandData = new Map(); } async initialize() { await fs.mkdir(this.basePath, { recursive: true }); console.log('โœ… Sandbox Manager initialized at:', this.basePath); } async createSandbox(sandboxId, options = {}) { const { timeout = 600000 } = options; const sandboxPath = path.join(this.basePath, sandboxId); await fs.mkdir(sandboxPath, { recursive: true }); const port = this.portCounter++; const sandbox = { sandboxId, path: sandboxPath, port, timeout, createdAt: Date.now(), lastActivity: Date.now(), url: `http://localhost:${port}`, }; this.sandboxes.set(sandboxId, sandbox); console.log(`โœ… Sandbox created: ${sandboxId} | URL: ${sandbox.url}`); return sandbox; } isTimedOut(sandboxId) { const sandbox = this.sandboxes.get(sandboxId); if (!sandbox) return true; return (Date.now() - sandbox.createdAt) > sandbox.timeout; } addStreamClient(id, cmdId, res) { const key = `${id}:${cmdId}`; if (!this.streamClients.has(key)) this.streamClients.set(key, []); this.streamClients.get(key).push(res); } removeStreamClient(id, cmdId, res) { const key = `${id}:${cmdId}`; const clients = this.streamClients.get(key); if (clients) { const index = clients.indexOf(res); if (index > -1) clients.splice(index, 1); if (clients.length === 0) this.streamClients.delete(key); } } sendToStreamClients(id, cmdId, type, data) { const key = `${id}:${cmdId}`; const clients = this.streamClients.get(key); if (!clients || clients.length === 0) return; const message = `data: ${JSON.stringify({ type, data, timestamp: Date.now() })}\n\n`; clients.forEach(client => { try { client.write(message); } catch (e) { this.removeStreamClient(id, cmdId, client); } }); } async executeCommand(sandboxId, command) { const sandbox = this.sandboxes.get(sandboxId); if (!sandbox) throw new Error('Sandbox not found'); sandbox.lastActivity = Date.now(); const proc = spawn(command, [], { cwd: sandbox.path, shell: true, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PORT: sandbox.port.toString() }, }); const cmdId = `cmd-${proc.pid}`; const dataKey = `${sandboxId}:${cmdId}`; this.commandData.set(dataKey, { stdout: '', stderr: '', exitCode: null, startedAt: Date.now() }); console.log(`[${sandboxId}] CMD (${cmdId}): ${command}`); proc.stdout.on('data', (data) => { const out = data.toString(); this.commandData.get(dataKey).stdout += out; this.sendToStreamClients(sandboxId, cmdId, 'stdout', out); }); proc.stderr.on('data', (data) => { const err = data.toString(); this.commandData.get(dataKey).stderr += err; this.sendToStreamClients(sandboxId, cmdId, 'stderr', err); }); proc.on('exit', (code) => { console.log(`[${sandboxId}] CMD (${cmdId}) exited with code: ${code}`); const cmdData = this.commandData.get(dataKey); if (cmdData) cmdData.exitCode = code; this.sendToStreamClients(sandboxId, cmdId, 'complete', { exitCode: code }); setTimeout(() => this.commandData.delete(dataKey), 10 * 60 * 1000); // 10 min cleanup }); proc.on('error', (err) => { console.error(`[${sandboxId}] CMD (${cmdId}) error:`, err); this.sendToStreamClients(sandboxId, cmdId, 'error', err.message); }); return { commandId: cmdId }; } getCommandData(sandboxId, cmdId) { return this.commandData.get(`${sandboxId}:${cmdId}`); } async writeFile(sandboxId, filePath, content) { const sandbox = this.sandboxes.get(sandboxId); if (!sandbox) throw new Error('Sandbox not found'); sandbox.lastActivity = Date.now(); const fullPath = path.join(sandbox.path, filePath); await fs.mkdir(path.dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, content, 'utf-8'); console.log(`๐Ÿ“ File written to ${sandboxId}: ${filePath}`); } async getURL(sandboxId) { const sandbox = this.sandboxes.get(sandboxId); if (!sandbox) throw new Error('Sandbox not found'); return sandbox.url; } async destroySandbox(sandboxId) { const sandbox = this.sandboxes.get(sandboxId); if (!sandbox) return; console.log(`๐Ÿงน Destroying sandbox: ${sandboxId}`); this.sandboxes.delete(sandboxId); try { await fs.rm(sandbox.path, { recursive: true, force: true }); } catch (e) { if (e.code !== 'ENOENT') console.error(`Cleanup error for ${sandboxId}:`, e.message); } } async destroyAllSandboxes() { await Promise.all(Array.from(this.sandboxes.keys()).map(id => this.destroySandbox(id))); } getSandbox(sandboxId) { return this.sandboxes.get(sandboxId); } async cleanupInactive() { const now = Date.now(); for (const [id, sandbox] of this.sandboxes.entries()) { if ((now - sandbox.createdAt) > sandbox.timeout) { console.log(`โฐ Cleaning up timed-out sandbox: ${id}`); await this.destroySandbox(id); } } } } module.exports = SimpleSandboxManager;