Ratan1870/code-execution
0
1// ─── Docker Executor Service ────────────────────────2// Core engine: creates Docker sandbox, runs code, collects metrics3// BATCH MODE: compile once, run all test cases in ONE container4 5const { execFile } = require("child_process");6const { exec: execCb } = require("child_process");7const { promisify } = require("util");8const path = require("path");9const config = require("../config");10const {11 createWorkDir,12 writeFile,13 readFile,14 cleanup,15 ensureDir,16} = require("../utils/file.util");17const logger = require("../utils/logger.util");18 19const execFileAsync = promisify(execFile);20const execAsync = promisify(execCb);21 22class DockerExecutor {23 /**24 * Execute code with a SINGLE input (used by /api/execute)25 * Wraps batchExecute with 1 test case26 */27 async execute({ language, code, input = "", timeLimit, memoryLimit }) {28 const results = await this.batchExecute({29 language,30 code,31 inputs: [input],32 timeLimit,33 memoryLimit,34 });35 return results[0];36 }37 38 /**39 * Execute code against MULTIPLE inputs in ONE Docker container.40 * This is the core method — compile once, run N times.41 *42 * Flow:43 * 1. Create temp dir with code + all input files44 * 2. ONE Docker container compiles + runs all test cases45 * 3. Read all results from mounted volume46 * 4. Cleanup47 *48 * @param {Object} params49 * @param {string} params.language - c | cpp | python50 * @param {string} params.code - Source code51 * @param {string[]} params.inputs - Array of stdin inputs52 * @param {number} params.timeLimit - Time limit per test case (seconds)53 * @param {number} params.memoryLimit - Memory limit (MB)54 * @returns {Object[]} Array of execution results55 */56 async batchExecute({ language, code, inputs = [], timeLimit, memoryLimit }) {57 const langConfig = config.LANGUAGES[language];58 if (!langConfig) {59 throw new Error(`Unsupported language: ${language}`);60 }61 62 timeLimit = Math.min(63 timeLimit || config.DEFAULT_TIME_LIMIT,64 config.MAX_TIME_LIMIT,65 );66 memoryLimit = Math.min(67 memoryLimit || config.DEFAULT_MEMORY_LIMIT,68 config.MAX_MEMORY_LIMIT,69 );70 71 const numCases = inputs.length;72 const workDir = await createWorkDir();73 const codeDir = path.join(workDir, "code");74 const tcDir = path.join(workDir, "testcases");75 const resDir = path.join(workDir, "results");76 77 try {78 // Create directories79 await Promise.all([ensureDir(tcDir), ensureDir(resDir)]);80 81 // Write code file + all input files in parallel82 const writeOps = [83 writeFile(path.join(codeDir, langConfig.fileName), code),84 ];85 for (let i = 0; i < numCases; i++) {86 writeOps.push(87 writeFile(path.join(tcDir, `${i + 1}.in`), inputs[i] || ""),88 );89 }90 await Promise.all(writeOps);91 92 // Make everything writable by container93 await execAsync(`chmod -R 777 ${workDir}`);94 95 // ─── Run ONE Docker container for ALL test cases ───96 const startTime = process.hrtime.bigint();97 const dockerResult = await this._runDocker(98 workDir,99 language,100 timeLimit,101 memoryLimit,102 numCases,103 );104 const wallTimeNs = process.hrtime.bigint() - startTime;105 const wallTimeMs = Number(wallTimeNs / 1_000_000n);106 107 logger.info(108 `🐳 Container finished: ${numCases} test cases in ${wallTimeMs}ms`,109 );110 111 // ─── Read ALL results in parallel ───112 const readOps = [];113 for (let i = 0; i < numCases; i++) {114 const idx = i + 1;115 readOps.push(116 Promise.all([117 readFile(path.join(resDir, `${idx}.out`)),118 readFile(path.join(resDir, `${idx}.err`)),119 readFile(path.join(resDir, `${idx}.meta`)),120 ]),121 );122 }123 const allResults = await Promise.all(readOps);124 125 // ─── Parse each result ───126 const results = allResults.map(([output, stderr, metaRaw], i) => {127 const meta = this._parseMeta(metaRaw);128 129 if (!meta.verdict) {130 if (dockerResult.oomKilled) meta.verdict = "MLE";131 else if (dockerResult.exitCode === 137) meta.verdict = "MLE";132 else if (dockerResult.exitCode !== 0) meta.verdict = "RE";133 else meta.verdict = "IE";134 }135 136 return {137 verdict: meta.verdict,138 output: this._truncate(output, 10000),139 error: this._truncate(stderr, 5000),140 executionTime: meta.time || 0,141 memoryUsed: meta.memory || 0,142 exitCode: meta.exitCode ?? dockerResult.exitCode,143 wallTime: wallTimeMs,144 language,145 timeLimit,146 memoryLimit,147 };148 });149 150 return results;151 } catch (err) {152 logger.error(`Execution failed: ${err.message}`);153 return inputs.map(() => ({154 verdict: "IE",155 output: "",156 error: `Internal Error: ${err.message}`,157 executionTime: 0,158 memoryUsed: 0,159 exitCode: -1,160 wallTime: 0,161 language,162 timeLimit,163 memoryLimit,164 }));165 } finally {166 await cleanup(workDir);167 }168 }169 170 /**171 * Launch Docker container — batch mode172 * run.sh <language> <time_limit> <num_test_cases>173 */174 async _runDocker(workDir, language, timeLimit, memoryLimit, numCases) {175 // Total timeout = time_limit * numCases + 20s overhead (compile + Docker)176 const totalTimeout = timeLimit * numCases + 20;177 178 const args = [179 "run",180 "--rm",181 "--network=none",182 `--memory=${memoryLimit}m`,183 `--memory-swap=${memoryLimit}m`,184 "--cpus=1",185 "--pids-limit=64",186 "--cap-drop=ALL",187 "--security-opt=no-new-privileges",188 "--ulimit",189 "fsize=10485760:10485760",190 "--ulimit",191 "nofile=64:64",192 "-v",193 `${workDir}:/sandbox`,194 config.SANDBOX_IMAGE,195 language,196 String(timeLimit),197 String(numCases),198 ];199 200 try {201 const { stdout, stderr } = await execFileAsync("docker", args, {202 timeout: totalTimeout * 1000,203 maxBuffer: 10 * 1024 * 1024,204 });205 return { exitCode: 0, stdout, stderr, oomKilled: false };206 } catch (err) {207 const exitCode = err.code || 1;208 const oomKilled = exitCode === 137;209 210 if (err.killed) {211 logger.warn("⚠️ Docker container killed by Node.js timeout");212 return {213 exitCode: 124,214 stdout: "",215 stderr: "Timed out",216 oomKilled: false,217 };218 }219 220 return {221 exitCode,222 stdout: err.stdout || "",223 stderr: err.stderr || "",224 oomKilled,225 };226 }227 }228 229 /**230 * Parse key=value metadata from run.sh231 */232 _parseMeta(raw) {233 const meta = {};234 if (!raw || !raw.trim()) return meta;235 for (const line of raw.trim().split("\n")) {236 const eq = line.indexOf("=");237 if (eq === -1) continue;238 const key = line.substring(0, eq).trim();239 const val = line.substring(eq + 1).trim();240 if (key === "verdict") meta.verdict = val;241 else if (key === "time") meta.time = parseInt(val) || 0;242 else if (key === "memory") meta.memory = parseInt(val) || 0;243 else if (key === "exitCode") meta.exitCode = parseInt(val) || 0;244 }245 return meta;246 }247 248 _truncate(str, maxLen) {249 if (!str) return "";250 if (str.length <= maxLen) return str;251 return str.substring(0, maxLen) + `\n... (truncated)`;252 }253}254 255module.exports = new DockerExecutor();256 