diff --git a/src/app/actions/judge.ts b/src/app/actions/judge.ts index 4c4d0b3..b87f062 100644 --- a/src/app/actions/judge.ts +++ b/src/app/actions/judge.ts @@ -13,64 +13,110 @@ async function prepareEnvironment(image: string, tag: string) { const reference = `${image}:${tag}`; const filters = { reference: [reference] }; const images = await docker.listImages({ filters }); - - if (images.length === 0) { - await docker.pull(reference); - } + if (images.length === 0) await docker.pull(reference); } // Create Docker container with keep-alive -async function createContainer(image: string, tag: string, workingDir: string) { +async function createContainer(image: string, tag: string, workingDir: string, memoryLimit?: number) { const container = await docker.createContainer({ Image: `${image}:${tag}`, Cmd: ["tail", "-f", "/dev/null"], // Keep container alive WorkingDir: workingDir, + HostConfig: { + Memory: memoryLimit ? memoryLimit * 1024 * 1024 : undefined, + MemorySwap: memoryLimit ? memoryLimit * 1024 * 1024 : undefined, + }, + NetworkDisabled: true, }); - await container.start(); return container; } // Create tar stream for code submission -function createTarStream(filePath: string, value: string) { +function createTarStream(file: string, value: string) { const pack = tar.pack(); - pack.entry({ name: filePath }, value); + pack.entry({ name: file }, value); pack.finalize(); return Readable.from(pack); } -// Compilation process handler -async function compileCode( - container: Docker.Container, - filePath: string, - fileName: string +export async function judge( + language: string, + value: string ): Promise { + const { fileName, fileExtension, image, tag, workingDir, memoryLimit, timeLimit, compileOutputLimit, runOutputLimit } = LanguageConfigs[language]; + const file = `${fileName}.${fileExtension}`; + let container: Docker.Container | undefined; + + try { + await prepareEnvironment(image, tag); + container = await createContainer(image, tag, workingDir, memoryLimit); + const tarStream = createTarStream(file, value); + await container.putArchive(tarStream, { path: workingDir }); + const compileResult = await compile(container, file, fileName, compileOutputLimit); + if (compileResult.exitCode === ExitCode.CE) { + return compileResult; + } + const runResult = await run(container, fileName, timeLimit, runOutputLimit); + return runResult; + } catch (error) { + console.error(error); + return { output: "System Error", exitCode: ExitCode.SE }; + } finally { + if (container) { + await container.kill(); + await container.remove(); + } + } +} + +async function compile(container: Docker.Container, file: string, fileName: string, maxOutput: number = 1 * 1024 * 1024): Promise { const compileExec = await container.exec({ - Cmd: ["gcc", filePath, "-o", fileName], + Cmd: ["gcc", "-O2", file, "-o", fileName], AttachStdout: true, AttachStderr: true, }); return new Promise((resolve, reject) => { compileExec.start({}, (error, stream) => { - if (error) return reject({ output: error.message, exitCode: ExitCode.SE }); - if (!stream) return reject({ output: "No stream", exitCode: ExitCode.SE }); + if (error || !stream) { + return reject({ output: "System Error", exitCode: ExitCode.SE }); + } const stdoutChunks: string[] = []; - const stderrChunks: string[] = []; - + let stdoutLength = 0; const stdoutStream = new Writable({ - write(chunk, encoding, callback) { - stdoutChunks.push(chunk.toString()); + write(chunk, _encoding, callback) { + let text = chunk.toString(); + if (stdoutLength + text.length > maxOutput) { + text = text.substring(0, maxOutput - stdoutLength); + stdoutChunks.push(text); + stdoutLength = maxOutput; + callback(); + return; + } + stdoutChunks.push(text); + stdoutLength += text.length; callback(); - } + }, }); + const stderrChunks: string[] = []; + let stderrLength = 0; const stderrStream = new Writable({ - write(chunk, encoding, callback) { - stderrChunks.push(chunk.toString()); + write(chunk, _encoding, callback) { + let text = chunk.toString(); + if (stderrLength + text.length > maxOutput) { + text = text.substring(0, maxOutput - stderrLength); + stderrChunks.push(text); + stderrLength = maxOutput; + callback(); + return; + } + stderrChunks.push(text); + stderrLength += text.length; callback(); - } + }, }); docker.modem.demuxStream(stream, stdoutStream, stderrStream); @@ -78,62 +124,28 @@ async function compileCode( stream.on("end", async () => { const stdout = stdoutChunks.join(""); const stderr = stderrChunks.join(""); - const details = await compileExec.inspect(); + const exitCode = (await compileExec.inspect()).ExitCode; - resolve({ - output: stderr || stdout, - exitCode: details.ExitCode === 0 ? ExitCode.AC : ExitCode.CE - }); + let result: JudgeResult; + + if (exitCode !== 0 || stderr) { + result = { output: stderr || "Compilation Error", exitCode: ExitCode.CE }; + } else { + result = { output: stdout, exitCode: ExitCode.CS }; + } + + resolve(result); }); - stream.on("error", (error) => { - reject({ output: error.message, exitCode: ExitCode.SE }); + stream.on("error", () => { + reject({ output: "System Error", exitCode: ExitCode.SE }); }); }); }); } -// Memory monitoring utility -async function monitorMemoryUsage( - container: Docker.Container, - memoryLimit: number, - timeoutHandle: NodeJS.Timeout -): Promise { - return new Promise((resolve, reject) => { - const interval = setInterval(async () => { - try { - const stats = await container.stats({ stream: false }); - const memoryUsage = stats.memory_stats.usage / (1024 * 1024); - - if (memoryUsage > memoryLimit) { - clearInterval(interval); - clearTimeout(timeoutHandle); - await container.stop({ t: 0 }); - await container.remove(); - reject({ - output: `Memory limit exceeded (${memoryUsage.toFixed(2)}MB)`, - exitCode: ExitCode.MLE - }); - } - } catch (error) { - clearInterval(interval); - reject({ - output: `Memory monitoring failed: ${error}`, - exitCode: ExitCode.SE - }); - } - }, 500); - }); -} - -// Code execution handler -async function runCode( - container: Docker.Container, - fileName: string, - timeout: number, - memoryLimit: number -): Promise { - const startTime = Date.now(); +// Run code and implement timeout +async function run(container: Docker.Container, fileName: string, timeLimit?: number, maxOutput: number = 1 * 1024 * 1024): Promise { const runExec = await container.exec({ Cmd: [`./${fileName}`], AttachStdout: true, @@ -141,121 +153,84 @@ async function runCode( }); return new Promise((resolve, reject) => { - let timeoutHandle: NodeJS.Timeout; + const stdoutChunks: string[] = []; + let stdoutLength = 0; + const stdoutStream = new Writable({ + write(chunk, _encoding, callback) { + let text = chunk.toString(); + if (stdoutLength + text.length > maxOutput) { + text = text.substring(0, maxOutput - stdoutLength); + stdoutChunks.push(text); + stdoutLength = maxOutput; + callback(); + return; + } + stdoutChunks.push(text); + stdoutLength += text.length; + callback(); + }, + }); + const stderrChunks: string[] = []; + let stderrLength = 0; + const stderrStream = new Writable({ + write(chunk, _encoding, callback) { + let text = chunk.toString(); + if (stderrLength + text.length > maxOutput) { + text = text.substring(0, maxOutput - stderrLength); + stderrChunks.push(text); + stderrLength = maxOutput; + callback(); + return; + } + stderrChunks.push(text); + stderrLength += text.length; + callback(); + }, + }); + + // Start the exec stream runExec.start({}, async (error, stream) => { - if (error) return reject({ output: error.message, exitCode: ExitCode.SE }); - if (!stream) return reject({ output: "No stream", exitCode: ExitCode.SE }); - - const stdoutChunks: string[] = []; - const stderrChunks: string[] = []; - - const stdoutStream = new Writable({ - write(chunk, encoding, callback) { - stdoutChunks.push(chunk.toString()); - callback(); - } - }); - - const stderrStream = new Writable({ - write(chunk, encoding, callback) { - stderrChunks.push(chunk.toString()); - callback(); - } - }); + if (error || !stream) { + return reject({ output: "System Error", exitCode: ExitCode.SE }); + } docker.modem.demuxStream(stream, stdoutStream, stderrStream); - // Timeout control - timeoutHandle = setTimeout(async () => { - try { - await container.stop({ t: 0 }); - await container.remove(); - reject({ - output: `Timeout after ${timeout}ms`, - exitCode: ExitCode.TLE - }); - } catch (error) { - reject({ - output: `Timeout handling failed: ${error}`, - exitCode: ExitCode.SE - }); - } - }, timeout); - - // Memory monitoring - monitorMemoryUsage(container, memoryLimit, timeoutHandle) - .catch(reject); + // Timeout mechanism + const timeoutId = setTimeout(() => { + // Timeout reached, kill the container + container.kill(); + resolve({ + output: "Time Limit Exceeded", + exitCode: ExitCode.TLE, + }); + }, timeLimit); stream.on("end", async () => { - clearTimeout(timeoutHandle); + clearTimeout(timeoutId); // Clear the timeout if the program finishes before the time limit const stdout = stdoutChunks.join(""); const stderr = stderrChunks.join(""); - const details = await runExec.inspect(); + const exitCode = (await runExec.inspect()).ExitCode; - resolve({ - output: stderr ? `${stdout}\n${stderr}` : stdout, - exitCode: details.ExitCode === 0 ? ExitCode.AC : ExitCode.RE, - executionTime: Date.now() - startTime - }); + let result: JudgeResult; + + // Exit code 0 means successful execution + if (exitCode === 0) { + result = { output: stdout, exitCode: ExitCode.AC }; + } else if (exitCode === 137) { + result = { output: stderr || "Memory Limit Exceeded", exitCode: ExitCode.MLE }; + } else { + result = { output: stderr || "Runtime Error", exitCode: ExitCode.RE }; + } + + resolve(result); }); - stream.on("error", (error) => { - reject({ output: error.message, exitCode: ExitCode.SE }); + stream.on("error", () => { + clearTimeout(timeoutId); // Clear timeout in case of error + reject({ output: "System Error", exitCode: ExitCode.SE }); }); }); }); } - -// Cleanup resources -async function cleanupContainer(container: Docker.Container) { - try { - await container.stop({ t: 0 }); - await container.remove(); - } catch (error) { - console.error("Cleanup failed:", error); - } -} - -// Main judge function -export async function judge( - language: string, - value: string -): Promise { - const config = LanguageConfigs[language]; - const filePath = `${config.fileName}.${config.extension}`; - let container: Docker.Container | undefined; - - try { - await prepareEnvironment(config.image, config.tag); - container = await createContainer(config.image, config.tag, config.workingDir); - - // Inject code into container - const tarStream = createTarStream(filePath, value); - await container.putArchive(tarStream, { path: config.workingDir }); - - // Compilation phase - const compileResult = await compileCode(container, filePath, config.fileName); - if (compileResult.exitCode !== ExitCode.AC) { - return compileResult; - } - - // Execution phase - return await runCode(container, config.fileName, config.timeout, config.memoryLimit); - } catch (error) { - // Error handling - console.error(error); - - const result: JudgeResult = { - output: "Unknow Error", - exitCode: ExitCode.SE, - }; - - return result; - } finally { - // Resource cleanup - if (container) { - cleanupContainer(container).catch(() => { }); - } - } -} diff --git a/src/config/judge.ts b/src/config/judge.ts index 2fba8c3..75caecc 100644 --- a/src/config/judge.ts +++ b/src/config/judge.ts @@ -40,7 +40,7 @@ export const LanguageConfigs: Record = { image: "gcc", tag: "latest", workingDir: "/src", - timeLimit: 10000, + timeLimit: 1000, memoryLimit: 128, compileOutputLimit: 1 * 1024 * 1024, runOutputLimit: 1 * 1024 * 1024, @@ -53,7 +53,7 @@ export const LanguageConfigs: Record = { image: "gcc", tag: "latest", workingDir: "/src", - timeLimit: 10000, + timeLimit: 1000, memoryLimit: 128, compileOutputLimit: 1 * 1024 * 1024, runOutputLimit: 1 * 1024 * 1024,