mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-05-18 15:26:36 +00:00
feat: add TestcaseDataConfig and prototype randomjudge.ts
This commit is contained in:
parent
2c7223a323
commit
16ec2d3730
7
bun.lock
7
bun.lock
@ -51,6 +51,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"normalize-url": "^8.0.1",
|
||||
"pino": "^9.6.0",
|
||||
"randexp": "^0.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
@ -777,6 +778,8 @@
|
||||
|
||||
"domutils": ["domutils@3.2.2", "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"drange": ["drange@1.1.1", "", {}, "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
@ -1441,6 +1444,8 @@
|
||||
|
||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||
|
||||
"randexp": ["randexp@0.5.3", "", { "dependencies": { "drange": "^1.0.2", "ret": "^0.2.0" } }, "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w=="],
|
||||
|
||||
"react": ["react@19.0.0", "https://registry.npmmirror.com/react/-/react-19.0.0.tgz", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.0.0", "https://registry.npmmirror.com/react-dom/-/react-dom-19.0.0.tgz", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
|
||||
@ -1519,6 +1524,8 @@
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"ret": ["ret@0.2.2", "", {}, "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rimraf": ["rimraf@3.0.2", "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
|
@ -60,6 +60,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"normalize-url": "^8.0.1",
|
||||
"pino": "^9.6.0",
|
||||
"randexp": "^0.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
@ -105,4 +106,4 @@
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,6 +163,8 @@ model TestcaseData {
|
||||
value String
|
||||
testcaseId String
|
||||
testcase Testcase @relation(fields: [testcaseId], references: [id], onDelete: Cascade)
|
||||
|
||||
TestcaseDataConfig TestcaseDataConfig[]
|
||||
}
|
||||
|
||||
model TestcaseResult {
|
||||
@ -236,3 +238,17 @@ model Authenticator {
|
||||
|
||||
@@id([userId, credentialID])
|
||||
}
|
||||
|
||||
// schema.prisma
|
||||
model TestcaseDataConfig {
|
||||
id String @id @default(cuid())
|
||||
testcaseDataId String @unique
|
||||
testcaseData TestcaseData @relation(fields: [testcaseDataId], references: [id])
|
||||
type String // 存储INT/FLOAT/STRING/BOOLEAN
|
||||
min Int? // 数值最小值
|
||||
max Int? // 数值最大值
|
||||
length Int? // 字符串长度
|
||||
pattern String? // 正则表达式模式
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
411
src/actions/randomjudge.ts
Normal file
411
src/actions/randomjudge.ts
Normal file
@ -0,0 +1,411 @@
|
||||
"use server";
|
||||
|
||||
import fs from "fs";
|
||||
import tar from "tar-stream";
|
||||
import Docker from "dockerode";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Readable, Writable } from "stream";
|
||||
import { Status } from "@/generated/client";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import type { EditorLanguage, Submission, TestcaseResult } from "@/generated/client";
|
||||
import RandExp from "randexp";
|
||||
|
||||
const isRemote = process.env.DOCKER_HOST_MODE === "remote";
|
||||
const docker = isRemote
|
||||
? new Docker({
|
||||
protocol: process.env.DOCKER_REMOTE_PROTOCOL as "https" | "http" | "ssh" | undefined,
|
||||
host: process.env.DOCKER_REMOTE_HOST,
|
||||
port: process.env.DOCKER_REMOTE_PORT,
|
||||
ca: fs.readFileSync(process.env.DOCKER_REMOTE_CA_PATH || "/certs/ca.pem"),
|
||||
cert: fs.readFileSync(process.env.DOCKER_REMOTE_CERT_PATH || "/certs/cert.pem"),
|
||||
key: fs.readFileSync(process.env.DOCKER_REMOTE_KEY_PATH || "/certs/key.pem"),
|
||||
})
|
||||
: new Docker({ socketPath: "/var/run/docker.sock" });
|
||||
|
||||
async function prepareEnvironment(image: string, tag: string): Promise<boolean> {
|
||||
try {
|
||||
const reference = `${image}:${tag}`;
|
||||
const filters = { reference: [reference] };
|
||||
const images = await docker.listImages({ filters });
|
||||
return images.length > 0;
|
||||
} catch (error) {
|
||||
console.error("Error checking Docker images:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createContainer(
|
||||
image: string,
|
||||
tag: string,
|
||||
workingDir: string,
|
||||
memoryLimit?: number
|
||||
) {
|
||||
const container = await docker.createContainer({
|
||||
Image: `${image}:${tag}`,
|
||||
Cmd: ["tail", "-f", "/dev/null"],
|
||||
WorkingDir: workingDir,
|
||||
HostConfig: {
|
||||
Memory: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
|
||||
MemorySwap: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
|
||||
},
|
||||
NetworkDisabled: true,
|
||||
});
|
||||
await container.start();
|
||||
return container;
|
||||
}
|
||||
|
||||
function createTarStream(file: string, value: string) {
|
||||
const pack = tar.pack();
|
||||
pack.entry({ name: file }, value);
|
||||
pack.finalize();
|
||||
return Readable.from(pack);
|
||||
}
|
||||
|
||||
// Generate random input cases based on TestcaseDataConfig
|
||||
async function generateRandomCases(problemId: string, count: number) {
|
||||
const configs = await prisma.testcaseDataConfig.findMany({
|
||||
where: { testcaseData: { testcase: { problemId } } },
|
||||
include: { testcaseData: true },
|
||||
});
|
||||
const grouped: Record<number, typeof configs> = {};
|
||||
configs.forEach(cfg => {
|
||||
const idx = cfg.testcaseData.index;
|
||||
grouped[idx] = grouped[idx] || [];
|
||||
grouped[idx].push(cfg);
|
||||
});
|
||||
const cases: { id: string; data: { index: number; value: string }[] }[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const inputs: { index: number; value: string }[] = [];
|
||||
Object.keys(grouped)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.forEach(idx => {
|
||||
grouped[idx].forEach(cfg => {
|
||||
let value: string;
|
||||
switch (cfg.type.toUpperCase()) {
|
||||
case "INT":
|
||||
value = String(
|
||||
Math.floor(
|
||||
Math.random() * ((cfg.max ?? 100) - (cfg.min ?? 0) + 1)
|
||||
) + (cfg.min ?? 0)
|
||||
);
|
||||
break;
|
||||
case "FLOAT": {
|
||||
const lo = cfg.min ?? 0;
|
||||
const hi = cfg.max ?? lo + 1;
|
||||
value = (Math.random() * (hi - lo) + lo).toFixed(3);
|
||||
break;
|
||||
}
|
||||
case "BOOLEAN":
|
||||
value = String(Math.random() < 0.5);
|
||||
break;
|
||||
case "STRING":
|
||||
if (cfg.pattern) value = new RandExp(cfg.pattern).gen();
|
||||
else {
|
||||
const len = cfg.length ?? 10;
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
value = Array.from({ length: len })
|
||||
.map(() =>
|
||||
chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
value = '';
|
||||
}
|
||||
inputs.push({ index: idx, value });
|
||||
});
|
||||
});
|
||||
cases.push({ id: String(i), data: inputs });
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
export async function judge(
|
||||
language: EditorLanguage,
|
||||
code: string,
|
||||
problemId: string,
|
||||
randomCount: number = 10
|
||||
): Promise<Submission> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) redirect("/sign-in");
|
||||
const userId = session.user.id;
|
||||
|
||||
let container: Docker.Container | null = null;
|
||||
let submission: Submission | null = null;
|
||||
try {
|
||||
const problem = await prisma.problem.findUnique({ where: { id: problemId } });
|
||||
if (!problem) {
|
||||
return prisma.submission.create({
|
||||
data: { language, code, status: Status.SE, userId, problemId, message: "Problem not found" },
|
||||
});
|
||||
}
|
||||
const config = await prisma.editorLanguageConfig.findUnique({
|
||||
where: { language },
|
||||
include: { dockerConfig: true },
|
||||
});
|
||||
if (!config?.dockerConfig) {
|
||||
return prisma.submission.create({
|
||||
data: { language, code, status: Status.SE, userId, problemId, message: "Missing docker configuration" },
|
||||
});
|
||||
}
|
||||
const { image, tag, workingDir, compileOutputLimit, runOutputLimit } = config.dockerConfig;
|
||||
const { fileName, fileExtension } = config;
|
||||
const file = `${fileName}.${fileExtension}`;
|
||||
|
||||
if (!(await prepareEnvironment(image, tag))) {
|
||||
return prisma.submission.create({
|
||||
data: { language, code, status: Status.SE, userId, problemId, message: "Docker environment not ready" },
|
||||
});
|
||||
}
|
||||
container = await createContainer(image, tag, workingDir, problem.memoryLimit);
|
||||
submission = await prisma.submission.create({
|
||||
data: { language, code, status: Status.PD, userId, problemId, message: "" },
|
||||
});
|
||||
|
||||
// Upload code
|
||||
const tarStream = createTarStream(file, code);
|
||||
await container.putArchive(tarStream, { path: workingDir });
|
||||
|
||||
// Compile
|
||||
const compileResult = await compile(
|
||||
container,
|
||||
file,
|
||||
fileName,
|
||||
compileOutputLimit,
|
||||
submission.id,
|
||||
language
|
||||
);
|
||||
if (compileResult.status === Status.CE) return compileResult;
|
||||
|
||||
// Generate random test cases
|
||||
const testcases = await generateRandomCases(problemId, randomCount);
|
||||
|
||||
// Run
|
||||
const runResult = await run(
|
||||
container,
|
||||
fileName,
|
||||
problem.timeLimit,
|
||||
runOutputLimit,
|
||||
submission.id,
|
||||
testcases
|
||||
);
|
||||
return runResult;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (submission) {
|
||||
return prisma.submission.update({
|
||||
where: { id: submission.id },
|
||||
data: { status: Status.SE, message: "System Error" },
|
||||
});
|
||||
}
|
||||
return prisma.submission.create({
|
||||
data: { language, code, status: Status.SE, userId, problemId, message: "System Error" },
|
||||
});
|
||||
} finally {
|
||||
revalidatePath(`/problems/${problemId}`);
|
||||
if (container) {
|
||||
try {
|
||||
await container.kill();
|
||||
await container.remove();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function compile(
|
||||
container: Docker.Container,
|
||||
file: string,
|
||||
fileName: string,
|
||||
compileOutputLimit: number,
|
||||
submissionId: string,
|
||||
language: EditorLanguage
|
||||
): Promise<Submission> {
|
||||
const compileCmd =
|
||||
language === "c"
|
||||
? ["gcc", "-O2", file, "-o", fileName]
|
||||
: language === "cpp"
|
||||
? ["g++", "-O2", file, "-o", fileName]
|
||||
: null;
|
||||
if (!compileCmd) {
|
||||
return prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: { status: Status.SE, message: "Unsupported language" },
|
||||
});
|
||||
}
|
||||
|
||||
const exec = await container.exec({
|
||||
Cmd: compileCmd,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
|
||||
return new Promise<Submission>((resolve, reject) => {
|
||||
exec.start({}, (err, stream) => {
|
||||
if (err || !stream) return reject(err || "No stream");
|
||||
|
||||
const stdoutChunks: string[] = [];
|
||||
const stderrChunks: string[] = [];
|
||||
let stdoutLen = 0;
|
||||
let stderrLen = 0;
|
||||
|
||||
const out = new Writable({
|
||||
write(chunk, _, cb) {
|
||||
let txt = chunk.toString();
|
||||
if (stdoutLen + txt.length > compileOutputLimit) {
|
||||
txt = txt.slice(0, compileOutputLimit - stdoutLen);
|
||||
}
|
||||
stdoutLen += txt.length;
|
||||
stdoutChunks.push(txt);
|
||||
cb();
|
||||
},
|
||||
});
|
||||
const errOut = new Writable({
|
||||
write(chunk, _, cb) {
|
||||
let txt = chunk.toString();
|
||||
if (stderrLen + txt.length > compileOutputLimit) {
|
||||
txt = txt.slice(0, compileOutputLimit - stderrLen);
|
||||
}
|
||||
stderrLen += txt.length;
|
||||
stderrChunks.push(txt);
|
||||
cb();
|
||||
},
|
||||
});
|
||||
|
||||
docker.modem.demuxStream(stream, out, errOut);
|
||||
stream.on("end", async () => {
|
||||
const { ExitCode } = await exec.inspect();
|
||||
const stdout = stdoutChunks.join("");
|
||||
const stderr = stderrChunks.join("");
|
||||
if (ExitCode !== 0 || stderr) {
|
||||
resolve(
|
||||
prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: { status: Status.CE, message: stderr || stdout },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
resolve(
|
||||
prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: { status: Status.CS, message: stdout },
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function run(
|
||||
container: Docker.Container,
|
||||
fileName: string,
|
||||
timeLimit: number,
|
||||
maxOutput: number,
|
||||
submissionId: string,
|
||||
testcases: { id: string; data: { index: number; value: string }[] }[]
|
||||
): Promise<Submission> {
|
||||
let maxTime = 0;
|
||||
for (const tc of testcases) {
|
||||
const input = tc.data.map(d => d.value).join("\n");
|
||||
const exec = await container.exec({
|
||||
Cmd: [`./${fileName}`],
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
});
|
||||
const result = await new Promise<Submission>((resolve) => {
|
||||
exec.start({ hijack: true }, (err, stream) => {
|
||||
if (err || !stream) {
|
||||
return resolve(
|
||||
prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: { status: Status.SE, message: "System Error" },
|
||||
})
|
||||
);
|
||||
}
|
||||
const stdoutChunks: string[] = [];
|
||||
const stderrChunks: string[] = [];
|
||||
let outLen = 0;
|
||||
let errLen = 0;
|
||||
const out = new Writable({ write(chunk, _, cb) {
|
||||
const txt = chunk.toString();
|
||||
if (outLen + txt.length > maxOutput) {
|
||||
stdoutChunks.push(txt.slice(0, maxOutput - outLen));
|
||||
outLen = maxOutput;
|
||||
} else {
|
||||
stdoutChunks.push(txt);
|
||||
outLen += txt.length;
|
||||
}
|
||||
cb();
|
||||
}});
|
||||
const errOut = new Writable({ write(chunk, _, cb) {
|
||||
const txt = chunk.toString();
|
||||
if (errLen + txt.length > maxOutput) {
|
||||
stderrChunks.push(txt.slice(0, maxOutput - errLen));
|
||||
errLen = maxOutput;
|
||||
} else {
|
||||
stderrChunks.push(txt);
|
||||
errLen += txt.length;
|
||||
}
|
||||
cb();
|
||||
}});
|
||||
docker.modem.demuxStream(stream, out, errOut);
|
||||
|
||||
stream.write(input);
|
||||
stream.end();
|
||||
const start = Date.now();
|
||||
const timeout = setTimeout(async () => {
|
||||
stream.destroy();
|
||||
resolve(
|
||||
prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: { status: Status.TLE, message: "Time Limit Exceeded" },
|
||||
})
|
||||
);
|
||||
}, timeLimit);
|
||||
|
||||
stream.on("end", async () => {
|
||||
clearTimeout(timeout);
|
||||
const execInfo = await exec.inspect();
|
||||
const elapsed = Date.now() - start;
|
||||
const stdout = stdoutChunks.join("");
|
||||
const stderr = stderrChunks.join("");
|
||||
switch (execInfo.ExitCode) {
|
||||
case 0:
|
||||
maxTime = Math.max(maxTime, elapsed);
|
||||
break;
|
||||
case 137:
|
||||
return resolve(
|
||||
prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: { status: Status.MLE, message: stderr || "Memory Limit Exceeded" },
|
||||
})
|
||||
);
|
||||
default:
|
||||
return resolve(
|
||||
prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: { status: Status.RE, message: stderr || stdout },
|
||||
})
|
||||
);
|
||||
}
|
||||
resolve(
|
||||
prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
if (result.status !== Status.CS) return result;
|
||||
}
|
||||
return prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: { status: Status.AC, message: "Random tests passed", executionTime: maxTime },
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user