├── .npmignore ├── TODO ├── .github ├── FUNDING.yml ├── workflows │ └── docker.yml └── upload.sh ├── .gitignore ├── judge ├── hosts │ ├── index.js │ ├── uoj.js │ ├── hydro.js │ └── vj4.js ├── judge │ ├── index.js │ ├── submit_answer.js │ ├── remotejudge.js │ ├── run.js │ ├── interactive.js │ └── default.js ├── checkers │ ├── index.js │ ├── qduoj.js │ ├── hustoj.js │ ├── lemon.js │ ├── syzoj.js │ ├── testlib.js │ └── default.js ├── remotejudge │ ├── index.js │ ├── vj4.js │ ├── vjudge.js │ └── hustoj.js ├── tmpfs.js ├── entrypoint.js ├── updater.js ├── signals.js ├── log.js ├── check.js ├── status.js ├── error.js ├── case │ ├── ini.js │ ├── conf.js │ ├── yaml.js │ └── auto.js ├── compile.js ├── cases.js ├── config.js ├── daemon.js ├── utils.js ├── sysinfo.js ├── sandbox.js └── data │ └── languages.json ├── .dockerignore ├── examples ├── judge.yaml ├── testdata.ini ├── testdata_remotejudge.yaml ├── langs.slim.yaml ├── langs.yaml └── testdata.yaml ├── .vscode └── settings.json ├── Dockerfile ├── default.Dockerfile ├── Dockerfile ├── alpine.Dockerfile └── slim.Dockerfile ├── tsconfig.json ├── docs ├── zh │ ├── RemoteJudge.md │ └── Testdata.md └── en │ ├── RemoteJudge.md │ ├── Testdata.md │ └── README.md ├── webpack.config.js ├── .eslintrc.js ├── README.md ├── package.json ├── setting.yaml ├── service.js └── service.ts /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | update dockerfile -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [masnn] 2 | custom: ['https://masnn.io:38443'] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.ignore.* 4 | *.log 5 | __* 6 | .build 7 | *.hydro 8 | *.js.map 9 | *.d.ts 10 | service.js -------------------------------------------------------------------------------- /judge/hosts/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | vj4: require('./vj4'), 3 | uoj: require('./uoj'), 4 | hydro: require('./hydro'), 5 | }; 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .github 3 | .git 4 | .eslintrc.json 5 | .gitignore 6 | *.log 7 | Dockerfile 8 | *.Dockerfile 9 | docs 10 | examples 11 | README.md 12 | node_modules -------------------------------------------------------------------------------- /examples/judge.yaml: -------------------------------------------------------------------------------- 1 | hosts: 2 | localhost: 3 | type: vj4 # In [vj4, uoj, ide] default to vj4 4 | server_url: http://localhost:8888/ 5 | uname: judge 6 | password: abc123 7 | detail: true # default to true -------------------------------------------------------------------------------- /judge/judge/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: require('./default'), 3 | submit_snawer: require('./submit_answer'), 4 | interactive: require('./interactive'), 5 | remotejudge: require('./remotejudge'), 6 | run: require('./run'), 7 | }; 8 | -------------------------------------------------------------------------------- /judge/checkers/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: require('./default'), 3 | hustoj: require('./hustoj'), 4 | lemon: require('./lemon'), 5 | qduoj: require('./qduoj'), 6 | syzoj: require('./syzoj'), 7 | testlib: require('./testlib'), 8 | }; 9 | -------------------------------------------------------------------------------- /judge/remotejudge/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | vj4: require('./vj4'), 3 | vjudge: require('./vjudge'), 4 | hustoj: require('./hustoj'), 5 | xjoi: require('./hustoj').XJOI, 6 | bzoj: require('./hustoj').BZOJ, 7 | ybt: require('./hustoj').YBT, 8 | }; 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.autoFixOnSave": true, 3 | "editor.tabSize": 4, 4 | "files.encoding": "utf8", 5 | "files.autoGuessEncoding": false, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | }, 9 | "files.eol": "\n" 10 | } -------------------------------------------------------------------------------- /examples/testdata.ini: -------------------------------------------------------------------------------- 1 | 10 2 | 1.in|1.out|1|10|128000 3 | 2.in|2.out|1|10|128000 4 | 3.in|3.out|1|10|128000 5 | 4.in|4.out|1|10|128000 6 | 5.in|5.out|1|10|128000 7 | 6.in|6.out|1|10|128000 8 | 7.in|7.out|1|10|128000 9 | 8.in|8.out|1|10|128000 10 | 9.in|9.out|1|10|128000 11 | 10.in|10.out|1|10|128000 -------------------------------------------------------------------------------- /judge/tmpfs.js: -------------------------------------------------------------------------------- 1 | const child = require('child_process'); 2 | const fs = require('fs-extra'); 3 | 4 | function mount(path, size = '32m') { 5 | fs.ensureDirSync(path); 6 | child.execSync(`mount tmpfs ${path} -t tmpfs -o size=${size}`); 7 | } 8 | 9 | function umount(path) { 10 | child.execSync(`umount ${path}`); 11 | } 12 | 13 | module.exports = { mount, umount }; 14 | -------------------------------------------------------------------------------- /examples/testdata_remotejudge.yaml: -------------------------------------------------------------------------------- 1 | type: remotejudge 2 | 3 | # 已经支持的类型有:vj4, vjudge, hustoj, xjoi(学军), bzoj, ybt(信奥一本通OJ) 4 | server_type: vj4 5 | # 服务器地址 6 | server_url: https://vijos.org 7 | 8 | # 用于提交程序的账号 9 | username: root 10 | password: rootroot 11 | 12 | 13 | # 对于vj4按照 domain_id/pid 格式填写。 eg: system/1000 14 | # 对于vjudge按照 host_id/pid 格式填写。 eg: CodeForces/33A 15 | # 其他类型之间填写题号即可。 16 | pid: system/1000 -------------------------------------------------------------------------------- /Dockerfile/default.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hydrooj/judge:latest 2 | RUN apt-get update && \ 3 | apt-get install -y \ 4 | gcc g++ rustc \ 5 | python python3 \ 6 | fp-compiler \ 7 | openjdk-8-jdk-headless \ 8 | php7.0-cli \ 9 | haskell-platform \ 10 | libjavascriptcoregtk-4.0-bin \ 11 | golang ruby \ 12 | mono-runtime mono-mcs && \ 13 | rm -rf /var/lib/apt/lists/* 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": ".", 6 | "rootDir": ".", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "allowJs": false, 12 | "resolveJsonModule": true 13 | }, 14 | "include": [ 15 | "service.ts" 16 | ], 17 | "exclude": [] 18 | } -------------------------------------------------------------------------------- /docs/zh/RemoteJudge.md: -------------------------------------------------------------------------------- 1 | ## 如何使用 RemoteJudge 功能 2 | 题目的样例配置文件 `config.yaml` 已放置于 [testdata_remotejudge.yaml](../../examples/testdata_remotejudge.yaml)。 3 | 如果 `username` 或 `password` 项没有填写,用户需要自行指定评测用账户。 4 | 5 | 您可以在您的代码中使用以下注释来传入参数: `` 6 | 目前为止,可用的参数共 `username` `password` `token` `language` 四个。 7 | 8 | 使用您自己的账号登录: ` ` 或 `` 9 | 使用 `` 来指定您使用的语言(通常用于语言列表中没有出现但远端OJ提供了支持的语言)。 10 | -------------------------------------------------------------------------------- /Dockerfile/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stretch 2 | WORKDIR /app 3 | COPY Judge_linux_amd64 /app/ 4 | RUN apt-get update && \ 5 | apt-get install -y unzip curl wget && \ 6 | mkdir /config /cache && \ 7 | chmod +x /app/hydrojudge-linux && \ 8 | curl -sSL https://raw.githubusercontent.com/hydro-dev/HydroJudge/master/examples/langs.yaml >/config/langs.yaml 9 | VOLUME [ "/config", "/cache"] 10 | ENV CONFIG_FILE=/config/config.yaml LANGS_FILE=/config/langs.yaml CACHE_DIR=/cache FILES_DIR=/files 11 | CMD "/app/hydrojudge-linux" 12 | -------------------------------------------------------------------------------- /docs/en/RemoteJudge.md: -------------------------------------------------------------------------------- 1 | ## How to use RemoteJudge 2 | The sample config.yaml is at [testdata_remotejudge.md](../examples/testdata_remotejudge.yaml). 3 | If `username` or `password` field is empty, users have to use their account to submit. 4 | 5 | You can write a comment in your code in the format `` to pass options. 6 | Until now, `username` `password` `token` `language` options are valid. 7 | 8 | Login to your account using ` ` or just submit ``. 9 | Sepecific Your Language using `` (If remote oj supports). -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin'); 3 | 4 | const config = { 5 | mode: 'production', 6 | entry: { 7 | judge: './judge/daemon.js', 8 | entrypoint: './judge/entrypoint.js', 9 | service: './service.js', 10 | }, 11 | output: { 12 | filename: '[name].js', 13 | path: `${__dirname}/dist`, 14 | }, 15 | target: 'node', 16 | module: {}, 17 | plugins: [ 18 | new webpack.ProgressPlugin(), 19 | new FriendlyErrorsWebpackPlugin(), 20 | ], 21 | }; 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /Dockerfile/alpine.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:slim-10 2 | WORKDIR /app 3 | COPY judge.js ExecutorServer /app/ 4 | RUN apk update && \ 5 | apk add unzip curl wget libc6-compat && \ 6 | mkdir /config /cache && \ 7 | chmod +x executorserver && \ 8 | curl -sSL https://raw.githubusercontent.com/hydro-dev/HydroJudge/master/examples/langs.yaml >/config/langs.yaml && \ 9 | rm -rf /var/cache/apk/* 10 | VOLUME [ "/config", "/cache" ] 11 | ENV CONFIG_FILE=/config/config.yaml LANGS_FILE=/config/langs.yaml CACHE_DIR=/cache FILES_DIR=/files 12 | ENV START_EXECUTOR_SERVER=/app/executorserver EXECUTOR_SERVER_ARGS="--silent" 13 | CMD ["node", "judge.js"] 14 | -------------------------------------------------------------------------------- /judge/entrypoint.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | 5 | if (os.type() === 'Linux' && os.arch() === 'x64') { 6 | if (__dirname.startsWith('/snapshot')) { 7 | const e = fs.readFileSync(path.join(__dirname, '..', 'executorserver')); 8 | fs.writeFileSync(path.resolve(os.tmpdir(), 'executorserver'), e, { mode: 755 }); 9 | process.env.START_EXECUTOR_SERVER = path.resolve(os.tmpdir(), 'executorserver'); 10 | } else process.env.START_EXECUTOR_SERVER = path.join(__dirname, '..', 'executorserver'); 11 | process.env.EXECUTOR_SERVER_ARGS = '--silent'; 12 | } 13 | require('./daemon')(); 14 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: DockerBuild 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '13.x' 15 | - run: | 16 | yarn 17 | yarn build 18 | yarn webpack 19 | 20 | - uses: actions/upload-artifact@v1 21 | with: 22 | name: judge.js 23 | path: dist/judge.js 24 | - uses: actions/upload-artifact@v1 25 | with: 26 | name: entrypoint.js 27 | path: dist/entrypoint.js 28 | - uses: JS-DevTools/npm-publish@v1 29 | with: 30 | token: ${{ secrets.NPM_TOKEN }} 31 | -------------------------------------------------------------------------------- /judge/judge/submit_answer.js: -------------------------------------------------------------------------------- 1 | const { STATUS_JUDGING } = require('../status'); 2 | const { check } = require('../check'); 3 | 4 | exports.judge = async function judge({ 5 | next, end, config, code, 6 | }) { 7 | next({ status: STATUS_JUDGING, progress: 0 }); 8 | const [status, score, message] = await check({ 9 | stdout: config.answer, 10 | user_stdout: code, 11 | checker_type: 'default', 12 | score: 100, 13 | detail: config.detail, 14 | }); 15 | next({ 16 | status, 17 | progress: 100, 18 | case: { 19 | status, score, time_ms: 0, memory_kb: 0, judge_text: message, 20 | }, 21 | }); 22 | end({ 23 | status, score, time_ms: 0, memory_kb: 0, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /.github/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | request(){ 4 | curl "https://vijos.org/d/hydro/$1" -b cookie -H 'User-Agent: Hydro-Registry' $2 $3 $4 $5 $6 $7 $8 $9 5 | } 6 | 7 | csrf(){ 8 | p=$* 9 | t=${p#*\"csrf_token\" value=\"} 10 | echo ${t%%\">*} 11 | } 12 | 13 | if [ ! -f "cookie" ] 14 | then 15 | curl "https://vijos.org/login" -c cookie -H 'User-Agent: Hydro-Registry' -d "uname=${upload_username}" -d "password=${upload_password}" 16 | fi 17 | 18 | p=$(request "p/${upload_id}/upload" -o /dev/null -s -w %{http_code}) 19 | if [ $p == '404' ] 20 | then 21 | CsrfToken=$(csrf $(request "p/create")) 22 | request "p/create" -d "title=${upload_name}" -d "numeric_pid=on" -d "content=${upload_id}" -d "csrf_token=${CsrfToken}" 23 | fi 24 | 25 | CsrfToken=$(csrf $(request "p/${upload_id}/upload" -s)) 26 | request "p/${upload_id}/upload" -F "file=@${upload_name}.zip" -F "csrf_token=$CsrfToken" 27 | -------------------------------------------------------------------------------- /docs/en/Testdata.md: -------------------------------------------------------------------------------- 1 | ## How to configure testdata 2 | 3 | - [Auto](#Auto) 4 | - [Using config.yaml (suggested)](#Config.yaml) 5 | - [Using config.ini (legacy)](#Config.ini) 6 | 7 | ## Auto 8 | 9 | File tree: 10 | 11 | ``` 12 | Testdata.zip 13 | | 14 | +- input0.in 15 | +- input1.in 16 | +- input2.in 17 | +- output0.out 18 | +- output1.out 19 | +- output2.out 20 | ``` 21 | 22 | Judge will use `1s, 256MB` limit as default. 23 | Filename should be as below: 24 | 25 | Input files: `([a-zA-Z0-9]*[0-9]+.in)|(input[0-9]+.txt)` 26 | Output files: `([a-zA-Z0-9]*[0-9]+.(out|ans))|(output[0-9]+.txt)` 27 | 28 | ## Config.yaml 29 | 30 | // TODO(masnn) 31 | 32 | ## Config.ini 33 | 34 | File tree: 35 | 36 | ``` 37 | Testdata.zip 38 | | 39 | +- Config.ini 40 | +- Input 41 | | +- input0.in 42 | | +- input1.in 43 | | +- input2.in 44 | +- Output 45 | +- output0.out 46 | +- output1.out 47 | +- output2.out 48 | ``` 49 | -------------------------------------------------------------------------------- /Dockerfile/slim.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hydrooj/judge:alpine 2 | ENV FPC_VERSION="3.0.4" \ 3 | FPC_ARCH="x86_64-linux" 4 | 5 | RUN apk add gcc g++ && \ 6 | curl -sSL https://raw.githubusercontent.com/hydro-dev/HydroJudge/master/examples/langs.slim.yaml >/config/langs.yaml && \ 7 | apk add --no-cache binutils && \ 8 | cd /tmp && \ 9 | wget "ftp://ftp.hu.freepascal.org/pub/fpc/dist/${FPC_VERSION}/${FPC_ARCH}/fpc-${FPC_VERSION}.${FPC_ARCH}.tar" -O fpc.tar && \ 10 | tar xf "fpc.tar" && \ 11 | cd "fpc-${FPC_VERSION}.${FPC_ARCH}" && \ 12 | rm demo* doc* && \ 13 | echo -e '/usr\nN\nN\nN\n' | sh ./install.sh && \ 14 | find "/usr/lib/fpc/${FPC_VERSION}/units/${FPC_ARCH}/" -type d -mindepth 1 -maxdepth 1 \ 15 | -not -name 'fcl-base' \ 16 | -not -name 'rtl' \ 17 | -not -name 'rtl-console' \ 18 | -not -name 'rtl-objpas' \ 19 | -exec rm -r {} \; && \ 20 | rm -r "/tmp/"* && \ 21 | rm -rf /var/cache/apk/* 22 | -------------------------------------------------------------------------------- /judge/updater.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const log = require('./log'); 3 | const { version } = require('../package.json'); 4 | 5 | const remote = 'https://cdn.jsdelivr.net/gh/hydro-dev/HydroJudge/package.json'; 6 | 7 | (async () => { 8 | let hasUpgrade = 0; let 9 | rversion = null; 10 | const response = await axios.get(remote).catch((e) => { 11 | if (!hasUpgrade) hasUpgrade = e.code; 12 | }); 13 | if (response) { 14 | rversion = response.data.version; 15 | if (!rversion) rversion = JSON.parse(response.data).version; 16 | if (rversion !== version) hasUpgrade = 1; 17 | } 18 | if (hasUpgrade === 1) { 19 | log.log(`正在运行 ${version}, 最新版本为 ${rversion}`); 20 | } else if (typeof hasUpgrade === 'string') { 21 | log.warn('检查更新时发生了错误。', hasUpgrade); 22 | } else { 23 | log.log('没有可用更新。'); 24 | } 25 | })().catch((e) => { 26 | log.error('Cannot check update:', e); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/langs.slim.yaml: -------------------------------------------------------------------------------- 1 | c: 2 | type: compiler 3 | compile: /usr/bin/gcc -O2 -Wall -std=c99 -o ${name} foo.c -lm 4 | code_file: foo.c 5 | execute: ${dir}/${name} 6 | cc: 7 | type: compiler 8 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 9 | code_file: foo.cc 10 | execute: ${dir}/${name} 11 | cc98: 12 | type: compiler 13 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 14 | code_file: foo.cc 15 | execute: ${dir}/${name} 16 | cc11: 17 | type: compiler 18 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 19 | code_file: foo.cc 20 | execute: ${dir}/${name} 21 | cc17: 22 | type: compiler 23 | compile: /usr/bin/g++ -O2 -Wall -std=c++17 -o ${name} foo.cc -lm 24 | code_file: foo.cc 25 | execute: ${dir}/${name} 26 | pas: 27 | type: compiler 28 | compile: /usr/bin/fpc -O2 -o${dir}/${name} foo.pas 29 | code_file: foo.pas 30 | execute: ${dir}/${name} -------------------------------------------------------------------------------- /judge/checkers/qduoj.js: -------------------------------------------------------------------------------- 1 | /* 2 | * argv[1]:输入 3 | * argv[2]:选手输出 4 | * exit code:返回判断结果 5 | */ 6 | const fs = require('fs-extra'); 7 | const { run } = require('../sandbox'); 8 | const { STATUS_ACCEPTED, STATUS_WRONG_ANSWER } = require('../status'); 9 | const _compile = require('../compile'); 10 | 11 | const fsp = fs.promises; 12 | 13 | async function check(config) { 14 | const { status, stdout } = await run('${dir}/checker input usrout', { 15 | copyIn: { 16 | usrout: { src: config.user_stdout }, 17 | input: { src: config.input }, 18 | }, 19 | }); 20 | const st = (status === STATUS_ACCEPTED) ? STATUS_ACCEPTED : STATUS_WRONG_ANSWER; 21 | return { status: st, score: (st === STATUS_ACCEPTED) ? config.score : 0, message: stdout }; 22 | } 23 | 24 | async function compile(checker, copyIn) { 25 | const file = await fsp.readFile(checker); 26 | return _compile(checker.split('.')[1], file, 'checker', copyIn); 27 | } 28 | 29 | module.exports = { check, compile }; 30 | -------------------------------------------------------------------------------- /judge/signals.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | '', // 0 3 | 'Hangup', // 1 4 | 'Interrupt', // 2 5 | 'Quit', // 3 6 | 'Illegal instruction', // 4 7 | 'Trace/breakpoint trap', // 5 8 | 'Aborted', // 6 9 | 'Bus error', // 7 10 | 'Floating point exception', // 8 11 | 'Killed', // 9 12 | 'User defined signal 1', // 10 13 | 'Segmentation fault', // 11 14 | 'User defined signal 2', // 12 15 | 'Broken pipe', // 13 16 | 'Alarm clock', // 14 17 | 'Terminated', // 15 18 | 'Stack fault', // 16 19 | 'Child exited', // 17 20 | 'Continued', // 18 21 | 'Stopped (signal)', // 19 22 | 'Stopped', // 20 23 | 'Stopped (tty input)', // 21 24 | 'Stopped (tty output)', // 22 25 | 'Urgent I/O condition', // 23 26 | 'CPU time limit exceeded', // 24 27 | 'File size limit exceeded', // 25 28 | 'Virtual timer expired', // 26 29 | 'Profiling timer expired', // 27 30 | 'Window changed', // 28 31 | 'I/O possible', // 29 32 | 'Power failure', // 30 33 | 'Bad system call', // 31 34 | ]; 35 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | commonjs: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly', 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 2018, 16 | }, 17 | rules: { 18 | indent: ['warn', 4], 19 | 'no-plusplus': 'off', 20 | 'no-underscore-dangle': 'off', 21 | 'no-console': 'off', 22 | 'no-extend-native': 'off', 23 | 'no-restricted-syntax': 'off', 24 | 'max-classes-per-file': 'off', 25 | radix: 'off', 26 | 'guard-for-in': 'off', 27 | 'no-param-reassign': 'off', 28 | 'global-require': 'off', 29 | 'no-multi-assign': 'off', 30 | 'consistent-return': 'off', 31 | 'no-template-curly-in-string': 'off', 32 | 'no-return-await': 'off', 33 | 'prefer-destructuring': 'off', 34 | camelcase: 'off', 35 | 'no-shadow': 'off', 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /judge/log.js: -------------------------------------------------------------------------------- 1 | function wrap(func) { 2 | return (...args) => { 3 | const dt = (new Date()).toString(); 4 | func(dt, ...args); 5 | }; 6 | } 7 | 8 | class Logger { 9 | constructor() { 10 | this.log = wrap(console.log, 'log'); 11 | this.error = wrap(console.error, 'error'); 12 | this.info = wrap(console.info, 'info'); 13 | this.warn = wrap(console.warn, 'warn'); 14 | this.debug = wrap(console.debug, 'debug'); 15 | this.submission = (id, payload = {}) => { 16 | console.log(`${new Date()} ${id}`, payload); 17 | }; 18 | } 19 | 20 | logger(logger) { 21 | this.log = wrap(logger.log, 'log'); 22 | this.error = wrap(logger.error, 'error'); 23 | this.info = wrap(logger.info, 'info'); 24 | this.warn = wrap(logger.warn, 'warn'); 25 | this.debug = wrap(logger.debug, 'debug'); 26 | this.submission = (id, payload = {}) => { 27 | logger.log(`${new Date()} ${id}`, payload); 28 | }; 29 | } 30 | } 31 | 32 | module.exports = new Logger(); 33 | -------------------------------------------------------------------------------- /judge/checkers/hustoj.js: -------------------------------------------------------------------------------- 1 | /* 2 | * argv[1]:输入 3 | * argv[2]:标准输出 4 | * argv[3]:选手输出 5 | * exit code:返回判断结果 6 | */ 7 | const fs = require('fs-extra'); 8 | const { run } = require('../sandbox'); 9 | const { STATUS_ACCEPTED, STATUS_WRONG_ANSWER } = require('../status'); 10 | const _compile = require('../compile'); 11 | 12 | const fsp = fs.promises; 13 | 14 | async function check(config) { 15 | const { code, stdout } = await run('${dir}/checker input stdout usrout', { 16 | copyIn: { 17 | usrout: { src: config.user_stdout }, 18 | stdout: { src: config.output }, 19 | input: { src: config.input }, 20 | }, 21 | }); 22 | const status = code ? STATUS_WRONG_ANSWER : STATUS_ACCEPTED; 23 | const message = (await fsp.readFile(stdout)).toString(); 24 | return { status, score: (status === STATUS_ACCEPTED) ? config.score : 0, message }; 25 | } 26 | 27 | async function compile(checker, copyIn) { 28 | const file = await fsp.readFile(checker); 29 | return _compile(checker.split('.')[1], file, 'checker', copyIn); 30 | } 31 | 32 | module.exports = { check, compile }; 33 | -------------------------------------------------------------------------------- /judge/check.js: -------------------------------------------------------------------------------- 1 | const checkers = require('./checkers'); 2 | const { SystemError } = require('./error'); 3 | 4 | async function check(config) { 5 | if (!checkers[config.checker_type]) throw new SystemError(`未知比较器类型:${config.checker_type}`); 6 | const { 7 | code, status, score, message, 8 | } = await checkers[config.checker_type].check({ 9 | input: config.stdin, 10 | output: config.stdout, 11 | user_stdout: config.user_stdout, 12 | user_stderr: config.user_stderr, 13 | score: config.score, 14 | copyIn: config.copyIn || {}, 15 | detail: config.detail, 16 | }); 17 | if (code) throw new SystemError(`比较器返回了非零值:${code}`); 18 | return [status, score, message]; 19 | } 20 | async function compileChecker(checkerType, checker, copyIn) { 21 | if (!checkers[checkerType]) { throw new SystemError(`未知比较器类型:${checkerType}`); } 22 | const { code, status, message } = await checkers[checkerType].compile(checker, copyIn); 23 | if (code) throw new SystemError(`比较器编译失败:${code}`); 24 | return [status, message]; 25 | } 26 | 27 | module.exports = { check, compileChecker }; 28 | -------------------------------------------------------------------------------- /judge/checkers/lemon.js: -------------------------------------------------------------------------------- 1 | /* 2 | argv[1]:输入文件 3 | argv[2]:选手输出文件 4 | argv[3]:标准输出文件 5 | argv[4]:单个测试点分值 6 | argv[5]:输出最终得分的文件 7 | argv[6]:输出错误报告的文件 8 | */ 9 | 10 | const fsp = require('fs').promises; 11 | const { run } = require('../sandbox'); 12 | const { STATUS_ACCEPTED, STATUS_WRONG_ANSWER } = require('../status'); 13 | const _compile = require('../compile'); 14 | 15 | async function check(config) { 16 | const { files } = await run(`\${dir}/checker input usrout stdout ${config.score} score message`, { 17 | copyIn: { 18 | usrout: { src: config.user_stdout }, 19 | stdout: { src: config.output }, 20 | input: { src: config.input }, 21 | }, 22 | copyOut: ['score', 'message'], 23 | }); 24 | const { message } = files; 25 | const score = parseInt(files.score); 26 | return { 27 | score, 28 | message, 29 | status: score === config.score ? STATUS_ACCEPTED : STATUS_WRONG_ANSWER, 30 | }; 31 | } 32 | 33 | async function compile(checker, copyIn) { 34 | const file = await fsp.readFile(checker); 35 | return _compile(checker.split('.')[1], file, 'checker', copyIn); 36 | } 37 | 38 | module.exports = { check, compile }; 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moved to [Hydro](https://github.com/hydro-dev/Hydro) 2 | 3 | # Judge Daemon 4 | 5 | [English](docs/en/README.md) 6 | 7 | ## 介绍 8 | HydroJudge 是一个用于信息学算法竞赛的高效评测后端。 9 | 和之前的版本相比,HydroJudge 支持了自定义比较器、子任务、交互器等多种新特性。 10 | 11 | ## 帮助中心 12 | 13 | - [RemoteJudge](docs/zh/RemoteJudge.md) 14 | 15 | ## 安装与使用 16 | 17 | 前置需求: 18 | 19 | - Linux 4.4+ 20 | - NodeJS 10+ 21 | 22 | 下载本仓库,并切换到仓库目录。 23 | 24 | ```sh 25 | npm install -g yarn # 如果已经安装yarn请跳过该步骤 26 | yarn 27 | ``` 28 | 29 | 创建设置目录 `~/.config/hydro` ,并放置 `judge.yaml` ,配置文件格式详见 [examples/judge.yaml](examples/judge.yaml) 30 | 启动 [go-sandbox](https://github.com/criyle/go-judge),监听端口5050。 31 | 您应当以 root 身份运行。 32 | 33 | ```sh 34 | node judge/daemon.js 35 | ``` 36 | 37 | ## 设置 38 | 39 | - 自定义配置文件位置: `--config=/path/to/config` 40 | - 自定义语言文件位置: `--langs=/path/to/langs` 41 | - 自定义临时目录: `--tmp=/path/to/tmp` 42 | - 自定义缓存目录: `--cache=/path/to/cache` 43 | - 自定义文件目录: `--files=/path/to/files` 44 | - 自定义沙箱地址: `--execute=http://executionhost/` 45 | 46 | ## 测试数据格式 47 | 48 | [测试数据格式](docs/zh/Testdata.md) 49 | 50 | 在压缩包中添加 config.yaml (无此文件表示自动识别,默认1s, 256MB)。 51 | 见 [测试数据格式](examples/testdata.yaml) 52 | 53 | 为旧版评测机设计的数据包仍然可用。 54 | 针对 problem.conf 的兼容性测试仍在进行中。 55 | -------------------------------------------------------------------------------- /judge/status.js: -------------------------------------------------------------------------------- 1 | const STATUS = { 2 | STATUS_ACCEPTED: 1, 3 | STATUS_WRONG_ANSWER: 2, 4 | STATUS_TIME_LIMIT_EXCEEDED: 3, 5 | STATUS_MEMORY_LIMIT_EXCEEDED: 4, 6 | STATUS_RUNTIME_ERROR: 6, 7 | STATUS_COMPILE_ERROR: 7, 8 | STATUS_SYSTEM_ERROR: 8, 9 | STATUS_JUDGING: 20, 10 | STATUS_COMPILING: 21, 11 | STATUS_IGNORED: 30, 12 | }; 13 | 14 | const STATUS_TEXTS = { 15 | [STATUS.STATUS_WAITING]: 'Waiting', 16 | [STATUS.STATUS_ACCEPTED]: 'Accepted', 17 | [STATUS.STATUS_WRONG_ANSWER]: 'Wrong Answer', 18 | [STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'Time Exceeded', 19 | [STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'Memory Exceeded', 20 | [STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED]: 'Output Exceeded', 21 | [STATUS.STATUS_RUNTIME_ERROR]: 'Runtime Error', 22 | [STATUS.STATUS_COMPILE_ERROR]: 'Compile Error', 23 | [STATUS.STATUS_SYSTEM_ERROR]: 'System Error', 24 | [STATUS.STATUS_CANCELED]: 'Cancelled', 25 | [STATUS.STATUS_ETC]: 'Unknown Error', 26 | [STATUS.STATUS_JUDGING]: 'Running', 27 | [STATUS.STATUS_COMPILING]: 'Compiling', 28 | [STATUS.STATUS_FETCHED]: 'Fetched', 29 | [STATUS.STATUS_IGNORED]: 'Ignored', 30 | }; 31 | 32 | module.exports = { ...STATUS, STATUS_TEXTS }; 33 | -------------------------------------------------------------------------------- /judge/checkers/syzoj.js: -------------------------------------------------------------------------------- 1 | /* 2 | * in:输入 3 | * user_out:选手输出 4 | * answer:标准输出 5 | * code:选手代码 6 | * stdout:输出最终得分 7 | * stderr:输出错误报告 8 | */ 9 | const fs = require('fs-extra'); 10 | const { run } = require('../sandbox'); 11 | const { SystemError } = require('../error'); 12 | const { STATUS_ACCEPTED, STATUS_WRONG_ANSWER } = require('../status'); 13 | const _compile = require('../compile'); 14 | 15 | const fsp = fs.promises; 16 | 17 | async function check(config) { 18 | // eslint-disable-next-line prefer-const 19 | let { status, stdout, stderr } = await run('${dir}/checker', { 20 | copyIn: { 21 | in: { src: config.input }, 22 | user_out: { src: config.user_stdout }, 23 | answer: { src: config.output }, 24 | code: { content: config.code }, 25 | }, 26 | }); 27 | if (status !== STATUS_ACCEPTED) throw new SystemError('Checker returned a non-zero value', [status]); 28 | const score = parseInt(stdout); 29 | status = score === config.score ? STATUS_ACCEPTED : STATUS_WRONG_ANSWER; 30 | return { status, score, message: stderr }; 31 | } 32 | 33 | async function compile(checker, copyIn) { 34 | const file = await fsp.readFile(checker); 35 | return _compile(checker.split('.')[1], file, 'checker', copyIn); 36 | } 37 | 38 | module.exports = { check, compile }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hydrooj/hydrojudge", 3 | "version": "1.4.5", 4 | "main": "judge/daemon.js", 5 | "author": "masnn", 6 | "os": [ 7 | "linux" 8 | ], 9 | "dependencies": { 10 | "axios": "^0.19.0", 11 | "bson": "^4.0.4", 12 | "fs-extra": "^9.0.1", 13 | "js-yaml": "^3.14.0", 14 | "lodash": "^4.17.19", 15 | "minimist": "^1.2.5", 16 | "p-queue": "^6.6.0", 17 | "shell-quote": "^1.7.2", 18 | "systeminformation": "^4.26.10", 19 | "ws": "^7.3.1" 20 | }, 21 | "license": "GPL-3.0-only", 22 | "devDependencies": { 23 | "@types/bson": "^4.0.2", 24 | "@types/fs-extra": "^9.0.1", 25 | "@types/js-yaml": "^3.12.5", 26 | "@types/node": "^14.0.27", 27 | "eslint": "^7.5.0", 28 | "eslint-config-airbnb-base": "^14.2.0", 29 | "eslint-plugin-import": "^2.22.0", 30 | "hydrooj": "^2.10.12", 31 | "typescript": "^3.9.7", 32 | "webpack": "^4.44.1", 33 | "webpack-cli": "^3.3.12" 34 | }, 35 | "bin": { 36 | "judge": "judge/entrypoint.js" 37 | }, 38 | "scripts": { 39 | "webpack": "webpack --config webpack.config.js", 40 | "pack": "pkg .", 41 | "build": "tsc", 42 | "lint": "eslint judge --fix" 43 | }, 44 | "pkg": { 45 | "scripts": [ 46 | "judge/**.js", 47 | "judge/**/**.js" 48 | ], 49 | "assets": [ 50 | "executorserver", 51 | "examples/*" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /judge/error.js: -------------------------------------------------------------------------------- 1 | const { STATUS_TEXTS } = require('./status'); 2 | 3 | class CompileError extends Error { 4 | constructor(obj) { 5 | super('Compile Error'); 6 | if (typeof obj === 'string') { 7 | this.stdout = obj; 8 | this.stderr = ''; 9 | } else { 10 | this.stdout = obj.stdout || ''; 11 | this.stderr = obj.stderr || ''; 12 | this.status = obj.status ? STATUS_TEXTS[obj.status] || '' : ''; 13 | } 14 | this.type = 'CompileError'; 15 | } 16 | } 17 | 18 | class FormatError extends Error { 19 | constructor(message, params = []) { 20 | super(message); 21 | this.type = 'FormatError'; 22 | this.params = params; 23 | } 24 | } 25 | 26 | class RuntimeError extends Error { 27 | constructor(detail, message) { 28 | super(message); 29 | this.type = 'RuntimeError'; 30 | this.detail = detail; 31 | } 32 | } 33 | 34 | class SystemError extends Error { 35 | constructor(message, params = []) { 36 | super(message); 37 | this.type = 'SystemError'; 38 | this.params = params; 39 | } 40 | } 41 | 42 | class TooFrequentError extends Error { 43 | constructor(message) { 44 | super(message); 45 | this.type = 'TooFrequentError'; 46 | } 47 | } 48 | 49 | module.exports = { 50 | CompileError, 51 | FormatError, 52 | RuntimeError, 53 | SystemError, 54 | TooFrequentError, 55 | }; 56 | -------------------------------------------------------------------------------- /judge/checkers/testlib.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { run } = require('../sandbox'); 4 | const { FILES_DIR } = require('../config'); 5 | const { STATUS_ACCEPTED, STATUS_WRONG_ANSWER } = require('../status'); 6 | const _compile = require('../compile'); 7 | 8 | const fsp = fs.promises; 9 | 10 | async function check(config) { 11 | const { stdout, stderr } = await run('${dir}/checker ${dir}/in ${dir}/user_out ${dir}/answer', { 12 | copyIn: { 13 | in: { src: config.input }, 14 | user_out: { src: config.user_stdout }, 15 | answer: { src: config.output }, 16 | }, 17 | }); 18 | return { 19 | status: stderr === 'ok \n' ? STATUS_ACCEPTED : STATUS_WRONG_ANSWER, 20 | score: stderr === 'ok \n' ? config.score : 0, 21 | message: stdout, 22 | }; 23 | } 24 | 25 | async function compileChecker(checker, copyIn) { 26 | copyIn['testlib.h'] = { src: path.resolve(FILES_DIR, 'testlib.h') }; 27 | const file = await fsp.readFile(checker); 28 | return await _compile(checker.split('.')[1], file, 'checker', copyIn); 29 | } 30 | 31 | async function compileInteractor(interactor, copyIn) { 32 | copyIn['testlib.h'] = { src: path.resolve(FILES_DIR, 'testlib.h') }; 33 | const file = await fsp.readFile(interactor); 34 | return await _compile(interactor.split('.')[1], file, 'interactor', copyIn); 35 | } 36 | 37 | module.exports = { 38 | check, 39 | compile: compileChecker, 40 | compileChecker, 41 | compileInteractor, 42 | }; 43 | -------------------------------------------------------------------------------- /judge/case/ini.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { FormatError } = require('../error'); 4 | const { ensureFile } = require('../utils'); 5 | 6 | const fsp = fs.promises; 7 | 8 | module.exports = async function readIniCases(folder) { 9 | const 10 | config = { 11 | checker_type: 'default', 12 | count: 0, 13 | subtasks: [], 14 | judge_extra_files: [], 15 | user_extra_files: [], 16 | }; 17 | const checkFile = ensureFile(folder); 18 | let configFile = (await fsp.readFile(path.resolve(folder, 'config.ini'))).toString(); 19 | configFile = configFile.split('\n'); 20 | try { 21 | const count = parseInt(configFile[0]); 22 | if (!count) throw new FormatError('line 1'); 23 | for (let i = 1; i <= count; i++) { 24 | const line = configFile[i].split('|'); 25 | if (!parseFloat(line[2]) || !parseInt(line[3])) throw new FormatError(`line ${1 + i}`); 26 | config.count++; 27 | config.subtasks.push({ 28 | score: parseInt(line[3]), 29 | time_limit_ms: parseInt(parseFloat(line[2]) * 1000), 30 | memory_limit_mb: parseInt(line[4]) / 1024 || 256, 31 | cases: [{ 32 | input: checkFile(`input/${line[0].toLowerCase()}`, '找不到输入文件 '), 33 | output: checkFile(`output/${line[1].toLowerCase()}`, '找不到输出文件 '), 34 | id: config.count, 35 | }], 36 | }); 37 | } 38 | } catch (e) { 39 | throw new FormatError('无效的 config.ini 。'); 40 | } 41 | return config; 42 | }; 43 | -------------------------------------------------------------------------------- /judge/compile.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | const fs = require('fs-extra'); 3 | const { run, del } = require('./sandbox'); 4 | const { CompileError, SystemError } = require('./error'); 5 | const log = require('./log'); 6 | const { STATUS_ACCEPTED } = require('./status'); 7 | const { compilerText } = require('./utils'); 8 | const { LANGS_FILE, LANGS } = require('./config'); 9 | 10 | let _langs = {}; 11 | try { 12 | if (LANGS) _langs = LANGS; 13 | else _langs = yaml.safeLoad(fs.readFileSync(LANGS_FILE).toString()); 14 | } catch (e) { 15 | log.error('Invalidate Language file %s', LANGS_FILE); 16 | log.error(e); 17 | if (!global.Hydro) process.exit(1); 18 | } 19 | async function compile(lang, code, target, copyIn, next) { 20 | if (!_langs[lang]) throw new SystemError(`不支持的语言:${lang}`); 21 | const info = _langs[lang]; const 22 | f = {}; 23 | if (info.type === 'compiler') { 24 | copyIn[info.code_file] = { content: code }; 25 | const { 26 | status, stdout, stderr, fileIds, 27 | } = await run( 28 | info.compile.replace(/\$\{name\}/g, target), 29 | { copyIn, copyOutCached: [target] }, 30 | ); 31 | if (status !== STATUS_ACCEPTED) throw new CompileError({ status, stdout, stderr }); 32 | if (!fileIds[target]) throw new CompileError({ stderr: '没有找到可执行文件' }); 33 | if (next) next({ compiler_text: compilerText(stdout, stderr) }); 34 | f[target] = { fileId: fileIds[target] }; 35 | return { execute: info.execute, copyIn: f, clean: () => del(fileIds[target]) }; 36 | } if (info.type === 'interpreter') { 37 | f[target] = { content: code }; 38 | return { execute: info.execute, copyIn: f, clean: () => Promise.resolve() }; 39 | } 40 | } 41 | module.exports = compile; 42 | -------------------------------------------------------------------------------- /judge/case/conf.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const assert = require('assert'); 4 | const { FormatError } = require('../error'); 5 | const readAutoCases = require('./auto'); 6 | 7 | const fsp = fs.promises; 8 | 9 | module.exports = async function readConfCases(folder, filename, { next }) { 10 | next({ judge_text: '警告:检测到 problem.conf 配置文件。暂不支持对ex测试点的评测。' }); 11 | const 12 | config = { 13 | checker_type: 'default', 14 | count: 0, 15 | subtasks: [], 16 | judge_extra_files: [], 17 | user_extra_files: [], 18 | }; 19 | const map = {}; 20 | try { 21 | const configFile = (await fsp.readFile(path.resolve(folder, 'problem.conf'))).toString().split('\n'); 22 | for (const line of configFile) { 23 | const i = line.split(' '); 24 | map[i[0]] = i[1]; // eslint-disable-line prefer-destructuring 25 | } 26 | assert(map.use_builtin_judge); 27 | assert(map.use_builtin_checker); 28 | assert(map.n_tests); 29 | assert(map.n_ex_tests); 30 | assert(map.n_sample_tests); 31 | assert(map.input_pre); 32 | assert(map.input_suf); 33 | assert(map.output_pre); 34 | assert(map.output_suf); 35 | assert(map.time_limit); 36 | assert(map.memory_limit); 37 | } catch (e) { 38 | throw new FormatError('无效的 problem.conf 文件。', [e]); 39 | } 40 | const c = await readAutoCases(folder, '', { next }); 41 | config.subtasks = c.subtasks; 42 | config.count = c.count; 43 | for (const i in config.subtasks) { 44 | config.subtasks[i].memory_limit_mb = map.memory_limit; 45 | config.subtasks[i].time_limit_ms = map.time_limit * 1000; 46 | } 47 | return config; 48 | }; 49 | -------------------------------------------------------------------------------- /judge/cases.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { FormatError } = require('./error'); 4 | const readIniCases = require('./case/ini'); 5 | const readYamlCases = require('./case/yaml'); 6 | const readAutoCases = require('./case/auto'); 7 | const readConfCases = require('./case/conf'); 8 | 9 | const map = [ 10 | ['config.ini', readIniCases], 11 | ['config.yaml', readYamlCases], 12 | ['Config.yaml', readYamlCases], 13 | ['config.yml', readYamlCases], 14 | ['Config.yml', readYamlCases], 15 | ['problem.conf', readConfCases], 16 | ]; 17 | 18 | async function readCases(folder, extra_config = {}, args) { 19 | args = args || {}; 20 | args.next = args.next || (() => { }); 21 | let config; 22 | const d = fs.readdirSync(folder); 23 | if (d.length === 2) { 24 | d.splice(d.indexOf('version'), 1); 25 | const s = fs.statSync(path.resolve(folder, d[0])); 26 | if (s.isDirectory()) return await readCases(path.resolve(folder, d[0]), extra_config, args); 27 | } 28 | if (args.config) { 29 | config = readYamlCases(folder, null, args); 30 | } else { 31 | for (const [filename, handler] of map) { 32 | if (fs.existsSync(path.resolve(folder, filename))) { 33 | // eslint-disable-next-line no-await-in-loop 34 | config = await handler(folder, filename, args); 35 | break; 36 | } 37 | } 38 | } 39 | if (!config) { 40 | args.next({ judge_text: '您没有提供题目配置文件。正在使用默认时空限制 1s 256M 。' }); 41 | config = await readAutoCases(folder, '', args); 42 | } 43 | config = Object.assign(extra_config, config); 44 | if (config.type !== 'remotejudge' && !config.count) throw new FormatError('没有找到测试数据'); 45 | return config; 46 | } 47 | 48 | module.exports = readCases; 49 | -------------------------------------------------------------------------------- /judge/judge/remotejudge.js: -------------------------------------------------------------------------------- 1 | const api = require('../remotejudge'); 2 | const { SystemError, TooFrequentError } = require('../error'); 3 | const { sleep } = require('../utils'); 4 | 5 | const RE_USERNAME = //i; 6 | const RE_PASSWORD = //i; 7 | const RE_LANGUAGE = //i; 8 | const RE_TOKEN = //i; 9 | 10 | exports.judge = async (ctx) => { 11 | let user_username; 12 | let user_password; 13 | let user_token; 14 | let data; 15 | if (RE_TOKEN.test(ctx.code)) { 16 | user_token = RE_TOKEN.exec(ctx.code)[1]; 17 | } else if (RE_USERNAME.test(ctx.code) && RE_PASSWORD.test(ctx.code)) { 18 | user_username = RE_USERNAME.exec(ctx.code)[1]; 19 | user_password = RE_PASSWORD.exec(ctx.code)[1]; 20 | } 21 | ctx.next({ judge_text: `正在使用 RemoteJudge: ${ctx.config.server_type} ${ctx.config.server_url}` }); 22 | if (RE_LANGUAGE.test(ctx.code)) ctx.lang = RE_LANGUAGE.exec(ctx.code)[1]; 23 | const remote = new api[ctx.config.server_type](ctx.config.server_url); 24 | if ((user_username && user_password) || user_token) { 25 | if (user_token) await remote.loginWithToken(user_token); 26 | else await remote.login(user_username, user_password); 27 | } else if (ctx.config.username && ctx.config.password) { 28 | await remote.login(ctx.config.username, ctx.config.password); 29 | } else throw new SystemError('无用于评测的账号'); 30 | try { 31 | data = await remote.submit(ctx.config.pid, ctx.code, ctx.lang, ctx.next); 32 | } catch (e) { 33 | if (e instanceof TooFrequentError) { 34 | ctx.next({ judge_text: '远端OJ提交过于频繁, 将在5秒后重试。' }); 35 | await sleep(5); 36 | data = await remote.submit(ctx.config.pid, ctx.code, ctx.lang, ctx.next); 37 | } else throw e; 38 | } 39 | await remote.monit(data, ctx.next, ctx.end); 40 | }; 41 | -------------------------------------------------------------------------------- /judge/checkers/default.js: -------------------------------------------------------------------------------- 1 | const { run } = require('../sandbox'); 2 | const { STATUS_ACCEPTED, STATUS_WRONG_ANSWER } = require('../status'); 3 | 4 | async function check(config) { 5 | const { stdout } = await run('/usr/bin/diff -BZ usrout stdout', { 6 | copyIn: { 7 | usrout: { src: config.user_stdout }, 8 | stdout: { src: config.output }, 9 | }, 10 | }); 11 | let status; 12 | let message = ''; 13 | if (stdout) { 14 | status = STATUS_WRONG_ANSWER; 15 | if (config.detail) { 16 | try { 17 | const pt = stdout.split('---'); 18 | const u = pt[0].split('\n')[1]; 19 | let usr = u.substr(2, u.length - 2).trim().split(' '); 20 | const t = pt[1].split('\n')[1]; 21 | let std = t.substr(2, t.length - 2).trim().split(' '); 22 | if (usr.length < std.length) message = '标准输出比选手输出长。'; 23 | else if (usr.length > std.length) message = '选手输出比标准输出长。'; 24 | else { 25 | for (const i in usr) { 26 | if (usr[i] !== std[i]) { 27 | usr = usr[i]; 28 | std = std[i]; 29 | break; 30 | } 31 | } 32 | if (usr.length > 20) usr = `${usr.substring(0, 16)}...`; 33 | if (std.length > 20) std = `${std.substring(0, 16)}...`; 34 | message = `读取到 ${usr} ,应为 ${std}`; 35 | } 36 | } catch (e) { 37 | message = stdout.substring(0, stdout.length - 1 <= 30 ? stdout.length - 1 : 30); 38 | } 39 | } 40 | } else status = STATUS_ACCEPTED; 41 | return { 42 | score: status === STATUS_ACCEPTED ? config.score : 0, 43 | status, 44 | message, 45 | }; 46 | } 47 | 48 | async function compile() { 49 | return {}; 50 | } 51 | 52 | module.exports = { check, compile }; 53 | -------------------------------------------------------------------------------- /judge/judge/run.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { 4 | STATUS_JUDGING, STATUS_COMPILING, STATUS_RUNTIME_ERROR, 5 | STATUS_TIME_LIMIT_EXCEEDED, STATUS_MEMORY_LIMIT_EXCEEDED, 6 | STATUS_ACCEPTED, 7 | } = require('../status'); 8 | const { run } = require('../sandbox'); 9 | const compile = require('../compile'); 10 | const signals = require('../signals'); 11 | 12 | exports.judge = async (ctx) => { 13 | ctx.stat.judge = new Date(); 14 | ctx.next({ status: STATUS_COMPILING }); 15 | ctx.execute = await compile(ctx.lang, ctx.code, 'code', {}, ctx.next); 16 | ctx.clean.push(ctx.execute.clean); 17 | ctx.next({ status: STATUS_JUDGING, progress: 0 }); 18 | const { copyIn } = ctx.execute; 19 | const stdin = path.resolve(ctx.tmpdir, 'stdin'); 20 | const stdout = path.resolve(ctx.tmpdir, 'stdout'); 21 | const stderr = path.resolve(ctx.tmpdir, 'stderr'); 22 | fs.writeFileSync(stdin, ctx.config.input || ''); 23 | const res = await run( 24 | ctx.execute.execute.replace(/\$\{name\}/g, 'code'), 25 | { 26 | stdin, 27 | stdout, 28 | stderr, 29 | copyIn, 30 | time_limit_ms: ctx.config.time, 31 | memory_limit_mb: ctx.config.memory, 32 | }, 33 | ); 34 | const { code, time_usage_ms, memory_usage_kb } = res; 35 | let { status } = res; 36 | if (!fs.existsSync(stdout)) fs.writeFileSync(stdout, ''); 37 | let message = ''; 38 | if (status === STATUS_ACCEPTED) { 39 | if (time_usage_ms > ctx.config.time) { 40 | status = STATUS_TIME_LIMIT_EXCEEDED; 41 | } else if (memory_usage_kb > ctx.config.memory * 1024) { 42 | status = STATUS_MEMORY_LIMIT_EXCEEDED; 43 | } 44 | } else if (code) { 45 | status = STATUS_RUNTIME_ERROR; 46 | if (code < 32) message = signals[code]; 47 | else message = `您的程序返回了 ${code}.`; 48 | } 49 | ctx.next({ 50 | status, 51 | case: { 52 | status, 53 | time_ms: time_usage_ms, 54 | memory_kb: memory_usage_kb, 55 | judge_text: message, 56 | }, 57 | }); 58 | ctx.stat.done = new Date(); 59 | ctx.next({ judge_text: JSON.stringify(ctx.stat) }); 60 | ctx.end({ 61 | status, 62 | score: status === STATUS_ACCEPTED ? 100 : 0, 63 | stdout: fs.readFileSync(stdout).toString(), 64 | stderr: fs.readFileSync(stderr).toString(), 65 | time_ms: Math.floor(time_usage_ms * 1000000) / 1000000, 66 | memory_kb: memory_usage_kb, 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /docs/zh/Testdata.md: -------------------------------------------------------------------------------- 1 | ## 如何配置测试数据 2 | 3 | - [自动配置](#Auto) 4 | - [使用 config.yaml (推荐)](#Config.yaml) 5 | - [使用 config.ini](#Config.ini) 6 | 7 | ## Auto 8 | 9 | 压缩包内文件格式如下: 10 | 11 | ``` 12 | Testdata.zip 13 | | 14 | +- input0.in 15 | +- input1.in 16 | +- input2.in 17 | +- output0.out 18 | +- output1.out 19 | +- output2.out 20 | ``` 21 | 22 | 评测机会自动获取测试包内的文件。并使用默认1s,256MB的限制。 23 | 文件命名格式如下: 24 | 25 | 输入文件:`([a-zA-Z0-9]*[0-9]+.in)|(input[0-9]+.txt)` 26 | 输出文件:`([a-zA-Z0-9]*[0-9]+.(out|ans))|(output[0-9]+.txt)` 27 | 28 | ## Config.yaml 29 | 30 | 压缩包内文件格式如下: 31 | 32 | ``` 33 | Testdata.zip 34 | | 35 | +- config.yaml 36 | +- input0.in 37 | +- input1.in 38 | +- input2.in 39 | +- output0.out 40 | +- output1.out 41 | +- output2.out 42 | ``` 43 | 44 | 如果您需要设置时空限制,请使用如下设置项: 45 | 46 | ```yaml 47 | time: 1s 48 | memory: 256m 49 | ``` 50 | 51 | 如果您需要针对测试点单独配置,请使用如下设置项: 52 | 53 | ```yaml 54 | subtasks: 55 | - score: 30 # Subtask 1 为 30 分 56 | type: min # 计分方式为 min (取测试点得分最小值) 支持 sum, max, min,此设置可忽略。默认值:min 57 | time: 1s # 时间限制 1s,此设置可忽略,默认值:1s 58 | memory: 64m # 空间限制 64m,此设置可忽略,默认值:256m 59 | cases: # 所包含的测试点列表 60 | - input: a1.in 61 | output: a1.out 62 | - input: a2.in 63 | output: a2.out 64 | - score: 70 65 | time: 0.5s 66 | memory: 32m 67 | cases: 68 | - input: b1.in 69 | output: b1.out 70 | - input: b2.in 71 | output: b2.out 72 | ``` 73 | 74 | 如果您需要自定义比较器,请使用如下设置项: 75 | 注意:使用Testlib编写比较器时,不要在文件中包含testlib.h。 76 | 77 | ```yaml 78 | checker_type: default # 比较器类型,支持的值有 default, hustoj, lemon, qduoj, syzoj, testlib,默认为default(内置比较器) 79 | checker: chk.cpp # 自定义比较器文件名 80 | ``` 81 | 82 | 如果您要在程序编译/运行过程中添加额外的文件,请使用如下设置项: 83 | 84 | ```yaml 85 | user_extra_files: 86 | - extra_input.txt # 文件名,每行一个 87 | - extra_header.hpp 88 | judge_extra_files: 89 | - extra_file.txt 90 | - extra_info.txt 91 | ``` 92 | 93 | 如果需要配置交互题,模板题,提交答案题,请参照 [testdata.yaml](../../examples/testdata.yaml) 94 | 如果需要配置RemoteJudge,请参照 [testdata.yaml](../../examples/testdata_remotejudge.yaml) 95 | 96 | ## Config.ini 97 | 98 | 压缩包内文件格式如下: 99 | 100 | ``` 101 | Testdata.zip 102 | | 103 | +- Config.ini 104 | +- Input 105 | | +- input0.in 106 | | +- input1.in 107 | | +- input2.in 108 | +- Output 109 | +- output0.out 110 | +- output1.out 111 | +- output2.out 112 | ``` 113 | 114 | ``` 115 | Config.ini格式 116 | 第一行包含一个整数n,表示总共有n组数据(即Input目录中文件总数等于Output目录中文件总数等于n); 117 | 接下来n行,第k行代表第k个测试点,格式为: 118 | [输入文件名]|[输出文件名]|[时限(单位为秒)]|[得分]|[内存限制(单位为KiB)] 119 | 其中,输入和输出文件名为 Input 或者 Output 目录中的文件名(不包含Input或者Output目录),且所有数据点得分之和必须为100,如: 120 | input0.in|output0.out|1|10|256000 121 | ``` -------------------------------------------------------------------------------- /examples/langs.yaml: -------------------------------------------------------------------------------- 1 | ccWithoutO2: 2 | type: compiler 3 | compile: /usr/bin/g++ -Wall -o ${name} foo.cc 4 | code_file: foo.cc 5 | execute: ${dir}/${name} 6 | c: 7 | type: compiler 8 | compile: /usr/bin/gcc -O2 -Wall -std=c99 -o ${name} foo.c -lm 9 | code_file: foo.c 10 | execute: ${dir}/${name} 11 | cc: 12 | type: compiler 13 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 14 | code_file: foo.cc 15 | execute: ${dir}/${name} 16 | cc98: 17 | type: compiler 18 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 19 | code_file: foo.cc 20 | execute: ${dir}/${name} 21 | cc11: 22 | type: compiler 23 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 24 | code_file: foo.cc 25 | execute: ${dir}/${name} 26 | cc17: 27 | type: compiler 28 | compile: /usr/bin/g++ -O2 -Wall -std=c++17 -o ${name} foo.cc -lm 29 | code_file: foo.cc 30 | execute: ${dir}/${name} 31 | pas: 32 | type: compiler 33 | compile: /usr/bin/fpc -O2 -o${dir}/${name} foo.pas 34 | code_file: foo.pas 35 | execute: ${dir}/${name} 36 | java: 37 | type: compiler 38 | compile: /usr/bin/javac -d ${dir} -encoding utf8 ./Main.java 39 | code_file: Main.java 40 | execute: /usr/bin/java Main 41 | py: 42 | type: compiler 43 | compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" 44 | code_file: foo.py 45 | execute: /usr/bin/python ${name} 46 | py2: 47 | type: compiler 48 | compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" 49 | code_file: foo.py 50 | execute: /usr/bin/python ${name} 51 | py3: 52 | type: compiler 53 | compile: /usr/bin/python3 -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" 54 | code_file: foo.py 55 | execute: /usr/bin/python3 ${name} 56 | php: 57 | type: interpreter 58 | code_file: foo.php 59 | execute: /usr/bin/php ${name} 60 | rs: 61 | type: compiler 62 | compile: /usr/bin/rustc -O -o ${dir}/${name} ${dir}/foo.rs 63 | code_file: foo.rs 64 | execute: ${dir}/${name} 65 | hs: 66 | type: compiler 67 | compile: /usr/bin/ghc -O -outputdir /tmp -o ${name} foo.hs 68 | code_file: foo.hs 69 | execute: ${dir}/${name} 70 | js: 71 | type: interpreter 72 | code_file: foo.js 73 | execute: /usr/bin/jsc ${dir}/${name} 74 | go: 75 | type: compiler 76 | compile: /usr/bin/go build -o ${name} foo.go 77 | code_file: foo.go 78 | execute: ${dir}/${name} 79 | rb: 80 | type: interpreter 81 | code_file: foo.rb 82 | execute: /usr/bin/ruby ${name} 83 | cs: 84 | type: compiler 85 | compile: /usr/bin/mcs -optimize+ -out:${dir}/${name} ${dir}/foo.cs 86 | code_file: foo.cs 87 | execute: /usr/bin/mono ${name} -------------------------------------------------------------------------------- /docs/en/README.md: -------------------------------------------------------------------------------- 1 | # Judge Daemon 2 | 3 | [中文文档](../../README.md) 4 | 5 | ## Introduction 6 | 7 | HydroJudge is a judging daemon for programming contests like OI and ACM. 8 | HydroJudge supports custom judge, subtask and other many new features. 9 | 10 | ## Help Center 11 | 12 | - [RemoteJudge](./RemoteJudge.md) 13 | 14 | ## Usage 15 | 16 | ### Run packed executable file (suggested) 17 | 18 | Step 1: Download the latest packed files at [GithubActions](https://github.com/hydro-dev/HydroJudge/actions) 19 | You should download one of the files below: 20 | 21 | - Judge_win_amd64.exe 22 | - Judge_linux_amd64 23 | - Judge_macos_amd64 24 | 25 | Step 2: Create configuration file 26 | 27 | ```yaml 28 | #$HOME/.config/hydro/judge.yaml 29 | hosts: 30 | localhost: 31 | server_url: e.g. https://vijos.org 32 | uname: Judge account username 33 | password: Judge account password 34 | ``` 35 | 36 | Step 3: Run! 37 | 38 | ```sh 39 | chmod +x ./Judge 40 | ./Judge 41 | ``` 42 | 43 | ### Run with docker 44 | 45 | Create `config.yaml`: 46 | 47 | ```yaml 48 | hosts: 49 | localhost: 50 | server_url: e.g. https://vijos.org 51 | uname: Judge account username 52 | password: Judge account password 53 | ``` 54 | 55 | Then use `docker run --privileged -d -v /path/to/config.yaml:/config/config.yaml hydrooj/judge:default` to start. 56 | **Replace /path/to/judge.yaml with your ABSOLUTE PATH!** 57 | Hint: there are 4 tags built for docker: 58 | 59 | - `hydrooj/judge:alpine` Smallest image based on AlpineLinux 60 | - `hydrooj/judge:latest` No compiler installed 61 | - `hydrooj/judge:default` Default compiler for vijos 62 | - `hydrooj/judge:slim` C C++ Pascal 63 | 64 | ## Configuration 65 | 66 | - Change the config file path: `--config=/path/to/config` 67 | - Change the language file path: `--langs=/path/to/langs` 68 | - Change temp directory: `--tmp=/path/to/tmp` 69 | - Change cache directory: `--cache=/path/to/cache` 70 | - Change files directory: `--files=/path/to/files` 71 | - Change execution host: `--execute=http://executionhost/` 72 | 73 | ## Development 74 | 75 | Prerequisites: 76 | 77 | - Linux 78 | - NodeJS Version 10+ 79 | 80 | Use the following command to install nodejs requirements: 81 | 82 | ```sh 83 | yarn 84 | ``` 85 | 86 | Put `judge.yaml` and `langs.yaml` in the configuration directory, usually 87 | in `$HOME/.config/hydro/`. `judge.yaml` includes the server address, user and 88 | password and `langs.yaml` includes the compiler options. Examples can be found 89 | under the `examples` directory. 90 | 91 | Run the [executor-server](https://github.com/criyle/go-judge) first, 92 | And use the following command to run the daemon: 93 | 94 | ```sh 95 | node judge/daemon.js 96 | ``` 97 | 98 | ## Testdata format 99 | [Testdata format](./Testdata.md) 100 | 101 | ## Copyright and License 102 | 103 | Copyright (c) 2020 Hydro Dev Team. All rights reserved. 104 | 105 | License: GPL-3.0-only 106 | -------------------------------------------------------------------------------- /judge/config.js: -------------------------------------------------------------------------------- 1 | const argv = require('minimist')(process.argv.slice(2)); 2 | const os = require('os'); 3 | const child = require('child_process'); 4 | const path = require('path'); 5 | const fs = require('fs-extra'); 6 | const log = require('./log'); 7 | 8 | const config = { 9 | CONFIG_FILE: path.resolve(os.homedir(), '.config', 'hydro', 'judge.yaml'), 10 | LANGS_FILE: path.resolve(os.homedir(), '.config', 'hydro', 'langs.yaml'), 11 | CACHE_DIR: path.resolve(os.homedir(), '.cache', 'hydro', 'judge'), 12 | FILES_DIR: path.resolve(os.homedir(), '.cache', 'hydro', 'files', 'judge'), 13 | SYSTEM_MEMORY_LIMIT_MB: 1024, 14 | SYSTEM_TIME_LIMIT_MS: 16000, 15 | SYSTEM_PROCESS_LIMIT: 32, 16 | RETRY_DELAY_SEC: 15, 17 | TEMP_DIR: path.resolve(os.tmpdir(), 'hydro', 'judge'), 18 | EXECUTION_HOST: 'http://localhost:5050', 19 | CONFIG: null, 20 | LANGS: null, 21 | changeDefault(name, from, to) { 22 | if (config[name] === from) config[name] = to; 23 | }, 24 | }; 25 | 26 | if (fs.existsSync(path.resolve(process.cwd(), '.env'))) { 27 | const env = {}; 28 | const f = fs.readFileSync('.env').toString(); 29 | for (const line of f) { 30 | const a = line.split('='); 31 | env[a[0]] = a[1]; 32 | } 33 | Object.assign(process.env, env); 34 | } 35 | 36 | if (process.env.CONFIG_FILE || argv.config) { 37 | config.CONFIG_FILE = path.resolve(process.env.CONFIG_FILE || argv.config); 38 | } 39 | if (process.env.LANGS_FILE || argv.langs) { 40 | config.LANGS_FILE = path.resolve(process.env.LANGS_FILE || argv.langs); 41 | } 42 | if (process.env.TEMP_DIR || argv.tmp) { 43 | config.TEMP_DIR = path.resolve(process.env.TEMP_DIR || argv.tmp); 44 | } 45 | if (process.env.CACHE_DIR || argv.cache) { 46 | config.CACHE_DIR = path.resolve(process.env.CACHE_DIR || argv.cache); 47 | } 48 | if (process.env.FILES_DIR || argv.files) { 49 | config.FILES_DIR = path.resolve(process.env.FILES_DIR || argv.files); 50 | } 51 | if (process.env.EXECUTION_HOST || argv.execute) { 52 | config.EXECUTION_HOST = path.resolve(process.env.EXECUTION_HOST || argv.execute); 53 | } 54 | if (process.env.START_EXECUTOR_SERVER) { 55 | const args = (process.env.EXECUTOR_SERVER_ARGS || '').split(' '); 56 | console.log('Starting executor server with args', args); 57 | const p = child.spawn(path.resolve(__dirname, process.env.START_EXECUTOR_SERVER), args); 58 | if (!p.stdout) throw new Error('Cannot start executorserver'); 59 | else { 60 | p.stdout.on('data', (data) => { 61 | const s = data.toString(); 62 | console.log(s.substr(0, s.length - 1)); 63 | }); 64 | p.stderr.on('data', (data) => { 65 | const s = data.toString(); 66 | console.log(s.substr(0, s.length - 1)); 67 | }); 68 | global.onDestory.push(() => { 69 | p.emit('exit'); 70 | }); 71 | } 72 | p.on('error', (error) => console.error(error)); 73 | } 74 | if (!(fs.existsSync(config.LANGS_FILE) || global.Hydro)) { 75 | fs.ensureDirSync(path.dirname(config.LANGS_FILE)); 76 | if (fs.existsSync(path.join(__dirname, '..', 'examples', 'langs.yaml'))) { 77 | log.error('Language file not found, using default.'); 78 | config.LANGS_FILE = path.join(__dirname, '..', 'examples', 'langs.yaml'); 79 | } else throw new Error('Language file not found'); 80 | } 81 | 82 | module.exports = config; 83 | -------------------------------------------------------------------------------- /judge/daemon.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-condition */ 2 | /* eslint-disable no-await-in-loop */ 3 | /* .. 4 | .' @`._ 5 | ~ ...._.' ,__.-; 6 | _..- - - /` .-' ~ 7 | : __./' , .'-'- .._ 8 | ~ `- -(.-'''- -. \`._ `. ~ 9 | _.- '( .______.'.-' `-.` `. 10 | : `-..____`-. ; 11 | `. ```` 稻花香里说丰年, ; ~ 12 | `-.__ 听取人生经验。 __.-' 13 | ````- - -.......- - -''' ~ 14 | ~ */ 15 | global.onDestory = []; 16 | require('./updater'); 17 | const fsp = require('fs').promises; 18 | const yaml = require('js-yaml'); 19 | const Session = require('./hosts/index'); 20 | const { sleep, Queue } = require('./utils'); 21 | const log = require('./log'); 22 | const { RETRY_DELAY_SEC, CONFIG_FILE, CONFIG } = require('./config'); 23 | 24 | const terminate = async () => { 25 | log.log('正在保存数据'); 26 | try { 27 | for (const f of global.onDestory) { 28 | const r = f(); 29 | if (r instanceof Promise) await r; 30 | } 31 | process.exit(1); 32 | } catch (e) { 33 | if (global.SI) process.exit(1); 34 | log.error(e); 35 | log.error('发生了错误。'); 36 | log.error('再次按下 Ctrl-C 可强制退出。'); 37 | global.SI = true; 38 | } 39 | }; 40 | process.on('SIGINT', terminate); 41 | process.on('SIGTERM', terminate); 42 | process.on('unhandledRejection', (reason, p) => { 43 | console.log('Unhandled Rejection at: Promise ', p); 44 | }); 45 | 46 | async function daemon() { 47 | let config; 48 | try { 49 | if (CONFIG) config = CONFIG; 50 | else { 51 | config = (await fsp.readFile(CONFIG_FILE)).toString(); 52 | config = yaml.safeLoad(config); 53 | } 54 | } catch (e) { 55 | log.error('配置文件无效或未找到。'); 56 | process.exit(1); 57 | } 58 | const hosts = {}; 59 | const queue = new Queue(); 60 | for (const i in config.hosts) { 61 | config.hosts[i].host = i; 62 | hosts[i] = new Session[config.hosts[i].type || 'vj4'](config.hosts[i]); 63 | await hosts[i].init(); 64 | } 65 | global.hosts = hosts; 66 | if (!CONFIG) { 67 | global.onDestory.push(async () => { 68 | const cfg = { hosts: {} }; 69 | for (const i in hosts) { 70 | cfg.hosts[i] = { 71 | host: i, 72 | type: hosts[i].config.type || 'vj4', 73 | uname: hosts[i].config.uname, 74 | password: hosts[i].config.password, 75 | server_url: hosts[i].config.server_url, 76 | }; 77 | if (hosts[i].config.cookie) cfg.hosts[i].cookie = hosts[i].config.cookie; 78 | if (hosts[i].config.detail) cfg.hosts[i].detail = hosts[i].config.detail; 79 | } 80 | await fsp.writeFile(CONFIG_FILE, yaml.safeDump(cfg)); 81 | }); 82 | } 83 | while ('Orz twd2') { 84 | try { 85 | for (const i in hosts) await hosts[i].consume(queue); 86 | while ('Orz iceb0y') { 87 | const [task] = await queue.get(); 88 | task.handle(); 89 | } 90 | } catch (e) { 91 | log.error(e, e.stack); 92 | log.info(`在 ${RETRY_DELAY_SEC} 秒后重试`); 93 | await sleep(RETRY_DELAY_SEC * 1000); 94 | } 95 | } 96 | } 97 | if (!module.parent) daemon(); 98 | module.exports = daemon; 99 | -------------------------------------------------------------------------------- /judge/remotejudge/vj4.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const WebSocket = require('ws'); 3 | const { SystemError } = require('../error'); 4 | 5 | const RE_CSRF = /\{"csrf_token":"(.*?)"/i; 6 | const RE_RID = /\{"socketUrl":"(.*?)"/i; 7 | 8 | module.exports = class VJ4 { 9 | constructor(serverUrl) { 10 | this.serverUrl = serverUrl.split('/', 3).join('/'); 11 | } 12 | 13 | async loginWithToken(cookie) { 14 | this.cookie = cookie; 15 | this.axios = axios.create({ 16 | baseURL: this.serverUrl, 17 | timeout: 30000, 18 | headers: { 19 | accept: 'application/json', 20 | 'Content-Type': 'application/x-www-form-urlencoded', 21 | cookie: this.cookie, 22 | }, 23 | transformRequest: [ 24 | (data) => { 25 | let ret = ''; 26 | for (const it in data) { 27 | ret += `${encodeURIComponent(it)}=${encodeURIComponent(data[it])}&`; 28 | } 29 | return ret; 30 | }, 31 | ], 32 | }); 33 | } 34 | 35 | async login(username, password) { 36 | await this.loginWithToken(''); 37 | const res = await this.axios.post('login', { uname: username, password }); 38 | await this.loginWithToken(res.headers['set-cookie'][0].split(';')[0]); 39 | } 40 | 41 | async submit(pidStr, code, lang) { 42 | const [domainId, pid] = pidStr.split('/'); 43 | const res = await this.axios.get(`/d/${domainId}/p/${pid}/submit`, { headers: { accept: 'document/html' } }); 44 | const csrf_token = RE_CSRF.exec(res.data)[1]; // eslint-disable-line camelcase 45 | const resp = await this.axios.post(`/d/${domainId}/p/${pid}/submit`, { 46 | lang, code, csrf_token, 47 | }, { headers: { accept: 'document/html' } }); 48 | return { 49 | socketUrl: RE_RID.exec(resp.data)[1], 50 | domain_id: domainId, 51 | pid, 52 | }; 53 | } 54 | 55 | async monit(data, next, end) { 56 | const wsinfo = await this.axios.get(data.socketUrl); 57 | this.ws = new WebSocket(`${this.serverUrl.replace(/^http/i, 'ws') + data.socketUrl}/websocket?t=${wsinfo.data.entropy}`, { 58 | headers: { cookie: this.cookie }, 59 | }); 60 | this.ws.on('close', (data) => { 61 | throw new SystemError(`RemoteJudge Websocket closed unexpectedly: ${data}`); 62 | }); 63 | this.ws.on('error', (e) => { 64 | throw new SystemError(`RemoteJudge Websocket closed unexpectedly: ${e}`); 65 | }); 66 | await new Promise((resolve) => { 67 | this.timeout = setTimeout(resolve, 5000); 68 | this.ws.on('message', () => { 69 | // TODO: handle real-time update 70 | clearTimeout(this.timeout); 71 | this.timeout = setTimeout(resolve, 5000); 72 | }); 73 | }); 74 | const s = await this.axios.get(`/d/${data.domain_id}/p/${data.pid}/submit`); 75 | const r = s.data.rdocs[0]; 76 | // eslint-disable-next-line camelcase 77 | for (const compiler_text of r.compiler_texts) next({ compiler_text }); 78 | // eslint-disable-next-line camelcase 79 | for (const judge_text of r.judge_texts) next({ judge_text }); 80 | for (const i of r.cases) next({ status: r.status, case: i, progress: 0 }); 81 | end({ 82 | status: r.status, 83 | score: r.score, 84 | time_ms: r.time_ms, 85 | memory_kb: r.memory_kb, 86 | }); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /setting.yaml: -------------------------------------------------------------------------------- 1 | langs: 2 | type: textarea 3 | category: system 4 | desc: Language file settings 5 | name: langs 6 | default: | 7 | ccWithoutO2: 8 | type: compiler 9 | compile: /usr/bin/g++ -Wall -o ${name} foo.cc 10 | code_file: foo.cc 11 | execute: ${dir}/${name} 12 | c: 13 | type: compiler 14 | compile: /usr/bin/gcc -O2 -Wall -std=c99 -o ${name} foo.c -lm 15 | code_file: foo.c 16 | execute: ${dir}/${name} 17 | cc: 18 | type: compiler 19 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 20 | code_file: foo.cc 21 | execute: ${dir}/${name} 22 | cc98: 23 | type: compiler 24 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 25 | code_file: foo.cc 26 | execute: ${dir}/${name} 27 | cc11: 28 | type: compiler 29 | compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm 30 | code_file: foo.cc 31 | execute: ${dir}/${name} 32 | cc17: 33 | type: compiler 34 | compile: /usr/bin/g++ -O2 -Wall -std=c++17 -o ${name} foo.cc -lm 35 | code_file: foo.cc 36 | execute: ${dir}/${name} 37 | pas: 38 | type: compiler 39 | compile: /usr/bin/fpc -O2 -o${dir}/${name} foo.pas 40 | code_file: foo.pas 41 | execute: ${dir}/${name} 42 | java: 43 | type: compiler 44 | compile: /usr/bin/javac -d ${dir} -encoding utf8 ./Main.java 45 | code_file: Main.java 46 | execute: /usr/bin/java Main 47 | py: 48 | type: compiler 49 | compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" 50 | code_file: foo.py 51 | execute: /usr/bin/python ${name} 52 | py2: 53 | type: compiler 54 | compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" 55 | code_file: foo.py 56 | execute: /usr/bin/python ${name} 57 | py3: 58 | type: compiler 59 | compile: /usr/bin/python3 -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)" 60 | code_file: foo.py 61 | execute: /usr/bin/python3 ${name} 62 | php: 63 | type: interpreter 64 | code_file: foo.php 65 | execute: /usr/bin/php ${name} 66 | rs: 67 | type: compiler 68 | compile: /usr/bin/rustc -O -o ${dir}/${name} ${dir}/foo.rs 69 | code_file: foo.rs 70 | execute: ${dir}/${name} 71 | hs: 72 | type: compiler 73 | compile: /usr/bin/ghc -O -outputdir /tmp -o ${name} foo.hs 74 | code_file: foo.hs 75 | execute: ${dir}/${name} 76 | js: 77 | type: interpreter 78 | code_file: foo.js 79 | execute: /usr/bin/jsc ${dir}/${name} 80 | go: 81 | type: compiler 82 | compile: /usr/bin/go build -o ${name} foo.go 83 | code_file: foo.go 84 | execute: ${dir}/${name} 85 | rb: 86 | type: interpreter 87 | code_file: foo.rb 88 | execute: /usr/bin/ruby ${name} 89 | cs: 90 | type: compiler 91 | compile: /usr/bin/mcs -optimize+ -out:${dir}/${name} ${dir}/foo.cs 92 | code_file: foo.cs 93 | execute: /usr/bin/mono ${name} -------------------------------------------------------------------------------- /judge/case/yaml.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const yaml = require('js-yaml'); 3 | const path = require('path'); 4 | const { FormatError } = require('../error'); 5 | const { parseTimeMS, parseMemoryMB, ensureFile } = require('../utils'); 6 | const readAutoCases = require('./auto'); 7 | 8 | const fsp = fs.promises; 9 | 10 | module.exports = async function readYamlCases(folder, name, args) { 11 | const config = { 12 | checker_type: 'default', 13 | count: 0, 14 | subtasks: [], 15 | judge_extra_files: [], 16 | user_extra_files: [], 17 | }; 18 | const next = args.next; 19 | const checkFile = ensureFile(folder); 20 | let configFile = args.config || (await fsp.readFile(path.resolve(folder, name))).toString(); 21 | 22 | configFile = yaml.safeLoad(configFile); 23 | config.checker_type = configFile.checker_type || 'default'; 24 | if (configFile.filename) config.filename = configFile.filename; 25 | if (configFile.checker) config.checker = checkFile(configFile.checker, '找不到比较器 '); 26 | if (configFile.judge_extra_files) { 27 | if (typeof configFile.judge_extra_files === 'string') { 28 | config.judge_extra_files = [checkFile(configFile.judge_extra_files, '找不到评测额外文件 ')]; 29 | } else if (configFile.judge_extra_files instanceof Array) { 30 | for (const file in configFile.judge_extra_files) { 31 | config.judge_extra_files.push(checkFile(file, '找不到评测额外文件 ')); 32 | } 33 | } else throw new FormatError('无效的 judge_extra_files 配置项'); 34 | } 35 | if (configFile.user_extra_files) { 36 | if (typeof configFile.user_extra_files === 'string') { 37 | config.user_extra_files = [checkFile(configFile.user_extra_files, '找不到用户额外文件 ')]; 38 | } else if (configFile.user_extra_files instanceof Array) { 39 | for (const file in configFile.user_extra_files) { 40 | config.user_extra_files.push(checkFile(file, '找不到用户额外文件 ')); 41 | } 42 | } else throw new FormatError('无效的 user_extra_files 配置项'); 43 | } 44 | if (configFile.cases) { 45 | config.subtasks = [{ 46 | score: parseInt(configFile.score) || Math.floor(100 / config.count), 47 | time_limit_ms: parseTimeMS(configFile.time), 48 | memory_limit_mb: parseMemoryMB(configFile.memory), 49 | cases: [], 50 | type: 'sum', 51 | }]; 52 | for (const c of configFile.cases) { 53 | config.count++; 54 | config.subtasks[0].cases.push({ 55 | input: checkFile(c.input, '找不到输入文件 '), 56 | output: checkFile(c.output, '找不到输出文件 '), 57 | id: config.count, 58 | }); 59 | } 60 | } else if (configFile.subtasks) { 61 | for (const subtask of configFile.subtasks) { 62 | const cases = []; 63 | for (const c of subtask) { 64 | config.count++; 65 | cases.push({ 66 | input: checkFile(c.input, '找不到输入文件 '), 67 | output: checkFile(c.output, '找不到输出文件 '), 68 | id: config.count, 69 | }); 70 | } 71 | config.subtasks.push({ 72 | score: parseInt(subtask.score), 73 | cases, 74 | time_limit_ms: parseTimeMS(subtask.time || configFile.time), 75 | memory_limit_mb: parseMemoryMB(subtask.memory || configFile.time), 76 | }); 77 | } 78 | } else if (configFile.type !== 'remotejudge') { 79 | const c = await readAutoCases(folder, '', { next }); 80 | config.subtasks = c.subtasks; 81 | config.count = c.count; 82 | } 83 | return Object.assign(configFile, config); 84 | }; 85 | -------------------------------------------------------------------------------- /judge/remotejudge/vjudge.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { SystemError, CompileError } = require('../error'); 3 | const status = require('../status'); 4 | 5 | const STATUS = { 6 | QUEUEING: status.STATUS_COMPILING, 7 | PENDING: status.STATUS_COMPILING, 8 | SUBMITTED: status.STATUS_COMPILING, 9 | JUDGING: status.STATUS_JUDGING, 10 | AC: status.STATUS_ACCEPTED, 11 | WA: status.STATUS_WRONG_ANSWER, 12 | RE: status.STATUS_RUNTIME_ERROR, 13 | CE: status.STATUS_COMPILE_ERROR, 14 | PE: status.STATUS_WRONG_ANSWER, 15 | TLE: status.STATUS_TIME_LIMIT_EXCEEDED, 16 | MLE: status.STATUS_MEMORY_LIMIT_EXCEEDED, 17 | OLE: status.STATUS_WRONG_ANSWER, 18 | FAILED_OTHER: status.STATUS_SYSTEM_ERROR, 19 | SUBMIT_FAILED_PERM: status.STATUS_SYSTEM_ERROR, 20 | SUBMIT_FAILED_TEMP: status.STATUS_SYSTEM_ERROR, 21 | }; 22 | const LANGS = require('../data/languages.json'); 23 | 24 | module.exports = class VJudge { 25 | constructor(serverUrl) { 26 | this.serverUrl = serverUrl; 27 | } 28 | 29 | async loginWithToken(cookie) { 30 | this.cookie = cookie; 31 | this.axios = axios.create({ 32 | baseURL: this.serverUrl, 33 | timeout: 30000, 34 | headers: { 35 | accept: 'application/json', 36 | 'Content-Type': 'application/x-www-form-urlencoded', 37 | cookie: this.cookie, 38 | }, 39 | transformRequest: [ 40 | (data) => { 41 | let ret = ''; 42 | for (const it in data) { 43 | ret += `${encodeURIComponent(it)}=${encodeURIComponent(data[it])}&`; 44 | } 45 | return ret; 46 | }, 47 | ], 48 | }); 49 | } 50 | 51 | async login(username, password) { 52 | await this.loginWithToken(''); 53 | const res = await this.axios.post('/user/login', { username, password }); 54 | await this.loginWithToken(res.headers['set-cookie'][0].split(';')[0]); 55 | } 56 | 57 | async submit(pidStr, code, lang, next) { 58 | const [oj, probNum] = pidStr.split('/'); 59 | if (!LANGS[oj]) throw new SystemError('Problem config error: Remote oj doesn\'t exist.', [oj]); 60 | const language = LANGS[oj][lang]; 61 | if (!language) throw new CompileError(`Language not supported by remote oj: ${lang}`); 62 | const source = Buffer.from(encodeURIComponent(code)).toString('base64'); 63 | const res = await this.axios.post('/problem/submit', { 64 | oj, probNum, share: 0, source, captcha: '', language, 65 | }); 66 | if (res.data.error) throw new SystemError(res.data.error); 67 | next({ judge_text: `Submitted: ID=${res.data.runId}` }); 68 | return { id: res.data.runId }; 69 | } 70 | 71 | monit(data, next, end) { 72 | return new Promise((resolve) => { 73 | let lastStatus = null; 74 | const fetch = async () => { 75 | const resp = await this.axios.get(`/solution/data/${data.id}`); 76 | const r = resp.data; 77 | if (!lastStatus) { next({ judge_text: `Using language: ${r.language}` }); } 78 | if (lastStatus !== r.status) { 79 | next({ judge_text: `Status=${r.status}` }); 80 | lastStatus = r.status; 81 | } 82 | if (!r.processing) { 83 | console.log(r); 84 | next({ compiler_text: r.additionalInfo }); 85 | end({ 86 | status: STATUS[r.statusCanonical], 87 | score: STATUS[r.statusCanonical] === status.STATUS_ACCEPTED ? 100 : 0, 88 | time_ms: r.runtime || 0, 89 | memory_kb: r.memory || 0, 90 | }); 91 | resolve(); 92 | } else setTimeout(() => { fetch(); }, 1000); 93 | }; 94 | fetch(); 95 | }); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /judge/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { parse } = require('shell-quote'); 4 | const _ = require('lodash'); 5 | const EventEmitter = require('events'); 6 | const { FormatError } = require('./error'); 7 | 8 | const max = (a, b) => (a > b ? a : b); 9 | const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i; 10 | const TIME_UNITS = { '': 1000, m: 1, u: 0.001 }; 11 | const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i; 12 | const MEMORY_UNITS = { k: 0.1, m: 1, g: 1024 }; 13 | const EMPTY_STR = /^[ \r\n\t]*$/i; 14 | 15 | function parseTimeMS(str) { 16 | const match = TIME_RE.exec(str); 17 | if (!match) throw new FormatError(str, 'error parsing time'); 18 | return parseInt(parseFloat(match[1]) * TIME_UNITS[match[2]]); 19 | } 20 | 21 | function parseMemoryMB(str) { 22 | const match = MEMORY_RE.exec(str); 23 | if (!match) throw new FormatError(str, 'error parsing memory'); 24 | return parseInt(parseFloat(match[1]) * MEMORY_UNITS[match[2]]); 25 | } 26 | 27 | function sleep(timeout) { 28 | return new Promise((resolve) => { 29 | setTimeout(() => { 30 | resolve(); 31 | }, timeout); 32 | }); 33 | } 34 | 35 | function parseFilename(path) { 36 | const t = path.split('/'); 37 | return t[t.length - 1]; 38 | } 39 | 40 | class Queue extends EventEmitter { 41 | constructor() { 42 | super(); 43 | this.queue = []; 44 | this.waiting = []; 45 | } 46 | 47 | get(count = 1) { 48 | if (this.empty() || this.queue.length < count) { 49 | return new Promise((resolve) => { 50 | this.waiting.push({ count, resolve }); 51 | }); 52 | } 53 | const items = []; 54 | for (let i = 0; i < count; i++) { items.push(this.queue[i]); } 55 | this.queue = _.drop(this.queue, count); 56 | return items; 57 | } 58 | 59 | empty() { 60 | return this.queue.length === 0; 61 | } 62 | 63 | push(value) { 64 | this.queue.push(value); 65 | if (this.waiting.length && this.waiting[0].count <= this.queue.length) { 66 | const items = []; 67 | for (let i = 0; i < this.waiting[0].count; i++) { items.push(this.queue[i]); } 68 | this.queue = _.drop(this.queue, this.waiting[0].count); 69 | this.waiting[0].resolve(items); 70 | this.waiting.shift(); 71 | } 72 | } 73 | } 74 | 75 | function compilerText(stdout, stderr) { 76 | const ret = []; 77 | if (!EMPTY_STR.test(stdout)) ret.push(stdout); 78 | if (!EMPTY_STR.test(stderr)) ret.push(stderr); 79 | ret.push('自豪的采用 HydroJudge 进行评测(github.com/hydro-dev/HydroJudge)'); 80 | return ret.join('\n'); 81 | } 82 | 83 | function copyInDir(dir) { 84 | const files = {}; 85 | if (fs.existsSync(dir)) { 86 | fs.readdirSync(dir).forEach((f1) => { 87 | const p1 = `${dir}/${f1}`; 88 | if (fs.statSync(p1).isDirectory()) { 89 | fs.readdirSync(p1).forEach((f2) => { 90 | files[`${f1}/${f2}`] = { src: `${dir}/${f1}/${f2}` }; 91 | }); 92 | } else files[f1] = { src: `${dir}/${f1}` }; 93 | }); 94 | } 95 | return files; 96 | } 97 | 98 | function restrictFile(p) { 99 | if (!p) return '/'; 100 | if (p[0] === '/') p = ''; 101 | return p.replace(/\.\./i, ''); 102 | } 103 | 104 | function ensureFile(folder) { 105 | return (file, message) => { 106 | const f = path.join(folder, restrictFile(file)); 107 | if (!fs.existsSync(f)) throw new FormatError(message + file); 108 | const stat = fs.statSync(f); 109 | if (!stat.isFile()) throw new FormatError(message + file); 110 | return f; 111 | }; 112 | } 113 | 114 | module.exports = { 115 | Queue, 116 | max, 117 | sleep, 118 | compilerText, 119 | copyInDir, 120 | parseMemoryMB, 121 | parseTimeMS, 122 | parseFilename, 123 | cmd: parse, 124 | ensureFile, 125 | }; 126 | -------------------------------------------------------------------------------- /judge/sysinfo.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const systeminformation = require('systeminformation'); 4 | const { judge } = require('./judge/run'); 5 | const { TEMP_DIR } = require('./config'); 6 | const tmpfs = require('./tmpfs'); 7 | 8 | function size(s, base = 1) { 9 | s *= base; 10 | const unit = 1024; 11 | const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; 12 | for (const unitName of unitNames) { 13 | if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName); 14 | s /= unit; 15 | } 16 | return '{0} {1}'.format(Math.round(s * unit), unitNames[unitNames.length - 1]); 17 | } 18 | 19 | const cache = {}; 20 | 21 | async function stackSize() { 22 | let output = ''; 23 | const context = { 24 | lang: 'ccWithoutO2', 25 | code: ` 26 | #include 27 | using namespace std; 28 | int i=1; 29 | int main(){ 30 | char a[1048576]={'1'}; 31 | cout<<" "<256) return 0; 34 | main(); 35 | }`, 36 | config: { 37 | time: 3000, 38 | memory: 256, 39 | }, 40 | stat: {}, 41 | clean: [], 42 | next: () => { }, 43 | end: (data) => { 44 | if (data.stdout) output = data.stdout; 45 | }, 46 | }; 47 | context.tmpdir = path.resolve(TEMP_DIR, 'tmp', 'sysinfo'); 48 | fs.ensureDirSync(context.tmpdir); 49 | tmpfs.mount(context.tmpdir, '64m'); 50 | await judge(context).catch((e) => console.error(e)); 51 | // eslint-disable-next-line no-await-in-loop 52 | for (const clean of context.clean) await clean().catch(); 53 | tmpfs.umount(context.tmpdir); 54 | fs.removeSync(context.tmpdir); 55 | const a = output.split(' '); 56 | return parseInt(a[a.length - 1]); 57 | } 58 | 59 | async function get() { 60 | const [ 61 | Cpu, Memory, OsInfo, 62 | CurrentLoad, CpuFlags, CpuTemp, 63 | Battery, stack, 64 | ] = await Promise.all([ 65 | systeminformation.cpu(), 66 | systeminformation.mem(), 67 | systeminformation.osInfo(), 68 | systeminformation.currentLoad(), 69 | systeminformation.cpuFlags(), 70 | systeminformation.cpuTemperature(), 71 | systeminformation.battery(), 72 | stackSize(), 73 | ]); 74 | const cpu = `${Cpu.manufacturer} ${Cpu.brand}`; 75 | const memory = `${size(Memory.active)}/${size(Memory.total)}`; 76 | const osinfo = `${OsInfo.distro} ${OsInfo.release} ${OsInfo.codename} ${OsInfo.kernel} ${OsInfo.arch}`; 77 | const load = `${CurrentLoad.avgload}`; 78 | const flags = CpuFlags; 79 | let battery; 80 | if (!Battery.hasbattery) battery = 'No battery'; 81 | else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`; 82 | const mid = OsInfo.serial; 83 | cache.cpu = cpu; 84 | cache.osinfo = osinfo; 85 | cache.flags = flags; 86 | cache.mid = mid; 87 | cache.stack = stack; 88 | return { 89 | mid, cpu, memory, osinfo, load, flags, CpuTemp, battery, stack, 90 | }; 91 | } 92 | 93 | async function update() { 94 | const [Memory, CurrentLoad, CpuTemp, Battery] = await Promise.all([ 95 | systeminformation.mem(), 96 | systeminformation.currentLoad(), 97 | systeminformation.cpuTemperature(), 98 | systeminformation.battery(), 99 | ]); 100 | const { 101 | mid, cpu, osinfo, flags, stack, 102 | } = cache; 103 | const memory = `${size(Memory.active)}/${size(Memory.total)}`; 104 | const load = `${CurrentLoad.avgload}`; 105 | let battery; 106 | if (!Battery.hasbattery) battery = 'No battery'; 107 | else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`; 108 | return [ 109 | mid, 110 | { 111 | memory, load, battery, CpuTemp, 112 | }, 113 | { 114 | mid, cpu, memory, osinfo, load, flags, battery, CpuTemp, stack, 115 | }, 116 | ]; 117 | } 118 | 119 | module.exports = { get, update }; 120 | -------------------------------------------------------------------------------- /examples/testdata.yaml: -------------------------------------------------------------------------------- 1 | # Problem type, allow [default, submit_answer, interactive] 2 | # 问题类型,允许的值有 [default, submit_answer, interactive] 3 | type: default 4 | 5 | # 题目模板(通常无需填写该设置项) 6 | # 如果设置此项,则用户程序将被插入在模板中间。 7 | template: 8 | cc: 9 | - >- 10 | #include 11 | using namespace std; 12 | int main(){ 13 | - >- 14 | } 15 | pas: 16 | - >- 17 | var a, b:longint; 18 | begin 19 | - >- 20 | end. 21 | 22 | # When Problem type is 'default', The following options are allowed: -------------+ 23 | # 当问题类型为 'default' 时,下面的设置可用: # 24 | # Checker type, allow [default, ccr, cena, hustoj, lemon, qduoj, syzoj, testlib] # 25 | # 比较器类型,支持的值有 [default, ccr, cena, hustoj, lemon, qduoj, syzoj, testlib] # 26 | # Attention: When using testlib, DO NOT INCLUDE testlib.h in your file. # 27 | # 注意:使用Testlib时,不要在文件中包含testlib.h。 # 28 | checker_type: default # 29 | # When CHECKER_TYPE isn't 'default', The following options are allowed: -----+ # 30 | # 当比较器类型不为 'default' 时,您应该提供自定义比较器文件。 # # 31 | # Checker file (path in the zipfile) 文件路径(位于压缩包中的路径) # # 32 | checker: chk.cpp # # 33 | # Time and memory limit. 时间与内存限制(此处的限制优先级低于测试点的限制) # 34 | # Can be overwrite by the following option: # 35 | # cases->time cases->memory subtasks->time subtasks->memory # 36 | time: 1s # 37 | memory: 128m # 38 | # Extra files 额外文件 # 39 | # These files will be copied to the working directory 这些文件将被复制到工作目录。 # 40 | user_extra_files: # 41 | - extra_input.txt # 42 | judge_extra_files: # 43 | - extra_file.txt # 44 | 45 | # When Problem type is 'interactive', The following options are allowed: ---------+ 46 | # 当问题类型为 'interactive' 时,下面的设置可用: # 47 | # Interactor file (path in the zipfile) 交互器路径(位于压缩包中的路径) # 48 | interactor: interactor.cpp # 49 | # Extra files 额外文件 # 50 | # These files will be copied to the working directory 这些文件将被复制到工作目录。 # 51 | # judge_extra_files will be copy into workdir of both checker and interactor. # 52 | user_extra_files: # 53 | - extra_input.txt # 54 | judge_extra_files: # 55 | - extra_file.txt # 56 | 57 | # Test Cases 测试数据列表 58 | # If neither CASES or SUBTASKS are set(or config.yaml doesn't exist), judge will try to locate them automaticly. 59 | # 如果 CASES 和 SUBTASKS 都没有设置或 config.yaml 不存在, 系统会自动尝试识别数据点。 60 | # We support these names for auto mode: 自动识别支持以下命名方式: 61 | # 1. [name(optional)][number].(in/out/ans) RegExp: /^([a-zA-Z]*)([0-9]+).in$/ 62 | # examples: 63 | # - c1.in / c1.out 64 | # - 1.in / 1.out 65 | # - c1.in / c1.ans 66 | # 2. input[number].txt / output[number].txt RegExp: /^(input)([0-9]+).txt$/ 67 | # - example: input1.txt / input2.txt 68 | # 69 | # The CASES option has higher priority than the SUBTASKS option! 70 | # 在有 CASES 设置项时,不会读取 SUBTASKS 设置项! 71 | score: 50 # 单个测试点分数 72 | time: 1s # 时间限制 73 | memory: 256m # 内存限制 74 | cases: 75 | - input: abc.in 76 | output: def.out 77 | - input: ghi.in 78 | output: jkl.out 79 | 80 | # 使用Subtask项: 81 | 82 | subtasks: 83 | - score: 30 84 | time: 1s 85 | memory: 64m 86 | cases: 87 | - input: a.in 88 | output: a.out 89 | - input: b.in 90 | output: b.out 91 | - score: 70 92 | time: 0.5s 93 | memory: 32m 94 | cases: 95 | - input: c.in 96 | output: c.out 97 | - input: d.in 98 | output: d.out 99 | -------------------------------------------------------------------------------- /judge/sandbox.js: -------------------------------------------------------------------------------- 1 | const Axios = require('axios'); 2 | const fs = require('fs-extra'); 3 | const { 4 | SYSTEM_MEMORY_LIMIT_MB, SYSTEM_PROCESS_LIMIT, SYSTEM_TIME_LIMIT_MS, EXECUTION_HOST, 5 | } = require('./config'); 6 | const { SystemError } = require('./error'); 7 | const status = require('./status'); 8 | const { cmd } = require('./utils'); 9 | const { STATUS_ACCEPTED } = require('./status'); 10 | 11 | const fsp = fs.promises; 12 | const env = ['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'HOME=/w']; 13 | const axios = Axios.create({ baseURL: EXECUTION_HOST }); 14 | 15 | const statusMap = { 16 | 'Time Limit Exceeded': status.STATUS_TIME_LIMIT_EXCEEDED, 17 | 'Memory Limit Exceeded': status.STATUS_MEMORY_LIMIT_EXCEEDED, 18 | 'Output Limit Exceeded': status.STATUS_RUNTIME_ERROR, 19 | Accepted: status.STATUS_ACCEPTED, 20 | 'Nonzero Exit Status': status.STATUS_RUNTIME_ERROR, 21 | 'Internal Error': status.STATUS_SYSTEM_ERROR, 22 | 'File Error': status.STATUS_SYSTEM_ERROR, 23 | Signalled: status.STATUS_RUNTIME_ERROR, 24 | }; 25 | 26 | function proc({ 27 | execute, 28 | time_limit_ms = SYSTEM_TIME_LIMIT_MS, 29 | memory_limit_mb = SYSTEM_MEMORY_LIMIT_MB, 30 | process_limit = SYSTEM_PROCESS_LIMIT, 31 | stdin, copyIn = {}, copyOut = [], copyOutCached = [], 32 | } = {}) { 33 | return { 34 | args: cmd(execute.replace(/\$\{dir\}/g, '/w')), 35 | env, 36 | files: [ 37 | stdin ? { src: stdin } : { content: '' }, 38 | { name: 'stdout', max: 1024 * 1024 * 16 }, 39 | { name: 'stderr', max: 1024 * 1024 * 16 }, 40 | ], 41 | cpuLimit: time_limit_ms * 1000 * 1000, 42 | realCpuLimit: time_limit_ms * 3000 * 1000, 43 | memoryLimit: memory_limit_mb * 1024 * 1024, 44 | procLimit: process_limit, 45 | copyIn, 46 | copyOut, 47 | copyOutCached, 48 | }; 49 | } 50 | 51 | async function runMultiple(execute) { 52 | let res; 53 | try { 54 | const body = { 55 | cmd: [ 56 | proc(execute[0]), 57 | proc(execute[1]), 58 | ], 59 | pipeMapping: [{ 60 | in: { index: 0, fd: 1 }, 61 | out: { index: 1, fd: 0 }, 62 | }, { 63 | in: { index: 1, fd: 1 }, 64 | out: { index: 0, fd: 0 }, 65 | }], 66 | }; 67 | body.cmd[0].files[0] = null; 68 | body.cmd[0].files[1] = null; 69 | body.cmd[1].files[0] = null; 70 | body.cmd[1].files[1] = null; 71 | res = await axios.post('/run', body); 72 | } catch (e) { 73 | throw new SystemError('Cannot connect to sandbox service'); 74 | } 75 | return res.data; 76 | } 77 | 78 | async function del(fileId) { 79 | const res = await axios.delete(`/file/${fileId}`); 80 | return res.data; 81 | } 82 | 83 | async function run(execute, params) { 84 | let result; 85 | // eslint-disable-next-line no-return-await 86 | if (typeof execute === 'object') return await runMultiple(execute); 87 | try { 88 | const body = { cmd: [proc({ execute, ...params })] }; 89 | const res = await axios.post('/run', body); 90 | [result] = res.data; 91 | } catch (e) { 92 | // FIXME request body larger than maxBodyLength limit 93 | throw new SystemError('Cannot connect to sandbox service ', e.message); 94 | } 95 | // FIXME: Signalled? 96 | const ret = { 97 | status: statusMap[result.status] || STATUS_ACCEPTED, 98 | time_usage_ms: result.time / 1000000, 99 | memory_usage_kb: result.memory / 1024, 100 | files: result.files, 101 | code: result.exitStatus, 102 | }; 103 | if (ret.time_usage_ms >= (params.time_limit_ms || SYSTEM_TIME_LIMIT_MS)) { 104 | ret.status = status.STATUS_TIME_LIMIT_EXCEEDED; 105 | } 106 | result.files = result.files || {}; 107 | if (params.stdout) await fsp.writeFile(params.stdout, result.files.stdout || ''); 108 | else ret.stdout = result.files.stdout || ''; 109 | if (params.stderr) await fsp.writeFile(params.stderr, result.files.stderr || ''); 110 | else ret.stderr = result.files.stderr || ''; 111 | if (result.error) { 112 | ret.error = result.error; 113 | } 114 | ret.files = result.files; 115 | ret.fileIds = result.fileIds || {}; 116 | return ret; 117 | } 118 | 119 | module.exports = { del, run, runMultiple }; 120 | -------------------------------------------------------------------------------- /judge/judge/interactive.js: -------------------------------------------------------------------------------- 1 | const { default: Queue } = require('p-queue'); 2 | const fs = require('fs-extra'); 3 | const { 4 | STATUS_JUDGING, STATUS_COMPILING, STATUS_RUNTIME_ERROR, 5 | STATUS_TIME_LIMIT_EXCEEDED, STATUS_MEMORY_LIMIT_EXCEEDED, 6 | } = require('../status'); 7 | const { parseFilename } = require('../utils'); 8 | const { run } = require('../sandbox'); 9 | const log = require('../log'); 10 | const compile = require('../compile'); 11 | const signals = require('../signals'); 12 | 13 | const Score = { 14 | sum: (a, b) => (a + b), 15 | max: Math.max, 16 | min: Math.min, 17 | }; 18 | 19 | function judgeCase(c) { 20 | return async (ctx, ctxSubtask) => { 21 | ctx.executeInteractor.copyIn.stdans = { src: c.input }; 22 | const [{ code, time_usage_ms, memory_usage_kb }, resInteractor] = await run([ 23 | { 24 | execute: ctx.executeUser.execute.replace(/\$\{name\}/g, 'code'), 25 | copyIn: ctx.executeUser.copyIn, 26 | time_limit_ms: ctxSubtask.subtask.time_limit_ms, 27 | memory_limit_mb: ctxSubtask.subtask.memory_limit_mb, 28 | }, { 29 | execute: ctx.executeInteractor.execute.replace(/\$\{name\}/g, 'code'), 30 | copyIn: ctx.executeInteractor.copyIn, 31 | time_limit_ms: ctxSubtask.subtask.time_limit_ms * 2, 32 | memory_limit_mb: ctxSubtask.subtask.memory_limit_mb * 2, 33 | }, 34 | ]); 35 | let status; 36 | let score = 0; 37 | let message = ''; 38 | if (time_usage_ms > ctxSubtask.subtask.time_limit_ms) { 39 | status = STATUS_TIME_LIMIT_EXCEEDED; 40 | } else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) { 41 | status = STATUS_MEMORY_LIMIT_EXCEEDED; 42 | } else if (code) { 43 | status = STATUS_RUNTIME_ERROR; 44 | if (code < 32) message = signals[code]; 45 | else message = `您的程序返回了 ${code}.`; 46 | } else [status, score, message] = resInteractor.files.stderr.split('\n'); 47 | ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score); 48 | ctxSubtask.status = Math.max(ctxSubtask.status, status); 49 | ctx.total_time_usage_ms += time_usage_ms; 50 | ctx.total_memory_usage_kb = Math.max(ctx.total_memory_usage_kb, memory_usage_kb); 51 | ctx.next({ 52 | status: STATUS_JUDGING, 53 | case: { 54 | status, 55 | score: 0, 56 | time_ms: time_usage_ms, 57 | memory_kb: memory_usage_kb, 58 | judge_text: message, 59 | }, 60 | progress: Math.floor((c.id * 100) / ctx.config.count), 61 | }); 62 | }; 63 | } 64 | 65 | function judgeSubtask(subtask) { 66 | return async (ctx) => { 67 | subtask.type = subtask.type || 'min'; 68 | const ctxSubtask = { 69 | subtask, 70 | status: 0, 71 | score: subtask.type === 'min' 72 | ? subtask.score 73 | : 0, 74 | }; 75 | const cases = []; 76 | for (const cid in subtask.cases) { 77 | cases.push(ctx.queue.add(() => judgeCase(subtask.cases[cid])(ctx, ctxSubtask))); 78 | } 79 | await Promise.all(cases); 80 | ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); 81 | ctx.total_score += ctxSubtask.score; 82 | }; 83 | } 84 | 85 | exports.judge = async (ctx) => { 86 | ctx.next({ status: STATUS_COMPILING }); 87 | [ctx.executeUser, ctx.executeInteractor] = await Promise.all([ 88 | (async () => { 89 | const copyIn = {}; 90 | for (const file of ctx.config.user_extra_files) { 91 | copyIn[parseFilename(file)] = { src: file }; 92 | } 93 | return await compile(ctx.lang, ctx.code, 'code', copyIn, ctx.next); 94 | })(), 95 | (async () => { 96 | const copyIn = {}; 97 | for (const file of ctx.config.judge_extra_files) { 98 | copyIn[parseFilename(file)] = { src: file }; 99 | } 100 | return await compile( 101 | parseFilename(ctx.checker).split('.')[1], 102 | fs.readFileSync(ctx.checker), 103 | 'interactor', 104 | copyIn, 105 | ); 106 | })(), 107 | ]); 108 | ctx.next({ status: STATUS_JUDGING, progress: 0 }); 109 | const tasks = []; 110 | ctx.total_status = ctx.total_score = ctx.total_memory_usage_kb = ctx.total_time_usage_ms = 0; 111 | ctx.queue = new Queue({ concurrency: ctx.config.concurrency || 2 }); 112 | for (const sid in ctx.config.subtasks) { 113 | tasks.push(judgeSubtask(ctx.config.subtasks[sid])(ctx)); 114 | } 115 | await Promise.all(tasks); 116 | ctx.stat.done = new Date(); 117 | ctx.next({ judge_text: JSON.stringify(ctx.stat) }); 118 | log.log({ 119 | status: ctx.total_status, 120 | score: ctx.total_score, 121 | time_ms: ctx.total_time_usage_ms, 122 | memory_kb: ctx.total_memory_usage_kb, 123 | }); 124 | ctx.end({ 125 | status: ctx.total_status, 126 | score: ctx.total_score, 127 | time_ms: ctx.total_time_usage_ms, 128 | memory_kb: ctx.total_memory_usage_kb, 129 | }); 130 | }; 131 | -------------------------------------------------------------------------------- /judge/case/auto.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { SystemError } = require('../error'); 4 | const { ensureFile } = require('../utils'); 5 | 6 | const fsp = fs.promises; 7 | 8 | const RE0 = [ 9 | { 10 | reg: /^([a-z+_\-A-Z]*)([0-9]+).in$/, 11 | output: (a) => `${a[1] + a[2]}.out`, 12 | id: (a) => parseInt(a[2]), 13 | }, 14 | { 15 | reg: /^([a-z+_\-A-Z]*)([0-9]+).in$/, 16 | output: (a) => `${a[1] + a[2]}.ans`, 17 | id: (a) => parseInt(a[2]), 18 | }, 19 | { 20 | reg: /^([a-z+_\-A-Z0-9]*)\.in([0-9]+)$/, 21 | output: (a) => `${a[1]}.ou${a[2]}`, 22 | id: (a) => parseInt(a[2]), 23 | }, 24 | { 25 | reg: /^(input)([0-9]+).txt$/, 26 | output: (a) => `output${a[2]}.txt`, 27 | id: (a) => parseInt(a[2]), 28 | }, 29 | ]; 30 | const RE1 = [ 31 | { 32 | reg: /^([a-z+_\-A-Z]*)([0-9]+)-([0-9]+).in$/, 33 | output: (a) => `${a[1] + a[2]}-${a[3]}.out`, 34 | subtask: (a) => parseInt(a[2]), 35 | id: (a) => parseInt(a[3]), 36 | }, 37 | ]; 38 | 39 | async function read0(folder, files, checkFile) { 40 | const cases = []; 41 | for (const file of files) { 42 | for (const REG of RE0) { 43 | if (REG.reg.test(file)) { 44 | const data = REG.reg.exec(file); 45 | const c = { input: file, output: REG.output(data), id: REG.id(data) }; 46 | if (fs.existsSync(path.resolve(folder, c.output))) { 47 | cases.push(c); 48 | break; 49 | } 50 | } 51 | } 52 | } 53 | cases.sort((a, b) => (a.id - b.id)); 54 | const extra = cases.length - (100 % cases.length); 55 | const config = { 56 | count: 0, 57 | subtasks: [{ 58 | time_limit_ms: 1000, 59 | memory_limit_mb: 256, 60 | type: 'sum', 61 | cases: [], 62 | score: Math.floor(100 / cases.length), 63 | }], 64 | }; 65 | for (let i = 0; i < extra; i++) { 66 | config.count++; 67 | config.subtasks[0].cases.push({ 68 | id: config.count, 69 | input: checkFile(cases[i].input), 70 | output: checkFile(cases[i].output), 71 | }); 72 | } 73 | if (extra < cases.length) { 74 | config.subtasks.push({ 75 | time_limit_ms: 1000, 76 | memory_limit_mb: 256, 77 | type: 'sum', 78 | cases: [], 79 | score: Math.floor(100 / cases.length) + 1, 80 | }); 81 | for (let i = extra; i < cases.length; i++) { 82 | config.count++; 83 | config.subtasks[1].cases.push({ 84 | id: config.count, 85 | input: checkFile(cases[i].input), 86 | output: checkFile(cases[i].output), 87 | }); 88 | } 89 | } 90 | return config; 91 | } 92 | 93 | async function read1(folder, files, checkFile) { 94 | const subtask = {}; const 95 | subtasks = []; 96 | for (const file of files) { 97 | for (const REG of RE1) { 98 | if (REG.reg.test(file)) { 99 | const data = REG.reg.exec(file); 100 | const c = { input: file, output: REG.output(data), id: REG.id(data) }; 101 | if (fs.existsSync(path.resolve(folder, c.output))) { 102 | if (!subtask[REG.subtask(data)]) { 103 | subtask[REG.subtask(data)] = [{ 104 | time_limit_ms: 1000, 105 | memory_limit_mb: 256, 106 | type: 'min', 107 | cases: [c], 108 | }]; 109 | } else subtask[REG.subtask(data)].cases.push(c); 110 | break; 111 | } 112 | } 113 | } 114 | } 115 | for (const i in subtask) { 116 | subtask[i].cases.sort((a, b) => (a.id - b.id)); 117 | subtasks.push(subtask[i]); 118 | } 119 | const base = Math.floor(100 / subtasks.length); 120 | const extra = subtasks.length - (100 % subtasks.length); 121 | const config = { count: 0, subtasks }; 122 | for (const i in subtask) { 123 | if (extra < i) subtask[i].score = base; 124 | else subtask[i].score = base + 1; 125 | for (const j of subtask[i].cases) { 126 | config.count++; 127 | j.input = checkFile(j.input); 128 | j.output = checkFile(j.output); 129 | j.id = config.count; 130 | } 131 | } 132 | return config; 133 | } 134 | 135 | module.exports = async function readAutoCases(folder, filename, { next }) { 136 | const 137 | config = { 138 | checker_type: 'default', 139 | count: 0, 140 | subtasks: [], 141 | judge_extra_files: [], 142 | user_extra_files: [], 143 | }; 144 | const checkFile = ensureFile(folder); 145 | try { 146 | const files = await fsp.readdir(folder); 147 | let result = await read0(folder, files, checkFile); 148 | if (!result.count) result = await read1(folder, files, checkFile); 149 | Object.assign(config, result); 150 | next({ judge_text: `识别到${config.count}个测试点` }); 151 | } catch (e) { 152 | throw new SystemError('在自动识别测试点的过程中出现了错误。', [e]); 153 | } 154 | return config; 155 | }; 156 | -------------------------------------------------------------------------------- /judge/judge/default.js: -------------------------------------------------------------------------------- 1 | const { default: Queue } = require('p-queue'); 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | const { 5 | STATUS_JUDGING, STATUS_COMPILING, STATUS_RUNTIME_ERROR, 6 | STATUS_TIME_LIMIT_EXCEEDED, STATUS_MEMORY_LIMIT_EXCEEDED, 7 | STATUS_ACCEPTED, 8 | } = require('../status'); 9 | const { CompileError } = require('../error'); 10 | const { copyInDir, parseFilename } = require('../utils'); 11 | const { run } = require('../sandbox'); 12 | const compile = require('../compile'); 13 | const signals = require('../signals'); 14 | const { check, compileChecker } = require('../check'); 15 | 16 | const Score = { 17 | sum: (a, b) => (a + b), 18 | max: Math.max, 19 | min: Math.min, 20 | }; 21 | 22 | function judgeCase(c) { 23 | return async (ctx, ctxSubtask) => { 24 | const { filename } = ctx.config; 25 | const { copyIn } = ctx.execute; 26 | if (ctx.config.filename) copyIn[`${filename}.in`] = { src: c.input }; 27 | const copyOut = filename ? [`${filename}.out`] : []; 28 | const stdin = filename ? null : c.input; 29 | const stdout = path.resolve(ctx.tmpdir, `${c.id}.out`); 30 | const stderr = path.resolve(ctx.tmpdir, `${c.id}.err`); 31 | const res = await run( 32 | ctx.execute.execute.replace(/\$\{name\}/g, 'code'), 33 | { 34 | stdin, 35 | stdout: filename ? null : stdout, 36 | stderr, 37 | copyIn, 38 | copyOut, 39 | time_limit_ms: ctxSubtask.subtask.time_limit_ms, 40 | memory_limit_mb: ctxSubtask.subtask.memory_limit_mb, 41 | }, 42 | ); 43 | const { code, time_usage_ms, memory_usage_kb } = res; 44 | let { status } = res; 45 | if (res.files[`${filename}.out`] || !fs.existsSync(stdout)) { 46 | fs.writeFileSync(stdout, res.files[`${filename}.out`] || ''); 47 | } 48 | let message = ''; 49 | let score = 0; 50 | if (status === STATUS_ACCEPTED) { 51 | if (time_usage_ms > ctxSubtask.subtask.time_limit_ms) { 52 | status = STATUS_TIME_LIMIT_EXCEEDED; 53 | } else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) { 54 | status = STATUS_MEMORY_LIMIT_EXCEEDED; 55 | } else { 56 | [status, score, message] = await check({ 57 | copyIn: copyInDir(path.resolve(ctx.tmpdir, 'checker')), 58 | stdin: c.input, 59 | stdout: c.output, 60 | user_stdout: stdout, 61 | user_stderr: stderr, 62 | checker: ctx.config.checker, 63 | checker_type: ctx.config.checker_type, 64 | score: ctxSubtask.subtask.score, 65 | detail: ctx.config.detail, 66 | }); 67 | } 68 | } else if (status === STATUS_RUNTIME_ERROR && code) { 69 | status = STATUS_RUNTIME_ERROR; 70 | if (code < 32) message = signals[code]; 71 | else message = `您的程序返回了 ${code}.`; 72 | } 73 | ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score); 74 | ctxSubtask.status = Math.max(ctxSubtask.status, status); 75 | ctx.total_time_usage_ms += time_usage_ms; 76 | ctx.total_memory_usage_kb = Math.max(ctx.total_memory_usage_kb, memory_usage_kb); 77 | ctx.next({ 78 | status: STATUS_JUDGING, 79 | case: { 80 | status, 81 | score: 0, 82 | time_ms: time_usage_ms, 83 | memory_kb: memory_usage_kb, 84 | judge_text: message, 85 | }, 86 | progress: Math.floor((c.id * 100) / ctx.config.count), 87 | }, c.id); 88 | }; 89 | } 90 | 91 | function judgeSubtask(subtask) { 92 | return async (ctx) => { 93 | subtask.type = subtask.type || 'min'; 94 | const ctxSubtask = { 95 | subtask, 96 | status: 0, 97 | score: subtask.type === 'min' 98 | ? subtask.score 99 | : 0, 100 | }; 101 | const cases = []; 102 | for (const cid in subtask.cases) { 103 | cases.push(ctx.queue.add(() => judgeCase(subtask.cases[cid])(ctx, ctxSubtask))); 104 | } 105 | await Promise.all(cases); 106 | ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); 107 | ctx.total_score += ctxSubtask.score; 108 | }; 109 | } 110 | 111 | exports.judge = async (ctx) => { 112 | if (ctx.config.template) { 113 | if (ctx.config.template[ctx.lang]) { 114 | const tpl = ctx.config.template[ctx.lang]; 115 | ctx.code = tpl[0] + ctx.code + tpl[1]; 116 | } else throw new CompileError('Language not supported by provided templates'); 117 | } 118 | ctx.next({ status: STATUS_COMPILING }); 119 | [ctx.execute] = await Promise.all([ 120 | (async () => { 121 | const copyIn = {}; 122 | for (const file of ctx.config.user_extra_files) { 123 | copyIn[parseFilename(file)] = { src: file }; 124 | } 125 | return await compile(ctx.lang, ctx.code, 'code', copyIn, ctx.next); 126 | })(), 127 | (async () => { 128 | const copyIn = {}; 129 | for (const file of ctx.config.judge_extra_files) { 130 | copyIn[parseFilename(file)] = { src: file }; 131 | } 132 | return await compileChecker( 133 | ctx.config.checker_type || 'default', 134 | ctx.config.checker, 135 | copyIn, 136 | ); 137 | })(), 138 | ]); 139 | ctx.clean.push(ctx.execute.clean); 140 | ctx.next({ status: STATUS_JUDGING, progress: 0 }); 141 | const tasks = []; 142 | ctx.total_status = 0; 143 | ctx.total_score = 0; 144 | ctx.total_memory_usage_kb = 0; 145 | ctx.total_time_usage_ms = 0; 146 | ctx.queue = new Queue({ concurrency: ctx.config.concurrency || 2 }); 147 | for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid])(ctx)); 148 | await Promise.all(tasks); 149 | ctx.stat.done = new Date(); 150 | ctx.next({ judge_text: JSON.stringify(ctx.stat) }); 151 | ctx.end({ 152 | status: ctx.total_status, 153 | score: ctx.total_score, 154 | time_ms: Math.floor(ctx.total_time_usage_ms * 1000000) / 1000000, 155 | memory_kb: ctx.total_memory_usage_kb, 156 | }); 157 | }; 158 | -------------------------------------------------------------------------------- /judge/hosts/uoj.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const assert = require('assert'); 3 | const child = require('child_process'); 4 | const axios = require('axios'); 5 | const fs = require('fs-extra'); 6 | const log = require('../log'); 7 | const { compilerText } = require('../utils'); 8 | const { CACHE_DIR, TEMP_DIR } = require('../config'); 9 | const { FormatError, CompileError } = require('../error'); 10 | const { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } = require('../status'); 11 | const readCases = require('../cases'); 12 | const judge = require('../judge'); 13 | 14 | const fsp = fs.promises; 15 | const LANGS_MAP = { 16 | C: 'c', 17 | 'C++': 'cc98', 18 | 'C++11': 'cc11', 19 | Java8: 'java', 20 | Java11: 'java', 21 | Pascal: 'pas', 22 | Python2: 'py2', 23 | Python3: 'py3', 24 | }; 25 | 26 | class JudgeTask { 27 | constructor(session, submission) { 28 | this.stat = {}; 29 | this.stat.receive = new Date(); 30 | console.log(submission.content); 31 | this.session = session; 32 | this.submission = submission; 33 | } 34 | 35 | async handle() { 36 | this.stat.handle = new Date(); 37 | this.host = this.session.config.host; 38 | this.rid = this.submission.id; 39 | this.pid = this.submission.problem_id; 40 | this.problem_mtime = this.submission.problem_mtime; 41 | for (const i of this.submission.content.config) { 42 | if (i[0] === 'answer_language') this.lang = LANGS_MAP[i[1]]; 43 | } 44 | this.tmpdir = path.resolve(TEMP_DIR, 'tmp', this.host, this.rid.toString()); 45 | this.details = { 46 | cases: [], 47 | compiler_text: [], 48 | judge_text: [], 49 | }; 50 | fs.ensureDirSync(this.tmpdir); 51 | log.submission(`${this.host}/${this.rid}`, { pid: this.pid }); 52 | try { 53 | await this.submission(); 54 | } catch (e) { 55 | if (e instanceof CompileError) { 56 | this.next({ compiler_text: compilerText(e.stdout, e.stderr) }); 57 | this.end({ 58 | status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0, 59 | }); 60 | } else if (e instanceof FormatError) { 61 | this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` }); 62 | this.end({ 63 | status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, 64 | }); 65 | } else { 66 | log.error(`${e.message}\n${e.stack}`); 67 | this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` }); 68 | this.end({ 69 | status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, 70 | }); 71 | } 72 | } 73 | fs.removeSync(this.tmpdir); 74 | } 75 | 76 | dir(p) { 77 | return path.resolve(this.tmpdir, p); 78 | } 79 | 80 | async submission() { 81 | this.stat.cache_start = new Date(); 82 | this.folder = await this.session.problemData( 83 | this.submission.problem_id, 84 | this.submission.problem_mtime, 85 | ); 86 | await this.session.uoj_download(this.submission.content.file_name, this.dir('all.zip')); 87 | await new Promise((resolve, reject) => { 88 | child.exec(`unzip ${this.dir('all.zip')} -d ${this.dir('all')}`, (e) => { 89 | if (e) reject(e); 90 | else resolve(); 91 | }); 92 | }); 93 | this.code = await fsp.readFile(this.dir('all/answer.code')); 94 | this.stat.read_cases = new Date(); 95 | this.config = await readCases(this.folder, { detail: this.session.config.detail }); 96 | this.stat.judge = new Date(); 97 | await judge[this.config.type || 'default'].judge(this); 98 | } 99 | 100 | async next(data) { 101 | console.log('next', data); 102 | if (data.compiler_text) this.details.compiler_text.push(data.compiler_text); 103 | if (data.judge_text) this.details.judge_text.push(data.judge_text); 104 | if (data.case) this.details.cases.push(data.case); 105 | return {}; 106 | } 107 | 108 | async end(result) { 109 | if (result.compiler_text) this.details.compiler_text.push(result.compiler_text); 110 | if (result.judge_text) this.details.judge_text.push(result.judge_text); 111 | if (result.case) this.details.cases.push(result.case); 112 | if (result.status) this.details.status = result.status; 113 | console.log('end', result); 114 | const data = { 115 | submit: true, 116 | result: {}, 117 | }; 118 | if (this.submission.is_hack) { 119 | data.is_hack = true; 120 | data.id = this.submission.hack.id; 121 | // if (result != false && result.score) 122 | // try { 123 | // files = { 124 | // 'hack_input': fs.createReadStream(this.dir('hack_input.txt')), 125 | // 'std_output': fs.createReadStream(this.dir('std_output.txt')) 126 | // }; 127 | // } catch (e) { 128 | // result = false; 129 | // } 130 | } else if (this.submission.is_custom_test) { 131 | data.is_custom_test = true; 132 | data.id = this.submission.id; 133 | } else { data.id = this.submission.id; } 134 | data.result.score = result.score; 135 | data.result.time = result.time_ms; 136 | data.result.memory = result.memory_kb; 137 | if (result.status === STATUS_SYSTEM_ERROR) { 138 | data.result.error = 'Judgement Failed'; 139 | data.result.details = this.details || ''; 140 | } 141 | data.result.status = 'Judged'; 142 | data.result = JSON.stringify(data.result); 143 | const res = await this.session.axios.post('/judge/submit', data); 144 | this.session.consume(); 145 | return res.data; 146 | } 147 | } 148 | 149 | module.exports = class UOJ { 150 | constructor(config) { 151 | this.config = config; 152 | assert(config.server_url); 153 | assert(config.uname); 154 | assert(config.password); 155 | if (!this.config.server_url.startsWith('http')) this.config.server_url = `http://${this.config.server_url}`; 156 | } 157 | 158 | async download(uri, file) { 159 | const res = await this.axios.post(`/judge/download${uri}`, {}, { responseType: 'stream' }); 160 | const w = fs.createWriteStream(file); 161 | res.data.pipe(w); 162 | return await new Promise((resolve, reject) => { 163 | w.on('finish', resolve); 164 | w.on('error', reject); 165 | }); 166 | } 167 | 168 | async init() { 169 | const { uname, password } = this.config; 170 | this.axios = axios.create({ 171 | baseURL: this.config.server_url, 172 | timeout: this.config.timeout || 3000, 173 | transformRequest: [ 174 | (data) => { 175 | Object.assign(data, { 176 | judge_name: uname, 177 | password, 178 | }); 179 | let ret = ''; 180 | for (const it in data) { 181 | ret += `${encodeURIComponent(it)}=${encodeURIComponent(data[it])}&`; 182 | } 183 | return ret; 184 | }, 185 | ], 186 | }); 187 | } 188 | 189 | async problemData(pid, version) { 190 | const filePath = path.join(CACHE_DIR, this.config.host, pid.toString()); 191 | if (fs.existsSync(filePath)) { 192 | let ver; 193 | try { 194 | ver = fs.readFileSync(path.join(filePath, 'version')).toString(); 195 | } catch (e) { /* ignore */ } 196 | if (version === ver) return `${filePath}/${pid}`; 197 | fs.removeSync(filePath); 198 | } 199 | log.info(`Getting problem data: ${this.config.host}/${pid}`); 200 | const tmpFilePath = path.resolve(CACHE_DIR, `download_${this.config.host}_${pid}`); 201 | await this.download(`/problem/${pid}`, tmpFilePath); 202 | fs.ensureDirSync(path.dirname(filePath)); 203 | await new Promise((resolve, reject) => { 204 | child.exec(`unzip ${tmpFilePath} -d ${filePath}`, (e) => { 205 | if (e) reject(e); 206 | else resolve(); 207 | }); 208 | }); 209 | await fsp.unlink(tmpFilePath); 210 | fs.writeFileSync(path.join(filePath, 'version'), version); 211 | return `${filePath}/${pid}`; 212 | } 213 | 214 | async consume(queue) { 215 | if (queue) this.queue = queue; 216 | const ret = await this.axios.post('/judge/submit', {}); 217 | if (ret.data === 'Nothing to judge') setTimeout(() => { this.consume(); }, 1000); 218 | else this.queue.push(new JudgeTask(this, ret.data)); 219 | } 220 | }; 221 | -------------------------------------------------------------------------------- /service.js: -------------------------------------------------------------------------------- 1 | // Hydro Integration 2 | /* eslint-disable no-await-in-loop */ 3 | const path = require('path'); 4 | const cluster = require('cluster'); 5 | const child = require('child_process'); 6 | const yaml = require('js-yaml'); 7 | const fs = require('fs-extra'); 8 | 9 | async function postInit() { 10 | // Only start a single daemon 11 | if (cluster.isMaster || !cluster.isFirstWorker) return; 12 | const config = require('./judge/config'); 13 | const log = require('./judge/log'); 14 | log.logger(global.Hydro.lib.logger); 15 | config.LANGS = yaml.safeLoad(await global.Hydro.model.system.get('judge.langs')); 16 | const { compilerText } = require('./judge/utils'); 17 | const tmpfs = require('./judge/tmpfs'); 18 | const { FormatError, CompileError, SystemError } = require('./judge/error'); 19 | const { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } = require('./judge/status'); 20 | const readCases = require('./judge/cases'); 21 | const judge = require('./judge/judge'); 22 | const sysinfo = require('./judge/sysinfo'); 23 | 24 | const { problem, file, task } = global.Hydro.model; 25 | const { judge: _judge, misc } = global.Hydro.handler; 26 | 27 | const info = await sysinfo.get(); 28 | misc.updateStatus(info); 29 | setInterval(async () => { 30 | const [mid, info] = await sysinfo.update(); 31 | misc.updateStatus({ mid, ...info }); 32 | }, 1200000); 33 | 34 | async function processData(folder) { 35 | let files = await fs.readdir(folder); 36 | let ini = false; 37 | for (const i of files) { 38 | if (i.toLowerCase() === 'config.ini') { 39 | ini = true; 40 | await fs.rename(`${folder}/${i}`, `${folder}/config.ini`); 41 | break; 42 | } 43 | } 44 | if (ini) { 45 | for (const i of files) { 46 | if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`); 47 | else if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`); 48 | } 49 | files = await fs.readdir(`${folder}/input`); 50 | for (const i of files) await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`); 51 | files = await fs.readdir(`${folder}/output`); 52 | for (const i of files) await fs.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`); 53 | } 54 | } 55 | 56 | async function problemData(domainId, pid, savePath) { 57 | const tmpFilePath = path.resolve(config.CACHE_DIR, `download_${domainId}_${pid}`); 58 | const pdoc = await problem.get(domainId, pid); 59 | const data = await file.get(pdoc.data); 60 | if (!data) throw new SystemError('Problem data not found.'); 61 | const w = await fs.createWriteStream(tmpFilePath); 62 | data.pipe(w); 63 | await new Promise((resolve, reject) => { 64 | w.on('finish', resolve); 65 | w.on('error', reject); 66 | }); 67 | fs.ensureDirSync(path.dirname(savePath)); 68 | await new Promise((resolve, reject) => { 69 | child.exec(`unzip ${tmpFilePath} -d ${savePath}`, (e) => { 70 | if (e) reject(e); 71 | else resolve(); 72 | }); 73 | }); 74 | await fs.unlink(tmpFilePath); 75 | await processData(savePath).catch(); 76 | return savePath; 77 | } 78 | 79 | async function cacheOpen(domainId, pid, version) { 80 | const filePath = path.join(config.CACHE_DIR, domainId, pid); 81 | if (fs.existsSync(filePath)) { 82 | let ver; 83 | try { 84 | ver = fs.readFileSync(path.join(filePath, 'version')).toString(); 85 | } catch (e) { /* ignore */ } 86 | if (version === ver) return filePath; 87 | fs.removeSync(filePath); 88 | } 89 | fs.ensureDirSync(filePath); 90 | await problemData(domainId, pid, filePath); 91 | fs.writeFileSync(path.join(filePath, 'version'), version); 92 | return filePath; 93 | } 94 | 95 | function getNext(that) { 96 | that.nextId = 1; 97 | that.nextWaiting = []; 98 | return (data, id) => { 99 | data.domainId = that.domainId; 100 | data.rid = that.rid; 101 | data.time = data.time_ms || data.time; 102 | data.memory = data.memory_kb || data.memory; 103 | data.message = data.judge_text || data.message; 104 | data.compilerText = data.compiler_text || data.compilerText; 105 | if (data.case) { 106 | data.case = { 107 | status: data.case.status, 108 | time: data.case.time_ms || data.case.time, 109 | memory: data.case.memory_kb || data.case.memory, 110 | message: data.judge_text || data.message || data.judgeText, 111 | }; 112 | } 113 | if (id) { 114 | if (id === that.nextId) { 115 | _judge.next(data); 116 | that.nextId++; 117 | let t = true; 118 | while (t) { 119 | t = false; 120 | for (const i in that.nextWaiting) { 121 | if (that.nextId === that.nextWaiting[i].id) { 122 | _judge.next(that.nextWaiting[i].data); 123 | that.nextId++; 124 | that.nextWaiting.splice(i, 1); 125 | t = true; 126 | } 127 | } 128 | } 129 | } else that.nextWaiting.push({ data, id }); 130 | } else _judge.next(data); 131 | }; 132 | } 133 | 134 | function getEnd(domainId, rid) { 135 | return (data) => { 136 | data.key = 'end'; 137 | data.rid = rid; 138 | data.domainId = domainId; 139 | data.rid = rid; 140 | data.time = data.time_ms || data.time; 141 | data.memory = data.memory_kb || data.memory; 142 | log.log({ 143 | status: data.status, 144 | score: data.score, 145 | time_ms: data.time_ms, 146 | memory_kb: data.memory_kb, 147 | }); 148 | _judge.end(data); 149 | }; 150 | } 151 | 152 | class JudgeTask { 153 | constructor(request) { 154 | this.stat = {}; 155 | this.stat.receive = new Date(); 156 | this.request = request; 157 | } 158 | 159 | async handle() { 160 | try { 161 | this.stat.handle = new Date(); 162 | this.event = this.request.event || 'judge'; 163 | this.pid = (this.request.pid || 'unknown').toString(); 164 | this.rid = this.request.rid.toString(); 165 | this.domainId = this.request.domainId; 166 | this.lang = this.request.lang; 167 | this.code = this.request.code; 168 | this.data = this.request.data; 169 | this.config = this.request.config; 170 | this.next = getNext(this); 171 | this.end = getEnd(this.domainId, this.rid); 172 | this.tmpdir = path.resolve(config.TEMP_DIR, 'tmp', this.rid); 173 | this.clean = []; 174 | fs.ensureDirSync(this.tmpdir); 175 | tmpfs.mount(this.tmpdir, '64m'); 176 | log.submission(`${this.rid}`, { pid: this.pid }); 177 | if (this.event === 'judge') await this.submission(); 178 | else if (this.event === 'run') await this.run(); 179 | else throw new SystemError(`Unsupported type: ${this.event}`); 180 | } catch (e) { 181 | if (e instanceof CompileError) { 182 | this.next({ compiler_text: compilerText(e.stdout, e.stderr) }); 183 | this.end({ 184 | status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0, 185 | }); 186 | } else if (e instanceof FormatError) { 187 | this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` }); 188 | this.end({ 189 | status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, 190 | }); 191 | } else { 192 | log.error(e); 193 | this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` }); 194 | this.end({ 195 | status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, 196 | }); 197 | } 198 | } 199 | // eslint-disable-next-line no-await-in-loop 200 | for (const clean of this.clean) await clean().catch(); 201 | tmpfs.umount(this.tmpdir); 202 | fs.removeSync(this.tmpdir); 203 | } 204 | 205 | async submission() { 206 | this.stat.cache_start = new Date(); 207 | this.folder = await cacheOpen(this.domainId, this.pid, this.data); 208 | this.stat.read_cases = new Date(); 209 | this.config = await readCases( 210 | this.folder, 211 | { detail: true }, 212 | { next: this.next }, 213 | ); 214 | this.stat.judge = new Date(); 215 | await judge[this.config.type || 'default'].judge(this); 216 | } 217 | 218 | async run() { 219 | this.stat.judge = new Date(); 220 | await judge.run.judge(this); 221 | } 222 | } 223 | 224 | task.consume({ type: 'judge' }, (t) => (new JudgeTask(t)).handle().catch((e) => log.error(e))); 225 | task.consume({ type: 'run' }, (t) => (new JudgeTask(t)).handle().catch((e) => log.error(e))); 226 | } 227 | 228 | global.Hydro.service.judge = module.exports = { postInit }; 229 | -------------------------------------------------------------------------------- /service.ts: -------------------------------------------------------------------------------- 1 | // Hydro Integration 2 | /* eslint-disable no-await-in-loop */ 3 | import 'hydrooj'; 4 | import path from 'path'; 5 | import cluster from 'cluster'; 6 | import child from 'child_process'; 7 | import { ObjectID } from 'bson'; 8 | import yaml from 'js-yaml'; 9 | import fs from 'fs-extra'; 10 | 11 | async function postInit() { 12 | // Only start a single daemon 13 | if (!cluster.isFirstWorker) return; 14 | const config = require('./judge/config'); 15 | const log = require('./judge/log'); 16 | log.logger(global.Hydro.lib.logger); 17 | config.LANGS = yaml.safeLoad(await global.Hydro.model.system.get('judge.langs')); 18 | const { compilerText } = require('./judge/utils'); 19 | const tmpfs = require('./judge/tmpfs'); 20 | const { FormatError, CompileError, SystemError } = require('./judge/error'); 21 | const { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } = require('./judge/status'); 22 | const readCases = require('./judge/cases'); 23 | const judge = require('./judge/judge'); 24 | const sysinfo = require('./judge/sysinfo'); 25 | 26 | const { problem, file, task } = global.Hydro.model; 27 | const { judge: _judge } = global.Hydro.handler; 28 | const { monitor } = global.Hydro.service; 29 | 30 | const info = await sysinfo.get(); 31 | monitor.updateJudger(info); 32 | setInterval(async () => { 33 | const [mid, info] = await sysinfo.update(); 34 | monitor.updateJudger({ mid, ...info }); 35 | }, 1200000); 36 | 37 | async function processData(folder: string) { 38 | let files = await fs.readdir(folder); 39 | let ini = false; 40 | for (const i of files) { 41 | if (i.toLowerCase() === 'config.ini') { 42 | ini = true; 43 | await fs.rename(`${folder}/${i}`, `${folder}/config.ini`); 44 | break; 45 | } 46 | } 47 | if (ini) { 48 | for (const i of files) { 49 | if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`); 50 | else if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`); 51 | } 52 | files = await fs.readdir(`${folder}/input`); 53 | for (const i of files) await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`); 54 | files = await fs.readdir(`${folder}/output`); 55 | for (const i of files) await fs.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`); 56 | } 57 | } 58 | 59 | async function problemData(domainId: string, pid: string, savePath: string) { 60 | const tmpFilePath = path.resolve(config.CACHE_DIR, `download_${domainId}_${pid}`); 61 | const pdoc = await problem.get(domainId, pid); 62 | const data = await file.get(pdoc.data); 63 | if (!data) throw new SystemError('Problem data not found.'); 64 | const w = fs.createWriteStream(tmpFilePath); 65 | data.pipe(w); 66 | await new Promise((resolve, reject) => { 67 | w.on('finish', resolve); 68 | w.on('error', reject); 69 | }); 70 | fs.ensureDirSync(path.dirname(savePath)); 71 | await new Promise((resolve, reject) => { 72 | child.exec(`unzip ${tmpFilePath} -d ${savePath}`, (e) => { 73 | if (e) reject(e); 74 | else resolve(); 75 | }); 76 | }); 77 | await fs.unlink(tmpFilePath); 78 | await processData(savePath).catch(); 79 | return savePath; 80 | } 81 | 82 | async function cacheOpen(domainId: string, pid: string, version: string) { 83 | const filePath = path.join(config.CACHE_DIR, domainId, pid); 84 | if (fs.existsSync(filePath)) { 85 | let ver: string; 86 | try { 87 | ver = fs.readFileSync(path.join(filePath, 'version')).toString(); 88 | } catch (e) { /* ignore */ } 89 | if (version === ver) return filePath; 90 | fs.removeSync(filePath); 91 | } 92 | fs.ensureDirSync(filePath); 93 | await problemData(domainId, pid, filePath); 94 | fs.writeFileSync(path.join(filePath, 'version'), version); 95 | return filePath; 96 | } 97 | 98 | function getNext(that) { 99 | that.nextId = 1; 100 | that.nextWaiting = []; 101 | return (data, id) => { 102 | data.domainId = that.domainId; 103 | data.rid = new ObjectID(that.rid); 104 | data.time = data.time_ms || data.time; 105 | data.memory = data.memory_kb || data.memory; 106 | data.message = data.judge_text || data.message; 107 | data.compilerText = data.compiler_text || data.compilerText; 108 | if (data.case) { 109 | data.case = { 110 | status: data.case.status, 111 | time: data.case.time_ms || data.case.time, 112 | memory: data.case.memory_kb || data.case.memory, 113 | message: data.judge_text || data.message || data.judgeText, 114 | }; 115 | } 116 | if (id) { 117 | if (id === that.nextId) { 118 | _judge.next(data); 119 | that.nextId++; 120 | let t = true; 121 | while (t) { 122 | t = false; 123 | for (const i in that.nextWaiting) { 124 | if (that.nextId === that.nextWaiting[i].id) { 125 | _judge.next(that.nextWaiting[i].data); 126 | that.nextId++; 127 | that.nextWaiting.splice(i, 1); 128 | t = true; 129 | } 130 | } 131 | } 132 | } else that.nextWaiting.push({ data, id }); 133 | } else _judge.next(data); 134 | }; 135 | } 136 | 137 | function getEnd(domainId: string, rid: string) { 138 | return (data) => { 139 | data.key = 'end'; 140 | data.rid = new ObjectID(rid); 141 | data.domainId = domainId; 142 | data.time = data.time_ms || data.time; 143 | data.memory = data.memory_kb || data.memory; 144 | log.log({ 145 | status: data.status, 146 | score: data.score, 147 | time_ms: data.time_ms, 148 | memory_kb: data.memory_kb, 149 | }); 150 | _judge.end(data); 151 | }; 152 | } 153 | 154 | class JudgeTask { 155 | stat: any; 156 | request: any; 157 | event: string; 158 | pid: string; 159 | rid: string; 160 | domainId: string; 161 | lang: string; 162 | code: string; 163 | data: string; 164 | config: any; 165 | next: (data: any, id?: any) => void; 166 | end: (data: any) => void; 167 | tmpdir: string; 168 | clean: any[]; 169 | folder: string; 170 | constructor(request) { 171 | this.stat = {}; 172 | this.stat.receive = new Date(); 173 | this.request = request; 174 | } 175 | 176 | async handle() { 177 | try { 178 | this.stat.handle = new Date(); 179 | this.pid = (this.request.pid || 'unknown').toString(); 180 | this.rid = this.request.rid.toString(); 181 | this.domainId = this.request.domainId; 182 | this.lang = this.request.lang; 183 | this.code = this.request.code; 184 | this.data = this.request.data; 185 | this.config = this.request.config; 186 | this.next = getNext(this); 187 | this.end = getEnd(this.domainId, this.rid); 188 | this.tmpdir = path.resolve(config.TEMP_DIR, 'tmp', this.rid); 189 | this.clean = []; 190 | fs.ensureDirSync(this.tmpdir); 191 | tmpfs.mount(this.tmpdir, '64m'); 192 | log.submission(`${this.rid}`, { pid: this.pid }); 193 | if (this.config.input) await this.run(); 194 | else await this.submission(); 195 | } catch (e) { 196 | if (e instanceof CompileError) { 197 | this.next({ compiler_text: compilerText(e.stdout, e.stderr) }); 198 | this.end({ 199 | status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0, 200 | }); 201 | } else if (e instanceof FormatError) { 202 | this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` }); 203 | this.end({ 204 | status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, 205 | }); 206 | } else { 207 | log.error(e); 208 | this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` }); 209 | this.end({ 210 | status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, 211 | }); 212 | } 213 | } 214 | // eslint-disable-next-line no-await-in-loop 215 | for (const clean of this.clean) await clean().catch(); 216 | tmpfs.umount(this.tmpdir); 217 | fs.removeSync(this.tmpdir); 218 | } 219 | 220 | async submission() { 221 | this.stat.cache_start = new Date(); 222 | this.folder = await cacheOpen(this.domainId, this.pid, this.data); 223 | this.stat.read_cases = new Date(); 224 | this.config = await readCases( 225 | this.folder, 226 | { detail: true }, 227 | { next: this.next }, 228 | ); 229 | this.stat.judge = new Date(); 230 | await judge[this.config.type || 'default'].judge(this); 231 | } 232 | 233 | async run() { 234 | this.stat.judge = new Date(); 235 | await judge.run.judge(this); 236 | } 237 | } 238 | 239 | task.consume({ type: 'judge' }, (t) => (new JudgeTask(t)).handle().catch((e) => log.error(e))); 240 | } 241 | 242 | global.Hydro.postInit.push(postInit); 243 | -------------------------------------------------------------------------------- /judge/hosts/hydro.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | const path = require('path'); 3 | const child = require('child_process'); 4 | const axios = require('axios'); 5 | const fs = require('fs-extra'); 6 | const tmpfs = require('../tmpfs'); 7 | const log = require('../log'); 8 | const sysinfo = require('../sysinfo'); 9 | const { compilerText } = require('../utils'); 10 | const { CACHE_DIR, TEMP_DIR } = require('../config'); 11 | const { FormatError, CompileError } = require('../error'); 12 | const { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } = require('../status'); 13 | const readCases = require('../cases'); 14 | const judge = require('../judge'); 15 | 16 | const fsp = fs.promises; 17 | 18 | class JudgeTask { 19 | constructor(session, request) { 20 | this.stat = {}; 21 | this.stat.receive = new Date(); 22 | this.session = session; 23 | this.host = session.config.host; 24 | this.request = request; 25 | } 26 | 27 | async handle() { 28 | try { 29 | this.stat.handle = new Date(); 30 | this.pid = (this.request.pid || 'unknown').toString(); 31 | this.rid = this.request.rid.toString(); 32 | this.domainId = this.request.domainId; 33 | this.lang = this.request.lang; 34 | this.code = this.request.code; 35 | this.data = this.request.data; 36 | this.next = this.getNext(this); 37 | this.end = this.getEnd(this.session, this.domainId, this.rid); 38 | this.tmpdir = path.resolve(TEMP_DIR, 'tmp', this.host, this.rid); 39 | this.clean = []; 40 | fs.ensureDirSync(this.tmpdir); 41 | tmpfs.mount(this.tmpdir, '64m'); 42 | log.submission(`${this.host}/${this.rid}`, { pid: this.pid }); 43 | await this.submission(); 44 | } catch (e) { 45 | if (e instanceof CompileError) { 46 | this.next({ compiler_text: compilerText(e.stdout, e.stderr) }); 47 | this.end({ 48 | status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0, 49 | }); 50 | } else if (e instanceof FormatError) { 51 | this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` }); 52 | this.end({ 53 | status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, 54 | }); 55 | } else { 56 | log.error(e); 57 | this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` }); 58 | this.end({ 59 | status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, 60 | }); 61 | } 62 | } 63 | for (const clean of this.clean) await clean().catch(); 64 | tmpfs.umount(this.tmpdir); 65 | fs.removeSync(this.tmpdir); 66 | } 67 | 68 | async submission() { 69 | this.stat.cache_start = new Date(); 70 | this.folder = await this.session.cacheOpen(this.pid, this.data); 71 | this.stat.read_cases = new Date(); 72 | this.config = await readCases( 73 | this.folder, 74 | { detail: this.session.config.detail }, 75 | { next: this.next, config: this.request.config }, 76 | ); 77 | this.stat.judge = new Date(); 78 | await judge[this.config.type || 'default'].judge(this); 79 | } 80 | 81 | async run() { 82 | this.stat.judge = new Date(); 83 | await judge.run.judge(this); 84 | } 85 | 86 | getNext(that) { // eslint-disable-line class-methods-use-this 87 | that.nextId = 1; 88 | that.nextWaiting = []; 89 | return (data, id) => { 90 | data.operation = 'next'; 91 | data.domainId = that.domainId; 92 | data.rid = that.rid; 93 | data.time = data.time_ms || data.time; 94 | data.memory = data.memory_kb || data.memory; 95 | data.message = data.judge_text || data.message; 96 | data.compilerText = data.compiler_text || data.compilerText; 97 | if (data.case) { 98 | data.case = { 99 | status: data.case.status, 100 | time: data.case.time_ms || data.case.time, 101 | memory: data.case.memory_kb || data.case.memory, 102 | message: data.judge_text || data.message || data.judgeText, 103 | }; 104 | } 105 | if (id) { 106 | if (id === that.nextId) { 107 | that.session.axios.post('/judge', data); 108 | that.nextId++; 109 | let t = true; 110 | while (t) { 111 | t = false; 112 | for (const i in that.nextWaiting) { 113 | if (that.nextId === that.nextWaiting[i].id) { 114 | that.session.axios.post('/judge', that.nextWaiting[i].data); 115 | that.nextId++; 116 | that.nextWaiting.splice(i, 1); 117 | t = true; 118 | } 119 | } 120 | } 121 | } else that.nextWaiting.push({ data, id }); 122 | } else that.session.axios.post('/judge', data); 123 | }; 124 | } 125 | 126 | getEnd(session, domainId, rid) { // eslint-disable-line class-methods-use-this 127 | return (data) => { 128 | data.operation = 'end'; 129 | data.domainId = domainId; 130 | data.rid = rid; 131 | data.time = data.time_ms || data.time; 132 | data.memory = data.memory_kb || data.memory; 133 | log.log({ 134 | status: data.status, 135 | score: data.score, 136 | time_ms: data.time_ms, 137 | memory_kb: data.memory_kb, 138 | }); 139 | session.axios.post('/judge', data); 140 | }; 141 | } 142 | } 143 | 144 | class Hydro { 145 | constructor(config) { 146 | this.config = config; 147 | this.config.detail = this.config.detail || true; 148 | this.config.cookie = this.config.cookie || ''; 149 | this.config.last_update_at = this.config.last_update_at || 0; 150 | if (!this.config.server_url.startsWith('http')) this.config.server_url = `http://${this.config.server_url}`; 151 | if (!this.config.server_url.endsWith('/')) this.config.server_url = `${this.config.server_url}/`; 152 | } 153 | 154 | async init() { 155 | await this.setCookie(this.config.cookie || ''); 156 | await this.ensureLogin(); 157 | const info = await sysinfo.get(); 158 | this.axios.post('/status/update', info); 159 | setInterval(async () => { 160 | const [_id, info] = await sysinfo.update(); 161 | this.axios.post('/status/update', { _id, ...info }); 162 | }, 1200000); 163 | } 164 | 165 | async problemData(pid, savePath, retry = 3) { 166 | log.info(`Getting problem data: ${this.config.host}/${pid}`); 167 | await this.ensureLogin(); 168 | const tmpFilePath = path.resolve(CACHE_DIR, `download_${this.config.host}_${pid}`); 169 | try { 170 | console.log(`${this.config.server_url}/p/${pid}/data`); 171 | const res = await this.axios.get(`${this.config.server_url}/p/${pid}/data`, { responseType: 'stream' }); 172 | const w = await fs.createWriteStream(tmpFilePath); 173 | res.data.pipe(w); 174 | await new Promise((resolve, reject) => { 175 | w.on('finish', resolve); 176 | w.on('error', reject); 177 | }); 178 | fs.ensureDirSync(path.dirname(savePath)); 179 | await new Promise((resolve, reject) => { 180 | child.exec(`unzip ${tmpFilePath} -d ${savePath}`, (e) => { 181 | if (e) reject(e); 182 | else resolve(); 183 | }); 184 | }); 185 | await fsp.unlink(tmpFilePath); 186 | await this.processData(savePath).catch(); 187 | } catch (e) { 188 | if (retry) await this.problemData(pid, savePath, retry - 1); 189 | else throw e; 190 | } 191 | return savePath; 192 | } 193 | 194 | async consume(queue) { 195 | setInterval(async () => { 196 | const res = await this.axios.get('/judge'); 197 | console.log(res.data); 198 | if (res.data.task) queue.push(new JudgeTask(this, res.data.task)); 199 | }, 1000); 200 | } 201 | 202 | async setCookie(cookie) { 203 | this.config.cookie = cookie; 204 | this.axios = axios.create({ 205 | baseURL: this.config.server_url, 206 | timeout: 30000, 207 | headers: { cookie: this.config.cookie }, 208 | }); 209 | } 210 | 211 | async login() { 212 | log.log(`[${this.config.host}] Updating session`); 213 | const res = await this.axios.post('login', { 214 | uname: this.config.uname, password: this.config.password, rememberme: 'on', 215 | }); 216 | await this.setCookie(res.headers['set-cookie'].join(';')); 217 | } 218 | 219 | async ensureLogin() { 220 | const res = await this.axios.get('/judge?check=true'); 221 | if (res.data.error) await this.login(); 222 | } 223 | 224 | async processData(folder) { // eslint-disable-line class-methods-use-this 225 | let files = await fsp.readdir(folder); let 226 | ini = false; 227 | for (const i of files) { 228 | if (i.toLowerCase() === 'config.ini') { 229 | ini = true; 230 | await fsp.rename(`${folder}/${i}`, `${folder}/config.ini`); 231 | break; 232 | } 233 | } 234 | if (ini) { 235 | for (const i of files) { 236 | if (i.toLowerCase() === 'input') { 237 | await fsp.rename(`${folder}/${i}`, `${folder}/input`); 238 | } else if (i.toLowerCase() === 'output') { 239 | await fsp.rename(`${folder}/${i}`, `${folder}/output`); 240 | } 241 | } 242 | files = await fsp.readdir(`${folder}/input`); 243 | for (const i of files) { 244 | await fsp.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`); 245 | } 246 | files = await fsp.readdir(`${folder}/output`); 247 | for (const i of files) { 248 | await fsp.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`); 249 | } 250 | } 251 | } 252 | 253 | async cacheOpen(pid, version) { 254 | console.log(this.config.host, pid); 255 | const filePath = path.join(CACHE_DIR, this.config.host, pid); 256 | if (fs.existsSync(filePath)) { 257 | let ver; 258 | try { 259 | ver = fs.readFileSync(path.join(filePath, 'version')).toString(); 260 | } catch (e) { /* ignore */ } 261 | if (version === ver) return filePath; 262 | fs.removeSync(filePath); 263 | } 264 | fs.ensureDirSync(filePath); 265 | await this.problemData(pid, filePath); 266 | fs.writeFileSync(path.join(filePath, 'version'), version); 267 | return filePath; 268 | } 269 | } 270 | 271 | Hydro.JudgeTask = JudgeTask; 272 | module.exports = Hydro; 273 | -------------------------------------------------------------------------------- /judge/remotejudge/hustoj.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | const axios = require('axios'); 3 | const { CompileError, TooFrequentError } = require('../error'); 4 | const { 5 | STATUS_ACCEPTED, STATUS_WRONG_ANSWER, STATUS_RUNTIME_ERROR, 6 | STATUS_TIME_LIMIT_EXCEEDED, STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR, 7 | STATUS_MEMORY_LIMIT_EXCEEDED, STATUS_JUDGING, 8 | } = require('../status'); 9 | const { sleep } = require('../utils'); 10 | 11 | class HUSTOJ { 12 | constructor(config) { 13 | this.server_url = config.server_url.split('/', 3).join('/'); 14 | this.state = {}; 15 | this.STATUS = { 16 | Accepted: STATUS_ACCEPTED, 17 | AC: STATUS_ACCEPTED, 18 | 正确: STATUS_ACCEPTED, 19 | Presentation_Error: STATUS_WRONG_ANSWER, 20 | 'Presentation Error': STATUS_WRONG_ANSWER, 21 | PE: STATUS_WRONG_ANSWER, 22 | 格式错误: STATUS_WRONG_ANSWER, 23 | Wrong_Answer: STATUS_WRONG_ANSWER, 24 | 'Wrong Answer': STATUS_WRONG_ANSWER, 25 | WA: STATUS_WRONG_ANSWER, 26 | 答案错误: STATUS_WRONG_ANSWER, 27 | Output_Limit_Exceed: STATUS_WRONG_ANSWER, 28 | 'Output Limit Exceed': STATUS_WRONG_ANSWER, 29 | Output_Limit_Exceeded: STATUS_WRONG_ANSWER, 30 | 'Output Limit Exceeded': STATUS_WRONG_ANSWER, 31 | OLE: STATUS_WRONG_ANSWER, 32 | 输出超限: STATUS_WRONG_ANSWER, 33 | Runtime_Error: STATUS_RUNTIME_ERROR, 34 | 'Runtime Error': STATUS_RUNTIME_ERROR, 35 | RE: STATUS_RUNTIME_ERROR, 36 | 运行时错误: STATUS_RUNTIME_ERROR, 37 | 'Dangerous Syscall': STATUS_RUNTIME_ERROR, 38 | Time_Limit_Exceed: STATUS_TIME_LIMIT_EXCEEDED, 39 | 'Time Limit Exceed': STATUS_TIME_LIMIT_EXCEEDED, 40 | Time_Limit_Exceeded: STATUS_TIME_LIMIT_EXCEEDED, 41 | 'Time Limit Exceeded': STATUS_TIME_LIMIT_EXCEEDED, 42 | TLE: STATUS_TIME_LIMIT_EXCEEDED, 43 | 时间超限: STATUS_TIME_LIMIT_EXCEEDED, 44 | Memory_Limit_Exceed: STATUS_MEMORY_LIMIT_EXCEEDED, 45 | 'Memory Limit Exceed': STATUS_MEMORY_LIMIT_EXCEEDED, 46 | Memory_Limit_Exceeded: STATUS_MEMORY_LIMIT_EXCEEDED, 47 | 'Memory Limit Exceeded': STATUS_MEMORY_LIMIT_EXCEEDED, 48 | MLE: STATUS_MEMORY_LIMIT_EXCEEDED, 49 | 内存超限: STATUS_MEMORY_LIMIT_EXCEEDED, 50 | Compile_Error: STATUS_COMPILE_ERROR, 51 | 'Compile Error': STATUS_COMPILE_ERROR, 52 | Compilation_Error: STATUS_COMPILE_ERROR, 53 | 'Compilation Error': STATUS_COMPILE_ERROR, 54 | CE: STATUS_COMPILE_ERROR, 55 | 编译错误: STATUS_COMPILE_ERROR, 56 | RF: STATUS_SYSTEM_ERROR, 57 | System_Error: STATUS_SYSTEM_ERROR, 58 | 'System Error': STATUS_SYSTEM_ERROR, 59 | SE: STATUS_SYSTEM_ERROR, 60 | 未知错误: STATUS_SYSTEM_ERROR, 61 | }; 62 | this.LANGUAGES = { 63 | c: 0, 64 | cc: 1, 65 | pas: 2, 66 | java: 3, 67 | rb: 4, 68 | sh: 5, 69 | py: 6, 70 | php: 7, 71 | pl: 8, 72 | cs: 9, 73 | js: 16, 74 | go: 17, 75 | }; 76 | this.PROCESSING = ['等待', '运行并评判', '编译成功', 'Pending', 'Judging', 'Compiling', 'Running_Judging']; 77 | this.SUBMIT = ['/submit.php', '']; 78 | this.CEINFO = [['编译错误', 'Compile_Error', 'Compile Error'], '/ceinfo.php?sid={rid}', /
(.*?)<\/pre>/gmi];
 79 |         this.MONIT = ['/status.php?pid={pid}&user_id={uid}'];
 80 |         this.ACCEPTED = ['正确', 'Accepted', 'AC'];
 81 |         this.RID = /\n([0-9]+)<\/td>/igm;
 82 |     }
 83 | 
 84 |     async loginWithToken(cookie) {
 85 |         this.cookie = cookie;
 86 |         this.axios = axios.create({
 87 |             baseURL: this.server_url,
 88 |             timeout: 30000,
 89 |             headers: {
 90 |                 Accept: 'application/json',
 91 |                 'Content-Type': 'application/x-www-form-urlencoded',
 92 |                 Cookie: this.cookie,
 93 |                 Referer: this.server_url,
 94 |                 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36',
 95 |             },
 96 |             transformRequest: [
 97 |                 (data) => {
 98 |                     let ret = '';
 99 |                     for (const it in data) {
100 |                         ret += `${encodeURIComponent(it)}=${encodeURIComponent(data[it])}&`;
101 |                     }
102 |                     return ret;
103 |                 },
104 |             ],
105 |         });
106 |     }
107 | 
108 |     async login(username, password) {
109 |         await this.loginWithToken('');
110 |         const res = await this.axios.post(this.LOGIN[0], {
111 |             [this.LOGIN[1]]: username,
112 |             [this.LOGIN[2]]: password,
113 |             ...this.LOGIN[3],
114 |         });
115 |         this.state.username = username;
116 |         await this.loginWithToken(res.headers['set-cookie'].join('\n'));
117 |     }
118 | 
119 |     async submit(pid, code, lang) {
120 |         const res = await this.axios.post(this.SUBMIT[0], {
121 |             [this.SUBTMI[1]]: pid,
122 |             [this.SUBMIT[2]]: this.LANGUAGES[lang],
123 |             [this.SUBMIT[3]]: code,
124 |             ...this.SUBMIT[5],
125 |         });
126 |         if (res.data.find(this.SUBMIT[4])) throw new TooFrequentError();
127 |         console.log(res.data);
128 |         const url = this.MONIT[0].replace('{uid}', this.state.username).replace('{pid}', pid);
129 |         const r = await this.axios.get(url);
130 |         const rid = this.RID.exec(r.data)[1];
131 |         return {
132 |             uid: this.state.username,
133 |             pid,
134 |             rid,
135 |         };
136 |     }
137 | 
138 |     async monit(data, next, end) {
139 |         if (this.supermonit) return await this.supermonit(data, next, end);
140 |         let url = this.MONIT[0].replace('{uid}', data.uid).replace('{pid}', data.pid);
141 |         const RE = new RegExp(`${data.rid}.*?.*?(.*?)(.*?)kb(.*?)ms`, 'gmi');
142 |         const res = await this.axios.get(url);
143 |         let score;
144 |         let [, status, time, memory] = RE.exec(res.data);
145 |         while (this.PROCESSING.includes(status)) {
146 |             next({ status: STATUS_JUDGING, progress: 0 });
147 |             await sleep(1000);
148 |             const resp = await this.axios.get(url);
149 |             [, status, time, memory] = RE.exec(resp.data);
150 |         }
151 |         if (this.CEINFO[0].includes(status)) {
152 |             url = this.CEINFO[1].replace('{rid}', data.rid);
153 |             const resp = await this.axios.get(url);
154 |             const compilerText = decodeURIComponent(this.CEINFO[2].exec(resp.data)
155 |                 .replace('\n', '').replace('
', '\n').replace('\n\n', '\n')); 156 | throw new CompileError(compilerText); 157 | } else if (this.ACCEPTED.includes(status)) score = 100; 158 | else score = 0; 159 | next({ 160 | status: STATUS_JUDGING, 161 | case: { 162 | status: this.STATUS[status], 163 | score, 164 | time_ms: parseInt(time), 165 | memory_kb: parseInt(memory), 166 | judge_text: status, 167 | }, 168 | progress: 99, 169 | }); 170 | end({ 171 | status: this.STATUS[status], 172 | score, 173 | time_ms: parseInt(time), 174 | memory_kb: parseInt(memory), 175 | judge_text: status, 176 | }); 177 | } 178 | } 179 | 180 | class YBT extends HUSTOJ { 181 | constructor(config) { 182 | super(config); 183 | this.LANGUAGES = { 184 | cc: 1, 185 | c: 2, 186 | java: 3, 187 | pas: 4, 188 | py: 5, 189 | py3: 5, 190 | }; 191 | this.LOGIN = ['/login.php', 'username', 'password', { login: '登录' }]; 192 | this.SUBMIT = ['/action.php', 'problem_id', 'language', 'source', '提交频繁啦!', { submit: '提交', user_id: this.state.username }]; 193 | this.CEINFO = ['/show_ce_info.php?runid={rid}', /(.*?)<\/td>/gmi]; 194 | } 195 | 196 | async supermonit(data, next, end) { 197 | const url = `/statusx1.php?runidx=${data.rid}`; 198 | let res = await this.axios.get(url); 199 | let staText = res.data.split(':'); 200 | while (this.PROCESSING.includes(staText[4])) { 201 | next({ status: STATUS_JUDGING, progress: 0 }); 202 | await sleep(1000); 203 | res = this.axios.get(url); 204 | staText = res.data.split(':'); 205 | } 206 | if (this.CEINFO[0].includes(staText[4])) { 207 | const url = this.CEINFO[1].replace('{rid}', data.rid); 208 | const res = await this.axios.get(url); 209 | const compilerText = decodeURIComponent(this.CEINFO[2].exec(res.data)[1] 210 | .replace('\n', '').replace('
', '\n').replace('\n\n', '\n')); 211 | throw new CompileError(compilerText); 212 | } 213 | staText[4] = staText[4].split('|'); 214 | staText[5] = staText[5].split(','); 215 | let total_time_usage_ms = 0; 216 | let total_memory_usage_kb = 0; 217 | let total_score = 0; 218 | let total_status = STATUS_WRONG_ANSWER; 219 | if (this.ACCEPTED.includes(staText[4][0])) { 220 | total_score = 100; 221 | total_status = STATUS_ACCEPTED; 222 | } 223 | for (const i in staText[5]) { 224 | if (staText[5][i]) { 225 | let score; 226 | staText[5][i] = staText[5][i].split('|'); 227 | if (this.ACCEPTED.includes(staText[5][i][0])) { 228 | score = 100; 229 | total_score += score; 230 | } else score = 0; 231 | staText[5][i][1] = staText[5][i][1].split('_'); 232 | total_memory_usage_kb += parseInt(staText[5][i][1][0]); 233 | total_time_usage_ms += parseInt(staText[5][i][1][1]); 234 | next({ 235 | status: STATUS_JUDGING, 236 | case: { 237 | status: this.STATUS[staText[5][i][0]], 238 | score, 239 | time_ms: parseInt(staText[5][i][1][1]), 240 | memory_kb: parseInt(staText[5][i][1][0]), 241 | }, 242 | progress: 99, 243 | }); 244 | } 245 | } 246 | if (this.ACCEPTED.includes(staText[4][0])) { 247 | total_score = 100; 248 | total_status = STATUS_ACCEPTED; 249 | } 250 | end({ 251 | status: total_status, 252 | score: total_score, 253 | time_ms: total_time_usage_ms, 254 | memory_kb: total_memory_usage_kb, 255 | }); 256 | } 257 | } 258 | 259 | class BZOJ extends HUSTOJ { 260 | constructor(config) { 261 | super(config); 262 | this.LOGIN = ['/login.php', 'user_id', 'password', { submit: 'Submit' }]; 263 | this.SUBMIT = ['/submit.php', 'id', 'language', 'source', 'You should not submit more than twice in 10 seconds.....', {}]; 264 | this.CEINFO = ['/ceinfo.php?sid={rid}', /
([\s\S]*?)<\/pre>/igm];
265 |         this.RID = /Submit_Time<\/td><\/tr>\n([0-9]+)/igm;
266 |     }
267 | }
268 | 
269 | class XJOI extends HUSTOJ {
270 |     constructor(config) {
271 |         super(config);
272 |         this.LANGUAGES = { cc: 'g++', c: 'gcc', pas: 'fpc' };
273 |         this.LOGIN = ['/', 'user', 'pass', { remember: 'on' }];
274 |         this.SUBMIT = ['/submit', 'proid', 'language', 'source', '请稍后再提交', {}];
275 |         this.SUPERMONIT = [
276 |             /