| 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; |
|
|
| 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, |
| }); |
|
|
| return { executionId: id }; |
| } |
|
|
| export async function streamExecution(id, res) { |
| const exec = activeExecutions.get(id); |
| if (!exec) { |
| res.status(404).end(); |
| return; |
| } |
|
|
| |
| res.writeHead(200, { |
| 'Content-Type': 'text/event-stream', |
| 'Cache-Control': 'no-cache, no-transform', |
| 'Connection': 'keep-alive', |
| 'X-Accel-Buffering': 'no', |
| }); |
| res.flushHeaders(); |
|
|
| exec.res = res; |
|
|
| const sendEvent = (type, data) => { |
| if (!exec.res) return; |
| exec.res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); |
| |
| 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; |
| } |
| }; |
|
|
| |
| res.on('close', cleanup); |
|
|
| try { |
| |
| 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'); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| export async function executeCode({ code, language, input = '' }) { |
| const { executionId } = await startExecution({ code, language }); |
| |
| throw new CompilerError('Batch execution disabled', 'disabled', 400); |
| } |
|
|
| export function getSupportedLanguages() { |
| return Object.keys(LANGUAGE_CONFIG); |
| } |
|
|