feat(judge): update container configuration and add limits for output and memory

This commit is contained in:
cfngc4594 2025-03-03 02:13:48 +08:00
parent 578e33d4c4
commit 03f3d9682a
2 changed files with 151 additions and 176 deletions

View File

@ -13,64 +13,110 @@ async function prepareEnvironment(image: string, tag: string) {
const reference = `${image}:${tag}`; const reference = `${image}:${tag}`;
const filters = { reference: [reference] }; const filters = { reference: [reference] };
const images = await docker.listImages({ filters }); 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 // 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({ const container = await docker.createContainer({
Image: `${image}:${tag}`, Image: `${image}:${tag}`,
Cmd: ["tail", "-f", "/dev/null"], // Keep container alive Cmd: ["tail", "-f", "/dev/null"], // Keep container alive
WorkingDir: workingDir, WorkingDir: workingDir,
HostConfig: {
Memory: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
MemorySwap: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
},
NetworkDisabled: true,
}); });
await container.start(); await container.start();
return container; return container;
} }
// Create tar stream for code submission // Create tar stream for code submission
function createTarStream(filePath: string, value: string) { function createTarStream(file: string, value: string) {
const pack = tar.pack(); const pack = tar.pack();
pack.entry({ name: filePath }, value); pack.entry({ name: file }, value);
pack.finalize(); pack.finalize();
return Readable.from(pack); return Readable.from(pack);
} }
// Compilation process handler export async function judge(
async function compileCode( language: string,
container: Docker.Container, value: string
filePath: string,
fileName: string
): Promise<JudgeResult> { ): Promise<JudgeResult> {
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<JudgeResult> {
const compileExec = await container.exec({ const compileExec = await container.exec({
Cmd: ["gcc", filePath, "-o", fileName], Cmd: ["gcc", "-O2", file, "-o", fileName],
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
}); });
return new Promise<JudgeResult>((resolve, reject) => { return new Promise<JudgeResult>((resolve, reject) => {
compileExec.start({}, (error, stream) => { compileExec.start({}, (error, stream) => {
if (error) return reject({ output: error.message, exitCode: ExitCode.SE }); if (error || !stream) {
if (!stream) return reject({ output: "No stream", exitCode: ExitCode.SE }); return reject({ output: "System Error", exitCode: ExitCode.SE });
}
const stdoutChunks: string[] = []; const stdoutChunks: string[] = [];
const stderrChunks: string[] = []; let stdoutLength = 0;
const stdoutStream = new Writable({ const stdoutStream = new Writable({
write(chunk, encoding, callback) { write(chunk, _encoding, callback) {
stdoutChunks.push(chunk.toString()); 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(); callback();
} },
}); });
const stderrChunks: string[] = [];
let stderrLength = 0;
const stderrStream = new Writable({ const stderrStream = new Writable({
write(chunk, encoding, callback) { write(chunk, _encoding, callback) {
stderrChunks.push(chunk.toString()); 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(); callback();
} },
}); });
docker.modem.demuxStream(stream, stdoutStream, stderrStream); docker.modem.demuxStream(stream, stdoutStream, stderrStream);
@ -78,62 +124,28 @@ async function compileCode(
stream.on("end", async () => { stream.on("end", async () => {
const stdout = stdoutChunks.join(""); const stdout = stdoutChunks.join("");
const stderr = stderrChunks.join(""); const stderr = stderrChunks.join("");
const details = await compileExec.inspect(); const exitCode = (await compileExec.inspect()).ExitCode;
resolve({ let result: JudgeResult;
output: stderr || stdout,
exitCode: details.ExitCode === 0 ? ExitCode.AC : ExitCode.CE 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) => { stream.on("error", () => {
reject({ output: error.message, exitCode: ExitCode.SE }); reject({ output: "System Error", exitCode: ExitCode.SE });
}); });
}); });
}); });
} }
// Memory monitoring utility // Run code and implement timeout
async function monitorMemoryUsage( async function run(container: Docker.Container, fileName: string, timeLimit?: number, maxOutput: number = 1 * 1024 * 1024): Promise<JudgeResult> {
container: Docker.Container,
memoryLimit: number,
timeoutHandle: NodeJS.Timeout
): Promise<void> {
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<JudgeResult> {
const startTime = Date.now();
const runExec = await container.exec({ const runExec = await container.exec({
Cmd: [`./${fileName}`], Cmd: [`./${fileName}`],
AttachStdout: true, AttachStdout: true,
@ -141,121 +153,84 @@ async function runCode(
}); });
return new Promise<JudgeResult>((resolve, reject) => { return new Promise<JudgeResult>((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) => { runExec.start({}, async (error, stream) => {
if (error) return reject({ output: error.message, exitCode: ExitCode.SE }); if (error || !stream) {
if (!stream) return reject({ output: "No stream", exitCode: ExitCode.SE }); return reject({ output: "System Error", 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();
}
});
docker.modem.demuxStream(stream, stdoutStream, stderrStream); docker.modem.demuxStream(stream, stdoutStream, stderrStream);
// Timeout control // Timeout mechanism
timeoutHandle = setTimeout(async () => { const timeoutId = setTimeout(() => {
try { // Timeout reached, kill the container
await container.stop({ t: 0 }); container.kill();
await container.remove(); resolve({
reject({ output: "Time Limit Exceeded",
output: `Timeout after ${timeout}ms`, exitCode: ExitCode.TLE,
exitCode: ExitCode.TLE });
}); }, timeLimit);
} catch (error) {
reject({
output: `Timeout handling failed: ${error}`,
exitCode: ExitCode.SE
});
}
}, timeout);
// Memory monitoring
monitorMemoryUsage(container, memoryLimit, timeoutHandle)
.catch(reject);
stream.on("end", async () => { stream.on("end", async () => {
clearTimeout(timeoutHandle); clearTimeout(timeoutId); // Clear the timeout if the program finishes before the time limit
const stdout = stdoutChunks.join(""); const stdout = stdoutChunks.join("");
const stderr = stderrChunks.join(""); const stderr = stderrChunks.join("");
const details = await runExec.inspect(); const exitCode = (await runExec.inspect()).ExitCode;
resolve({ let result: JudgeResult;
output: stderr ? `${stdout}\n${stderr}` : stdout,
exitCode: details.ExitCode === 0 ? ExitCode.AC : ExitCode.RE, // Exit code 0 means successful execution
executionTime: Date.now() - startTime 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) => { stream.on("error", () => {
reject({ output: error.message, exitCode: ExitCode.SE }); 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<JudgeResult> {
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(() => { });
}
}
}

View File

@ -40,7 +40,7 @@ export const LanguageConfigs: Record<string, LanguageConfig> = {
image: "gcc", image: "gcc",
tag: "latest", tag: "latest",
workingDir: "/src", workingDir: "/src",
timeLimit: 10000, timeLimit: 1000,
memoryLimit: 128, memoryLimit: 128,
compileOutputLimit: 1 * 1024 * 1024, compileOutputLimit: 1 * 1024 * 1024,
runOutputLimit: 1 * 1024 * 1024, runOutputLimit: 1 * 1024 * 1024,
@ -53,7 +53,7 @@ export const LanguageConfigs: Record<string, LanguageConfig> = {
image: "gcc", image: "gcc",
tag: "latest", tag: "latest",
workingDir: "/src", workingDir: "/src",
timeLimit: 10000, timeLimit: 1000,
memoryLimit: 128, memoryLimit: 128,
compileOutputLimit: 1 * 1024 * 1024, compileOutputLimit: 1 * 1024 * 1024,
runOutputLimit: 1 * 1024 * 1024, runOutputLimit: 1 * 1024 * 1024,