From 8b956575eeabadb6fd190877b61f5fdb533fb631 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 9 Sep 2020 13:36:08 +0800 Subject: [PATCH] draft: hack? --- packages/hydrojudge/src/check.ts | 2 +- packages/hydrojudge/src/judge/hack.ts | 132 +++++++++++++++++++++++++ packages/hydrojudge/src/judge/index.ts | 3 +- packages/hydrojudge/src/service.ts | 14 +++ packages/hydrooj/TODO | 4 +- packages/hydrooj/src/handler/record.ts | 46 +++++++++ packages/hydrooj/src/interface.ts | 8 +- packages/hydrooj/src/model/builtin.ts | 3 + packages/hydrooj/src/model/record.ts | 8 +- packages/hydrooj/src/service/server.ts | 3 +- 10 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 packages/hydrojudge/src/judge/hack.ts diff --git a/packages/hydrojudge/src/check.ts b/packages/hydrojudge/src/check.ts index b901e9460..22fcc99f4 100644 --- a/packages/hydrojudge/src/check.ts +++ b/packages/hydrojudge/src/check.ts @@ -24,5 +24,5 @@ export async function compileChecker(checkerType: string, checker: string, copyI if (!checkers[checkerType]) { throw new SystemError(`未知比较器类型:${checkerType}`); } const file = await fs.readFile(checker); // TODO cache compiled checker - await compile(checker.split('.')[1], file.toString(), 'checker', copyIn); + return await compile(checker.split('.')[1], file.toString(), 'checker', copyIn); } diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts new file mode 100644 index 000000000..b324ff2fd --- /dev/null +++ b/packages/hydrojudge/src/judge/hack.ts @@ -0,0 +1,132 @@ +// Not implemented +import path from 'path'; +import fs from 'fs-extra'; +import { STATUS } from 'hydrooj/dist/model/builtin'; +import { CompileError } from '../error'; +import { copyInDir, parseFilename } from '../utils'; +import { run } from '../sandbox'; +import compile from '../compile'; +import signals from '../signals'; +import { check, compileChecker } from '../check'; + +export const judge = async (ctx) => { + if (ctx.config.template) { + if (ctx.config.template[ctx.lang]) { + const tpl = ctx.config.template[ctx.lang]; + ctx.code = tpl[0] + ctx.code + tpl[1]; + } else throw new CompileError('Language not supported by provided templates'); + } + ctx.next({ status: STATUS.STATUS_COMPILING }); + if (!ctx.config.validator || !ctx.config.std) throw new CompileError('config.validator or config.std is missing.'); + const [execute, executeValidator, executeStd, executeChecker] = await Promise.all([ + // UserProgram + (async () => { + const copyIn = {}; + for (const file of ctx.config.user_extra_files) { + copyIn[parseFilename(file)] = { src: file }; + } + return await compile(ctx.lang, ctx.code, 'code', copyIn, ctx.next); + })(), + // Validator + (async () => { + const copyIn = {}; + for (const file of ctx.config.judge_extra_files) { + copyIn[parseFilename(file)] = { src: file }; + } + const file = await fs.readFile(ctx.config.validator); + return await compile(parseFilename(ctx.config.validator).split('.')[1], file.toString(), 'validator', copyIn); + })(), + // Std + (async () => { + const copyIn = {}; + for (const file of ctx.config.user_extra_files) { + copyIn[parseFilename(file)] = { src: file }; + } + const file = await fs.readFile(ctx.config.std); + return await compile(parseFilename(ctx.config.std).split('.')[1], file.toString(), 'std', copyIn); + })(), + // Checker + (async () => { + if (!ctx.config.checker_type || ctx.config.checker_type === 'default') return null; + const copyIn = {}; + for (const file of ctx.config.judge_extra_files) { + copyIn[parseFilename(file)] = { src: file }; + } + return await compileChecker( + ctx.config.checker_type, + ctx.config.checker, + copyIn, + ); + })(), + ]); + ctx.clean.push(execute.clean, executeValidator.clean, executeStd.clean, executeChecker.clean); + ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); + const { filename } = ctx.config; + const input = path.resolve(ctx.tmpdir, 'input'); + await fs.writeFile(input, ctx.config.hack); + if (filename) execute.copyIn[`${filename}.in`] = { src: input }; + const copyOut = filename ? [`${filename}.out`] : []; + const stdin = filename ? null : input; + const stdout = path.resolve(ctx.tmpdir, 'user.out'); + const stderr = path.resolve(ctx.tmpdir, 'user.err'); + const res = await run( + ctx.execute.execute.replace(/\$\{name\}/g, 'code'), + { + stdin, + stdout: filename ? null : stdout, + stderr, + copyIn: execute.copyIn, + copyOut, + time_limit_ms: 1000, + memory_limit_mb: 256, + }, + ); + const { code, time_usage_ms, memory_usage_kb } = res; + let { status } = res; + if (res.files[`${filename}.out`] || !fs.existsSync(stdout)) { + fs.writeFileSync(stdout, res.files[`${filename}.out`] || ''); + } + let message = ''; + if (status === STATUS.STATUS_ACCEPTED) { + if (time_usage_ms > 1000) { + status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; + } else if (memory_usage_kb > 256 * 1024) { + status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; + } else { + [status, , message] = await check({ + copyIn: copyInDir(path.resolve(ctx.tmpdir, 'checker')), + stdin: input, + stdout, + user_stdout: stdout, + user_stderr: stderr, + checker: ctx.config.checker, + checker_type: ctx.config.checker_type, + score: 100, + detail: ctx.config.detail, + }); + } + } else if (status === STATUS.STATUS_RUNTIME_ERROR && code) { + status = STATUS.STATUS_RUNTIME_ERROR; + if (code < 32) message = signals[code]; + else message = `您的程序返回了 ${code}.`; + } + ctx.next({ + status: STATUS.STATUS_JUDGING, + case: { + status, + score: 0, + time_ms: time_usage_ms, + memory_kb: memory_usage_kb, + message, + }, + }); + + ctx.stat.done = new Date(); + ctx.next({ message: JSON.stringify(ctx.stat) }); + ctx.end({ + status, + score: ctx.total_score, + time_ms: Math.floor(ctx.total_time_usage_ms * 1000000) / 1000000, + memory_kb: ctx.total_memory_usage_kb, + }); +}; diff --git a/packages/hydrojudge/src/judge/index.ts b/packages/hydrojudge/src/judge/index.ts index 891813d77..0429e235b 100644 --- a/packages/hydrojudge/src/judge/index.ts +++ b/packages/hydrojudge/src/judge/index.ts @@ -2,7 +2,8 @@ import * as def from './default'; import * as interactive from './interactive'; import * as run from './run'; import * as submit_answer from './submit_answer'; +import * as hack from './hack'; export = { - default: def, interactive, run, submit_answer, + default: def, interactive, run, submit_answer, hack, } diff --git a/packages/hydrojudge/src/service.ts b/packages/hydrojudge/src/service.ts index 7f6b2a12c..bb8a7fade 100644 --- a/packages/hydrojudge/src/service.ts +++ b/packages/hydrojudge/src/service.ts @@ -210,6 +210,7 @@ async function postInit() { tmpfs.mount(this.tmpdir, '64m'); console.log(`Submission: ${this.rid}`, { pid: this.pid }); if (this.config.input) await this.run(); + else if (this.config.hack) await this.hack(); else await this.submission(); } catch (e) { if (e instanceof CompileError) { @@ -249,6 +250,19 @@ async function postInit() { await judge[this.config.type || 'default'].judge(this); } + async hack() { + this.stat.cache_start = new Date(); + this.folder = await cacheOpen(this.domainId, this.pid, this.data); + this.stat.read_cases = new Date(); + this.config = await readYamlCases( + this.folder, + this.config, + { next: this.next }, + ); + this.stat.judge = new Date(); + await judge.hack.judge(this); + } + async run() { this.stat.judge = new Date(); await judge.run.judge(this); diff --git a/packages/hydrooj/TODO b/packages/hydrooj/TODO index 63ed2be53..1406c3b6e 100644 --- a/packages/hydrooj/TODO +++ b/packages/hydrooj/TODO @@ -4,8 +4,8 @@ SETTINGS_PRIVACY.allowRegisteredUsers model.setting.ui.name pdoc.secretConfig? message.markAsRead -基于document的model难以lint,建议拆分 检查多余的索引 +hack 评测队列优先级系统 -讨论浏览数量显示 \ No newline at end of file +讨论浏览数量显示 +解离mail模块 \ No newline at end of file diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index f0e65087f..2ba82fceb 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -2,6 +2,7 @@ import { ObjectID } from 'mongodb'; import { PermissionError, RecordNotFoundError } from '../error'; import { PERM, CONSTANT } from '../model/builtin'; import * as problem from '../model/problem'; +import * as file from '../model/file'; import * as record from '../model/record'; import * as contest from '../model/contest'; import * as user from '../model/user'; @@ -11,6 +12,7 @@ import { Route, Handler, Connection, ConnectionHandler, Types, param, } from '../service/server'; import { Rdoc } from '../interface'; +import { streamToBuffer } from '../utils'; const RecordHandler = contest.ContestHandlerMixin(Handler); @@ -84,8 +86,51 @@ class RecordDetailHandler extends RecordHandler { pdoc, }; } + + @param('rid', Types.ObjectID) + async postRejudge(domainId: string, rid: ObjectID) { + this.checkPerm(PERM.PERM_REJUDGE); + const rdoc = await record.get(domainId, rid); + if (rdoc) { + await record.reset(domainId, rid, true); + await record.judge(domainId, rid); + } + this.back(); + } } +class RecordHackHandler extends Handler { + @param('rid', Types.ObjectID) + @param('ufid', Types.ObjectID) + async get(domainId: string, rid: ObjectID, ufid?: ObjectID) { + if (ufid) { + const stream = await file.get(ufid); + const buf = await streamToBuffer(stream); + const input = buf.toString(); + await this.post(domainId, rid, input); + } else this.response.template = 'record_hack.html'; + } + + @param('rid', Types.ObjectID) + @param('input', Types.String) + async post(domainId: string, rid: ObjectID, input: string) { + const rdoc = await record.get(domainId, rid); + if (!rdoc) throw new RecordNotFoundError(domainId, rid); + const newRid = await record.add( + domainId, rdoc.pid, this.user._id, + rdoc.lang, rdoc.code, true, + { hack: input }, + ); + bus.boardcast('record/change', rdoc); + this.response.body = { rid: newRid }; + this.response.redirect = this.url('record_detail', { rid: newRid }); + // TODO handle hack(modify testdata, rejudge problem) + } +} + +/** + * @deprecated + */ class RecordRejudgeHandler extends Handler { @param('rid', Types.ObjectID) async post(domainId: string, rid: ObjectID) { @@ -183,6 +228,7 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti export async function apply() { Route('record_main', '/record', RecordListHandler); Route('record_detail', '/record/:rid', RecordDetailHandler); + Route('record_hack', '/record/:rid/hack', RecordHackHandler); Route('record_rejudge', '/record/:rid/rejudge', RecordRejudgeHandler); Connection('record_conn', '/record-conn', RecordMainConnectionHandler); Connection('record_detail_conn', '/record-detail-conn', RecordDetailConnectionHandler); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index f5542bc28..a516c4a7c 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -199,12 +199,18 @@ export interface TestCase { message: string, } +export interface HackConfig { + hack: string, +} + export interface PretestConfig { time?: string, memory?: string, input: string, } +export type RunConfig = HackConfig | PretestConfig; + export interface ContestInfo { type: 30 | 60, tid: ObjectID, @@ -229,7 +235,7 @@ export interface Rdoc { status: number, hidden: boolean, progress?: number, - config?: PretestConfig, + config?: RunConfig, contest?: ContestInfo, } diff --git a/packages/hydrooj/src/model/builtin.ts b/packages/hydrooj/src/model/builtin.ts index a1c1b8fd4..ee662d3fc 100644 --- a/packages/hydrooj/src/model/builtin.ts +++ b/packages/hydrooj/src/model/builtin.ts @@ -550,6 +550,7 @@ export const STATUS = { STATUS_SYSTEM_ERROR: 8, STATUS_CANCELED: 9, STATUS_ETC: 10, + STATUS_HACKED: 11, STATUS_JUDGING: 20, STATUS_COMPILING: 21, STATUS_FETCHED: 22, @@ -568,6 +569,7 @@ export const STATUS_TEXTS = { [STATUS.STATUS_SYSTEM_ERROR]: 'System Error', [STATUS.STATUS_CANCELED]: 'Cancelled', [STATUS.STATUS_ETC]: 'Unknown Error', + [STATUS.STATUS_HACKED]: 'Hacked', [STATUS.STATUS_JUDGING]: 'Running', [STATUS.STATUS_COMPILING]: 'Compiling', [STATUS.STATUS_FETCHED]: 'Fetched', @@ -586,6 +588,7 @@ export const STATUS_CODES = { 8: 'fail', 9: 'ignored', 10: 'fail', + 11: 'fail', 20: 'progress', 21: 'progress', 22: 'progress', diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index 9b0501499..3f03119d1 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -6,7 +6,7 @@ import { STATUS } from './builtin'; import * as task from './task'; import * as problem from './problem'; import { - Rdoc, TestCase, PretestConfig, ContestInfo, ProblemConfig, + Rdoc, TestCase, RunConfig, ContestInfo, ProblemConfig, } from '../interface'; import * as db from '../service/db'; @@ -56,7 +56,7 @@ export async function get(domainId: string, _id: ObjectID): Promise export async function judge(domainId: string, rid: ObjectID) { const rdoc = await get(domainId, rid); - let config: ProblemConfig = rdoc.config; + let config: RunConfig | ProblemConfig = rdoc.config; if (!config) { const pdoc = await problem.get(domainId, rdoc.pid); config = pdoc?.config || {}; @@ -73,7 +73,7 @@ export async function judge(domainId: string, rid: ObjectID) { export async function add( domainId: string, pid: number, uid: number, - lang: string, code: string, addTask: boolean, contestOrConfig?: ContestInfo | PretestConfig, + lang: string, code: string, addTask: boolean, contestOrConfig?: ContestInfo | RunConfig, ) { const data: Rdoc = { status: STATUS.STATUS_WAITING, @@ -98,7 +98,7 @@ export async function add( if ((contestOrConfig as ContestInfo).type) { data.contest = contestOrConfig as ContestInfo; } else { - data.config = contestOrConfig as PretestConfig; + data.config = contestOrConfig as RunConfig; } } const res = await coll.insertOne(data); diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 8b04bc581..93408a616 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -170,7 +170,8 @@ export function param(name: string, ...args: any): MethodDecorator { if (!target.__param[target.constructor.name][funcName]) { target.__param[target.constructor.name][funcName] = [{ name: 'domainId', type: 'string' }]; const originalMethod = obj.value; - obj.value = function validate(rawArgs: any) { + obj.value = function validate(rawArgs: any, ...extra: any[]) { + if (typeof rawArgs === 'string' || extra.length) return originalMethod.call(this, rawArgs, ...extra); const c = []; const arglist: ParamOption[] = this.__param[target.constructor.name][funcName]; for (const item of arglist) {