|
|
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); |
|
|
}); |
|
|
|
|
|
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; |
|
|
|