judge4c/src/lib/judge-runtime.ts

212 lines
4.9 KiB
TypeScript
Raw Normal View History

import "server-only";
import { docker } from "@/app/actions/docker";
import { PassThrough } from "stream";
import tar from "tar-stream";
export const createStreamCollector = () => {
let stdout = "";
let stderr = "";
const stdoutStream = new PassThrough();
const stderrStream = new PassThrough();
stdoutStream.on("data", (chunk) => {
stdout += chunk.toString();
});
stderrStream.on("data", (chunk) => {
stderr += chunk.toString();
});
return {
getStdout: () => stdout,
getStderr: () => stderr,
stdoutStream,
stderrStream,
};
};
export const uploadFileToContainer = async (
containerId: string,
sourceCode: string,
fileName: string,
filePath: string
) => {
const container = docker.getContainer(containerId);
// Ensure target directory exists before archive upload.
const mkdirExec = await container.exec({
Cmd: ["mkdir", "-p", filePath],
AttachStdout: false,
AttachStderr: false,
});
await mkdirExec.start({ hijack: false, stdin: false, Detach: false, Tty: false });
const pack = tar.pack();
pack.entry({ name: fileName }, sourceCode);
pack.finalize();
await container.putArchive(pack as unknown as NodeJS.ReadableStream, {
path: filePath,
});
};
export const deleteContainer = async (containerId: string) => {
const container = docker.getContainer(containerId);
await container.remove({ force: true });
};
export const prepareContainer = async (
image: string,
tag: string,
memoryLimit: number,
workingDir: string
) => {
const imageWithTag = `${image}:${tag}`;
const stream = await docker.pull(imageWithTag);
await new Promise((resolve, reject) => {
docker.modem.followProgress(stream, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
const container = await docker.createContainer({
Image: imageWithTag,
Cmd: ["tail", "-f", "/dev/null"],
WorkingDir: workingDir,
HostConfig: {
CpuCount: 1,
Memory: memoryLimit,
MemorySwap: 0,
NetworkMode: "none",
},
});
await container.start();
return container.id;
};
export const prepareCompileExec = async (containerId: string, cmd: string[]) => {
const container = docker.getContainer(containerId);
const compileExec = await container.exec({
Cmd: cmd,
AttachStdin: false,
AttachStdout: true,
AttachStderr: true,
});
return compileExec.id;
};
export const prepareRunExec = async (containerId: string, cmd: string[]) => {
const container = docker.getContainer(containerId);
const runExec = await container.exec({
Cmd: cmd,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
});
return runExec.id;
};
export const compile = async (compileExecId: string) => {
const compileExec = docker.getExec(compileExecId);
const stream = await compileExec.start({
hijack: false,
stdin: false,
Detach: false,
Tty: false,
});
const { getStdout, getStderr, stdoutStream, stderrStream } =
createStreamCollector();
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
return new Promise<{
exitCode: number | null;
stdout: string;
stderr: string;
}>((resolve) => {
stream.on("end", async () => {
const exitCode = (await compileExec.inspect()).ExitCode;
resolve({
exitCode,
stdout: getStdout(),
stderr: getStderr(),
});
});
});
};
export const run = async (
runExecId: string,
stdin: string | null,
timeLimit: number
) => {
const runExec = docker.getExec(runExecId);
const startTime = Date.now();
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort();
}, timeLimit);
const stream = await runExec.start({
hijack: stdin ? true : false,
stdin: stdin ? true : false,
Detach: false,
Tty: false,
abortSignal: abortController.signal,
});
if (stdin) {
stream.write(stdin);
stream.end();
}
const { getStdout, getStderr, stdoutStream, stderrStream } =
createStreamCollector();
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
return new Promise<{
exitCode: number | null;
stdout: string;
stderr: string;
timeUsage: number;
isTimeout: boolean;
}>((resolve) => {
stream.on("end", async () => {
clearTimeout(timeoutId);
const endTime = Date.now();
const timeUsage = endTime - startTime;
const exitCode = (await runExec.inspect()).ExitCode;
resolve({
exitCode,
stdout: getStdout(),
stderr: getStderr(),
timeUsage,
isTimeout: abortController.signal.aborted,
});
});
abortController.signal.addEventListener("abort", () => {
clearTimeout(timeoutId);
const endTime = Date.now();
resolve({
exitCode: null,
stdout: getStdout(),
stderr: getStderr(),
timeUsage: endTime - startTime,
isTimeout: true,
});
});
});
};