feat(judge): enhance code execution handling with detailed results and error handling

This commit is contained in:
cfngc4594 2025-03-02 14:19:29 +08:00
parent 180dc5e310
commit bc5c9cc699

View File

@ -3,10 +3,12 @@
import tar from "tar-stream"; import tar from "tar-stream";
import Docker from "dockerode"; import Docker from "dockerode";
import { Readable, Writable } from "stream"; import { Readable, Writable } from "stream";
import { LanguageConfigs } from "@/config/judge"; import { ExitCode, JudgeResult, LanguageConfigs } from "@/config/judge";
// Docker client initialization
const docker = new Docker({ socketPath: "/var/run/docker.sock" }); const docker = new Docker({ socketPath: "/var/run/docker.sock" });
// Prepare Docker image environment
async function prepareEnvironment(image: string, tag: string) { async function prepareEnvironment(image: string, tag: string) {
const reference = `${image}:${tag}`; const reference = `${image}:${tag}`;
const filters = { reference: [reference] }; const filters = { reference: [reference] };
@ -17,10 +19,11 @@ async function prepareEnvironment(image: string, tag: string) {
} }
} }
// Create Docker container with keep-alive
async function createContainer(image: string, tag: string, workingDir: string) { async function createContainer(image: string, tag: string, workingDir: string) {
const container = await docker.createContainer({ const container = await docker.createContainer({
Image: `${image}:${tag}`, Image: `${image}:${tag}`,
Cmd: ["tail", "-f", "/dev/null"], Cmd: ["tail", "-f", "/dev/null"], // Keep container alive
WorkingDir: workingDir, WorkingDir: workingDir,
}); });
@ -28,6 +31,7 @@ async function createContainer(image: string, tag: string, workingDir: string) {
return container; return container;
} }
// Create tar stream for code submission
function createTarStream(filePath: string, value: string) { function createTarStream(filePath: string, value: string) {
const pack = tar.pack(); const pack = tar.pack();
pack.entry({ name: filePath }, value); pack.entry({ name: filePath }, value);
@ -35,21 +39,22 @@ function createTarStream(filePath: string, value: string) {
return Readable.from(pack); return Readable.from(pack);
} }
async function compileCode(container: Docker.Container, filePath: string, fileName: string) { // Compilation process handler
async function compileCode(
container: Docker.Container,
filePath: string,
fileName: string
): Promise<JudgeResult> {
const compileExec = await container.exec({ const compileExec = await container.exec({
Cmd: ["gcc", filePath, "-o", fileName], Cmd: ["gcc", filePath, "-o", fileName],
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
}); });
return new Promise<string>((resolve, reject) => { return new Promise<JudgeResult>((resolve, reject) => {
compileExec.start({}, (error, stream) => { compileExec.start({}, (error, stream) => {
if (error) { if (error) return reject({ output: error.message, exitCode: ExitCode.SE });
return reject(error); if (!stream) return reject({ output: "No stream", exitCode: ExitCode.SE });
}
if (!stream) {
return reject(new Error("Stream is undefined"));
}
const stdoutChunks: string[] = []; const stdoutChunks: string[] = [];
const stderrChunks: string[] = []; const stderrChunks: string[] = [];
@ -70,63 +75,77 @@ async function compileCode(container: Docker.Container, filePath: string, fileNa
docker.modem.demuxStream(stream, stdoutStream, stderrStream); docker.modem.demuxStream(stream, stdoutStream, stderrStream);
stream.on("end", () => { stream.on("end", async () => {
const stdout = stdoutChunks.join(""); const stdout = stdoutChunks.join("");
const stderr = stderrChunks.join(""); const stderr = stderrChunks.join("");
resolve(stderr ? stdout + stderr : stdout); const details = await compileExec.inspect();
resolve({
output: stderr || stdout,
exitCode: details.ExitCode === 0 ? ExitCode.AC : ExitCode.CE
});
}); });
stream.on("error", reject); stream.on("error", (error) => {
reject({ output: error.message, exitCode: ExitCode.SE });
});
}); });
}); });
} }
async function monitorMemoryUsage(container: Docker.Container, memoryLimit: number, timeoutHandle: NodeJS.Timeout) { // Memory monitoring utility
return new Promise<void>((resolve, reject) => { async function monitorMemoryUsage(
container: Docker.Container,
memoryLimit: number,
timeoutHandle: NodeJS.Timeout
): Promise<void> {
return new Promise((resolve, reject) => {
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
const stats = await container.stats({ stream: false }); const stats = await container.stats({ stream: false });
const memoryUsage = stats.memory_stats.usage / (1024 * 1024); // Convert to MB const memoryUsage = stats.memory_stats.usage / (1024 * 1024);
console.log(`Memory usage: ${memoryUsage.toFixed(2)} MB`);
if (memoryUsage > memoryLimit) { if (memoryUsage > memoryLimit) {
console.warn(`Memory limit exceeded: ${memoryUsage.toFixed(2)} MB > ${memoryLimit} MB`);
clearInterval(interval); clearInterval(interval);
clearTimeout(timeoutHandle); // Clear the timeout timer clearTimeout(timeoutHandle);
await container.stop({ t: 0 }); await container.stop({ t: 0 });
await container.remove(); await container.remove();
reject(new Error("Memory limit exceeded, container stopped and removed")); reject({
output: `Memory limit exceeded (${memoryUsage.toFixed(2)}MB)`,
exitCode: ExitCode.MLE
});
} }
} catch (error) { } catch (error) {
console.error("Error monitoring memory:", error);
clearInterval(interval); clearInterval(interval);
reject(error); reject({
output: `Memory monitoring failed: ${error}`,
exitCode: ExitCode.SE
});
} }
}, 500); // Check every 500ms }, 500);
resolve();
}); });
} }
async function runCode(container: Docker.Container, fileName: string, timeout: number, memoryLimit: number) { // Code execution handler
async function runCode(
container: Docker.Container,
fileName: string,
timeout: number,
memoryLimit: number
): Promise<JudgeResult> {
const startTime = Date.now();
const runExec = await container.exec({ const runExec = await container.exec({
Cmd: [`./${fileName}`], Cmd: [`./${fileName}`],
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
}); });
return new Promise<string>((resolve, reject) => { return new Promise<JudgeResult>((resolve, reject) => {
let timeoutHandle: NodeJS.Timeout; let timeoutHandle: NodeJS.Timeout;
let memoryMonitor: Promise<void>;
runExec.start({}, async (error, stream) => { runExec.start({}, async (error, stream) => {
if (error) { if (error) return reject({ output: error.message, exitCode: ExitCode.SE });
return reject(error); if (!stream) return reject({ output: "No stream", exitCode: ExitCode.SE });
}
if (!stream) {
return reject(new Error("Stream is undefined"));
}
const stdoutChunks: string[] = []; const stdoutChunks: string[] = [];
const stderrChunks: string[] = []; const stderrChunks: string[] = [];
@ -147,72 +166,94 @@ async function runCode(container: Docker.Container, fileName: string, timeout: n
docker.modem.demuxStream(stream, stdoutStream, stderrStream); docker.modem.demuxStream(stream, stdoutStream, stderrStream);
// Timeout monitoring // Timeout control
timeoutHandle = setTimeout(async () => { timeoutHandle = setTimeout(async () => {
console.warn("Execution timed out, stopping container...");
try { try {
await container.stop({ t: 0 }); await container.stop({ t: 0 });
await container.remove(); await container.remove();
reject(new Error("Execution timed out and container was removed")); reject({
} catch (stopError) { output: `Timeout after ${timeout}ms`,
console.error('Error stopping/removing container:', stopError); exitCode: ExitCode.TLE
reject(new Error("Execution timed out, but failed to stop/remove container")); });
} catch (error) {
reject({
output: `Timeout handling failed: ${error}`,
exitCode: ExitCode.SE
});
} }
}, timeout); // Use the configured timeout }, timeout);
// Memory monitoring // Memory monitoring
memoryMonitor = monitorMemoryUsage(container, memoryLimit, timeoutHandle); monitorMemoryUsage(container, memoryLimit, timeoutHandle)
.catch(reject);
stream.on("end", () => { stream.on("end", async () => {
clearTimeout(timeoutHandle); clearTimeout(timeoutHandle);
memoryMonitor.then(() => { const stdout = stdoutChunks.join("");
const stdout = stdoutChunks.join(""); const stderr = stderrChunks.join("");
const stderr = stderrChunks.join(""); const details = await runExec.inspect();
resolve(stderr ? stdout + stderr : stdout);
}).catch(reject); resolve({
output: stderr ? `${stdout}\n${stderr}` : stdout,
exitCode: details.ExitCode === 0 ? ExitCode.AC : ExitCode.RE,
executionTime: Date.now() - startTime
});
}); });
stream.on("error", (error) => { stream.on("error", (error) => {
clearTimeout(timeoutHandle); reject({ output: error.message, exitCode: ExitCode.SE });
reject(error);
}); });
}); });
}); });
} }
// Cleanup resources
async function cleanupContainer(container: Docker.Container) { async function cleanupContainer(container: Docker.Container) {
try { try {
console.log("Stopping container...");
await container.stop({ t: 0 }); await container.stop({ t: 0 });
console.log("Removing container...");
await container.remove(); await container.remove();
} catch (error) { } catch (error) {
console.error("Container cleanup failed:", error); console.error("Cleanup failed:", error);
} }
} }
export async function judge(language: string, value: string) { // Main judge function
const { image, tag, fileName, extension, workingDir, timeout, memoryLimit } = LanguageConfigs[language]; export async function judge(
const filePath = `${fileName}.${extension}`; language: string,
value: string
): Promise<JudgeResult> {
const config = LanguageConfigs[language];
const filePath = `${config.fileName}.${config.extension}`;
let container: Docker.Container | undefined; let container: Docker.Container | undefined;
try { try {
await prepareEnvironment(image, tag); await prepareEnvironment(config.image, config.tag);
container = await createContainer(image, tag, workingDir); container = await createContainer(config.image, config.tag, config.workingDir);
// Inject code into container
const tarStream = createTarStream(filePath, value); const tarStream = createTarStream(filePath, value);
await container.putArchive(tarStream, { path: workingDir }); await container.putArchive(tarStream, { path: config.workingDir });
const compileOutput = await compileCode(container, filePath, fileName); // Compilation phase
if (compileOutput) { const compileResult = await compileCode(container, filePath, config.fileName);
return compileOutput; if (compileResult.exitCode !== ExitCode.AC) {
return compileResult;
} }
return await runCode(container, fileName, timeout, memoryLimit); // Execution phase
return await runCode(container, config.fileName, config.timeout, config.memoryLimit);
} catch (error) { } catch (error) {
console.error("Error during judging:", error); // Error handling
throw error; console.error(error);
const result: JudgeResult = {
output: "Unknow Error",
exitCode: ExitCode.SE,
};
return result;
} finally { } finally {
// Resource cleanup
if (container) { if (container) {
cleanupContainer(container).catch(() => { }); cleanupContainer(container).catch(() => { });
} }