import { spawn } from 'child_process'; import { writeFile, mkdir, rm } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; import { randomUUID } from 'crypto'; export class CompilerError extends Error { constructor(message, status, statusCode = 500, executionTime = 0) { super(message); this.name = 'CompilerError'; this.status = status; this.statusCode = statusCode; this.executionTime = executionTime; } } const LANGUAGE_CONFIG = { python: { file: 'main.py', run: ['python3', '-u', 'main.py'] }, python3: { file: 'main.py', run: ['python3', '-u', 'main.py'] }, javascript: { file: 'main.js', run: ['node', 'main.js'] }, java: { file: 'Main.java', compile: ['javac', 'Main.java'], run: ['java', 'Main'] }, c: { file: 'main.c', compile: ['gcc', 'main.c', '-o', 'main.exe', '-lm'], run: ['main.exe'] }, cpp: { file: 'main.cpp', compile: ['g++', 'main.cpp', '-o', 'main.exe'], run: ['main.exe'] }, }; const LANGUAGE_ALIASES = { py: 'python', 'python3': 'python3', 'c++': 'cpp', js: 'javascript', node: 'javascript' }; function normalizeLanguage(lang) { const key = String(lang ?? '').trim().toLowerCase(); return LANGUAGE_ALIASES[key] ?? key; } const TIMEOUT_MS = 60000; // 60 seconds to allow user typing time const activeExecutions = new Map(); function runProcess(cmd, args, cwd) { return spawn(cmd, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], shell: false }); } export async function startExecution({ code, language }) { const normalized = normalizeLanguage(language); const config = LANGUAGE_CONFIG[normalized]; if (!config) { throw new CompilerError(`Unsupported language: ${language}.`, 'unsupported_language', 400); } if (!code?.trim()) { throw new CompilerError('Code is required.', 'validation_error', 400); } const id = randomUUID(); const workDir = join(tmpdir(), `ryp-${id}`); await mkdir(workDir, { recursive: true }); await writeFile(join(workDir, config.file), code, 'utf8'); activeExecutions.set(id, { id, config, workDir, status: 'pending', child: null, startedAt: Date.now(), buffer: [], res: null, // the SSE response object }); return { executionId: id }; } export async function streamExecution(id, res) { const exec = activeExecutions.get(id); if (!exec) { res.status(404).end(); return; } // Set SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-transform', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); res.flushHeaders(); // Flush headers immediately so connection is established exec.res = res; const sendEvent = (type, data) => { if (!exec.res) return; exec.res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); // Flush immediately so browser receives each chunk without buffering if (typeof exec.res.flush === 'function') exec.res.flush(); }; const cleanup = () => { if (exec.child) exec.child.kill('SIGKILL'); if (exec.timer) clearTimeout(exec.timer); activeExecutions.delete(id); rm(exec.workDir, { recursive: true, force: true }).catch(() => {}); if (exec.res) { exec.res.end(); exec.res = null; } }; // We can attach close listener on `res`. res.on('close', cleanup); try { // Compile if needed if (exec.config.compile) { sendEvent('stdout', '[compile] Compiling...'); const [cmd, ...args] = exec.config.compile; const compileProcess = runProcess(cmd, args, exec.workDir); let compileErr = ''; compileProcess.stderr.on('data', d => { compileErr += d.toString(); }); compileProcess.stdout.on('data', d => { compileErr += d.toString(); }); compileProcess.on('error', (err) => { compileErr += `\n[System Error: Failed to start compiler - ${err.message}]`; }); const compileExitCode = await new Promise(resolve => compileProcess.on('close', resolve)); if (compileExitCode !== 0) { sendEvent('stderr', '\nCompilation Failed:\n' + compileErr); sendEvent('done', { status: 'compile_error', executionTime: Date.now() - exec.startedAt }); cleanup(); return; } sendEvent('stdout', ' done.\n'); } // Run const [runCmd, ...runArgs] = exec.config.run; exec.child = runProcess(runCmd, runArgs, exec.workDir); exec.status = 'running'; exec.timer = setTimeout(() => { sendEvent('stderr', '\n[Execution timed out after 60s]'); sendEvent('done', { status: 'timeout', executionTime: Date.now() - exec.startedAt }); cleanup(); }, TIMEOUT_MS); exec.child.stdout.on('data', d => sendEvent('stdout', d.toString())); exec.child.stderr.on('data', d => sendEvent('stderr', d.toString())); exec.child.on('error', (err) => { sendEvent('stderr', `\n[System Error: Failed to start process - ${err.message}]`); sendEvent('done', { status: 'server_error', executionTime: Date.now() - exec.startedAt }); cleanup(); }); exec.child.on('close', (code) => { const time = Date.now() - exec.startedAt; sendEvent('done', { status: code === 0 ? 'success' : 'runtime_error', executionTime: time }); cleanup(); }); } catch (err) { sendEvent('stderr', err.message); sendEvent('done', { status: 'server_error', executionTime: 0 }); cleanup(); } } export function provideInput(id, input) { const exec = activeExecutions.get(id); if (!exec || !exec.child || exec.status !== 'running') { throw new CompilerError('Execution not found or not running', 'not_found', 404); } exec.child.stdin.write(input); } // Keeping a legacy batch execution function so things don't break immediately while testing export async function executeCode({ code, language, input = '' }) { const { executionId } = await startExecution({ code, language }); // This is a dummy wrapper for old code just in case, though we will rewrite routes. throw new CompilerError('Batch execution disabled', 'disabled', 400); } export function getSupportedLanguages() { return Object.keys(LANGUAGE_CONFIG); }