diff --git a/.eslintrc.js b/.eslintrc.js index 0894c8643..4a9f347a3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { 'import/extensions': 'off', 'import/no-cycle': 'off', 'import/no-extraneous-dependencies': 'off', + 'import/no-named-as-default': 'off', 'import/no-named-as-default-member': 'off', 'import/prefer-default-export': 'off', indent: ['warn', 4], diff --git a/packages/hydrojudge/locales/zh_CN.yaml b/packages/hydrojudge/locales/zh_CN.yaml new file mode 100644 index 000000000..39fdc910b --- /dev/null +++ b/packages/hydrojudge/locales/zh_CN.yaml @@ -0,0 +1,26 @@ +'Error parsing memory: {0}': 无法解析内存:{0} +'Error parsing time: {0}': 无法解析时间:{0} +Cannot connect to sandbox service.: 无法连接到沙箱服务。 +Cannot find checker {0}.: 找不到比较器 {0}。 +Cannot find input file {0}.: 找不到输入文件 {0}。 +Cannot find judge extra file {0}.: 找不到评测额外文件 {0}。 +Cannot find output file {0}.: 找不到输出文件 {0}。 +Cannot find user extra file {0}.: 找不到用户额外文件 {0}。 +Cannot parse testdata.: 无法解析测试数据。 +Checker returned {0}.: 比较器返回了{0}。 +Executable file not found.: 没有找到可执行文件。 +Found {0} testcases.: 识别到 {0} 个测试点。 +Invalid judge_extra_files config.: 无效的 judge_extra_files 配置。 +Invalid user_extra_files config.: 无效的 user_extra_files 配置。 +Problem data not found.: 没有找到测试数据。 +Read {0}, expect {1}.: 读取到 {0},应为 {1}。 +Standard answer longer than user output.: 标准输出比选手输出长。 +Testdata configuration incorrect.: 题目配置错误。 +Too many files.: 文件过多。 +Too many testcases. Cancelled.: 测试数据过多。评测取消。 +Total time limit longer than 60s. Cancelled.: 总时限超过60s。评测取消。 +Unknown checker type {0}.: 未知的比较器类型 {0}。 +Unknown language type.: 未知的语言类型。 +Unsupported language {0}.: 不支持的语言 {0}。 +User output longer than standard answer.: 选手输出比标准输出长。 +Your program returned {0}.: 您的程序返回了{0}。 \ No newline at end of file diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 181c92023..18f559ff3 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "2.1.25", + "version": "2.1.26", "main": "package.json", "author": "masnn", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index 17ff02178..a4a68e5a7 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -186,9 +186,9 @@ async function readAutoCases(folder, { next }) { let result = await read0(folder, files, checkFile); if (!result.count) result = await read1(folder, files, checkFile); Object.assign(config, result); - next({ message: `识别到${config.count}个测试点` }); + next({ message: { message: 'Found {0} testcases.', params: config.count } }); } catch (e) { - throw new SystemError('在自动识别测试点的过程中出现了错误。', [e]); + throw new SystemError('Cannot parse testdata.', [e]); } return config; } @@ -204,24 +204,24 @@ export async function readYamlCases(folder: string, cfg: Dictionary = {}, a const next = args.next; const checkFile = ensureFile(folder); config.checker_type = cfg.checker_type || 'default'; - if (cfg.checker) config.checker = checkFile(cfg.checker, '找不到比较器 '); + if (cfg.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.'); if (cfg.judge_extra_files) { if (typeof cfg.judge_extra_files === 'string') { - config.judge_extra_files = [checkFile(cfg.judge_extra_files, '找不到评测额外文件 ')]; + config.judge_extra_files = [checkFile(cfg.judge_extra_files, 'Cannot find judge extra file {0}.')]; } else if (cfg.judge_extra_files instanceof Array) { for (const file of cfg.judge_extra_files) { - config.judge_extra_files.push(checkFile(file, '找不到评测额外文件 ')); + config.judge_extra_files.push(checkFile(file, 'Cannot find judge extra file {0}.')); } - } else throw new FormatError('无效的 judge_extra_files 配置项'); + } else throw new FormatError('Invalid judge_extra_files config.'); } if (cfg.user_extra_files) { if (typeof cfg.user_extra_files === 'string') { - config.user_extra_files = [checkFile(cfg.user_extra_files, '找不到用户额外文件 ')]; + config.user_extra_files = [checkFile(cfg.user_extra_files, 'Cannot find user extra file {0}.')]; } else if (cfg.user_extra_files instanceof Array) { for (const file of cfg.user_extra_files) { - config.user_extra_files.push(checkFile(file, '找不到用户额外文件 ')); + config.user_extra_files.push(checkFile(file, 'Cannot find user extra file {0}.')); } - } else throw new FormatError('无效的 user_extra_files 配置项'); + } else throw new FormatError('Invalid user_extra_files config.'); } if (cfg.cases) { config.subtasks = [{ @@ -234,8 +234,8 @@ export async function readYamlCases(folder: string, cfg: Dictionary = {}, a for (const c of cfg.cases) { config.count++; config.subtasks[0].cases.push({ - input: checkFile(c.input, '找不到输入文件 '), - output: checkFile(c.output, '找不到输出文件 '), + input: checkFile(c.input, 'Cannot find input file {0}.'), + output: checkFile(c.output, 'Cannot find output file {0}.'), id: config.count, }); } @@ -245,8 +245,8 @@ export async function readYamlCases(folder: string, cfg: Dictionary = {}, a for (const c of subtask.cases) { config.count++; cases.push({ - input: checkFile(c.input, '找不到输入文件 '), - output: checkFile(c.output, '找不到输出文件 '), + input: checkFile(c.input, 'Cannot find input file {0}.'), + output: checkFile(c.output, 'Cannot find output file {0}.'), id: config.count, }); } @@ -284,12 +284,12 @@ function convertIniConfig(ini: string) { } function isValidConfig(config) { - if (config.count > 100) throw new FormatError('测试数据组数过多,拒绝评测'); + if (config.count > 100) throw new FormatError('Too many testcases. Cancelled.'); let total_time = 0; for (const subtask of config.subtasks) { total_time += subtask.time_limit_ms * subtask.cases.length; } - if (total_time > 60 * 1000) throw new FormatError('总时限超过限制60s,拒绝评测'); + if (total_time > 60 * 1000) throw new FormatError('Total time limit longer than 60s. Cancelled.'); } export default async function readCases(folder: string, cfg: Record = {}, args) { diff --git a/packages/hydrojudge/src/check.ts b/packages/hydrojudge/src/check.ts index 4c67e1f0c..72ba8e992 100644 --- a/packages/hydrojudge/src/check.ts +++ b/packages/hydrojudge/src/check.ts @@ -17,12 +17,12 @@ export async function check(config): Promise<[number, number, string]> { copyIn: config.copyIn || {}, detail: config.detail, }); - if (code) throw new SystemError(`比较器返回了非零值:${code}`); + if (code) throw new SystemError('Checker returned {0}.', [code]); return [status, score, message]; } export async function compileChecker(checkerType: string, checker: string, copyIn: any) { - if (!checkers[checkerType]) throw new SystemError(`未知比较器类型:${checkerType}`); + if (!checkers[checkerType]) throw new SystemError('Unknown checker type {0}.', [checkerType]); const file = await fs.readFile(checker); // TODO cache compiled checker return await compile(parseFilename(checker).split('.')[1], file.toString(), 'checker', copyIn); diff --git a/packages/hydrojudge/src/checkers.ts b/packages/hydrojudge/src/checkers.ts index bb305f3f7..9d9e4e059 100644 --- a/packages/hydrojudge/src/checkers.ts +++ b/packages/hydrojudge/src/checkers.ts @@ -23,7 +23,7 @@ const checkers: Record = { }, }); let status; - let message = ''; + let message: any = ''; if (stdout) { status = STATUS.STATUS_WRONG_ANSWER; if (config.detail) { @@ -33,8 +33,8 @@ const checkers: Record = { let usr = u.substr(2, u.length - 2).trim().split(' '); const t = pt[1].split('\n')[1]; let std = t.substr(2, t.length - 2).trim().split(' '); - if (usr.length < std.length) message = '标准输出比选手输出长。'; - else if (usr.length > std.length) message = '选手输出比标准输出长。'; + if (usr.length < std.length) message = 'Standard answer longer than user output.'; + else if (usr.length > std.length) message = 'User output longer than standard answer.'; else { for (const i in usr) { if (usr[i] !== std[i]) { @@ -45,7 +45,7 @@ const checkers: Record = { } if (usr.length > 20) usr = `${usr.substring(0, 16)}...`; if (std.length > 20) std = `${std.substring(0, 16)}...`; - message = `读取到 ${usr} ,应为 ${std}`; + message = { message: 'Read {0}, expect {1}.', params: [usr, std] }; } } catch (e) { message = stdout.substring(0, stdout.length - 1 <= 30 ? stdout.length - 1 : 30); diff --git a/packages/hydrojudge/src/compile.ts b/packages/hydrojudge/src/compile.ts index 1f2d8af37..725fcca53 100644 --- a/packages/hydrojudge/src/compile.ts +++ b/packages/hydrojudge/src/compile.ts @@ -11,7 +11,7 @@ export = async function compile( const LANGS = global.Hydro ? yaml.safeLoad(await global.Hydro.model.system.get('hydrojudge.langs')) : require('./config').LANGS; - if (!LANGS[lang]) throw new SystemError(`不支持的语言:${lang}`); + if (!LANGS[lang]) throw new SystemError('Unsupported language {0}.', [lang]); const info = LANGS[lang]; target = info.target || target; const f = {}; @@ -24,7 +24,7 @@ export = async function compile( { copyIn, copyOutCached: [target] }, ); if (status !== STATUS.STATUS_ACCEPTED) throw new CompileError({ status, stdout, stderr }); - if (!fileIds[target]) throw new CompileError({ stderr: '没有找到可执行文件' }); + if (!fileIds[target]) throw new CompileError({ stderr: 'Executable file not found.' }); if (next) next({ compiler_text: compilerText(stdout, stderr) }); f[target] = { fileId: fileIds[target] }; return { @@ -36,5 +36,5 @@ export = async function compile( execute: info.execute, copyIn: f, clean: () => Promise.resolve(), time: info.time || 1, }; } - throw new SystemError('Unknown language type'); + throw new SystemError('Unknown language type.'); } diff --git a/packages/hydrojudge/src/hosts/hydro.ts b/packages/hydrojudge/src/hosts/hydro.ts index d044f5cdf..21c20785b 100644 --- a/packages/hydrojudge/src/hosts/hydro.ts +++ b/packages/hydrojudge/src/hosts/hydro.ts @@ -4,6 +4,7 @@ import axios from 'axios'; import AdmZip from 'adm-zip'; import fs from 'fs-extra'; import WebSocket from 'ws'; +import { argv } from 'yargs'; import { noop } from 'lodash'; import { ObjectID } from 'bson'; import * as tmpfs from '../tmpfs'; @@ -89,14 +90,14 @@ class JudgeTask { status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0, }); } else if (e instanceof FormatError) { - this.next({ judge_text: '题目配置出现错误。请联系题目上传者。' }); - this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` }); + this.next({ judge_text: 'Testdata configuration incorrect.' }); + this.next({ judge_text: { message: e.message, params: e.params } }); this.end({ status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, }); } else { log.error(e); - this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` }); + this.next({ message: { message: e.message, params: e.params, ...argv.debug ? { stack: e.stack } : {} } }); this.end({ status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, }); diff --git a/packages/hydrojudge/src/hosts/vj4.ts b/packages/hydrojudge/src/hosts/vj4.ts index bef488d8c..a66c64bec 100644 --- a/packages/hydrojudge/src/hosts/vj4.ts +++ b/packages/hydrojudge/src/hosts/vj4.ts @@ -134,10 +134,16 @@ class JudgeTask { data.judge_text = data.message; delete data.message; } + if (data.judge_text && typeof data.judge_text !== 'string') { + data.judge_text = `${data.judge_text.message} ${JSON.stringify(data.judge_text.params || {})}`; + } if (data.case?.message !== undefined) { data.case.judge_text = data.case.message; delete data.case.message; } + if (data.case?.judge_text && typeof data.case?.judge_text !== 'string') { + data.case.judge_text = `${data.case.judge_text.message} ${JSON.stringify(data.case.judge_text.params || {})}`; + } data.key = 'next'; data.tag = this.tag; if (id) { diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index dc0f33c9f..1b0c03aca 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -42,7 +42,7 @@ function judgeCase(c) { if (res.files[`${filename}.out`] || !fs.existsSync(stdout)) { fs.writeFileSync(stdout, res.files[`${filename}.out`] || ''); } - let message = ''; + let message: any = ''; let score = 0; if (status === STATUS.STATUS_ACCEPTED) { if (time_usage_ms > ctxSubtask.subtask.time_limit_ms * ctx.execute.time) { @@ -64,7 +64,7 @@ function judgeCase(c) { } } else if (status === STATUS.STATUS_RUNTIME_ERROR && code) { if (code < 32) message = signals[code]; - else message = `您的程序返回了 ${code}.`; + else message = { message: 'Your program returned {0}.', params: [code] }; } ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score); ctxSubtask.status = Math.max(ctxSubtask.status, status); @@ -105,7 +105,7 @@ function judgeSubtask(subtask) { } export const judge = async (ctx) => { - if (!ctx.config.subtasks.length) throw new SystemError('没有找到测试数据'); + if (!ctx.config.subtasks.length) throw new SystemError('Problem data not found.'); if (ctx.config.template) { if (ctx.config.template[ctx.lang]) { const tpl = ctx.config.template[ctx.lang]; diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index 7b32e795d..3a7df1037 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -30,7 +30,7 @@ function judgeCase(c) { ]); let status; let score = 0; - let message = ''; + let message: any = ''; if (time_usage_ms > ctxSubtask.subtask.time_limit_ms) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; } else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) { @@ -38,7 +38,7 @@ function judgeCase(c) { } else if (code) { status = STATUS.STATUS_RUNTIME_ERROR; if (code < 32) message = signals[code]; - else message = `您的程序返回了 ${code}.`; + else message = { message: 'Your program returned {0}.', params: [code] }; } else [status, score, message] = resInteractor.files.stderr.split('\n'); ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score); ctxSubtask.status = Math.max(ctxSubtask.status, status); diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts index 193d348b2..12d1da7ba 100644 --- a/packages/hydrojudge/src/judge/run.ts +++ b/packages/hydrojudge/src/judge/run.ts @@ -31,7 +31,7 @@ export const judge = async (ctx) => { const { code, time_usage_ms, memory_usage_kb } = res; let { status } = res; if (!fs.existsSync(stdout)) fs.writeFileSync(stdout, ''); - let message = fs.readFileSync(stdout).toString() + fs.readFileSync(stderr).toString(); + let message: any = fs.readFileSync(stdout).toString() + fs.readFileSync(stderr).toString(); if (status === STATUS.STATUS_ACCEPTED) { if (time_usage_ms > ctx.config.time) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; @@ -41,7 +41,7 @@ export const judge = async (ctx) => { } else if (code) { status = STATUS.STATUS_RUNTIME_ERROR; if (code < 32) message += signals[code]; - else message += `您的程序返回了 ${code}.`; + else message = { message: 'Your program returned {0}.', params: [code] }; } ctx.next({ status, diff --git a/packages/hydrojudge/src/sandbox.ts b/packages/hydrojudge/src/sandbox.ts index 964a8ea87..92916519a 100644 --- a/packages/hydrojudge/src/sandbox.ts +++ b/packages/hydrojudge/src/sandbox.ts @@ -65,7 +65,7 @@ export async function runMultiple(execute) { body.cmd[1].files[1] = null; res = await axios.post('/run', body); } catch (e) { - throw new SystemError('Cannot connect to sandbox service'); + throw new SystemError('Cannot connect to sandbox service.'); } return res.data; } @@ -86,7 +86,7 @@ export async function run(execute, params?) { [result] = res.data; } catch (e) { // FIXME request body larger than maxBodyLength limit - throw new SystemError('Cannot connect to sandbox service ', e.message); + throw new SystemError('Cannot connect to sandbox service.', e.message); } // FIXME: Signalled? const ret: any = { diff --git a/packages/hydrojudge/src/service.ts b/packages/hydrojudge/src/service.ts index 65bacc111..e52fdf85d 100644 --- a/packages/hydrojudge/src/service.ts +++ b/packages/hydrojudge/src/service.ts @@ -2,11 +2,19 @@ /* eslint-disable no-await-in-loop */ import 'hydrooj'; import path from 'path'; +import { argv } from 'yargs'; import { ObjectID } from 'bson'; import fs from 'fs-extra'; import { homedir, tmpdir } from 'os'; import AdmZip from 'adm-zip'; import { noop } from 'lodash'; +import { Logger } from 'hydrooj/dist/logger'; +import * as monitor from 'hydrooj/dist/service/monitor'; +import * as tmpfs from './tmpfs'; +import { FormatError, CompileError, SystemError } from './error'; +import { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } from './status'; +import { compilerText } from './utils'; +import readYamlCases from './cases'; declare module 'hydrooj/dist/interface' { interface SystemKeys { @@ -14,22 +22,16 @@ declare module 'hydrooj/dist/interface' { } } +const logger = new Logger('judge'); + async function postInit() { // Only start a single daemon if (!global.Hydro.isFirstWorker) return; - const { compilerText } = require('./utils'); - const tmpfs = require('./tmpfs'); - const { FormatError, CompileError, SystemError } = require('./error'); - const { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } = global.Hydro.model.builtin.STATUS; - const { default: readYamlCases } = require('./cases'); const judge = require('./judge'); const sysinfo = require('./sysinfo'); const { problem, file, task } = global.Hydro.model; const _judge = global.Hydro.handler.judge as any; - const { monitor } = global.Hydro.service; - const { Logger } = global.Hydro.Logger; - const logger = new Logger('judge'); const info = await sysinfo.get(); monitor.updateJudger(info); @@ -76,7 +78,7 @@ async function postInit() { fs.ensureDirSync(path.dirname(savePath)); const zip = new AdmZip(tmpFilePath); const entries = zip.getEntries(); - if (entries.length > 256) throw new FormatError('Too many files'); + if (entries.length > 256) throw new FormatError('Too many files.'); await new Promise((resolve, reject) => { zip.extractAllToAsync(savePath, true, (e) => { if (e) reject(e); @@ -227,13 +229,13 @@ async function postInit() { status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0, }); } else if (e instanceof FormatError) { - this.next({ message: `${e.message}\n${JSON.stringify(e.params)}` }); + this.next({ message: { message: e.message, params: e.params } }); this.end({ status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, }); } else { logger.error(e); - this.next({ message: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` }); + this.next({ message: { message: e.message, params: e.params, ...argv.debug ? { stack: e.stack } : {} } }); this.end({ status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, }); diff --git a/packages/hydrojudge/src/utils.ts b/packages/hydrojudge/src/utils.ts index ba8934f57..f75a03f3d 100644 --- a/packages/hydrojudge/src/utils.ts +++ b/packages/hydrojudge/src/utils.ts @@ -17,13 +17,13 @@ export function noop() { } export function parseTimeMS(str: string) { const match = TIME_RE.exec(str); - if (!match) throw new FormatError(str, ['error parsing time']); + if (!match) throw new FormatError('Error parsing time: {0}', [str]); return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]); } export function parseMemoryMB(str: string) { const match = MEMORY_RE.exec(str); - if (!match) throw new FormatError(str, ['error parsing memory']); + if (!match) throw new FormatError('Error parsing memory: {0}', [str]); return Math.floor(parseFloat(match[1]) * MEMORY_UNITS[match[2]]); } @@ -110,9 +110,9 @@ export function restrictFile(p: string) { export function ensureFile(folder: string) { return (file: string, message: string) => { const f = path.join(folder, restrictFile(file)); - if (!fs.existsSync(f)) throw new FormatError(message + file); + if (!fs.existsSync(f)) throw new FormatError(message, [file]); const stat = fs.statSync(f); - if (!stat.isFile()) throw new FormatError(message + file); + if (!stat.isFile()) throw new FormatError(message, [file]); return f; }; }