File size: 6,214 Bytes
f91a684 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | 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);
}
|