mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
hydrojudge intergration
This commit is contained in:
1 parent
2a09213f5d
commit
6b88570bd6
24 files changed
+1719
-9
No files matched your search
+1
-1
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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}
|
||||
@@ -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<any>, 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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<CheckResult>;
|
||||
|
||||
const checkers: Record<string, Checker> = {
|
||||
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;
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 <iostream>
|
||||
using namespace std;
|
||||
int i=1;
|
||||
int main(){
|
||||
char a[1048576]={'1'};
|
||||
cout<<" "<<i<<flush;
|
||||
i++;
|
||||
if (i>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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 <masnn0@outlook.com>",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"references": [
|
||||
{ "path": "packages/hydrooj" },
|
||||
{ "path": "packages/hydrojudge" },
|
||||
{ "path": "packages/geoip" },
|
||||
{ "path": "packages/login-with-github" },
|
||||
{ "path": "packages/login-with-google" },
|
||||
|
||||
Reference in new issue
Block a user