mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 09:38:19 +08:00
core: handle hack rejudge
This commit is contained in:
1 parent
ce8627bce8
commit
f87c320e42
7 files changed
+143
-46
No files matched your search
@@ -72,7 +72,6 @@ export const runFlow = async (ctx: Context, task: Task) => {
|
||||
ctx.next({ status: STATUS.STATUS_COMPILING });
|
||||
await task.compile();
|
||||
ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 });
|
||||
const tasks = [];
|
||||
ctx.total_status = 0;
|
||||
ctx.total_score = 0;
|
||||
ctx.total_memory = 0;
|
||||
@@ -80,21 +79,38 @@ export const runFlow = async (ctx: Context, task: Task) => {
|
||||
ctx.rerun = getConfig('rerun') || 0;
|
||||
ctx.queue = new Queue({ concurrency: getConfig('singleTaskParallelism') });
|
||||
ctx.failed = {};
|
||||
for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid, task.judgeCase)(ctx));
|
||||
const scores = await Promise.all(tasks);
|
||||
for (const sid in ctx.config.subtasks) {
|
||||
let effective = true;
|
||||
for (const required of ctx.config.subtasks[sid].if || []) {
|
||||
if (ctx.failed[required.toString()]) effective = false;
|
||||
if (ctx.meta.hackRejudge) {
|
||||
const subtask = ctx.config.subtasks.find((i) => i.cases.find((j) => j.input === ctx.meta.hackRejudge));
|
||||
const ctxSubtask = {
|
||||
subtask,
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
score: subtask.type === 'min' ? subtask.score : 0,
|
||||
};
|
||||
const runner = task.judgeCase(subtask.cases.find((i) => i.input === ctx.meta.hackRejudge));
|
||||
const res = await runner(ctx, ctxSubtask, runner);
|
||||
if (res) ctx.next({ case: res });
|
||||
ctx.end({
|
||||
status: ctxSubtask.status === STATUS.STATUS_ACCEPTED ? STATUS.STATUS_ACCEPTED : STATUS.STATUS_HACKED,
|
||||
score: 97,
|
||||
});
|
||||
} else {
|
||||
const tasks = [];
|
||||
for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid, task.judgeCase)(ctx));
|
||||
const scores = await Promise.all(tasks);
|
||||
for (const sid in ctx.config.subtasks) {
|
||||
let effective = true;
|
||||
for (const required of ctx.config.subtasks[sid].if || []) {
|
||||
if (ctx.failed[required.toString()]) effective = false;
|
||||
}
|
||||
if (effective) ctx.total_score += scores[sid];
|
||||
}
|
||||
if (effective) ctx.total_score += scores[sid];
|
||||
ctx.end({
|
||||
status: ctx.total_status,
|
||||
score: ctx.total_score,
|
||||
time: Math.floor(ctx.total_time * 1000000) / 1000000,
|
||||
memory: ctx.total_memory,
|
||||
});
|
||||
}
|
||||
ctx.stat.done = new Date();
|
||||
if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) });
|
||||
ctx.end({
|
||||
status: ctx.total_status,
|
||||
score: ctx.total_score,
|
||||
time: Math.floor(ctx.total_time * 1000000) / 1000000,
|
||||
memory: ctx.total_memory,
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,12 @@
|
||||
import assert from 'assert';
|
||||
import yaml from 'js-yaml';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { JudgeResultBody, RecordDoc, TestCase } from '../interface';
|
||||
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';
|
||||
@@ -18,28 +23,6 @@ import { sleep } from '../utils';
|
||||
|
||||
const logger = new Logger('judge');
|
||||
|
||||
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 pdoc = (accept && updated)
|
||||
? await problem.inc(rdoc.domainId, rdoc.pid, 'nAccept', 1)
|
||||
: await problem.get(rdoc.domainId, rdoc.pid);
|
||||
if (pdoc) {
|
||||
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${rdoc.score}`, 1),
|
||||
]);
|
||||
}
|
||||
await bus.serial('record/judge', rdoc, updated);
|
||||
}
|
||||
|
||||
export async function next(body: Partial<JudgeResultBody>) {
|
||||
body.rid = new ObjectID(body.rid);
|
||||
let rdoc = await record.get(body.rid);
|
||||
@@ -74,6 +57,58 @@ export async function next(body: Partial<JudgeResultBody>) {
|
||||
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 pdoc = (accept && updated)
|
||||
? await problem.inc(rdoc.domainId, rdoc.pid, 'nAccept', 1)
|
||||
: await problem.get(rdoc.domainId, rdoc.pid, undefined, true);
|
||||
if (pdoc) {
|
||||
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${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}`);
|
||||
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(this.user._id, -rdocs.length * 5 - 50);
|
||||
await Promise.all(rdocs.map(
|
||||
(r) => record.judge(r.domainId, r._id, priority, r.contest ? { detail: false } : {}, { 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.serial('record/judge', rdoc, updated);
|
||||
}
|
||||
|
||||
export async function end(body: Partial<JudgeResultBody>) {
|
||||
if (body.rid) body.rid = new ObjectID(body.rid);
|
||||
let rdoc = await record.get(body.rid);
|
||||
@@ -90,9 +125,9 @@ export async function end(body: Partial<JudgeResultBody>) {
|
||||
$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 (Number.isSafeInteger(body.score)) $set.score = body.score;
|
||||
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
|
||||
|
||||
@@ -555,7 +555,7 @@ export class ProblemHackHandler extends ProblemDetailHandler {
|
||||
if (!contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(this.tdoc.docId);
|
||||
}
|
||||
if (this.rdoc.uid === this.user._id) throw new BadRequestError('You cannot hack your own submission');
|
||||
if (this.psdoc.status !== STATUS.STATUS_ACCEPTED) throw new ForbiddenError('You must accept this problem before hacking.');
|
||||
if (this.psdoc?.status !== STATUS.STATUS_ACCEPTED) throw new ForbiddenError('You must accept this problem before hacking.');
|
||||
if (this.rdoc.status !== STATUS.STATUS_ACCEPTED) throw new ForbiddenError('You cannot hack a unsuccessful submission.');
|
||||
}
|
||||
|
||||
@@ -575,7 +575,6 @@ export class ProblemHackHandler extends ProblemDetailHandler {
|
||||
await this.limitRate('add_record', 60, system.get('limit.submission_user'), true);
|
||||
await this.limitRate('add_record', 60, system.get('limit.submission'));
|
||||
const id = `${this.user._id}/${nanoid()}`;
|
||||
console.log(input);
|
||||
if (this.request.files?.file?.size > 0) {
|
||||
const file = this.request.files.file;
|
||||
if (!file || file.size > 128 * 1024 * 1024) throw new ValidationError('input');
|
||||
|
||||
@@ -181,7 +181,7 @@ export interface ProblemConfig {
|
||||
type: string;
|
||||
subType?: string;
|
||||
target?: string;
|
||||
hackable?:boolean;
|
||||
hackable?: boolean;
|
||||
}
|
||||
|
||||
export interface PlainContentNode {
|
||||
@@ -304,6 +304,7 @@ export interface RecordDoc {
|
||||
|
||||
export interface JudgeMeta {
|
||||
problemOwner: number;
|
||||
hackRejudge?: string;
|
||||
}
|
||||
|
||||
export interface JudgeRequest extends Omit<RecordDoc, '_id' | 'testCases'> {
|
||||
|
||||
@@ -62,12 +62,12 @@ class RecordModel {
|
||||
};
|
||||
}
|
||||
|
||||
static async judge(domainId: string, rid: ObjectID, priority = 0, config: ProblemConfigFile = {}) {
|
||||
static async judge(domainId: string, rid: ObjectID, priority = 0, config: ProblemConfigFile = {}, meta: Partial<JudgeMeta> = {}) {
|
||||
const rdoc = await RecordModel.get(domainId, rid);
|
||||
if (!rdoc) return null;
|
||||
let data: FileInfo[] = [];
|
||||
let source = `${domainId}/${rdoc.pid}`;
|
||||
const meta: JudgeMeta = { problemOwner: 1 };
|
||||
meta = { ...meta, problemOwner: 1 };
|
||||
if (rdoc.pid) {
|
||||
let pdoc = await problem.get(rdoc.domainId, rdoc.pid);
|
||||
if (!pdoc) throw new ProblemNotFoundError(rdoc.domainId, rdoc.pid);
|
||||
|
||||
@@ -90,6 +90,29 @@ export class StorageModel {
|
||||
);
|
||||
return await storage.signDownloadLink(res.value?._id || target, filename, noExpire, useAlternativeEndpointFor);
|
||||
}
|
||||
|
||||
static async copy(src: string, dst: string) {
|
||||
const { value } = await StorageModel.coll.findOneAndUpdate(
|
||||
{ path: src, autoDelete: null },
|
||||
{ $set: { lastUsage: new Date() } },
|
||||
{ returnDocument: 'after' },
|
||||
);
|
||||
const meta = {};
|
||||
await StorageModel.del([dst]);
|
||||
meta['Content-Type'] = (dst.endsWith('.ans') || dst.endsWith('.out'))
|
||||
? 'text/plain'
|
||||
: lookup(dst) || 'application/octet-stream';
|
||||
let _id = `${nanoid(3)}/${nanoid()}${extname(dst)}`;
|
||||
// Make sure id is not used
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
while (await StorageModel.coll.findOne({ _id })) _id = `${nanoid(3)}/${nanoid()}${extname(dst)}`;
|
||||
const result = await storage.copy(value._id, dst);
|
||||
const { metaData, size, etag } = await storage.getMeta(_id);
|
||||
await StorageModel.coll.insertOne({
|
||||
_id, meta: metaData, path: dst, size, etag, lastModified: new Date(), owner: value.owner || 1,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanFiles() {
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
remove, stat, writeFile,
|
||||
} from 'fs-extra';
|
||||
import { lookup } from 'mime-types';
|
||||
import { BucketItem, Client, ItemBucketMetadata } from 'minio';
|
||||
import {
|
||||
BucketItem, Client, CopyConditions, ItemBucketMetadata,
|
||||
} from 'minio';
|
||||
import { Logger } from '../logger';
|
||||
import { builtinConfig } from '../settings';
|
||||
import { MaybeArray } from '../typeutils';
|
||||
@@ -237,6 +239,18 @@ class RemoteStorageService {
|
||||
extraFormData: policyResult.formData,
|
||||
};
|
||||
}
|
||||
|
||||
async copy(src: string, target: string) {
|
||||
if (src.includes('..') || src.includes('//')) throw new Error('Invalid path');
|
||||
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||
try {
|
||||
const result = await this.client.copyObject(this.opts.bucket, target, src, new CopyConditions());
|
||||
return result;
|
||||
} catch (e) {
|
||||
e.stack = new Error().stack;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LocalStorageService {
|
||||
@@ -314,6 +328,15 @@ class LocalStorageService {
|
||||
async list() {
|
||||
throw new Error('deprecated');
|
||||
}
|
||||
|
||||
async copy(src: string, target: string) {
|
||||
if (src.includes('..') || src.includes('//')) throw new Error('Invalid path');
|
||||
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||
src = resolve(this.dir, src);
|
||||
target = resolve(this.dir, target);
|
||||
await copyFile(src, target);
|
||||
return { etag: target, lastModified: new Date() };
|
||||
}
|
||||
}
|
||||
|
||||
let service; // eslint-disable-line import/no-mutable-exports
|
||||
|
||||
Reference in new issue
Block a user