mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
Merge pull request #92 from hydro-dev/develop
This commit is contained in:
46 files changed
+427
-273
No files matched your search
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "@hydrooj/hydrojudge",
|
||||
"bin": "bin/hydrojudge.js",
|
||||
"version": "2.4.19",
|
||||
"version": "2.5.1",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.  will navigate to 
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,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>",
|
||||
|
||||
@@ -74,7 +74,6 @@ const tasks = {
|
||||
return res;
|
||||
},
|
||||
},
|
||||
ac_msg: 'acMsg',
|
||||
parent_doc_id: {
|
||||
field: 'parentId',
|
||||
processer: (parentId, doc) => {
|
||||
|
||||
@@ -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,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 %}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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",
|
||||
|
||||
Reference in new issue
Block a user