mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-18 07:16:34 +00:00
feat(judge): update container configuration and add limits for output and memory
This commit is contained in:
parent
578e33d4c4
commit
03f3d9682a
@ -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();
|
callback();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
stdoutChunks.push(text);
|
||||||
|
stdoutLength += text.length;
|
||||||
|
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();
|
callback();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
stderrChunks.push(text);
|
||||||
|
stderrLength += text.length;
|
||||||
|
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;
|
|
||||||
|
|
||||||
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 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();
|
callback();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
stdoutChunks.push(text);
|
||||||
|
stdoutLength += text.length;
|
||||||
|
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();
|
callback();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
stderrChunks.push(text);
|
||||||
|
stderrLength += text.length;
|
||||||
|
callback();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start the exec stream
|
||||||
|
runExec.start({}, async (error, stream) => {
|
||||||
|
if (error || !stream) {
|
||||||
|
return reject({ output: "System Error", exitCode: ExitCode.SE });
|
||||||
|
}
|
||||||
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
}, timeLimit);
|
||||||
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(() => { });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user