mirror of
https://github.com/massbug/judge4c.git
synced 2026-05-20 13:18:52 +00:00
212 lines
4.9 KiB
TypeScript
212 lines
4.9 KiB
TypeScript
|
|
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,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
};
|