Ratan1729/code-execution
1
1// ─── Process Executor Service ────────────────────────2// Core engine: runs code directly via child_process (no Docker)3// For Hugging Face Spaces and environments without Docker-in-Docker4// BATCH MODE: compile once, run all test cases sequentially5 6const { spawn } = require("child_process");7const path = require("path");8const config = require("../config");9const {10 createWorkDir,11 writeFile,12 readFile,13 cleanup,14 ensureDir,15} = require("../utils/file.util");16const logger = require("../utils/logger.util");17 18class ProcessExecutor {19 /**20 * Execute code with a SINGLE input (used by /api/execute)21 * Wraps batchExecute with 1 test case22 */23 async execute({ language, code, input = "", timeLimit, memoryLimit }) {24 const results = await this.batchExecute({25 language,26 code,27 inputs: [input],28 timeLimit,29 memoryLimit,30 });31 return results[0];32 }33 34 /**35 * Execute code against MULTIPLE inputs sequentially.36 * Compile once, run N times — just like Docker version but using subprocess.37 *38 * @param {Object} params39 * @param {string} params.language - c | cpp | python40 * @param {string} params.code - Source code41 * @param {string[]} params.inputs - Array of stdin inputs42 * @param {number} params.timeLimit - Time limit per test case (seconds)43 * @param {number} params.memoryLimit - Memory limit (MB)44 * @returns {Object[]} Array of execution results45 */46 async batchExecute({ language, code, inputs = [], timeLimit, memoryLimit }) {47 const langConfig = config.LANGUAGES[language];48 if (!langConfig) {49 throw new Error(`Unsupported language: ${language}`);50 }51 52 timeLimit = Math.min(53 timeLimit || config.DEFAULT_TIME_LIMIT,54 config.MAX_TIME_LIMIT,55 );56 memoryLimit = Math.min(57 memoryLimit || config.DEFAULT_MEMORY_LIMIT,58 config.MAX_MEMORY_LIMIT,59 );60 61 const numCases = inputs.length;62 const workDir = await createWorkDir();63 const codeDir = path.join(workDir, "code");64 65 try {66 // Write code file67 const codeFile = path.join(codeDir, langConfig.fileName);68 await writeFile(codeFile, code);69 70 // ─── Phase 1: Compilation ───71 const startCompile = process.hrtime.bigint();72 const compileResult = await this._compile(language, codeDir, workDir);73 const compileTimeMs = Number(74 (process.hrtime.bigint() - startCompile) / 1_000_000n,75 );76 77 if (compileResult.error) {78 // Compilation failed — return CE for all test cases79 return inputs.map(() => ({80 verdict: "CE",81 output: "",82 error: this._truncate(compileResult.error, 5000),83 executionTime: 0,84 memoryUsed: 0,85 exitCode: 1,86 wallTime: compileTimeMs,87 language,88 timeLimit,89 memoryLimit,90 }));91 }92 93 logger.info(`🔨 Compiled ${language} in ${compileTimeMs}ms`);94 95 // ─── Phase 2: Run each test case ───96 const results = [];97 const overallStart = process.hrtime.bigint();98 99 for (let i = 0; i < numCases; i++) {100 const result = await this._runTestCase(101 language,102 compileResult.execCmd,103 inputs[i] || "",104 timeLimit,105 memoryLimit,106 workDir,107 );108 results.push({109 ...result,110 language,111 timeLimit,112 memoryLimit,113 });114 }115 116 const wallTimeMs = Number(117 (process.hrtime.bigint() - overallStart) / 1_000_000n,118 );119 logger.info(`⚡ Executed ${numCases} test cases in ${wallTimeMs}ms`);120 121 return results;122 } catch (err) {123 logger.error(`Execution failed: ${err.message}`);124 return inputs.map(() => ({125 verdict: "IE",126 output: "",127 error: `Internal Error: ${err.message}`,128 executionTime: 0,129 memoryUsed: 0,130 exitCode: -1,131 wallTime: 0,132 language,133 timeLimit,134 memoryLimit,135 }));136 } finally {137 await cleanup(workDir);138 }139 }140 141 /**142 * Compile code based on language143 * Returns { execCmd } on success or { error } on failure144 */145 async _compile(language, codeDir, workDir) {146 const binaryPath = path.join(workDir, "solution");147 148 switch (language) {149 case "c": {150 const sourceFile = path.join(codeDir, "solution.c");151 const result = await this._runProcess(152 "gcc",153 ["-O2", "-Wall", "-o", binaryPath, sourceFile, "-lm"],154 { timeout: 10000 },155 );156 if (result.exitCode !== 0) {157 return {158 error: result.stderr || result.stdout || "Compilation failed",159 };160 }161 return { execCmd: [binaryPath] };162 }163 164 case "cpp": {165 const sourceFile = path.join(codeDir, "solution.cpp");166 const result = await this._runProcess(167 "g++",168 ["-O2", "-std=c++17", "-Wall", "-o", binaryPath, sourceFile, "-lm"],169 { timeout: 10000 },170 );171 if (result.exitCode !== 0) {172 return {173 error: result.stderr || result.stdout || "Compilation failed",174 };175 }176 return { execCmd: [binaryPath] };177 }178 179 case "python": {180 const sourceFile = path.join(codeDir, "solution.py");181 // Syntax check only182 const result = await this._runProcess(183 "python3",184 ["-m", "py_compile", sourceFile],185 { timeout: 10000 },186 );187 if (result.exitCode !== 0) {188 return { error: result.stderr || result.stdout || "Syntax error" };189 }190 return { execCmd: ["python3", sourceFile] };191 }192 193 default:194 return { error: `Unsupported language: ${language}` };195 }196 }197 198 /**199 * Run a single test case200 */201 async _runTestCase(202 language,203 execCmd,204 input,205 timeLimit,206 memoryLimit,207 workDir,208 ) {209 const startTime = process.hrtime.bigint();210 211 const result = await this._runProcess(execCmd[0], execCmd.slice(1), {212 timeout: timeLimit * 1000,213 input,214 memoryLimit,215 });216 217 const executionTime = Number(218 (process.hrtime.bigint() - startTime) / 1_000_000n,219 );220 221 // Determine verdict222 let verdict = "OK";223 if (result.timedOut) {224 verdict = "TLE";225 } else if (result.exitCode === 137) {226 verdict = "MLE";227 } else if (result.exitCode === 139 || result.signal === "SIGSEGV") {228 verdict = "RE";229 result.stderr = (result.stderr || "") + "\nSegmentation fault (SIGSEGV)";230 } else if (result.exitCode === 136 || result.signal === "SIGFPE") {231 verdict = "RE";232 result.stderr =233 (result.stderr || "") + "\nFloating point exception (SIGFPE)";234 } else if (result.exitCode === 134 || result.signal === "SIGABRT") {235 verdict = "RE";236 result.stderr = (result.stderr || "") + "\nAborted (SIGABRT)";237 } else if (result.exitCode !== 0) {238 verdict = "RE";239 }240 241 return {242 verdict,243 output: this._truncate(result.stdout, 10000),244 error: this._truncate(result.stderr, 5000),245 executionTime,246 memoryUsed: result.memoryUsed || 0,247 exitCode: result.exitCode,248 wallTime: executionTime,249 };250 }251 252 /**253 * Run a process with timeout and optional stdin input254 */255 _runProcess(cmd, args, options = {}) {256 return new Promise((resolve) => {257 const { timeout = 30000, input = "", memoryLimit = 256 } = options;258 259 let stdout = "";260 let stderr = "";261 let timedOut = false;262 let memoryUsed = 0;263 264 const proc = spawn(cmd, args, {265 stdio: ["pipe", "pipe", "pipe"],266 timeout,267 });268 269 // Set up timeout270 const timer = setTimeout(() => {271 timedOut = true;272 proc.kill("SIGKILL");273 }, timeout);274 275 // Write input276 if (input) {277 proc.stdin.write(input);278 }279 proc.stdin.end();280 281 // Collect stdout282 proc.stdout.on("data", (data) => {283 if (stdout.length < 100000) {284 stdout += data.toString();285 }286 });287 288 // Collect stderr289 proc.stderr.on("data", (data) => {290 if (stderr.length < 50000) {291 stderr += data.toString();292 }293 });294 295 proc.on("close", (code, signal) => {296 clearTimeout(timer);297 298 // Try to get memory usage (best effort, may not be accurate)299 try {300 if (proc.memoryUsage) {301 memoryUsed = Math.round(proc.memoryUsage().rss / 1024);302 }303 } catch {304 // Ignore memory measurement errors305 }306 307 resolve({308 exitCode: code ?? (timedOut ? 124 : -1),309 signal,310 stdout,311 stderr,312 timedOut,313 memoryUsed,314 });315 });316 317 proc.on("error", (err) => {318 clearTimeout(timer);319 resolve({320 exitCode: -1,321 stdout: "",322 stderr: err.message,323 timedOut: false,324 memoryUsed: 0,325 });326 });327 });328 }329 330 _truncate(str, maxLen) {331 if (!str) return "";332 if (str.length <= maxLen) return str;333 return str.substring(0, maxLen) + `\n... (truncated)`;334 }335}336 337module.exports = new ProcessExecutor();338 