Merge pull request #92 from hydro-dev/develop

This commit is contained in:
undefined authored and GitHub committed 2021-04-14 23:46:19 +08:00
commit af7920c787
46 files changed
+427 -273

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ sysctl -p
# Install NodeJS
echo "Installing NodeJS"
curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | bash
curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | sed 's/github.com\/nvm-sh\/nvm.git/gitee.com\/imirror\/nvm/g' | bash
export NVM_DIR=/root/.nvm
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 14
+1 -1
View File
@@ -17,7 +17,7 @@ mkdir -p /data/db /data/file
# Install NodeJS
echo "Installing NodeJS"
curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | bash
curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | sed 's/github.com\/nvm-sh\/nvm.git/gitee.com\/imirror\/nvm/g' | bash
export NVM_DIR=/root/.nvm
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14
+1 -1
View File
@@ -17,7 +17,7 @@ mkdir -p /data/db /data/file
# Install NodeJS
echo "Installing NodeJS"
curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | bash
curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | sed 's/github.com\/nvm-sh\/nvm.git/gitee.com\/imirror\/nvm/g' | bash
export NVM_DIR=/root/.nvm
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14
+1 -1
View File
@@ -17,7 +17,7 @@ mkdir -p /data/db /data/file
# Install NodeJS
echo "Installing NodeJS"
curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | bash
curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | sed 's/github.com\/nvm-sh\/nvm.git/gitee.com\/imirror\/nvm/g' | bash
export NVM_DIR=/root/.nvm
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@hydrooj/hydrojudge",
"bin": "bin/hydrojudge.js",
"version": "2.4.19",
"version": "2.5.1",
"os": [
"linux"
],
+26 -106
View File
@@ -1,7 +1,9 @@
import fs from 'fs-extra';
import path from 'path';
import yaml from 'js-yaml';
import { Dictionary, sum } from 'lodash';
import { sum } from 'lodash';
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
import { changeErrorType } from '@hydrooj/utils/lib/utils';
import { FormatError, SystemError } from './error';
import { parseTimeMS, parseMemoryMB, ensureFile } from './utils';
import { getConfig } from './config';
@@ -86,8 +88,8 @@ async function read0(folder: string, files: string[], checkFile, cfg) {
const config = {
count: 0,
subtasks: [{
time_limit_ms: parseTimeMS(cfg.time || '1s'),
memory_limit_mb: parseMemoryMB(cfg.memory || '256m'),
time: parseTimeMS(cfg.time || '1s'),
memory: parseMemoryMB(cfg.memory || '256m'),
type: 'sum',
cases: [],
score: Math.floor(100 / cases.length),
@@ -103,8 +105,8 @@ async function read0(folder: string, files: string[], checkFile, cfg) {
}
if (extra < cases.length) {
config.subtasks.push({
time_limit_ms: parseTimeMS(cfg.time || '1s'),
memory_limit_mb: parseMemoryMB(cfg.memory || '256m'),
time: parseTimeMS(cfg.time || '1s'),
memory: parseMemoryMB(cfg.memory || '256m'),
type: 'sum',
cases: [],
score: Math.floor(100 / cases.length) + 1,
@@ -132,8 +134,8 @@ async function read1(folder: string, files: string[], checkFile, cfg) {
if (fs.existsSync(path.resolve(folder, c.output))) {
if (!subtask[REG.subtask(data)]) {
subtask[REG.subtask(data)] = [{
time_limit_ms: parseTimeMS(cfg.time || '1s'),
memory_limit_mb: parseMemoryMB(cfg.memory || '256m'),
time: parseTimeMS(cfg.time || '1s'),
memory: parseMemoryMB(cfg.memory || '256m'),
type: 'min',
cases: [c],
}];
@@ -193,107 +195,11 @@ async function readAutoCases(folder, { next }, cfg) {
return config;
}
export async function readYamlCases(folder: string, cfg: Dictionary<any> = {}, args) {
const config: any = {
checker_type: 'default',
count: 0,
subtasks: [],
judge_extra_files: [],
user_extra_files: [],
};
const next = args.next;
const checkFile = ensureFile(folder);
config.checker_type = cfg.checker_type || 'default';
if (cfg.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.');
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
if (cfg.judge_extra_files) {
if (typeof cfg.judge_extra_files === 'string') {
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, 'Cannot find judge extra file {0}.'));
}
} 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, '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, 'Cannot find user extra file {0}.'));
}
} else throw new FormatError('Invalid user_extra_files config.');
}
if (cfg.outputs) {
config.type = 'submit_answer';
} else if (cfg.cases) {
config.subtasks = [{
score: parseInt(cfg.score, 10) || Math.floor(100 / cfg.cases.length),
time_limit_ms: parseTimeMS(cfg.time || '1s'),
memory_limit_mb: parseMemoryMB(cfg.memory || '512m'),
cases: [],
type: 'sum',
}];
for (const c of cfg.cases) {
config.count++;
config.subtasks[0].cases.push({
input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null',
output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null',
id: config.count,
});
}
} else if (cfg.subtasks) {
for (const subtask of cfg.subtasks) {
const cases = [];
for (const c of subtask.cases) {
config.count++;
cases.push({
input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null',
output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null',
id: config.count,
});
}
config.subtasks.push({
score: parseInt(subtask.score, 10),
if: subtask.if || [],
cases,
time_limit_ms: parseTimeMS(subtask.time || cfg.time || '1s'),
memory_limit_mb: parseMemoryMB(subtask.memory || cfg.memory || '512m'),
});
}
} else {
const c = await readAutoCases(folder, { next }, cfg);
config.subtasks = c.subtasks;
config.count = c.count;
}
if (config.type === 'submit_answer' && !cfg.outputs) throw new FormatError('outputs config not found');
return Object.assign(cfg, config);
}
function convertIniConfig(ini: string) {
const f = ini.split('\n');
const count = parseInt(f[0], 10);
const res = { subtasks: [] };
for (let i = 1; i <= count; i++) {
if (!f[i] || !f[i].trim()) throw new FormatError('Testcada count incorrect.');
const [input, output, time, score, memory] = f[i].split('|');
const cur = {
cases: [{ input: `input/${input.toLowerCase()}`, output: `output/${output.toLowerCase()}` }],
score: parseInt(score, 10),
time: `${time}s`,
memory: '512m',
};
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
res.subtasks.push(cur);
}
return res;
}
function isValidConfig(config) {
if (config.count > (getConfig('testcases_max') || 100)) {
throw new FormatError('Too many testcases. Cancelled.');
}
const total_time = sum(config.subtasks.map((subtask) => subtask.time_limit_ms * subtask.cases.length));
const total_time = sum(config.subtasks.map((subtask) => subtask.time * subtask.cases.length));
if (total_time > (getConfig('total_time_limit') || 60) * 1000) {
throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]);
}
@@ -309,9 +215,23 @@ export default async function readCases(folder: string, cfg: Record<string, any>
} else if (fs.existsSync(ymlConfig)) {
config = { ...yaml.load(fs.readFileSync(ymlConfig).toString()) as object, ...cfg };
} else if (fs.existsSync(iniConfig)) {
config = { ...convertIniConfig(fs.readFileSync(iniConfig).toString()), ...cfg };
try {
config = { ...convertIniConfig(fs.readFileSync(iniConfig).toString()), ...cfg };
} catch (e) {
throw changeErrorType(e, FormatError);
}
} else config = cfg;
const result = await readYamlCases(folder, config, args);
let result;
try {
result = await readYamlCases(config, ensureFile(folder));
} catch (e) {
throw changeErrorType(e, FormatError);
}
if (!(result.outputs || result.subtasks.length)) {
const c = await readAutoCases(folder, args, cfg);
config.subtasks = c.subtasks;
config.count = c.count;
}
isValidConfig(result);
return result;
}
+4 -4
View File
@@ -50,8 +50,8 @@ function judgeCase(c, sid) {
stderr,
copyIn,
copyOut,
time_limit_ms: ctxSubtask.subtask.time_limit_ms * ctx.execute.time,
memory_limit_mb: ctxSubtask.subtask.memory_limit_mb,
time: ctxSubtask.subtask.time * ctx.execute.time,
memory: ctxSubtask.subtask.memory,
},
);
const { code, time_usage_ms, memory_usage_kb } = res;
@@ -62,9 +62,9 @@ function judgeCase(c, sid) {
let message: any = '';
let score = 0;
if (status === STATUS.STATUS_ACCEPTED) {
if (time_usage_ms > ctxSubtask.subtask.time_limit_ms * ctx.execute.time) {
if (time_usage_ms > ctxSubtask.subtask.time * ctx.execute.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) {
} else if (memory_usage_kb > ctxSubtask.subtask.memory * 1024) {
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
} else {
[status, score, message] = await check({
+2 -2
View File
@@ -77,8 +77,8 @@ export const judge = async (ctx) => {
stderr,
copyIn: execute.copyIn,
copyOut,
time_limit_ms: 1000,
memory_limit_mb: 512,
time: 1000,
memory: 512,
},
);
const { code, time_usage_ms, memory_usage_kb } = res;
+6 -6
View File
@@ -24,21 +24,21 @@ function judgeCase(c) {
{
execute: ctx.executeUser.execute.replace(/\$\{name\}/g, 'code'),
copyIn: ctx.executeUser.copyIn,
time_limit_ms: ctxSubtask.subtask.time_limit_ms,
memory_limit_mb: ctxSubtask.subtask.memory_limit_mb,
time: ctxSubtask.subtask.time,
memory: ctxSubtask.subtask.memory,
}, {
execute: `${ctx.executeInteractor.execute.replace(/\$\{name\}/g, 'interactor')} /w/in /w/tout`,
copyIn: ctx.executeInteractor.copyIn,
time_limit_ms: ctxSubtask.subtask.time_limit_ms * 2,
memory_limit_mb: ctxSubtask.subtask.memory_limit_mb * 2,
time: ctxSubtask.subtask.time * 2,
memory: ctxSubtask.subtask.memory * 2,
},
]);
let status;
let score = 0;
let message: any = '';
if (time_usage_ms > ctxSubtask.subtask.time_limit_ms) {
if (time_usage_ms > ctxSubtask.subtask.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) {
} else if (memory_usage_kb > ctxSubtask.subtask.memory * 1024) {
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
} else if (code) {
status = STATUS.STATUS_RUNTIME_ERROR;
+2 -2
View File
@@ -69,8 +69,8 @@ export const judge = async (ctx) => {
stderr,
copyIn,
copyOut,
time_limit_ms: parseTimeMS(ctx.config.time || '1s'),
memory_limit_mb: parseMemoryMB(ctx.config.memory || '128m'),
time: parseTimeMS(ctx.config.time || '1s'),
memory: parseMemoryMB(ctx.config.memory || '128m'),
},
);
const { code, time_usage_ms, memory_usage_kb } = res;
+6 -6
View File
@@ -25,8 +25,8 @@ const statusMap = {
function proc({
execute = '',
time_limit_ms = 16000,
memory_limit_mb = 1024,
time = 16000,
memory = 1024,
process_limit = 32,
stdin = '', copyIn = {}, copyOut = [], copyOutCached = [],
} = {}) {
@@ -39,9 +39,9 @@ function proc({
{ name: 'stdout', max: 1024 * 1024 * size },
{ name: 'stderr', max: 1024 * 1024 * size },
],
cpuLimit: time_limit_ms * 1000 * 1000,
realCpuLimit: time_limit_ms * 3000 * 1000,
memoryLimit: memory_limit_mb * 1024 * 1024,
cpuLimit: time * 1000 * 1000,
realCpuLimit: time * 3000 * 1000,
memoryLimit: memory * 1024 * 1024,
procLimit: process_limit,
copyIn,
copyOut,
@@ -58,7 +58,7 @@ async function adaptResult(result, params) {
files: result.files,
code: result.exitStatus,
};
if (ret.time_usage_ms >= (params.time_limit_ms || 16000)) {
if (ret.time_usage_ms >= (params.time || 16000)) {
ret.status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
}
ret.files = result.files || {};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "2.21.19",
"version": "2.22.1",
"bin": "bin/hydrooj.js",
"main": "dist/loader.js",
"typings": "dist/loader.d.ts",
+30 -12
View File
@@ -1,11 +1,13 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable import/no-dynamic-require */
import { argv } from 'yargs';
import { ObjectID } from 'mongodb';
import {
lib, service, model,
builtinLib, builtinModel,
lib, service, model, script,
builtinLib, builtinModel, builtinScript,
} from './common';
import options from '../options';
import { validate } from '../lib/validator';
import * as bus from '../service/bus';
import db from '../service/db';
@@ -20,8 +22,23 @@ function parseParameters(fn: Function) {
return result === null ? [] : result;
}
async function runScript(name: string, arg: any) {
if (!global.Hydro.script[name]) return console.error('Script %s not found.', name);
validate(global.Hydro.script[name].validate, arg);
return await global.Hydro.script[name].run(arg, console.info);
}
async function cli() {
const [, modelName, func, ...args] = argv._;
const [, modelName, func, ...args] = argv._ as [string, string, string, ...any[]];
if (modelName === 'script') {
let arg: any;
try {
arg = JSON.parse(args.join(' '));
} catch (e) {
return console.error('Invalid argument');
}
return await runScript(func, arg);
}
if (!global.Hydro.model[modelName]) {
return console.error(`Model ${modelName} doesn't exist.`);
}
@@ -45,6 +62,11 @@ async function cli() {
console.error(`Too few arguments. Min ${parameterMin}`);
return console.error(parameters.join(', '));
}
for (let i = 0; i < args.length; i++) {
if (ObjectID.isValid(args[i])) {
args[i] = new ObjectID(args[i]);
}
}
let result = global.Hydro.model[modelName][func](...args);
if (result instanceof Promise) result = await result;
return console.log(result);
@@ -59,22 +81,18 @@ export async function load() {
require('../options');
const opts = options();
await db.start(opts);
const modelSystem = require('../model/system');
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
'file.endPointForUser', 'file.endPointForJudge',
]);
const sopts = {
endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge,
};
const storage = require('../service/storage');
await storage.start(sopts);
await storage.start();
for (const i of builtinLib) require(`../lib/${i}`);
await lib(pending, fail);
const systemModel = require('../model/system');
await systemModel.runConfig();
require('../service/gridfs');
await service(pending, fail);
for (const i of builtinModel) require(`../model/${i}`);
await model(pending, fail);
for (const i of builtinScript) require(`../script/${i}`);
await script(pending, fail, []);
await bus.parallel('app/started');
await cli();
}
+1 -2
View File
@@ -12,8 +12,7 @@ const logger = new Logger('common', true);
export const builtinLib = [
'jwt', 'download', 'i18n', 'mail', 'useragent',
'crypto', 'misc', 'paginate', 'hash.hydro', 'rank',
'validator', 'ui', 'testdata.convert.ini', 'testdataConfig', 'difficulty',
'content',
'validator', 'ui', 'testdataConfig', 'difficulty', 'content',
];
export const builtinModel = [
+2 -15
View File
@@ -57,15 +57,8 @@ export async function load(call: Entry) {
await modelSystem.set('file.secretKey', process.env.MINIO_SECRET_KEY);
await modelSystem.set('file.endPoint', 'http://localhost:9000/');
}
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
'file.endPointForUser', 'file.endPointForJudge',
]);
const sopts = {
endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge,
};
const storage = require('../service/storage');
storage.start(sopts);
await storage.start();
require('../service/monitor');
for (const i of builtinModel) require(`../model/${i}`);
const scripts = require('../upgrade').default;
@@ -75,16 +68,10 @@ export async function load(call: Entry) {
while (dbVer < expected) {
logger.info('Upgrading database: from %d to %d', dbVer, expected);
const func = scripts[dbVer];
if (typeof func !== 'function') {
if (typeof func !== 'function' || (isFresh && func.toString().includes('_FRESH_INSTALL_IGNORE'))) {
dbVer++;
continue;
}
if (isFresh) {
if (func.toString().includes('_FRESH_INSTALL_IGNORE')) {
dbVer++;
continue;
}
}
const result = await func();
if (!result) break;
dbVer++;
+1 -8
View File
@@ -34,15 +34,8 @@ export async function load() {
const modelSystem = require('../model/system');
await modelSystem.runConfig();
if (argv.loaderDetail) logger.info('finish: config');
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
'file.endPointForUser', 'file.endPointForJudge',
]);
const sopts = {
endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge,
};
const storage = require('../service/storage');
storage.start(sopts);
await storage.start();
if (argv.loaderDetail) logger.info('finish: storage.connect');
for (const i of builtinLib) {
let t;
+8
View File
@@ -96,6 +96,14 @@ class ContestDetailHandler extends ContestHandler {
await contest.attend(domainId, tid, this.user._id);
this.back();
}
@param('tid', Types.ObjectID)
async postDelete(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid);
if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_CONTEST);
await contest.del(domainId, tid);
this.response.redirect = this.url('contest_main');
}
}
class ContestBoardcastHandler extends ContestHandler {
+8
View File
@@ -108,6 +108,14 @@ class HomeworkDetailHandler extends HomeworkHandler {
await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK);
this.back();
}
@param('tid', Types.ObjectID)
async postDelete(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
await contest.del(domainId, tid, document.TYPE_HOMEWORK);
this.response.redirect = this.url('homework_main');
}
}
class HomeworkDetailProblemHandler extends HomeworkHandler {
+24 -2
View File
@@ -6,11 +6,12 @@ import {
SolutionNotFoundError, ProblemNotFoundError, BadRequestError,
} from '../error';
import {
Pdoc, User, Rdoc, PathComponent,
Pdoc, User, Rdoc, PathComponent, ProblemStatusDoc,
} from '../interface';
import paginate from '../lib/paginate';
import { isPid } from '../lib/validator';
import difficultyAlgorithm from '../lib/difficulty';
import { parseConfig } from '../lib/testdataConfig';
import * as system from '../model/system';
import problem from '../model/problem';
import record from '../model/record';
@@ -156,8 +157,12 @@ export class ProblemRandomHandler extends ProblemHandler {
}
}
export interface PdocWithPsdoc extends Pdoc {
psdoc?: ProblemStatusDoc
}
export class ProblemDetailHandler extends ProblemHandler {
pdoc: Pdoc;
pdoc: PdocWithPsdoc;
udoc: User;
@route('pid', Types.Name, true, null, parsePid)
@@ -189,6 +194,15 @@ export class ProblemDetailHandler extends ProblemHandler {
// e.g. ![img](a.jpg) will navigate to ![img](./pid/file/a.jpg)
this.response.body.pdoc.content = this.response.body.pdoc.content
.replace(/\(file:\/\//g, `(./${this.pdoc.docId}/file/`);
// Get time and memory limit
try {
this.response.body.pdoc.config = await parseConfig(this.pdoc.config);
} catch (e) {
this.response.body.pdoc.config = `Cannot parse: ${e.message}`;
}
if (this.pdoc.psdoc) {
this.response.body.rdoc = await record.get(this.domainId, this.pdoc.psdoc.rid);
}
}
@param('pid', Types.UnsignedInt)
@@ -574,6 +588,13 @@ export class ProblemCreateHandler extends Handler {
}
}
export class ProblemPrefixListHandler extends Handler {
@param('prefix', Types.Name)
async get(domainId: string, prefix: string) {
this.response.body = await problem.getPrefixList(domainId, prefix);
}
}
export async function apply() {
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_category', '/p/category/:category', ProblemCategoryHandler, PERM.PERM_VIEW_PROBLEM);
@@ -589,6 +610,7 @@ export async function apply() {
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionRawHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM);
Route('problem_prefix_list', '/problem/list', ProblemPrefixListHandler, PERM.PERM_VIEW_PROBLEM);
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler, PERM.PERM_SUBMIT_PROBLEM);
}
-2
View File
@@ -189,7 +189,6 @@ declare module './model/problem' {
data: FileInfo[],
additional_file: FileInfo[],
hidden: boolean,
acMsg?: string,
html?: boolean,
difficulty?: number,
/** @deprecated */
@@ -546,7 +545,6 @@ export interface Lib extends Record<string, any> {
rank: typeof import('./lib/rank'),
rating: typeof import('./lib/rating'),
testdataConfig: typeof import('./lib/testdataConfig'),
'testdata.convert.ini': typeof import('./lib/testdata.convert.ini'),
useragent: typeof import('./lib/useragent'),
validator: typeof import('./lib/validator'),
template?: any,
@@ -1,22 +0,0 @@
import yaml from 'js-yaml';
function convert(ini: string) {
const f = ini.split('\n');
const count = parseInt(f[0], 10);
const res = { subtasks: [] };
for (let i = 1; i <= count; i++) {
const [input, output, time, score, memory] = f[i].split('|');
const cur = {
cases: [{ input: `input/${input}`, output: `output/${output}` }],
score: parseInt(score, 10),
time: `${time}s`,
memory: '512m',
};
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
res.subtasks.push(cur);
}
return yaml.dump(res);
}
global.Hydro.lib['testdata.convert.ini'] = convert;
export = convert;
+14 -28
View File
@@ -1,23 +1,7 @@
import Zip from 'adm-zip';
import readYamlCases from '@hydrooj/utils/lib/cases';
import { load } from 'js-yaml';
import type { ProblemConfig } from '../interface';
export async function readConfig(file: string | Buffer) {
const data = new Zip(file);
const entries = data.getEntries();
for (const entry of entries) {
if (entry.name.toLowerCase() === 'config.yaml') {
return entry.getData().toString();
}
if (entry.name.toLowerCase() === 'config.ini') {
const ini = entry.getData().toString();
const conv = global.Hydro.lib['testdata.convert.ini'];
return conv(ini);
}
}
return '';
}
interface ParseResult {
count: number,
memoryMax: number,
@@ -37,24 +21,26 @@ export async function parseConfig(config: string | ProblemConfig) {
let cfg: ProblemConfig = {};
if (typeof config === 'string') {
// TODO should validate here?
cfg = load(config) as ProblemConfig;
cfg = await readYamlCases(load(config) as Record<string, any>) as ProblemConfig;
} else cfg = config;
if (cfg.cases) {
for (const c of cfg.cases) {
result.memoryMax = Math.max(result.memoryMax, c.memory);
result.memoryMin = Math.min(result.memoryMin, c.memory);
result.timeMax = Math.max(result.timeMax, c.time);
result.timeMin = Math.min(result.timeMin, c.time);
result.count++;
}
} else if (cfg.subtasks) {
if (cfg.subtasks.length) {
for (const subtask of cfg.subtasks) {
result.memoryMax = Math.max(result.memoryMax, subtask.memory);
result.memoryMin = Math.min(result.memoryMin, subtask.memory);
result.timeMax = Math.max(result.timeMax, subtask.time);
result.timeMin = Math.min(result.timeMin, subtask.time);
}
} else if (cfg.time || cfg.memory) {
if (cfg.time) result.timeMax = result.timeMin = cfg.time as unknown as number;
if (cfg.memory) result.memoryMax = result.memoryMin = cfg.memory as unknown as number;
}
if (result.memoryMax < result.memoryMin) {
result.memoryMax = result.memoryMin = 256;
}
if (result.timeMax < result.timeMin) {
result.timeMax = result.timeMin = 1000;
}
return result;
}
global.Hydro.lib.testdataConfig = { readConfig, parseConfig };
global.Hydro.lib.testdataConfig = { parseConfig };
+9 -4
View File
@@ -1,6 +1,6 @@
import type { Readable } from 'stream';
import { ObjectID, FilterQuery } from 'mongodb';
import { Dictionary, pick } from 'lodash';
import { Dictionary, escapeRegExp, pick } from 'lodash';
import { STATUS } from './builtin';
import * as document from './document';
import domain from './domain';
@@ -28,8 +28,7 @@ export class ProblemModel {
static PROJECTION_PUBLIC: Field[] = [
...ProblemModel.PROJECTION_LIST,
'content', 'html', 'data', 'config', 'acMsg',
'additional_file',
'content', 'html', 'data', 'config', 'additional_file',
];
static extend(getter: Getter) {
@@ -209,6 +208,13 @@ export class ProblemModel {
return Object.assign(r, l);
}
static async getPrefixList(domainId: string, prefix: string) {
prefix = prefix.toLowerCase();
const $regex = new RegExp(`\\A${escapeRegExp(prefix)}`, 'gmi');
const filter = { $or: [{ pid: { $regex } }, { title: { $regex } }] };
return await document.getMulti(domainId, document.TYPE_PROBLEM, filter, ['domainId', 'docId', 'pid', 'title']).toArray();
}
static async getListStatus(domainId: string, uid: number, pids: number[]) {
const psdocs = await ProblemModel.getMultiStatus(
domainId, { uid, docId: { $in: Array.from(new Set(pids)) } },
@@ -258,7 +264,6 @@ ProblemModel.extend((docId, pid) => ({
additional_file: [],
hidden: true,
config: '',
acMsg: '',
difficulty: 0,
}));
+1 -1
View File
@@ -67,7 +67,7 @@ export async function runConfig() {
}
const config = await coll.find().toArray();
for (const i of config) cache[i._id] = i.value;
bus.emit('database/config');
await bus.emit('database/config');
}
bus.on('system/setting', (args) => {
+2 -1
View File
@@ -1,4 +1,5 @@
import { Collection } from 'mongodb';
import { escapeRegExp } from 'lodash';
import * as system from './system';
import token from './token';
import * as setting from './setting';
@@ -224,7 +225,7 @@ class UserModel {
@ArgMethod
static async getPrefixList(domainId: string, prefix: string, limit: number = 50) {
prefix = prefix.toLowerCase();
const $regex = new RegExp(`\\A\\Q${prefix}\\E`, 'gmi');
const $regex = new RegExp(`\\A${escapeRegExp(prefix)}`, 'gmi');
const uids = await coll.find({ unameLower: { $regex } })
.limit(limit).map((doc) => doc._id).toArray();
const users = [];
+1 -1
View File
@@ -38,7 +38,7 @@ export interface EventMap extends Record<string, any> {
'message/run': (command: string) => VoidReturn
'database/connect': (db: Db) => void
'database/config': () => void
'database/config': () => VoidReturn
'system/setting': (args: Record<string, any>) => VoidReturn
+9 -10
View File
@@ -79,9 +79,15 @@ class StorageService {
private opts: StorageOptions;
private replaceWithAlternativeUrlFor: Record<'user' | 'judge', (originalUrl: string) => string>;
async start(opts: StorageOptions) {
async start() {
try {
this.opts = opts;
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = system.getMany([
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
'file.endPointForUser', 'file.endPointForJudge',
]);
this.opts = {
endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge,
};
this.client = new Client({
...parseMainEndpointUrl(this.opts.endPoint),
accessKey: this.opts.accessKey,
@@ -103,14 +109,7 @@ class StorageService {
logger.warn('Storage init fail. will retry later.');
this.error = e.toString();
setTimeout(async () => {
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = system.getMany([
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
'file.endPointForUser', 'file.endPointForJudge',
]);
const sopts = {
endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge,
};
await this.start(sopts);
await this.start();
}, 10000);
}
}
+30 -3
View File
@@ -4,21 +4,21 @@ import { FilterQuery, ObjectID } from 'mongodb';
import AdmZip from 'adm-zip';
import Queue from 'p-queue';
import yaml from 'js-yaml';
import { convertIniConfig } from '@hydrooj/utils/lib/cases';
import { Progress } from './ui';
import { Rdoc } from './interface';
import { Logger } from './logger';
import { streamToBuffer } from './utils';
import { iterateAllDomain, iterateAllProblem } from './pipelineUtils';
import gridfs from './service/gridfs';
import storage from './service/storage';
import db from './service/db';
import difficultyAlgorithm from './lib/difficulty';
import problem from './model/problem';
import domain from './model/domain';
import * as document from './model/document';
import * as system from './model/system';
import difficultyAlgorithm from './lib/difficulty';
import { STATUS } from './model/builtin';
import RecordModel from './model/record';
import { Rdoc } from './interface';
const logger = new Logger('upgrade');
type UpgradeScript = void | (() => Promise<boolean | void>);
@@ -262,6 +262,33 @@ const scripts: UpgradeScript[] = [
}
return true;
},
async function _20_21() {
await iterateAllProblem([], async (pdoc) => {
let config: string;
try {
const file = await storage.get(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/config.yaml`);
config = (await streamToBuffer(file)).toString('utf-8');
logger.info(`Loaded config for ${pdoc.domainId}/${pdoc.docId} from config.yaml`);
} catch (e) {
try {
const file = await storage.get(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/config.yml`);
config = (await streamToBuffer(file)).toString('utf-8');
logger.info(`Loaded config for ${pdoc.domainId}/${pdoc.docId} from config.yml`);
} catch (err) {
try {
const file = await storage.get(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/config.ini`);
config = yaml.dump(convertIniConfig((await streamToBuffer(file)).toString('utf-8')));
logger.info(`Loaded config for ${pdoc.domainId}/${pdoc.docId} from config.ini`);
} catch (error) {
logger.warn('Config for %s/%s(%s) not found', pdoc.domainId, pdoc.docId, pdoc.pid || pdoc.docId);
// no config found
}
}
}
if (config) await problem.edit(pdoc.domainId, pdoc.docId, { config });
});
return true;
},
];
export default scripts;
+1 -1
View File
@@ -11,7 +11,7 @@ export async function connect() {
while (dbVer < expected) {
const func = scripts[dbVer];
dbVer++;
if (func.toString().includes('_FRESH_INSTALL_IGNORE')) continue;
if (typeof func !== 'function' || func.toString().includes('_FRESH_INSTALL_IGNORE')) continue;
// eslint-disable-next-line no-await-in-loop
await func();
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/import-qduoj",
"version": "1.1.6",
"version": "1.1.7",
"description": "Import QDUOJ problem export",
"main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro.git",
@@ -11,4 +11,4 @@
"decode-html": "^2.0.0",
"fs-extra": "^9.1.0"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/migrate-vijos",
"version": "1.4.0",
"version": "1.4.1",
"main": "package.json",
"repository": "git@github.com:hydro-dev/Hydro.git",
"author": "undefined <i@undefined.moe>",
-1
View File
@@ -74,7 +74,6 @@ const tasks = {
return res;
},
},
ac_msg: 'acMsg',
parent_doc_id: {
field: 'parentId',
processer: (parentId, doc) => {
+1 -1
View File
@@ -88,7 +88,7 @@ class Loader extends nunjucks.Loader {
};
}
let fullpath = null;
if (typeof argv.template !== 'string') argv.template = path.resolve(__dirname, '..', 'template');
if (typeof argv.template !== 'string') argv.template = path.resolve(__dirname, '..', 'templates');
const p = path.resolve(process.cwd(), argv.template, name);
if (fs.existsSync(p)) fullpath = p;
if (!fullpath) {
@@ -0,0 +1,41 @@
import _ from 'lodash';
import tpl from 'vj/utils/tpl';
import request from 'vj/utils/request';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from './index';
function getText(pdoc) {
return pdoc.docId;
}
function getItems(prefix) {
return request.get('/problem/list', { prefix });
}
function renderItem(pdoc) {
return tpl`
<div class="media">
<div class="media__body medium">
<div class="problem-select__name">${pdoc.pid ? `${pdoc.pid} ` : ''}${pdoc.title}</div>
<div class="problem-select__id">ID = ${pdoc.docId}</div>
</div>
</div>
`;
}
export default class ProblemSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'vjProblemSelectAutoCompleteInstance';
constructor($dom, options) {
super($dom, {
classes: 'problem-select',
items: getItems,
render: renderItem,
text: getText,
...options,
});
}
}
_.assign(ProblemSelectAutoComplete, DOMAttachedObject);
window.Hydro.components.ProblemSelectAutoComplete = ProblemSelectAutoComplete;
@@ -22,6 +22,7 @@ export default class AutoComplete extends DOMAttachedObject {
clearDefaultValue: true,
position: 'bottom left',
classes: '',
multi: false,
...options,
};
this.clear(this.options.clearDefaultValue);
@@ -46,9 +47,7 @@ export default class AutoComplete extends DOMAttachedObject {
}
clear(clearValue = true) {
if (clearValue) {
this.$dom.val('');
}
if (clearValue) this.$dom.val('');
this._value = null;
this.lastText = null;
}
@@ -80,19 +79,14 @@ export default class AutoComplete extends DOMAttachedObject {
}
onKeyUp(ev) {
if (ev.which === 27) {
// ESC
if (ev.which === 27 /* ESC */) {
this.close();
return;
}
if (this.$dom.val() === this.lastText) {
return;
}
if (this.$dom.val() === this.lastText) return;
this.lastText = this.$dom.val();
this.updateOpenState();
if (this.isOpen) {
this.renderList();
}
if (this.isOpen) this.renderList();
}
onMenuClick(ev) {
@@ -109,7 +103,8 @@ export default class AutoComplete extends DOMAttachedObject {
this._value = item;
this.$dom.trigger('vjAutoCompleteSelect', item);
if (text != null) {
this.$dom.val(text);
if (!this.options.multi) this.$dom.val(text);
else this.$dom.val(`${this.$dom.val() + text}, `);
}
this.close();
}
@@ -141,7 +136,11 @@ export default class AutoComplete extends DOMAttachedObject {
}
async renderList() {
const val = this.$dom.val();
let val = this.$dom.val();
if (this.options.multi) {
const elems = val.split(',').map((i) => i.trim());
val = elems[elems.length - 1];
}
const items = await this.getItems(val);
const html = this.getHtml(items);
this.currentItems = items;
@@ -0,0 +1,6 @@
.menu.problem-select
width: rem(200px)
.problem-select__uid
font-size: rem($font-size-small)
color: $userselect-uid-color
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.5.14",
"version": "4.5.16",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
@@ -0,0 +1,34 @@
import { NamedPage } from 'vj/misc/Page';
import { ConfirmDialog } from 'vj/components/dialog';
import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete';
import tpl from 'vj/utils/tpl';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => {
ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true });
if (pagename.endsWith('edit')) {
let confirmed = false;
$(document).on('click', '[name="operation"]', (ev) => {
ev.preventDefault();
if (confirmed) {
return request.post('.', { operation: 'delete' }).then((res) => {
window.location.href = res.url;
});
}
const message = `Confirm deleting this ${pagename.split('_')[0]}? Its status will be deleted as well.`;
return new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n(message)}</p>
</div>`,
}).open().then((action) => {
if (action !== 'yes') return;
confirmed = true;
ev.target.click();
});
});
}
});
export default page;
@@ -47,6 +47,7 @@
{% set disabled = args.disabled or false %}
{% set required = args.required or false %}
{% set autofocus = args.autofocus or false %}
{% set noAutocomplete = args.noAutocomplete or false %}
{% set extra_style = args.extra_style or false %}
{% set date = args.date or false %}
{% set time = args.time or false %}
@@ -54,6 +55,7 @@
{% if disabled %}disabled{% endif %}
{% if required %}required{% endif %}
{% if autofocus %}autofocus data-autofocus{% endif %}
{% if noAutocomplete %}autocomplete="off"{% endif %}
{% if date %}data-pick-date{% endif %}
{% if time %}data-pick-time{% endif %}
{% if extra_style %}style="{{extra_style}}"{% endif %}
@@ -62,7 +62,9 @@
columns:null,
label:'Problems',
name:'pids',
value:pids
value:pids,
noAutocomplete:true,
placeholder:_("Seperated with ','")
}) }}
{{ form.form_textarea({
columns:null,
@@ -77,6 +79,11 @@
<button type="submit" class="rounded primary button">
{{ _('Update') }}
</button>
{% if tdoc.owner == handler.user._id or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}
<button name="operation" value="delete" type="submit" class="rounded button">
{{ _('Delete') }}
</button>
{% endif %}
{% else %}
<button type="submit" class="rounded primary button">
{{ _('Create') }}
@@ -85,6 +85,11 @@
<button type="submit" class="rounded primary button">
{{ _('Update') }}
</button>
{% if tdoc.owner == handler.user._id or handler.user.hasPerm(perm.PERM_EDIT_HOMEWORK) %}
<button name="operation" value="delete" type="submit" class="rounded button">
{{ _('Delete') }}
</button>
{% endif %}
{% else %}
<button type="submit" class="rounded primary button">
{{ _('Create') }}
@@ -114,6 +114,10 @@
<dl class="large horizontal">
<dt>{{ _('ID') }}</dt>
<dd>{{ pdoc.docId }}</dd>
<dt>{{ _('Time') }}</dt>
<dd>{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</dd>
<dt>{{ _('Memory') }}</dt>
<dd>{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</dd>
{% if page_name != 'discussion_node' and page_name != 'discussion_detail' %}
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
<dt>{{ _('Submission') }}</dt>
@@ -126,10 +130,6 @@
{% else %}
{{ _('(None)') }}
{% endif %}
{% if pdoc['acMsg'] and pdoc['psdoc']['status'] == STATUS.STATUS_ACCEPTED %}
<dt>{{ _('AC Message') }}</dt>
<dd>{{ pdoc['acMsg'] }}</dd>
{% endif %}
</dd>
{% endif %}
{% endif %}
@@ -24,8 +24,18 @@
<div class="medium-9 columns">
<div class="section">
<div class="problem-content-container"><div class="problem-content" data-marker-enabled>
<div class="section__header non-scratchpad--hide">
<h1>{{ pdoc.title }}</h1>
<div class="section__header">
{% if rdoc %}
<h1 class="section__title scratchpad--hide">
<span class="icon record-status--icon {{ model.builtin.STATUS_CODES[rdoc['status']] }}"></span>
<span class="record-status--text {{ model.builtin.STATUS_CODES[rdoc['status']] }}">
{{ 'Accepted' if rdoc.status == model.builtin.STATUS.STATUS_ACCEPTED else rdoc.score }}
</span>
</h1>
{% endif %}
<h1 class="section__title non-scratchpad--hide">
{{ pdoc.title }}
</h1>
</div>
<div class="section__body typo">
{% if tdoc %}
+94
View File
@@ -0,0 +1,94 @@
import { parseTimeMS, parseMemoryMB } from './utils';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (s: string, errMsg: string) => s) {
const config: any = {
checker_type: 'default',
count: 0,
subtasks: [],
judge_extra_files: [],
user_extra_files: [],
};
config.checker_type = cfg.checker_type || 'default';
if (cfg.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.');
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
if (cfg.judge_extra_files) {
if (typeof cfg.judge_extra_files === 'string') {
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, 'Cannot find judge extra file {0}.'));
}
} else throw new Error('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, '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, 'Cannot find user extra file {0}.'));
}
} else throw new Error('Invalid user_extra_files config.');
}
if (cfg.outputs) {
config.type = 'submit_answer';
} else if (cfg.cases) {
config.subtasks = [{
score: parseInt(cfg.score, 10) || Math.floor(100 / cfg.cases.length),
time: parseTimeMS(cfg.time || '1s'),
memory: parseMemoryMB(cfg.memory || '256m'),
cases: [],
type: 'sum',
}];
for (const c of cfg.cases) {
config.count++;
config.subtasks[0].cases.push({
input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null',
output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null',
id: config.count,
});
}
} else if (cfg.subtasks) {
for (const subtask of cfg.subtasks) {
const cases = [];
for (const c of subtask.cases) {
config.count++;
cases.push({
input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null',
output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null',
id: config.count,
});
}
config.subtasks.push({
score: parseInt(subtask.score, 10),
if: subtask.if || [],
cases,
time: parseTimeMS(subtask.time || cfg.time || '1s'),
memory: parseMemoryMB(subtask.memory || cfg.memory || '256m'),
});
}
}
if (cfg.time) config.time = parseTimeMS(cfg.time);
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
if (config.type === 'submit_answer' && !cfg.outputs) throw new Error('outputs config not found');
return Object.assign(cfg, config);
}
export function convertIniConfig(ini: string) {
const f = ini.split('\n');
const count = parseInt(f[0], 10);
const res = { subtasks: [] };
for (let i = 1; i <= count; i++) {
if (!f[i] || !f[i].trim()) throw new Error('Testcada count incorrect.');
const [input, output, time, score, memory] = f[i].split('|');
const cur = {
cases: [{ input: `input/${input.toLowerCase()}`, output: `output/${output.toLowerCase()}` }],
score: parseInt(score, 10),
time: `${time}s`,
memory: '256m',
};
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
res.subtasks.push(cur);
}
return res;
}
+10 -2
View File
@@ -161,13 +161,15 @@ const TIME_UNITS = { '': 1000, m: 1, u: 0.001 };
const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i;
const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 };
export function parseTimeMS(str: string) {
export function parseTimeMS(str: string | number) {
if (typeof str === 'number') return str;
const match = TIME_RE.exec(str);
if (!match) throw new Error(`${str} error parsing time`);
return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]);
}
export function parseMemoryMB(str: string) {
export function parseMemoryMB(str: string | number) {
if (typeof str === 'number') return str;
const match = MEMORY_RE.exec(str);
if (!match) throw new Error(`${str} error parsing memory`);
return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2]]);
@@ -297,6 +299,12 @@ export function formatSeconds(_seconds = '0') {
);
}
export function changeErrorType(err: Error, Err: any) {
const e = new Err(err.message);
e.stack = err.stack;
return e;
}
export async function retry(func: Function, ...args: any[]): Promise<any>;
export async function retry(times: number, func: Function, ...args: any[]): Promise<any>;
// eslint-disable-next-line consistent-return
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/utils",
"version": "1.0.1",
"version": "1.0.2",
"description": "hydrooj utils",
"main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro.git",