mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
244 lines
9.6 KiB
TypeScript
244 lines
9.6 KiB
TypeScript
import assert from 'assert';
|
|
import yaml from 'js-yaml';
|
|
import { omit } from 'lodash';
|
|
import { ObjectID } from 'mongodb';
|
|
import {
|
|
JudgeResultBody, ProblemConfigFile, RecordDoc, TestCase,
|
|
} from '../interface';
|
|
import { Logger } from '../logger';
|
|
import * as builtin from '../model/builtin';
|
|
import { STATUS } from '../model/builtin';
|
|
import * as contest from '../model/contest';
|
|
import domain from '../model/domain';
|
|
import problem from '../model/problem';
|
|
import record from '../model/record';
|
|
import * as setting from '../model/setting';
|
|
import storage from '../model/storage';
|
|
import task from '../model/task';
|
|
import * as bus from '../service/bus';
|
|
import { updateJudge } from '../service/monitor';
|
|
import {
|
|
ConnectionHandler, Handler, post, subscribe, Types,
|
|
} from '../service/server';
|
|
import { sleep } from '../utils';
|
|
|
|
const logger = new Logger('judge');
|
|
|
|
export async function next(body: Partial<JudgeResultBody>) {
|
|
body.rid = new ObjectID(body.rid);
|
|
let rdoc = await record.get(body.rid);
|
|
if (!rdoc) return;
|
|
const $set: Partial<RecordDoc> = {};
|
|
const $push: any = {};
|
|
if (body.case) {
|
|
const c: Required<TestCase> = {
|
|
...body.case,
|
|
id: body.case.id || 0,
|
|
subtaskId: body.case.subtaskId || 0,
|
|
score: body.case.score || 0,
|
|
message: body.case.message || '',
|
|
};
|
|
rdoc.testCases.push(c);
|
|
$push.testCases = c;
|
|
}
|
|
if (body.message) {
|
|
rdoc.judgeTexts.push(body.message);
|
|
$push.judgeTexts = body.message;
|
|
}
|
|
if (body.compilerText) {
|
|
rdoc.compilerTexts.push(body.compilerText);
|
|
$push.compilerTexts = body.compilerText;
|
|
}
|
|
if (body.status) $set.status = body.status;
|
|
if (body.score !== undefined) $set.score = body.score;
|
|
if (body.time !== undefined) $set.time = body.time;
|
|
if (body.memory !== undefined) $set.memory = body.memory;
|
|
if (body.progress !== undefined) $set.progress = body.progress;
|
|
rdoc = await record.update(rdoc.domainId, body.rid, $set, $push, {}, body.addProgress ? { progress: body.addProgress } : {});
|
|
bus.broadcast('record/change', rdoc!, $set, $push);
|
|
}
|
|
|
|
export async function postJudge(rdoc: RecordDoc) {
|
|
if (typeof rdoc.input === 'string') return;
|
|
const accept = rdoc.status === builtin.STATUS.STATUS_ACCEPTED;
|
|
const updated = await problem.updateStatus(rdoc.domainId, rdoc.pid, rdoc.uid, rdoc._id, rdoc.status, rdoc.score);
|
|
if (rdoc.contest) {
|
|
await contest.updateStatus(
|
|
rdoc.domainId, rdoc.contest, rdoc.uid, rdoc._id,
|
|
rdoc.pid, rdoc.status, rdoc.score,
|
|
);
|
|
} else if (accept && updated) await domain.incUserInDomain(rdoc.domainId, rdoc.uid, 'nAccept', 1);
|
|
const isNormalSubmission = ![
|
|
STATUS.STATUS_ETC, STATUS.STATUS_HACK_SUCCESSFUL, STATUS.STATUS_HACK_UNSUCCESSFUL,
|
|
STATUS.STATUS_FORMAT_ERROR, STATUS.STATUS_SYSTEM_ERROR, STATUS.STATUS_CANCELED,
|
|
].includes(rdoc.status);
|
|
const pdoc = (accept && updated)
|
|
? await problem.inc(rdoc.domainId, rdoc.pid, 'nAccept', 1)
|
|
: await problem.get(rdoc.domainId, rdoc.pid, undefined, true);
|
|
if (pdoc) {
|
|
if (isNormalSubmission) {
|
|
await Promise.all([
|
|
problem.inc(pdoc.domainId, pdoc.docId, `stats.${builtin.STATUS_SHORT_TEXTS[rdoc.status]}`, 1),
|
|
problem.inc(pdoc.domainId, pdoc.docId, `stats.s${Math.floor(rdoc.score)}`, 1),
|
|
]);
|
|
}
|
|
if (rdoc.status === STATUS.STATUS_HACK_SUCCESSFUL) {
|
|
try {
|
|
const config = yaml.load(pdoc.config as string) as ProblemConfigFile;
|
|
assert(config.subtasks instanceof Array);
|
|
const file = await storage.get(`submission/${rdoc.files.hack.split('#')[0]}`);
|
|
assert(file);
|
|
const hackSubtask = config.subtasks[config.subtasks.length - 1];
|
|
hackSubtask.cases ||= [];
|
|
const input = `hack-${rdoc._id}-${hackSubtask.cases.length + 1}.in`;
|
|
hackSubtask.cases.push({ input, output: '/dev/null' });
|
|
await Promise.all([
|
|
problem.addTestdata(rdoc.domainId, rdoc.pid, input, file),
|
|
problem.addTestdata(rdoc.domainId, rdoc.pid, 'config.yaml', Buffer.from(yaml.dump(config))),
|
|
]);
|
|
// trigger rejudge
|
|
const rdocs = await record.getMulti(rdoc.domainId, {
|
|
pid: rdoc.pid,
|
|
status: STATUS.STATUS_ACCEPTED,
|
|
contest: { $ne: new ObjectID('0'.repeat(24)) },
|
|
}).project({ _id: 1, contest: 1 }).toArray();
|
|
const priority = await record.submissionPriority(rdoc.uid, -5000 - rdocs.length * 5 - 50);
|
|
await record.judge(rdoc.domainId, rdocs.map((r) => r._id), priority, {}, { hackRejudge: input });
|
|
} catch (e) {
|
|
next({
|
|
rid: rdoc._id, domainId: rdoc.domainId, key: 'next', message: { message: 'Unable to apply hack: {0}', params: [e.message] },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
await bus.parallel('record/judge', rdoc, updated);
|
|
}
|
|
|
|
export async function end(body: Partial<JudgeResultBody>) {
|
|
body.rid &&= new ObjectID(body.rid);
|
|
let rdoc = await record.get(body.rid);
|
|
if (!rdoc) return;
|
|
const $set: Partial<RecordDoc> = {};
|
|
const $push: any = {};
|
|
const $unset: any = { progress: '' };
|
|
if (body.message) {
|
|
rdoc.judgeTexts.push(body.message);
|
|
$push.judgeTexts = body.message;
|
|
}
|
|
if (body.compilerText) {
|
|
rdoc.compilerTexts.push(body.compilerText);
|
|
$push.compilerTexts = body.compilerText;
|
|
}
|
|
if (body.status) $set.status = body.status;
|
|
if (Number.isFinite(body.score)) $set.score = Math.floor(body.score * 100) / 100;
|
|
if (Number.isFinite(body.time)) $set.time = body.time;
|
|
if (Number.isFinite(body.memory)) $set.memory = body.memory;
|
|
$set.judgeAt = new Date();
|
|
$set.judger = body.judger ?? 1;
|
|
await sleep(100); // Make sure that all 'next' event already triggered
|
|
rdoc = await record.update(rdoc.domainId, body.rid, $set, $push, $unset);
|
|
await postJudge(rdoc);
|
|
bus.broadcast('record/change', rdoc); // trigger a full update
|
|
}
|
|
|
|
export class JudgeFilesDownloadHandler extends Handler {
|
|
async get() {
|
|
this.response.body = 'ok';
|
|
}
|
|
|
|
noCheckPermView = true;
|
|
@post('files', Types.Set)
|
|
@post('pid', Types.UnsignedInt)
|
|
async post(domainId: string, files: Set<string>, pid: number) {
|
|
const pdoc = await problem.get(domainId, pid);
|
|
if (!pdoc) this.response.body.links = null;
|
|
const links = {};
|
|
for (const file of files) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
links[file] = await storage.signDownloadLink(
|
|
`problem/${pdoc.domainId}/${pdoc.docId}/testdata/${file}`,
|
|
file, true, 'judge',
|
|
);
|
|
}
|
|
this.response.body.links = links;
|
|
}
|
|
}
|
|
|
|
export class SubmissionDataDownloadHandler extends Handler {
|
|
@post('id', Types.String)
|
|
async post(domainId: string, id: string) {
|
|
this.response.body = { url: await storage.signDownloadLink(`submission/${id}`, 'code', true, 'judge') };
|
|
}
|
|
}
|
|
|
|
class JudgeConnectionHandler extends ConnectionHandler {
|
|
category = '#judge';
|
|
processing: any = null;
|
|
closed = false;
|
|
query: any = { type: 'judge' };
|
|
ip: string;
|
|
|
|
async prepare() {
|
|
logger.info('Judge daemon connected from ', this.request.ip);
|
|
this.sendLanguageConfig();
|
|
// Ensure language sent
|
|
await sleep(100);
|
|
this.newTask();
|
|
}
|
|
|
|
@subscribe('system/setting')
|
|
sendLanguageConfig() {
|
|
this.send({ language: setting.langs });
|
|
}
|
|
|
|
async newTask() {
|
|
if (this.processing) return;
|
|
let t;
|
|
while (!t) {
|
|
if (this.closed) return;
|
|
// eslint-disable-next-line no-await-in-loop
|
|
t = await task.getFirst(this.query);
|
|
// eslint-disable-next-line no-await-in-loop
|
|
if (!t) await sleep(500);
|
|
}
|
|
let rdoc = await record.get(t.domainId, t.rid);
|
|
this.send({ task: { ...rdoc, ...t } });
|
|
this.processing = t;
|
|
const $set = { status: builtin.STATUS.STATUS_FETCHED };
|
|
rdoc = await record.update(t.domainId, t.rid, $set, {});
|
|
bus.broadcast('record/change', rdoc, $set, {});
|
|
}
|
|
|
|
async message(msg) {
|
|
if (msg.key !== 'ping' && msg.key !== 'prio') logger[['status', 'next'].includes(msg.key) ? 'debug' : 'info']('%o', omit(msg, 'key'));
|
|
if (msg.key === 'next') await next(msg);
|
|
else if (msg.key === 'end') {
|
|
if (!msg.nop) await end({ judger: this.user._id, ...msg }).catch((e) => logger.error(e));
|
|
this.processing = null;
|
|
await this.newTask();
|
|
} else if (msg.key === 'status') {
|
|
await updateJudge(msg.info);
|
|
} else if (msg.key === 'prio') {
|
|
this.query.priority = { $gt: msg.prio };
|
|
}
|
|
}
|
|
|
|
async cleanup() {
|
|
logger.info('Judge daemon disconnected from ', this.request.ip);
|
|
if (this.processing) {
|
|
await record.reset(this.processing.domainId, this.processing.rid, false);
|
|
await task.add(this.processing);
|
|
}
|
|
this.closed = true;
|
|
}
|
|
}
|
|
|
|
export async function apply(ctx) {
|
|
ctx.Route('judge_files_download', '/judge/files', JudgeFilesDownloadHandler, builtin.PRIV.PRIV_JUDGE);
|
|
ctx.Route('judge_submission_download', '/judge/code', SubmissionDataDownloadHandler, builtin.PRIV.PRIV_JUDGE);
|
|
ctx.Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE);
|
|
}
|
|
|
|
apply.next = next;
|
|
apply.end = end;
|