RYP / server /services /localExecutor.js
Soumya79's picture
Upload 1361 files
f91a684 verified
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);
}