From 6b88570bd6e92a83e76156121bb2629f237eac9d Mon Sep 17 00:00:00 2001 From: undefined Date: Sun, 9 Aug 2020 19:02:03 +0800 Subject: [PATCH] hydrojudge intergration --- package.json | 2 +- packages/hydrojudge/package.json | 31 +++ packages/hydrojudge/service.ts | 256 ++++++++++++++++++ packages/hydrojudge/setting.yaml | 93 +++++++ packages/hydrojudge/src/cases.ts | 229 ++++++++++++++++ packages/hydrojudge/src/check.ts | 28 ++ packages/hydrojudge/src/checkers.ts | 173 ++++++++++++ packages/hydrojudge/src/compile.ts | 32 +++ packages/hydrojudge/src/error.ts | 69 +++++ packages/hydrojudge/src/judge/default.ts | 154 +++++++++++ packages/hydrojudge/src/judge/index.ts | 8 + packages/hydrojudge/src/judge/interactive.ts | 126 +++++++++ packages/hydrojudge/src/judge/run.ts | 62 +++++ .../hydrojudge/src/judge/submit_answer.ts | 25 ++ packages/hydrojudge/src/sandbox.ts | 113 ++++++++ packages/hydrojudge/src/signals.ts | 36 +++ packages/hydrojudge/src/sysinfo.ts | 116 ++++++++ packages/hydrojudge/src/tmpfs.ts | 11 + packages/hydrojudge/src/utils.ts | 117 ++++++++ packages/hydrojudge/tsconfig.json | 24 ++ packages/hydrooj/src/handler/domain.ts | 18 +- packages/hydrooj/src/handler/judge.ts | 2 +- packages/login-with-osu/package.json | 2 +- tsconfig.json | 1 + 24 files changed, 1719 insertions(+), 9 deletions(-) create mode 100644 packages/hydrojudge/package.json create mode 100644 packages/hydrojudge/service.ts create mode 100644 packages/hydrojudge/setting.yaml create mode 100644 packages/hydrojudge/src/cases.ts create mode 100644 packages/hydrojudge/src/check.ts create mode 100644 packages/hydrojudge/src/checkers.ts create mode 100644 packages/hydrojudge/src/compile.ts create mode 100644 packages/hydrojudge/src/error.ts create mode 100644 packages/hydrojudge/src/judge/default.ts create mode 100644 packages/hydrojudge/src/judge/index.ts create mode 100644 packages/hydrojudge/src/judge/interactive.ts create mode 100644 packages/hydrojudge/src/judge/run.ts create mode 100644 packages/hydrojudge/src/judge/submit_answer.ts create mode 100644 packages/hydrojudge/src/sandbox.ts create mode 100644 packages/hydrojudge/src/signals.ts create mode 100644 packages/hydrojudge/src/sysinfo.ts create mode 100644 packages/hydrojudge/src/tmpfs.ts create mode 100644 packages/hydrojudge/src/utils.ts create mode 100644 packages/hydrojudge/tsconfig.json diff --git a/package.json b/package.json index 5c0390786..44f5effda 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint": "eslint packages --ext ts --fix", "pub": "node build/publish", "jest": "node build/jest", - "start": "node --async-stack-traces --trace-deprecation --enable-source-maps node_modules/.bin/hydrooj" + "start": "node --async-stack-traces --trace-deprecation --enable-source-maps build/start" }, "version": "1.0.0", "license": "AGPL-3.0-only", diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json new file mode 100644 index 000000000..d882e845f --- /dev/null +++ b/packages/hydrojudge/package.json @@ -0,0 +1,31 @@ +{ + "name": "@hydrooj/hydrojudge", + "version": "2.0.0", + "main": "package.json", + "author": "masnn", + "os": [ + "linux" + ], + "dependencies": { + "axios": "^0.19.0", + "bson": "^4.0.4", + "fs-extra": "^9.0.1", + "hydrooj": "^2.12.0", + "js-yaml": "^3.14.0", + "lodash": "^4.17.19", + "p-queue": "^6.6.0", + "shell-quote": "^1.7.2", + "systeminformation": "^4.26.10" + }, + "license": "GPL-3.0-only", + "devDependencies": { + "@types/bson": "^4.0.2", + "@types/fs-extra": "^9.0.1", + "@types/js-yaml": "^3.12.5", + "@types/shell-quote": "^1.7.0" + }, + "scripts": { + "build": "tsc", + "lint": "eslint . --ext ts --fix" + } +} \ No newline at end of file diff --git a/packages/hydrojudge/service.ts b/packages/hydrojudge/service.ts new file mode 100644 index 000000000..89cc67639 --- /dev/null +++ b/packages/hydrojudge/service.ts @@ -0,0 +1,256 @@ +// Hydro Integration +/* eslint-disable no-await-in-loop */ +import { } from 'hydrooj'; +import path from 'path'; +import cluster from 'cluster'; +import child from 'child_process'; +import { ObjectID } from 'bson'; +import fs from 'fs-extra'; +import { homedir, tmpdir } from 'os'; + +async function postInit() { + // Only start a single daemon + if (!cluster.isFirstWorker) return; + const { compilerText } = require('./src/utils'); + const tmpfs = require('./src/tmpfs'); + const { FormatError, CompileError, SystemError } = require('./src/error'); + const { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } = global.Hydro.model.builtin.STATUS; + const readYamlCases = require('./src/cases'); + const judge = require('./src/judge'); + const sysinfo = require('./src/sysinfo'); + + const { problem, file, task } = global.Hydro.model; + const _judge = global.Hydro.handler.judge as any; + const { monitor } = global.Hydro.service; + + const info = await sysinfo.get(); + monitor.updateJudger(info); + setInterval(async () => { + const [mid, inf] = await sysinfo.update(); + monitor.updateJudger({ mid, ...inf }); + }, 1200000); + + async function processData(folder: string) { + let files = await fs.readdir(folder); + let ini = false; + for (const i of files) { + if (i.toLowerCase() === 'config.ini') { + ini = true; + await fs.rename(`${folder}/${i}`, `${folder}/config.ini`); + break; + } + } + if (ini) { + for (const i of files) { + if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`); + else if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`); + } + files = await fs.readdir(`${folder}/input`); + for (const i of files) await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`); + files = await fs.readdir(`${folder}/output`); + for (const i of files) await fs.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`); + } + } + + async function problemData(domainId: string, pid: string, savePath: string) { + const tmpFilePath = path.resolve(homedir(), '.cache', 'hydro', 'judge', `download_${domainId}_${pid}`); + const pdoc = await problem.get(domainId, pid); + // FIXME doesnt work on copied problems + // @ts-ignore + const data = await file.get(pdoc.data); + if (!data) throw new SystemError('Problem data not found.'); + const w = fs.createWriteStream(tmpFilePath); + data.pipe(w); + await new Promise((resolve, reject) => { + w.on('finish', resolve); + w.on('error', reject); + }); + fs.ensureDirSync(path.dirname(savePath)); + await new Promise((resolve, reject) => { + child.exec(`unzip ${tmpFilePath} -d ${savePath}`, (e) => { + if (e) reject(e); + else resolve(); + }); + }); + await fs.unlink(tmpFilePath); + await processData(savePath).catch(); + return savePath; + } + + async function cacheOpen(domainId: string, pid: string, version: string) { + const filePath = path.join(homedir(), '.cache', 'hydro', 'judge', domainId, pid); + console.log(filePath); + if (fs.existsSync(filePath)) { + let ver: string; + try { + ver = fs.readFileSync(path.join(filePath, 'version')).toString(); + } catch (e) { /* ignore */ } + if (version === ver) return filePath; + fs.removeSync(filePath); + } + fs.ensureDirSync(filePath); + await problemData(domainId, pid, filePath); + fs.writeFileSync(path.join(filePath, 'version'), version); + return filePath; + } + + function getNext(that) { + that.nextId = 1; + that.nextWaiting = []; + return (data, id) => { + data.domainId = that.domainId; + data.rid = new ObjectID(that.rid); + data.time = data.time_ms || data.time; + data.memory = data.memory_kb || data.memory; + data.message = data.judge_text || data.message; + data.compilerText = data.compiler_text || data.compilerText; + if (data.case) { + data.case = { + status: data.case.status, + time: data.case.time_ms || data.case.time, + memory: data.case.memory_kb || data.case.memory, + message: data.judge_text || data.message || data.judgeText, + }; + } + if (id) { + if (id === that.nextId) { + _judge.next(data); + that.nextId++; + let t = true; + while (t) { + t = false; + for (const i in that.nextWaiting) { + if (that.nextId === that.nextWaiting[i].id) { + _judge.next(that.nextWaiting[i].data); + that.nextId++; + that.nextWaiting.splice(i, 1); + t = true; + } + } + } + } else that.nextWaiting.push({ data, id }); + } else _judge.next(data); + }; + } + + function getEnd(domainId: string, rid: string) { + return (data) => { + data.key = 'end'; + data.rid = new ObjectID(rid); + data.domainId = domainId; + data.time = data.time_ms || data.time; + data.memory = data.memory_kb || data.memory; + console.log({ + status: data.status, + score: data.score, + time_ms: data.time_ms, + memory_kb: data.memory_kb, + }); + _judge.end(data); + }; + } + + class JudgeTask { + stat: any; + + request: any; + + event: string; + + pid: string; + + rid: string; + + domainId: string; + + lang: string; + + code: string; + + data: string; + + config: any; + + next: (data: any, id?: any) => void; + + end: (data: any) => void; + + tmpdir: string; + + clean: any[]; + + folder: string; + + constructor(request) { + this.stat = {}; + this.stat.receive = new Date(); + this.request = request; + } + + async handle() { + try { + this.stat.handle = new Date(); + this.pid = (this.request.pid || 'unknown').toString(); + this.rid = this.request.rid.toString(); + this.domainId = this.request.domainId; + this.lang = this.request.lang; + this.code = this.request.code; + this.data = this.request.data; + this.config = this.request.config; + this.next = getNext(this); + this.end = getEnd(this.domainId, this.rid); + this.tmpdir = path.resolve(tmpdir(), 'tmp', this.rid); + this.clean = []; + fs.ensureDirSync(this.tmpdir); + tmpfs.mount(this.tmpdir, '64m'); + console.log(`Submission: ${this.rid}`, { pid: this.pid }); + if (this.config.input) await this.run(); + else await this.submission(); + } catch (e) { + if (e instanceof CompileError) { + this.next({ compiler_text: compilerText(e.stdout, e.stderr) }); + this.end({ + status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0, + }); + } else if (e instanceof FormatError) { + this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` }); + this.end({ + status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, + }); + } else { + console.error(e); + this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` }); + this.end({ + status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, + }); + } + } + // eslint-disable-next-line no-await-in-loop + for (const clean of this.clean) await clean().catch(); + tmpfs.umount(this.tmpdir); + fs.removeSync(this.tmpdir); + } + + async submission() { + 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[this.config.type || 'default'].judge(this); + } + + async run() { + this.stat.judge = new Date(); + await judge.run.judge(this); + } + } + + task.consume({ type: 'judge' }, (t) => (new JudgeTask(t)).handle().catch(console.error)); +} + +global.Hydro.postInit.push(postInit); diff --git a/packages/hydrojudge/setting.yaml b/packages/hydrojudge/setting.yaml new file mode 100644 index 000000000..202d11719 --- /dev/null +++ b/packages/hydrojudge/setting.yaml @@ -0,0 +1,93 @@ +langs: + type: textarea + category: system + desc: Language file settings + name: langs + default: | + ccWithoutO2: + type: compiler + compile: /usr/bin/g++ -Wall -o ${name} foo.cc + code_file: foo.cc + execute: ${dir}/${name} + c: + type: compiler + compile: /usr/bin/gcc -O2 -Wall -std=c99 -o ${name} foo.c -lm + code_file: foo.c + execute: ${dir}/${name} + cc: + type: compiler + compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm + code_file: foo.cc + execute: ${dir}/${name} + cc98: + type: compiler + compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm + code_file: foo.cc + execute: ${dir}/${name} + cc11: + type: compiler + compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm + code_file: foo.cc + execute: ${dir}/${name} + cc17: + type: compiler + compile: /usr/bin/g++ -O2 -Wall -std=c++17 -o ${name} foo.cc -lm + code_file: foo.cc + execute: ${dir}/${name} + pas: + type: compiler + compile: /usr/bin/fpc -O2 -o${dir}/${name} foo.pas + code_file: foo.pas + execute: ${dir}/${name} + java: + type: compiler + compile: /usr/bin/javac -d ${dir} -encoding utf8 ./Main.java + code_file: Main.java + execute: /usr/bin/java Main + py: + type: compiler + compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" + code_file: foo.py + execute: /usr/bin/python ${name} + py2: + type: compiler + compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" + code_file: foo.py + execute: /usr/bin/python ${name} + py3: + type: compiler + compile: /usr/bin/python3 -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" + code_file: foo.py + execute: /usr/bin/python3 ${name} + php: + type: interpreter + code_file: foo.php + execute: /usr/bin/php ${name} + rs: + type: compiler + compile: /usr/bin/rustc -O -o ${dir}/${name} ${dir}/foo.rs + code_file: foo.rs + execute: ${dir}/${name} + hs: + type: compiler + compile: /usr/bin/ghc -O -outputdir /tmp -o ${name} foo.hs + code_file: foo.hs + execute: ${dir}/${name} + js: + type: interpreter + code_file: foo.js + execute: /usr/bin/jsc ${dir}/${name} + go: + type: compiler + compile: /usr/bin/go build -o ${name} foo.go + code_file: foo.go + execute: ${dir}/${name} + rb: + type: interpreter + code_file: foo.rb + execute: /usr/bin/ruby ${name} + cs: + type: compiler + compile: /usr/bin/mcs -optimize+ -out:${dir}/${name} ${dir}/foo.cs + code_file: foo.cs + execute: /usr/bin/mono ${name} \ No newline at end of file diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts new file mode 100644 index 000000000..a04291c5e --- /dev/null +++ b/packages/hydrojudge/src/cases.ts @@ -0,0 +1,229 @@ +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import path from 'path'; +import { Dictionary } from 'lodash'; +import { FormatError, SystemError } from './error'; +import { parseTimeMS, parseMemoryMB, ensureFile } from './utils'; + +const RE0 = [ + { + reg: /^([a-z+_\-A-Z]*)([0-9]+).in$/, + output: (a) => `${a[1] + a[2]}.out`, + id: (a) => parseInt(a[2], 10), + }, + { + reg: /^([a-z+_\-A-Z]*)([0-9]+).in$/, + output: (a) => `${a[1] + a[2]}.ans`, + id: (a) => parseInt(a[2], 10), + }, + { + reg: /^([a-z+_\-A-Z0-9]*)\.in([0-9]+)$/, + output: (a) => `${a[1]}.ou${a[2]}`, + id: (a) => parseInt(a[2], 10), + }, + { + reg: /^(input)([0-9]+).txt$/, + output: (a) => `output${a[2]}.txt`, + id: (a) => parseInt(a[2], 10), + }, +]; +const RE1 = [ + { + reg: /^([a-z+_\-A-Z]*)([0-9]+)-([0-9]+).in$/, + output: (a) => `${a[1] + a[2]}-${a[3]}.out`, + subtask: (a) => parseInt(a[2], 10), + id: (a) => parseInt(a[3], 10), + }, +]; + +async function read0(folder, files, checkFile) { + const cases = []; + for (const file of files) { + for (const REG of RE0) { + if (REG.reg.test(file)) { + const data = REG.reg.exec(file); + const c = { input: file, output: REG.output(data), id: REG.id(data) }; + if (fs.existsSync(path.resolve(folder, c.output))) { + cases.push(c); + break; + } + } + } + } + cases.sort((a, b) => (a.id - b.id)); + const extra = cases.length - (100 % cases.length); + const config = { + count: 0, + subtasks: [{ + time_limit_ms: 1000, + memory_limit_mb: 256, + type: 'sum', + cases: [], + score: Math.floor(100 / cases.length), + }], + }; + for (let i = 0; i < extra; i++) { + config.count++; + config.subtasks[0].cases.push({ + id: config.count, + input: checkFile(cases[i].input), + output: checkFile(cases[i].output), + }); + } + if (extra < cases.length) { + config.subtasks.push({ + time_limit_ms: 1000, + memory_limit_mb: 256, + type: 'sum', + cases: [], + score: Math.floor(100 / cases.length) + 1, + }); + for (let i = extra; i < cases.length; i++) { + config.count++; + config.subtasks[1].cases.push({ + id: config.count, + input: checkFile(cases[i].input), + output: checkFile(cases[i].output), + }); + } + } + return config; +} + +async function read1(folder, files, checkFile) { + const subtask = {}; + const subtasks = []; + for (const file of files) { + for (const REG of RE1) { + if (REG.reg.test(file)) { + const data = REG.reg.exec(file); + const c = { input: file, output: REG.output(data), id: REG.id(data) }; + if (fs.existsSync(path.resolve(folder, c.output))) { + if (!subtask[REG.subtask(data)]) { + subtask[REG.subtask(data)] = [{ + time_limit_ms: 1000, + memory_limit_mb: 256, + type: 'min', + cases: [c], + }]; + } else subtask[REG.subtask(data)].cases.push(c); + break; + } + } + } + } + for (const i in subtask) { + subtask[i].cases.sort((a, b) => (a.id - b.id)); + subtasks.push(subtask[i]); + } + const base = Math.floor(100 / subtasks.length); + const extra = subtasks.length - (100 % subtasks.length); + const config = { count: 0, subtasks }; + const keys = Object.keys(subtask); + for (let i = 0; i < keys.length; i++) { + if (extra < i) subtask[keys[i]].score = base; + else subtask[keys[i]].score = base + 1; + for (const j of subtask[keys[i]].cases) { + config.count++; + j.input = checkFile(j.input); + j.output = checkFile(j.output); + j.id = config.count; + } + } + return config; +} + +async function readAutoCases(folder, { next }) { + const + config = { + checker_type: 'default', + count: 0, + subtasks: [], + judge_extra_files: [], + user_extra_files: [], + }; + const checkFile = ensureFile(folder); + try { + const files = await fs.readdir(folder); + let result = await read0(folder, files, checkFile); + if (!result.count) result = await read1(folder, files, checkFile); + Object.assign(config, result); + next({ judge_text: `识别到${config.count}个测试点` }); + } catch (e) { + throw new SystemError('在自动识别测试点的过程中出现了错误。', [e]); + } + return config; +} + +export = async function readYamlCases(folder: string, cfg: Dictionary, args) { + const config: any = { + checker_type: 'default', + count: 0, + subtasks: [], + judge_extra_files: [], + user_extra_files: [], + }; + const next = args.next; + const checkFile = ensureFile(folder); + config.checker_type = cfg.checker_type || 'default'; + if (cfg.filename) config.filename = cfg.filename; + if (cfg.checker) config.checker = checkFile(cfg.checker, '找不到比较器 '); + if (cfg.judge_extra_files) { + if (typeof cfg.judge_extra_files === 'string') { + config.judge_extra_files = [checkFile(cfg.judge_extra_files, '找不到评测额外文件 ')]; + } else if (cfg.judge_extra_files instanceof Array) { + for (const file in cfg.judge_extra_files) { + config.judge_extra_files.push(checkFile(file, '找不到评测额外文件 ')); + } + } else throw new FormatError('无效的 judge_extra_files 配置项'); + } + if (cfg.user_extra_files) { + if (typeof cfg.user_extra_files === 'string') { + config.user_extra_files = [checkFile(cfg.user_extra_files, '找不到用户额外文件 ')]; + } else if (cfg.user_extra_files instanceof Array) { + for (const file in cfg.user_extra_files) { + config.user_extra_files.push(checkFile(file, '找不到用户额外文件 ')); + } + } else throw new FormatError('无效的 user_extra_files 配置项'); + } + if (cfg.cases) { + config.subtasks = [{ + score: parseInt(cfg.score, 10) || Math.floor(100 / config.count), + time_limit_ms: parseTimeMS(cfg.time), + memory_limit_mb: parseMemoryMB(cfg.memory), + cases: [], + type: 'sum', + }]; + for (const c of cfg.cases) { + config.count++; + config.subtasks[0].cases.push({ + input: checkFile(c.input, '找不到输入文件 '), + output: checkFile(c.output, '找不到输出文件 '), + id: config.count, + }); + } + } else if (cfg.subtasks) { + for (const subtask of cfg.subtasks) { + const cases = []; + for (const c of subtask) { + config.count++; + cases.push({ + input: checkFile(c.input, '找不到输入文件 '), + output: checkFile(c.output, '找不到输出文件 '), + id: config.count, + }); + } + config.subtasks.push({ + score: parseInt(subtask.score, 10), + cases, + time_limit_ms: parseTimeMS(subtask.time || cfg.time), + memory_limit_mb: parseMemoryMB(subtask.memory || cfg.time), + }); + } + } else { + const c = await readAutoCases(folder, { next }); + config.subtasks = c.subtasks; + config.count = c.count; + } + return Object.assign(cfg, config); +}; diff --git a/packages/hydrojudge/src/check.ts b/packages/hydrojudge/src/check.ts new file mode 100644 index 000000000..b901e9460 --- /dev/null +++ b/packages/hydrojudge/src/check.ts @@ -0,0 +1,28 @@ +import fs from 'fs-extra'; +import checkers from './checkers'; +import compile from './compile'; +import { SystemError } from './error'; + +export async function check(config): Promise<[number, number, string]> { + if (!checkers[config.checker_type]) throw new SystemError(`未知比较器类型:${config.checker_type}`); + const { + code, status, score, message, + } = await checkers[config.checker_type]({ + input: config.stdin, + output: config.stdout, + user_stdout: config.user_stdout, + user_stderr: config.user_stderr, + score: config.score, + copyIn: config.copyIn || {}, + detail: config.detail, + }); + if (code) throw new SystemError(`比较器返回了非零值:${code}`); + return [status, score, message]; +} + +export async function compileChecker(checkerType: string, checker: string, copyIn: any) { + 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); +} diff --git a/packages/hydrojudge/src/checkers.ts b/packages/hydrojudge/src/checkers.ts new file mode 100644 index 000000000..0f033cf95 --- /dev/null +++ b/packages/hydrojudge/src/checkers.ts @@ -0,0 +1,173 @@ +/* eslint-disable no-template-curly-in-string */ +import { STATUS } from 'hydrooj/dist/model/builtin'; +import fs from 'fs-extra'; +import { run } from './sandbox'; +import { SystemError } from './error'; + +interface CheckResult { + status: number, + score: number, + message: string, + code?: number, +} + +type Checker = (config: any) => Promise; + +const checkers: Record = { + async default(config) { + const { stdout } = await run('/usr/bin/diff -BZ usrout stdout', { + copyIn: { + usrout: { src: config.user_stdout }, + stdout: { src: config.output }, + }, + }); + let status; + let message = ''; + if (stdout) { + status = STATUS.STATUS_WRONG_ANSWER; + if (config.detail) { + try { + const pt = stdout.split('---'); + const u = pt[0].split('\n')[1]; + let usr = u.substr(2, u.length - 2).trim().split(' '); + const t = pt[1].split('\n')[1]; + let std = t.substr(2, t.length - 2).trim().split(' '); + if (usr.length < std.length) message = '标准输出比选手输出长。'; + else if (usr.length > std.length) message = '选手输出比标准输出长。'; + else { + for (const i in usr) { + if (usr[i] !== std[i]) { + usr = usr[i]; + std = std[i]; + break; + } + } + if (usr.length > 20) usr = `${usr.substring(0, 16)}...`; + if (std.length > 20) std = `${std.substring(0, 16)}...`; + message = `读取到 ${usr} ,应为 ${std}`; + } + } catch (e) { + message = stdout.substring(0, stdout.length - 1 <= 30 ? stdout.length - 1 : 30); + } + } + } else status = STATUS.STATUS_ACCEPTED; + return { + score: status === STATUS.STATUS_ACCEPTED ? config.score : 0, + status, + message, + }; + }, + + /* + * argv[1]:输入 + * argv[2]:标准输出 + * argv[3]:选手输出 + * exit code:返回判断结果 + */ + async hustoj(config) { + const { code, stdout } = await run('${dir}/checker input stdout usrout', { + copyIn: { + usrout: { src: config.user_stdout }, + stdout: { src: config.output }, + input: { src: config.input }, + }, + }); + const status = code ? STATUS.STATUS_WRONG_ANSWER : STATUS.STATUS_ACCEPTED; + const message = (await fs.readFile(stdout)).toString(); + return { + status, + score: status === STATUS.STATUS_ACCEPTED ? config.score : 0, + message, + }; + }, + + /* + * argv[1]:输入文件 + * argv[2]:选手输出文件 + * argv[3]:标准输出文件 + * argv[4]:单个测试点分值 + * argv[5]:输出最终得分的文件 + * argv[6]:输出错误报告的文件 + */ + async lemon(config) { + const { files } = await run(`\${dir}/checker input usrout stdout ${config.score} score message`, { + copyIn: { + usrout: { src: config.user_stdout }, + stdout: { src: config.output }, + input: { src: config.input }, + }, + copyOut: ['score', 'message'], + }); + const { message } = files; + const score = parseInt(files.score, 10); + return { + score, + message, + status: score === config.score + ? STATUS.STATUS_ACCEPTED + : STATUS.STATUS_WRONG_ANSWER, + }; + }, + + /* + * argv[1]:输入 + * argv[2]:选手输出 + * exit code:返回判断结果 + */ + async qduoj(config) { + const { status, stdout } = await run('${dir}/checker input usrout', { + copyIn: { + usrout: { src: config.user_stdout }, + input: { src: config.input }, + }, + }); + const st = (status === STATUS.STATUS_ACCEPTED) + ? STATUS.STATUS_ACCEPTED : STATUS.STATUS_WRONG_ANSWER; + return { + status: st, + score: (status === STATUS.STATUS_ACCEPTED) ? config.score : 0, + message: stdout, + }; + }, + + /* + * in:输入 + * user_out:选手输出 + * answer:标准输出 + * code:选手代码 + * stdout:输出最终得分 + * stderr:输出错误报告 + */ + async syzoj(config) { + // eslint-disable-next-line prefer-const + let { status, stdout, stderr } = await run('${dir}/checker', { + copyIn: { + in: { src: config.input }, + user_out: { src: config.user_stdout }, + answer: { src: config.output }, + code: { content: config.code }, + }, + }); + if (status !== STATUS.STATUS_ACCEPTED) throw new SystemError('Checker returned a non-zero value', [status]); + const score = parseInt(stdout, 10); + status = score === config.score ? STATUS.STATUS_ACCEPTED : STATUS.STATUS_WRONG_ANSWER; + return { status, score, message: stderr }; + }, + + async testlib(config) { + const { stdout, stderr } = await run('${dir}/checker ${dir}/in ${dir}/user_out ${dir}/answer', { + copyIn: { + in: { src: config.input }, + user_out: { src: config.user_stdout }, + answer: { src: config.output }, + }, + }); + return { + status: stderr === 'ok \n' ? STATUS.STATUS_ACCEPTED : STATUS.STATUS_WRONG_ANSWER, + score: stderr === 'ok \n' ? config.score : 0, + message: stdout, + }; + }, +}; + +export = checkers; diff --git a/packages/hydrojudge/src/compile.ts b/packages/hydrojudge/src/compile.ts new file mode 100644 index 000000000..a4c5d7c22 --- /dev/null +++ b/packages/hydrojudge/src/compile.ts @@ -0,0 +1,32 @@ +import yaml from 'js-yaml'; +import { STATUS } from 'hydrooj/dist/model/builtin'; +import { run, del } from './sandbox'; +import { CompileError, SystemError } from './error'; +import { compilerText } from './utils'; + +export = async function compile( + lang: string, code: string, target: string, copyIn: any, next?: Function, +) { + const LANGS = yaml.safeLoad(await global.Hydro.model.system.get('hydrojudge.langs')); + if (!LANGS[lang]) throw new SystemError(`不支持的语言:${lang}`); + const info = LANGS[lang]; + const f = {}; + if (info.type === 'compiler') { + copyIn[info.code_file] = { content: code }; + const { + status, stdout, stderr, fileIds, + } = await run( + info.compile.replace(/\$\{name\}/g, target), + { copyIn, copyOutCached: [target] }, + ); + if (status !== STATUS.STATUS_ACCEPTED) throw new CompileError({ status, stdout, stderr }); + if (!fileIds[target]) throw new CompileError({ stderr: '没有找到可执行文件' }); + if (next) next({ compiler_text: compilerText(stdout, stderr) }); + f[target] = { fileId: fileIds[target] }; + return { execute: info.execute, copyIn: f, clean: () => del(fileIds[target]) }; + } if (info.type === 'interpreter') { + f[target] = { content: code }; + return { execute: info.execute, copyIn: f, clean: () => Promise.resolve() }; + } + throw new SystemError('Unknown language type'); +} diff --git a/packages/hydrojudge/src/error.ts b/packages/hydrojudge/src/error.ts new file mode 100644 index 000000000..f11799c43 --- /dev/null +++ b/packages/hydrojudge/src/error.ts @@ -0,0 +1,69 @@ +import { STATUS_TEXTS } from 'hydrooj/dist/model/builtin'; + +export class CompileError extends Error { + stdout: string; + + stderr: string; + + status: string; + + type: string; + + constructor(obj) { + super('Compile Error'); + if (typeof obj === 'string') { + this.stdout = obj; + this.stderr = ''; + } else { + this.stdout = obj.stdout || ''; + this.stderr = obj.stderr || ''; + this.status = obj.status ? STATUS_TEXTS[obj.status] || '' : ''; + } + this.type = 'CompileError'; + } +} + +export class FormatError extends Error { + type: string; + + params: any[]; + + constructor(message, params = []) { + super(message); + this.type = 'FormatError'; + this.params = params; + } +} + +export class RuntimeError extends Error { + type: string; + + detail: any; + + constructor(detail, message) { + super(message); + this.type = 'RuntimeError'; + this.detail = detail; + } +} + +export class SystemError extends Error { + type: string; + + params: any[]; + + constructor(message: string, params = []) { + super(message); + this.type = 'SystemError'; + this.params = params; + } +} + +export class TooFrequentError extends Error { + type: string; + + constructor(message: string) { + super(message); + this.type = 'TooFrequentError'; + } +} diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts new file mode 100644 index 000000000..b0ac803e7 --- /dev/null +++ b/packages/hydrojudge/src/judge/default.ts @@ -0,0 +1,154 @@ +import Queue from 'p-queue'; +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'; + +const Score = { + sum: (a, b) => (a + b), + max: Math.max, + min: Math.min, +}; + +function judgeCase(c) { + return async (ctx, ctxSubtask) => { + const { filename } = ctx.config; + const { copyIn } = ctx.execute; + if (ctx.config.filename) copyIn[`${filename}.in`] = { src: c.input }; + const copyOut = filename ? [`${filename}.out`] : []; + const stdin = filename ? null : c.input; + const stdout = path.resolve(ctx.tmpdir, `${c.id}.out`); + const stderr = path.resolve(ctx.tmpdir, `${c.id}.err`); + const res = await run( + ctx.execute.execute.replace(/\$\{name\}/g, 'code'), + { + stdin, + stdout: filename ? null : stdout, + stderr, + copyIn, + copyOut, + time_limit_ms: ctxSubtask.subtask.time_limit_ms, + memory_limit_mb: ctxSubtask.subtask.memory_limit_mb, + }, + ); + 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 = ''; + let score = 0; + if (status === STATUS.STATUS_ACCEPTED) { + if (time_usage_ms > ctxSubtask.subtask.time_limit_ms) { + status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; + } else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) { + status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; + } else { + [status, score, message] = await check({ + copyIn: copyInDir(path.resolve(ctx.tmpdir, 'checker')), + stdin: c.input, + stdout: c.output, + user_stdout: stdout, + user_stderr: stderr, + checker: ctx.config.checker, + checker_type: ctx.config.checker_type, + score: ctxSubtask.subtask.score, + 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}.`; + } + ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score); + ctxSubtask.status = Math.max(ctxSubtask.status, status); + ctx.total_time_usage_ms += time_usage_ms; + ctx.total_memory_usage_kb = Math.max(ctx.total_memory_usage_kb, memory_usage_kb); + ctx.next({ + status: STATUS.STATUS_JUDGING, + case: { + status, + score: 0, + time_ms: time_usage_ms, + memory_kb: memory_usage_kb, + judge_text: message, + }, + progress: Math.floor((c.id * 100) / ctx.config.count), + }, c.id); + }; +} + +function judgeSubtask(subtask) { + return async (ctx) => { + subtask.type = subtask.type || 'min'; + const ctxSubtask = { + subtask, + status: 0, + score: subtask.type === 'min' + ? subtask.score + : 0, + }; + const cases = []; + for (const cid in subtask.cases) { + cases.push(ctx.queue.add(() => judgeCase(subtask.cases[cid])(ctx, ctxSubtask))); + } + await Promise.all(cases); + ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); + ctx.total_score += ctxSubtask.score; + }; +} + +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 }); + [ctx.execute] = await Promise.all([ + (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); + })(), + (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(ctx.execute.clean); + ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); + const tasks = []; + ctx.total_status = 0; + ctx.total_score = 0; + ctx.total_memory_usage_kb = 0; + ctx.total_time_usage_ms = 0; + ctx.queue = new Queue({ concurrency: ctx.config.concurrency || 2 }); + for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid])(ctx)); + await Promise.all(tasks); + ctx.stat.done = new Date(); + ctx.next({ judge_text: JSON.stringify(ctx.stat) }); + ctx.end({ + status: ctx.total_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 new file mode 100644 index 000000000..891813d77 --- /dev/null +++ b/packages/hydrojudge/src/judge/index.ts @@ -0,0 +1,8 @@ +import * as def from './default'; +import * as interactive from './interactive'; +import * as run from './run'; +import * as submit_answer from './submit_answer'; + +export = { + default: def, interactive, run, submit_answer, +} diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts new file mode 100644 index 000000000..ccc122fec --- /dev/null +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -0,0 +1,126 @@ +import Queue from 'p-queue'; +import fs from 'fs-extra'; +import { STATUS } from 'hydrooj/dist/model/builtin'; +import { parseFilename } from '../utils'; +import { run } from '../sandbox'; +import compile from '../compile'; +import signals from '../signals'; + +const Score = { + sum: (a, b) => (a + b), + max: Math.max, + min: Math.min, +}; + +function judgeCase(c) { + return async (ctx, ctxSubtask) => { + ctx.executeInteractor.copyIn.stdans = { src: c.input }; + const [{ code, time_usage_ms, memory_usage_kb }, resInteractor] = await run([ + { + execute: ctx.executeUser.execute.replace(/\$\{name\}/g, 'code'), + copyIn: ctx.executeUser.copyIn, + time_limit_ms: ctxSubtask.subtask.time_limit_ms, + memory_limit_mb: ctxSubtask.subtask.memory_limit_mb, + }, { + execute: ctx.executeInteractor.execute.replace(/\$\{name\}/g, 'code'), + copyIn: ctx.executeInteractor.copyIn, + time_limit_ms: ctxSubtask.subtask.time_limit_ms * 2, + memory_limit_mb: ctxSubtask.subtask.memory_limit_mb * 2, + }, + ]); + let status; + let score = 0; + let message = ''; + if (time_usage_ms > ctxSubtask.subtask.time_limit_ms) { + status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; + } else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) { + status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; + } else if (code) { + status = STATUS.STATUS_RUNTIME_ERROR; + if (code < 32) message = signals[code]; + else message = `您的程序返回了 ${code}.`; + } else [status, score, message] = resInteractor.files.stderr.split('\n'); + ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score); + ctxSubtask.status = Math.max(ctxSubtask.status, status); + ctx.total_time_usage_ms += time_usage_ms; + ctx.total_memory_usage_kb = Math.max(ctx.total_memory_usage_kb, memory_usage_kb); + ctx.next({ + status: STATUS.STATUS_JUDGING, + case: { + status, + score: 0, + time_ms: time_usage_ms, + memory_kb: memory_usage_kb, + judge_text: message, + }, + progress: Math.floor((c.id * 100) / ctx.config.count), + }); + }; +} + +function judgeSubtask(subtask) { + return async (ctx) => { + subtask.type = subtask.type || 'min'; + const ctxSubtask = { + subtask, + status: 0, + score: subtask.type === 'min' + ? subtask.score + : 0, + }; + const cases = []; + for (const cid in subtask.cases) { + cases.push(ctx.queue.add(() => judgeCase(subtask.cases[cid])(ctx, ctxSubtask))); + } + await Promise.all(cases); + ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); + ctx.total_score += ctxSubtask.score; + }; +} + +export const judge = async (ctx) => { + ctx.next({ status: STATUS.STATUS_COMPILING }); + [ctx.executeUser, ctx.executeInteractor] = await Promise.all([ + (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); + })(), + (async () => { + const copyIn = {}; + for (const file of ctx.config.judge_extra_files) { + copyIn[parseFilename(file)] = { src: file }; + } + return await compile( + parseFilename(ctx.checker).split('.')[1], + fs.readFileSync(ctx.checker).toString(), + 'interactor', + copyIn, + ); + })(), + ]); + ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); + const tasks = []; + ctx.total_status = ctx.total_score = ctx.total_memory_usage_kb = ctx.total_time_usage_ms = 0; + ctx.queue = new Queue({ concurrency: ctx.config.concurrency || 2 }); + for (const sid in ctx.config.subtasks) { + tasks.push(judgeSubtask(ctx.config.subtasks[sid])(ctx)); + } + await Promise.all(tasks); + ctx.stat.done = new Date(); + ctx.next({ judge_text: JSON.stringify(ctx.stat) }); + console.log({ + status: ctx.total_status, + score: ctx.total_score, + time_ms: ctx.total_time_usage_ms, + memory_kb: ctx.total_memory_usage_kb, + }); + ctx.end({ + status: ctx.total_status, + score: ctx.total_score, + time_ms: ctx.total_time_usage_ms, + memory_kb: ctx.total_memory_usage_kb, + }); +}; diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts new file mode 100644 index 000000000..ad6f90d23 --- /dev/null +++ b/packages/hydrojudge/src/judge/run.ts @@ -0,0 +1,62 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { STATUS } from 'hydrooj/dist/model/builtin'; +import { run } from '../sandbox'; +import compile from '../compile'; +import signals from '../signals'; + +export const judge = async (ctx) => { + ctx.stat.judge = new Date(); + ctx.next({ status: STATUS.STATUS_COMPILING }); + ctx.execute = await compile(ctx.lang, ctx.code, 'code', {}, ctx.next); + ctx.clean.push(ctx.execute.clean); + ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); + const { copyIn } = ctx.execute; + const stdin = path.resolve(ctx.tmpdir, 'stdin'); + const stdout = path.resolve(ctx.tmpdir, 'stdout'); + const stderr = path.resolve(ctx.tmpdir, 'stderr'); + fs.writeFileSync(stdin, ctx.config.input || ''); + const res = await run( + ctx.execute.execute.replace(/\$\{name\}/g, 'code'), + { + stdin, + stdout, + stderr, + copyIn, + time_limit_ms: ctx.config.time, + memory_limit_mb: ctx.config.memory, + }, + ); + const { code, time_usage_ms, memory_usage_kb } = res; + let { status } = res; + if (!fs.existsSync(stdout)) fs.writeFileSync(stdout, ''); + let message = fs.readFileSync(stdout).toString() + fs.readFileSync(stderr).toString(); + if (status === STATUS.STATUS_ACCEPTED) { + if (time_usage_ms > ctx.config.time) { + status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; + } else if (memory_usage_kb > ctx.config.memory * 1024) { + status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; + } + } else if (code) { + status = STATUS.STATUS_RUNTIME_ERROR; + if (code < 32) message += signals[code]; + else message += `您的程序返回了 ${code}.`; + } + ctx.next({ + status, + case: { + status, + time_ms: time_usage_ms, + memory_kb: memory_usage_kb, + judge_text: message, + }, + }); + ctx.stat.done = new Date(); + ctx.next({ judge_text: JSON.stringify(ctx.stat) }); + ctx.end({ + status, + score: status === STATUS.STATUS_ACCEPTED ? 100 : 0, + time_ms: Math.floor(time_usage_ms * 1000000) / 1000000, + memory_kb: memory_usage_kb, + }); +}; diff --git a/packages/hydrojudge/src/judge/submit_answer.ts b/packages/hydrojudge/src/judge/submit_answer.ts new file mode 100644 index 000000000..0107c933a --- /dev/null +++ b/packages/hydrojudge/src/judge/submit_answer.ts @@ -0,0 +1,25 @@ +import { STATUS } from 'hydrooj/dist/model/builtin'; +import { check } from '../check'; + +export async function judge({ + next, end, config, code, +}) { + next({ status: STATUS.STATUS_JUDGING, progress: 0 }); + const [status, score, message] = await check({ + stdout: config.answer, + user_stdout: code, + checker_type: 'default', + score: 100, + detail: config.detail, + }); + next({ + status, + progress: 100, + case: { + status, score, time_ms: 0, memory_kb: 0, judge_text: message, + }, + }); + end({ + status, score, time_ms: 0, memory_kb: 0, + }); +} diff --git a/packages/hydrojudge/src/sandbox.ts b/packages/hydrojudge/src/sandbox.ts new file mode 100644 index 000000000..6d980f8a3 --- /dev/null +++ b/packages/hydrojudge/src/sandbox.ts @@ -0,0 +1,113 @@ +import Axios from 'axios'; +import fs from 'fs-extra'; +import { STATUS } from 'hydrooj/dist/model/builtin'; +import { SystemError } from './error'; +import { cmd } from './utils'; + +const fsp = fs.promises; +const env = ['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'HOME=/w']; +const axios = Axios.create({ baseURL: 'http://localhost:5050' }); + +const statusMap = { + 'Time Limit Exceeded': STATUS.STATUS_TIME_LIMIT_EXCEEDED, + 'Memory Limit Exceeded': STATUS.STATUS_MEMORY_LIMIT_EXCEEDED, + 'Output Limit Exceeded': STATUS.STATUS_RUNTIME_ERROR, + Accepted: STATUS.STATUS_ACCEPTED, + 'Nonzero Exit Status': STATUS.STATUS_RUNTIME_ERROR, + 'Internal Error': STATUS.STATUS_SYSTEM_ERROR, + 'File Error': STATUS.STATUS_SYSTEM_ERROR, + Signalled: STATUS.STATUS_RUNTIME_ERROR, +}; + +function proc({ + execute = null, + time_limit_ms = 16000, + memory_limit_mb = 1024, + process_limit = 32, + stdin = '', copyIn = {}, copyOut = [], copyOutCached = [], +} = {}) { + return { + args: cmd(execute.replace(/\$\{dir\}/g, '/w')), + env, + files: [ + stdin ? { src: stdin } : { content: '' }, + { name: 'stdout', max: 1024 * 1024 * 16 }, + { name: 'stderr', max: 1024 * 1024 * 16 }, + ], + cpuLimit: time_limit_ms * 1000 * 1000, + realCpuLimit: time_limit_ms * 3000 * 1000, + memoryLimit: memory_limit_mb * 1024 * 1024, + procLimit: process_limit, + copyIn, + copyOut, + copyOutCached, + }; +} + +export async function runMultiple(execute) { + let res; + try { + const body = { + cmd: [ + proc(execute[0]), + proc(execute[1]), + ], + pipeMapping: [{ + in: { index: 0, fd: 1 }, + out: { index: 1, fd: 0 }, + }, { + in: { index: 1, fd: 1 }, + out: { index: 0, fd: 0 }, + }], + }; + body.cmd[0].files[0] = null; + body.cmd[0].files[1] = null; + body.cmd[1].files[0] = null; + body.cmd[1].files[1] = null; + res = await axios.post('/run', body); + } catch (e) { + throw new SystemError('Cannot connect to sandbox service'); + } + return res.data; +} + +export async function del(fileId) { + const res = await axios.delete(`/file/${fileId}`); + return res.data; +} + +export async function run(execute, params?) { + let result; + // eslint-disable-next-line no-return-await + if (typeof execute === 'object') return await runMultiple(execute); + try { + const body = { cmd: [proc({ execute, ...params })] }; + const res = await axios.post('/run', body); + [result] = res.data; + } catch (e) { + // FIXME request body larger than maxBodyLength limit + throw new SystemError('Cannot connect to sandbox service ', e.message); + } + // FIXME: Signalled? + const ret: any = { + status: statusMap[result.status] || STATUS.STATUS_ACCEPTED, + time_usage_ms: result.time / 1000000, + memory_usage_kb: result.memory / 1024, + files: result.files, + code: result.exitStatus, + }; + if (ret.time_usage_ms >= (params.time_limit_ms || 16000)) { + ret.status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; + } + result.files = result.files || {}; + if (params.stdout) await fsp.writeFile(params.stdout, result.files.stdout || ''); + else ret.stdout = result.files.stdout || ''; + if (params.stderr) await fsp.writeFile(params.stderr, result.files.stderr || ''); + else ret.stderr = result.files.stderr || ''; + if (result.error) { + ret.error = result.error; + } + ret.files = result.files; + ret.fileIds = result.fileIds || {}; + return ret; +} diff --git a/packages/hydrojudge/src/signals.ts b/packages/hydrojudge/src/signals.ts new file mode 100644 index 000000000..b2e78e142 --- /dev/null +++ b/packages/hydrojudge/src/signals.ts @@ -0,0 +1,36 @@ +const signals = [ + '', // 0 + 'Hangup', // 1 + 'Interrupt', // 2 + 'Quit', // 3 + 'Illegal instruction', // 4 + 'Trace/breakpoint trap', // 5 + 'Aborted', // 6 + 'Bus error', // 7 + 'Floating point exception', // 8 + 'Killed', // 9 + 'User defined signal 1', // 10 + 'Segmentation fault', // 11 + 'User defined signal 2', // 12 + 'Broken pipe', // 13 + 'Alarm clock', // 14 + 'Terminated', // 15 + 'Stack fault', // 16 + 'Child exited', // 17 + 'Continued', // 18 + 'Stopped (signal)', // 19 + 'Stopped', // 20 + 'Stopped (tty input)', // 21 + 'Stopped (tty output)', // 22 + 'Urgent I/O condition', // 23 + 'CPU time limit exceeded', // 24 + 'File size limit exceeded', // 25 + 'Virtual timer expired', // 26 + 'Profiling timer expired', // 27 + 'Window changed', // 28 + 'I/O possible', // 29 + 'Power failure', // 30 + 'Bad system call', // 31 +]; + +export = signals; diff --git a/packages/hydrojudge/src/sysinfo.ts b/packages/hydrojudge/src/sysinfo.ts new file mode 100644 index 000000000..13333c6c5 --- /dev/null +++ b/packages/hydrojudge/src/sysinfo.ts @@ -0,0 +1,116 @@ +import path from 'path'; +import fs from 'fs-extra'; +import systeminformation from 'systeminformation'; +import { tmpdir } from 'os'; +import { judge } from './judge/run'; +import * as tmpfs from './tmpfs'; + +function size(s: number, base = 1) { + s *= base; + const unit = 1024; + const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + for (const unitName of unitNames) { + if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName); + s /= unit; + } + return '{0} {1}'.format(Math.round(s * unit), unitNames[unitNames.length - 1]); +} + +const cache: any = {}; + +async function stackSize() { + let output = ''; + const context: any = { + lang: 'ccWithoutO2', + code: `#include +using namespace std; +int i=1; +int main(){ + char a[1048576]={'1'}; + cout<<" "<256) return 0; + main(); +}`, + config: { + time: 3000, + memory: 256, + }, + stat: {}, + clean: [], + next: () => { }, + end: (data) => { + if (data.stdout) output = data.stdout; + }, + }; + context.tmpdir = path.resolve(tmpdir(), 'hydro', 'tmp', 'sysinfo'); + fs.ensureDirSync(context.tmpdir); + tmpfs.mount(context.tmpdir, '64m'); + await judge(context).catch((e) => console.error(e)); + // eslint-disable-next-line no-await-in-loop + for (const clean of context.clean) await clean().catch(); + tmpfs.umount(context.tmpdir); + fs.removeSync(context.tmpdir); + const a = output.split(' '); + return parseInt(a[a.length - 1], 10); +} + +export async function get() { + const [ + Cpu, Memory, OsInfo, + CurrentLoad, CpuFlags, CpuTemp, + Battery, stack, + ] = await Promise.all([ + systeminformation.cpu(), + systeminformation.mem(), + systeminformation.osInfo(), + systeminformation.currentLoad(), + systeminformation.cpuFlags(), + systeminformation.cpuTemperature(), + systeminformation.battery(), + stackSize(), + ]); + const cpu = `${Cpu.manufacturer} ${Cpu.brand}`; + const memory = `${size(Memory.active)}/${size(Memory.total)}`; + const osinfo = `${OsInfo.distro} ${OsInfo.release} ${OsInfo.codename} ${OsInfo.kernel} ${OsInfo.arch}`; + const load = `${CurrentLoad.avgload}`; + const flags = CpuFlags; + let battery; + if (!Battery.hasbattery) battery = 'No battery'; + else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`; + const mid = OsInfo.serial; + cache.cpu = cpu; + cache.osinfo = osinfo; + cache.flags = flags; + cache.mid = mid; + cache.stack = stack; + return { + mid, cpu, memory, osinfo, load, flags, CpuTemp, battery, stack, + }; +} + +export async function update() { + const [Memory, CurrentLoad, CpuTemp, Battery] = await Promise.all([ + systeminformation.mem(), + systeminformation.currentLoad(), + systeminformation.cpuTemperature(), + systeminformation.battery(), + ]); + const { + mid, cpu, osinfo, flags, stack, + } = cache; + const memory = `${size(Memory.active)}/${size(Memory.total)}`; + const load = `${CurrentLoad.avgload}`; + let battery; + if (!Battery.hasbattery) battery = 'No battery'; + else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`; + return [ + mid, + { + memory, load, battery, CpuTemp, + }, + { + mid, cpu, memory, osinfo, load, flags, battery, CpuTemp, stack, + }, + ]; +} diff --git a/packages/hydrojudge/src/tmpfs.ts b/packages/hydrojudge/src/tmpfs.ts new file mode 100644 index 000000000..3d5a26743 --- /dev/null +++ b/packages/hydrojudge/src/tmpfs.ts @@ -0,0 +1,11 @@ +import child from 'child_process'; +import fs from 'fs-extra'; + +export function mount(path: string, size = '32m') { + fs.ensureDirSync(path); + child.execSync(`mount tmpfs ${path} -t tmpfs -o size=${size}`); +} + +export function umount(path: string) { + child.execSync(`umount ${path}`); +} diff --git a/packages/hydrojudge/src/utils.ts b/packages/hydrojudge/src/utils.ts new file mode 100644 index 000000000..bf183d167 --- /dev/null +++ b/packages/hydrojudge/src/utils.ts @@ -0,0 +1,117 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { parse } from 'shell-quote'; +import _ from 'lodash'; +import { EventEmitter } from 'events'; +import { FormatError } from './error'; + +const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i; +const TIME_UNITS = { '': 1000, m: 1, u: 0.001 }; +const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i; +const MEMORY_UNITS = { k: 0.1, m: 1, g: 1024 }; +const EMPTY_STR = /^[ \r\n\t]*$/i; + +export const cmd = parse; + +export function parseTimeMS(str) { + const match = TIME_RE.exec(str); + if (!match) throw new FormatError(str, ['error parsing time']); + return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]); +} + +export function parseMemoryMB(str) { + const match = MEMORY_RE.exec(str); + if (!match) throw new FormatError(str, ['error parsing memory']); + return Math.floor(parseFloat(match[1]) * MEMORY_UNITS[match[2]]); +} + +export function sleep(timeout) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, timeout); + }); +} + +export function parseFilename(filePath: string) { + const t = filePath.split('/'); + return t[t.length - 1]; +} + +export class Queue extends EventEmitter { + queue: any[]; + + waiting: any[]; + + constructor() { + super(); + this.queue = []; + this.waiting = []; + } + + get(count = 1) { + if (this.empty() || this.queue.length < count) { + return new Promise((resolve) => { + this.waiting.push({ count, resolve }); + }); + } + const items = []; + for (let i = 0; i < count; i++) { items.push(this.queue[i]); } + this.queue = _.drop(this.queue, count); + return items; + } + + empty() { + return this.queue.length === 0; + } + + push(value) { + this.queue.push(value); + if (this.waiting.length && this.waiting[0].count <= this.queue.length) { + const items = []; + for (let i = 0; i < this.waiting[0].count; i++) { items.push(this.queue[i]); } + this.queue = _.drop(this.queue, this.waiting[0].count); + this.waiting[0].resolve(items); + this.waiting.shift(); + } + } +} + +export function compilerText(stdout: string, stderr: string) { + const ret = []; + if (!EMPTY_STR.test(stdout)) ret.push(stdout); + if (!EMPTY_STR.test(stderr)) ret.push(stderr); + ret.push('自豪的采用 HydroJudge 进行评测(github.com/hydro-dev/HydroJudge)'); + return ret.join('\n'); +} + +export function copyInDir(dir) { + const files = {}; + if (fs.existsSync(dir)) { + fs.readdirSync(dir).forEach((f1) => { + const p1 = `${dir}/${f1}`; + if (fs.statSync(p1).isDirectory()) { + fs.readdirSync(p1).forEach((f2) => { + files[`${f1}/${f2}`] = { src: `${dir}/${f1}/${f2}` }; + }); + } else files[f1] = { src: `${dir}/${f1}` }; + }); + } + return files; +} + +export function restrictFile(p: string) { + if (!p) return '/'; + if (p[0] === '/') p = ''; + return p.replace(/\.\./gmi, ''); +} + +export function ensureFile(folder: string) { + return (file: string, message: string) => { + const f = path.join(folder, restrictFile(file)); + if (!fs.existsSync(f)) throw new FormatError(message + file); + const stat = fs.statSync(f); + if (!stat.isFile()) throw new FormatError(message + file); + return f; + }; +} diff --git a/packages/hydrojudge/tsconfig.json b/packages/hydrojudge/tsconfig.json new file mode 100644 index 000000000..ee0b5120f --- /dev/null +++ b/packages/hydrojudge/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "outDir": ".", + "rootDir": ".", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "allowJs": false, + "resolveJsonModule": true + }, + "include": [ + "service.ts", + "src" + ], + "paths": { + "src/*": [ + "dist/*" + ] + }, + "exclude": [] +} \ No newline at end of file diff --git a/packages/hydrooj/src/handler/domain.ts b/packages/hydrooj/src/handler/domain.ts index 4248939bf..cba8b2e75 100644 --- a/packages/hydrooj/src/handler/domain.ts +++ b/packages/hydrooj/src/handler/domain.ts @@ -193,12 +193,13 @@ class DomainJoinApplicationsHandler extends ManageHandler { async get() { const r = await domain.getRoles(this.domain); const roles = r.map((role) => role._id).sort(); - const rolesWithText = roles.map((role) => [role, role]); - const joinSettings = domain.getJoinSettings(this.domain, roles); - const expirations = { ...domain.JOIN_EXPIRATION_RANGE }; - if (!joinSettings) delete expirations[domain.JOIN_EXPIRATION_KEEP_CURRENT]; + this.response.body.rolesWithText = roles.map((role) => [role, role]); + this.response.body.joinSettings = domain.getJoinSettings(this.domain, roles); + this.response.body.expirations = { ...domain.JOIN_EXPIRATION_RANGE }; + if (!this.response.body.joinSettings) { + delete this.response.body.expirations[domain.JOIN_EXPIRATION_KEEP_CURRENT]; + } this.response.template = 'domain_join_applications.html'; - this.response.body = { rolesWithText, joinSettings, expirations }; } @param('method', Types.UnsignedInt) @@ -243,7 +244,12 @@ class DomainJoinHandler extends Handler { @param('code', Types.String, true) async get(domainId: string, code: string) { this.response.template = 'domain_join.html'; - this.response.body = { joinSettings: this.joinSettings, code }; + this.response.body.joinSettings = this.joinSettings; + this.response.body.code = code; + this.response.body.path = [ + ['Hydro', 'homepage'], + ['domain_join', 'domain_join', { domainId, code }], + ]; } @param('code', Types.String, true) diff --git a/packages/hydrooj/src/handler/judge.ts b/packages/hydrooj/src/handler/judge.ts index 5e0d69d6f..ecd791a8f 100644 --- a/packages/hydrooj/src/handler/judge.ts +++ b/packages/hydrooj/src/handler/judge.ts @@ -1,4 +1,4 @@ -import { ObjectID, PushOperator, MatchKeysAndValues } from 'mongodb'; +import { ObjectID } from 'mongodb'; import { JudgeResultBody, Rdoc, TestCase } from '../interface'; import * as record from '../model/record'; import * as problem from '../model/problem'; diff --git a/packages/login-with-osu/package.json b/packages/login-with-osu/package.json index 110532027..8806b0058 100644 --- a/packages/login-with-osu/package.json +++ b/packages/login-with-osu/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/login-with-osu", - "version": "0.0.1", + "version": "0.0.2", "main": "package.json", "repository": "git@github.com:hydro-dev/Hydro.git", "author": "undefined ", diff --git a/tsconfig.json b/tsconfig.json index 6215b71af..15342665a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ }, "references": [ { "path": "packages/hydrooj" }, + { "path": "packages/hydrojudge" }, { "path": "packages/geoip" }, { "path": "packages/login-with-github" }, { "path": "packages/login-with-google" },