mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
Merge pull request #92 from hydro-dev/develop
This commit is contained in:
46 files changed
+425
-271
No files matched your search
+1
-1
@@ -30,7 +30,7 @@ sysctl -p
|
|||||||
|
|
||||||
# Install NodeJS
|
# Install NodeJS
|
||||||
echo "Installing 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
|
export NVM_DIR=/root/.nvm
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
nvm install 14
|
nvm install 14
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ mkdir -p /data/db /data/file
|
|||||||
|
|
||||||
# Install NodeJS
|
# Install NodeJS
|
||||||
echo "Installing 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
|
export NVM_DIR=/root/.nvm
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14
|
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
|
# Install NodeJS
|
||||||
echo "Installing 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
|
export NVM_DIR=/root/.nvm
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14
|
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
|
# Install NodeJS
|
||||||
echo "Installing 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
|
export NVM_DIR=/root/.nvm
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14
|
NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/hydrojudge",
|
"name": "@hydrooj/hydrojudge",
|
||||||
"bin": "bin/hydrojudge.js",
|
"bin": "bin/hydrojudge.js",
|
||||||
"version": "2.4.19",
|
"version": "2.5.1",
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import yaml from 'js-yaml';
|
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 { FormatError, SystemError } from './error';
|
||||||
import { parseTimeMS, parseMemoryMB, ensureFile } from './utils';
|
import { parseTimeMS, parseMemoryMB, ensureFile } from './utils';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
@@ -86,8 +88,8 @@ async function read0(folder: string, files: string[], checkFile, cfg) {
|
|||||||
const config = {
|
const config = {
|
||||||
count: 0,
|
count: 0,
|
||||||
subtasks: [{
|
subtasks: [{
|
||||||
time_limit_ms: parseTimeMS(cfg.time || '1s'),
|
time: parseTimeMS(cfg.time || '1s'),
|
||||||
memory_limit_mb: parseMemoryMB(cfg.memory || '256m'),
|
memory: parseMemoryMB(cfg.memory || '256m'),
|
||||||
type: 'sum',
|
type: 'sum',
|
||||||
cases: [],
|
cases: [],
|
||||||
score: Math.floor(100 / cases.length),
|
score: Math.floor(100 / cases.length),
|
||||||
@@ -103,8 +105,8 @@ async function read0(folder: string, files: string[], checkFile, cfg) {
|
|||||||
}
|
}
|
||||||
if (extra < cases.length) {
|
if (extra < cases.length) {
|
||||||
config.subtasks.push({
|
config.subtasks.push({
|
||||||
time_limit_ms: parseTimeMS(cfg.time || '1s'),
|
time: parseTimeMS(cfg.time || '1s'),
|
||||||
memory_limit_mb: parseMemoryMB(cfg.memory || '256m'),
|
memory: parseMemoryMB(cfg.memory || '256m'),
|
||||||
type: 'sum',
|
type: 'sum',
|
||||||
cases: [],
|
cases: [],
|
||||||
score: Math.floor(100 / cases.length) + 1,
|
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 (fs.existsSync(path.resolve(folder, c.output))) {
|
||||||
if (!subtask[REG.subtask(data)]) {
|
if (!subtask[REG.subtask(data)]) {
|
||||||
subtask[REG.subtask(data)] = [{
|
subtask[REG.subtask(data)] = [{
|
||||||
time_limit_ms: parseTimeMS(cfg.time || '1s'),
|
time: parseTimeMS(cfg.time || '1s'),
|
||||||
memory_limit_mb: parseMemoryMB(cfg.memory || '256m'),
|
memory: parseMemoryMB(cfg.memory || '256m'),
|
||||||
type: 'min',
|
type: 'min',
|
||||||
cases: [c],
|
cases: [c],
|
||||||
}];
|
}];
|
||||||
@@ -193,107 +195,11 @@ async function readAutoCases(folder, { next }, cfg) {
|
|||||||
return config;
|
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) {
|
function isValidConfig(config) {
|
||||||
if (config.count > (getConfig('testcases_max') || 100)) {
|
if (config.count > (getConfig('testcases_max') || 100)) {
|
||||||
throw new FormatError('Too many testcases. Cancelled.');
|
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) {
|
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]);
|
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)) {
|
} else if (fs.existsSync(ymlConfig)) {
|
||||||
config = { ...yaml.load(fs.readFileSync(ymlConfig).toString()) as object, ...cfg };
|
config = { ...yaml.load(fs.readFileSync(ymlConfig).toString()) as object, ...cfg };
|
||||||
} else if (fs.existsSync(iniConfig)) {
|
} else if (fs.existsSync(iniConfig)) {
|
||||||
|
try {
|
||||||
config = { ...convertIniConfig(fs.readFileSync(iniConfig).toString()), ...cfg };
|
config = { ...convertIniConfig(fs.readFileSync(iniConfig).toString()), ...cfg };
|
||||||
|
} catch (e) {
|
||||||
|
throw changeErrorType(e, FormatError);
|
||||||
|
}
|
||||||
} else config = cfg;
|
} 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);
|
isValidConfig(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -50,8 +50,8 @@ function judgeCase(c, sid) {
|
|||||||
stderr,
|
stderr,
|
||||||
copyIn,
|
copyIn,
|
||||||
copyOut,
|
copyOut,
|
||||||
time_limit_ms: ctxSubtask.subtask.time_limit_ms * ctx.execute.time,
|
time: ctxSubtask.subtask.time * ctx.execute.time,
|
||||||
memory_limit_mb: ctxSubtask.subtask.memory_limit_mb,
|
memory: ctxSubtask.subtask.memory,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { code, time_usage_ms, memory_usage_kb } = res;
|
const { code, time_usage_ms, memory_usage_kb } = res;
|
||||||
@@ -62,9 +62,9 @@ function judgeCase(c, sid) {
|
|||||||
let message: any = '';
|
let message: any = '';
|
||||||
let score = 0;
|
let score = 0;
|
||||||
if (status === STATUS.STATUS_ACCEPTED) {
|
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;
|
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;
|
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
||||||
} else {
|
} else {
|
||||||
[status, score, message] = await check({
|
[status, score, message] = await check({
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ export const judge = async (ctx) => {
|
|||||||
stderr,
|
stderr,
|
||||||
copyIn: execute.copyIn,
|
copyIn: execute.copyIn,
|
||||||
copyOut,
|
copyOut,
|
||||||
time_limit_ms: 1000,
|
time: 1000,
|
||||||
memory_limit_mb: 512,
|
memory: 512,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { code, time_usage_ms, memory_usage_kb } = res;
|
const { code, time_usage_ms, memory_usage_kb } = res;
|
||||||
|
|||||||
@@ -24,21 +24,21 @@ function judgeCase(c) {
|
|||||||
{
|
{
|
||||||
execute: ctx.executeUser.execute.replace(/\$\{name\}/g, 'code'),
|
execute: ctx.executeUser.execute.replace(/\$\{name\}/g, 'code'),
|
||||||
copyIn: ctx.executeUser.copyIn,
|
copyIn: ctx.executeUser.copyIn,
|
||||||
time_limit_ms: ctxSubtask.subtask.time_limit_ms,
|
time: ctxSubtask.subtask.time,
|
||||||
memory_limit_mb: ctxSubtask.subtask.memory_limit_mb,
|
memory: ctxSubtask.subtask.memory,
|
||||||
}, {
|
}, {
|
||||||
execute: `${ctx.executeInteractor.execute.replace(/\$\{name\}/g, 'interactor')} /w/in /w/tout`,
|
execute: `${ctx.executeInteractor.execute.replace(/\$\{name\}/g, 'interactor')} /w/in /w/tout`,
|
||||||
copyIn: ctx.executeInteractor.copyIn,
|
copyIn: ctx.executeInteractor.copyIn,
|
||||||
time_limit_ms: ctxSubtask.subtask.time_limit_ms * 2,
|
time: ctxSubtask.subtask.time * 2,
|
||||||
memory_limit_mb: ctxSubtask.subtask.memory_limit_mb * 2,
|
memory: ctxSubtask.subtask.memory * 2,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
let status;
|
let status;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let message: any = '';
|
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;
|
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;
|
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
||||||
} else if (code) {
|
} else if (code) {
|
||||||
status = STATUS.STATUS_RUNTIME_ERROR;
|
status = STATUS.STATUS_RUNTIME_ERROR;
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ export const judge = async (ctx) => {
|
|||||||
stderr,
|
stderr,
|
||||||
copyIn,
|
copyIn,
|
||||||
copyOut,
|
copyOut,
|
||||||
time_limit_ms: parseTimeMS(ctx.config.time || '1s'),
|
time: parseTimeMS(ctx.config.time || '1s'),
|
||||||
memory_limit_mb: parseMemoryMB(ctx.config.memory || '128m'),
|
memory: parseMemoryMB(ctx.config.memory || '128m'),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { code, time_usage_ms, memory_usage_kb } = res;
|
const { code, time_usage_ms, memory_usage_kb } = res;
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const statusMap = {
|
|||||||
|
|
||||||
function proc({
|
function proc({
|
||||||
execute = '',
|
execute = '',
|
||||||
time_limit_ms = 16000,
|
time = 16000,
|
||||||
memory_limit_mb = 1024,
|
memory = 1024,
|
||||||
process_limit = 32,
|
process_limit = 32,
|
||||||
stdin = '', copyIn = {}, copyOut = [], copyOutCached = [],
|
stdin = '', copyIn = {}, copyOut = [], copyOutCached = [],
|
||||||
} = {}) {
|
} = {}) {
|
||||||
@@ -39,9 +39,9 @@ function proc({
|
|||||||
{ name: 'stdout', max: 1024 * 1024 * size },
|
{ name: 'stdout', max: 1024 * 1024 * size },
|
||||||
{ name: 'stderr', max: 1024 * 1024 * size },
|
{ name: 'stderr', max: 1024 * 1024 * size },
|
||||||
],
|
],
|
||||||
cpuLimit: time_limit_ms * 1000 * 1000,
|
cpuLimit: time * 1000 * 1000,
|
||||||
realCpuLimit: time_limit_ms * 3000 * 1000,
|
realCpuLimit: time * 3000 * 1000,
|
||||||
memoryLimit: memory_limit_mb * 1024 * 1024,
|
memoryLimit: memory * 1024 * 1024,
|
||||||
procLimit: process_limit,
|
procLimit: process_limit,
|
||||||
copyIn,
|
copyIn,
|
||||||
copyOut,
|
copyOut,
|
||||||
@@ -58,7 +58,7 @@ async function adaptResult(result, params) {
|
|||||||
files: result.files,
|
files: result.files,
|
||||||
code: result.exitStatus,
|
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.status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
||||||
}
|
}
|
||||||
ret.files = result.files || {};
|
ret.files = result.files || {};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "2.21.19",
|
"version": "2.22.1",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "dist/loader.js",
|
"main": "dist/loader.js",
|
||||||
"typings": "dist/loader.d.ts",
|
"typings": "dist/loader.d.ts",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
/* eslint-disable import/no-dynamic-require */
|
/* eslint-disable import/no-dynamic-require */
|
||||||
import { argv } from 'yargs';
|
import { argv } from 'yargs';
|
||||||
|
import { ObjectID } from 'mongodb';
|
||||||
import {
|
import {
|
||||||
lib, service, model,
|
lib, service, model, script,
|
||||||
builtinLib, builtinModel,
|
builtinLib, builtinModel, builtinScript,
|
||||||
} from './common';
|
} from './common';
|
||||||
import options from '../options';
|
import options from '../options';
|
||||||
|
import { validate } from '../lib/validator';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
|
|
||||||
@@ -20,8 +22,23 @@ function parseParameters(fn: Function) {
|
|||||||
return result === null ? [] : result;
|
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() {
|
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]) {
|
if (!global.Hydro.model[modelName]) {
|
||||||
return console.error(`Model ${modelName} doesn't exist.`);
|
return console.error(`Model ${modelName} doesn't exist.`);
|
||||||
}
|
}
|
||||||
@@ -45,6 +62,11 @@ async function cli() {
|
|||||||
console.error(`Too few arguments. Min ${parameterMin}`);
|
console.error(`Too few arguments. Min ${parameterMin}`);
|
||||||
return console.error(parameters.join(', '));
|
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);
|
let result = global.Hydro.model[modelName][func](...args);
|
||||||
if (result instanceof Promise) result = await result;
|
if (result instanceof Promise) result = await result;
|
||||||
return console.log(result);
|
return console.log(result);
|
||||||
@@ -59,22 +81,18 @@ export async function load() {
|
|||||||
require('../options');
|
require('../options');
|
||||||
const opts = options();
|
const opts = options();
|
||||||
await db.start(opts);
|
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');
|
const storage = require('../service/storage');
|
||||||
await storage.start(sopts);
|
await storage.start();
|
||||||
for (const i of builtinLib) require(`../lib/${i}`);
|
for (const i of builtinLib) require(`../lib/${i}`);
|
||||||
await lib(pending, fail);
|
await lib(pending, fail);
|
||||||
|
const systemModel = require('../model/system');
|
||||||
|
await systemModel.runConfig();
|
||||||
require('../service/gridfs');
|
require('../service/gridfs');
|
||||||
await service(pending, fail);
|
await service(pending, fail);
|
||||||
for (const i of builtinModel) require(`../model/${i}`);
|
for (const i of builtinModel) require(`../model/${i}`);
|
||||||
await model(pending, fail);
|
await model(pending, fail);
|
||||||
|
for (const i of builtinScript) require(`../script/${i}`);
|
||||||
|
await script(pending, fail, []);
|
||||||
await bus.parallel('app/started');
|
await bus.parallel('app/started');
|
||||||
await cli();
|
await cli();
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,7 @@ const logger = new Logger('common', true);
|
|||||||
export const builtinLib = [
|
export const builtinLib = [
|
||||||
'jwt', 'download', 'i18n', 'mail', 'useragent',
|
'jwt', 'download', 'i18n', 'mail', 'useragent',
|
||||||
'crypto', 'misc', 'paginate', 'hash.hydro', 'rank',
|
'crypto', 'misc', 'paginate', 'hash.hydro', 'rank',
|
||||||
'validator', 'ui', 'testdata.convert.ini', 'testdataConfig', 'difficulty',
|
'validator', 'ui', 'testdataConfig', 'difficulty', 'content',
|
||||||
'content',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const builtinModel = [
|
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.secretKey', process.env.MINIO_SECRET_KEY);
|
||||||
await modelSystem.set('file.endPoint', 'http://localhost:9000/');
|
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');
|
const storage = require('../service/storage');
|
||||||
storage.start(sopts);
|
await storage.start();
|
||||||
require('../service/monitor');
|
require('../service/monitor');
|
||||||
for (const i of builtinModel) require(`../model/${i}`);
|
for (const i of builtinModel) require(`../model/${i}`);
|
||||||
const scripts = require('../upgrade').default;
|
const scripts = require('../upgrade').default;
|
||||||
@@ -75,16 +68,10 @@ export async function load(call: Entry) {
|
|||||||
while (dbVer < expected) {
|
while (dbVer < expected) {
|
||||||
logger.info('Upgrading database: from %d to %d', dbVer, expected);
|
logger.info('Upgrading database: from %d to %d', dbVer, expected);
|
||||||
const func = scripts[dbVer];
|
const func = scripts[dbVer];
|
||||||
if (typeof func !== 'function') {
|
if (typeof func !== 'function' || (isFresh && func.toString().includes('_FRESH_INSTALL_IGNORE'))) {
|
||||||
dbVer++;
|
dbVer++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isFresh) {
|
|
||||||
if (func.toString().includes('_FRESH_INSTALL_IGNORE')) {
|
|
||||||
dbVer++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = await func();
|
const result = await func();
|
||||||
if (!result) break;
|
if (!result) break;
|
||||||
dbVer++;
|
dbVer++;
|
||||||
|
|||||||
@@ -34,15 +34,8 @@ export async function load() {
|
|||||||
const modelSystem = require('../model/system');
|
const modelSystem = require('../model/system');
|
||||||
await modelSystem.runConfig();
|
await modelSystem.runConfig();
|
||||||
if (argv.loaderDetail) logger.info('finish: config');
|
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');
|
const storage = require('../service/storage');
|
||||||
storage.start(sopts);
|
await storage.start();
|
||||||
if (argv.loaderDetail) logger.info('finish: storage.connect');
|
if (argv.loaderDetail) logger.info('finish: storage.connect');
|
||||||
for (const i of builtinLib) {
|
for (const i of builtinLib) {
|
||||||
let t;
|
let t;
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ class ContestDetailHandler extends ContestHandler {
|
|||||||
await contest.attend(domainId, tid, this.user._id);
|
await contest.attend(domainId, tid, this.user._id);
|
||||||
this.back();
|
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 {
|
class ContestBoardcastHandler extends ContestHandler {
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ class HomeworkDetailHandler extends HomeworkHandler {
|
|||||||
await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK);
|
await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK);
|
||||||
this.back();
|
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 {
|
class HomeworkDetailProblemHandler extends HomeworkHandler {
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import {
|
|||||||
SolutionNotFoundError, ProblemNotFoundError, BadRequestError,
|
SolutionNotFoundError, ProblemNotFoundError, BadRequestError,
|
||||||
} from '../error';
|
} from '../error';
|
||||||
import {
|
import {
|
||||||
Pdoc, User, Rdoc, PathComponent,
|
Pdoc, User, Rdoc, PathComponent, ProblemStatusDoc,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
import paginate from '../lib/paginate';
|
import paginate from '../lib/paginate';
|
||||||
import { isPid } from '../lib/validator';
|
import { isPid } from '../lib/validator';
|
||||||
import difficultyAlgorithm from '../lib/difficulty';
|
import difficultyAlgorithm from '../lib/difficulty';
|
||||||
|
import { parseConfig } from '../lib/testdataConfig';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import record from '../model/record';
|
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 {
|
export class ProblemDetailHandler extends ProblemHandler {
|
||||||
pdoc: Pdoc;
|
pdoc: PdocWithPsdoc;
|
||||||
udoc: User;
|
udoc: User;
|
||||||
|
|
||||||
@route('pid', Types.Name, true, null, parsePid)
|
@route('pid', Types.Name, true, null, parsePid)
|
||||||
@@ -189,6 +194,15 @@ export class ProblemDetailHandler extends ProblemHandler {
|
|||||||
// e.g.  will navigate to 
|
// e.g.  will navigate to 
|
||||||
this.response.body.pdoc.content = this.response.body.pdoc.content
|
this.response.body.pdoc.content = this.response.body.pdoc.content
|
||||||
.replace(/\(file:\/\//g, `(./${this.pdoc.docId}/file/`);
|
.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)
|
@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() {
|
export async function apply() {
|
||||||
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_category', '/p/category/:category', ProblemCategoryHandler, 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_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_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_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);
|
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler, PERM.PERM_SUBMIT_PROBLEM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,6 @@ declare module './model/problem' {
|
|||||||
data: FileInfo[],
|
data: FileInfo[],
|
||||||
additional_file: FileInfo[],
|
additional_file: FileInfo[],
|
||||||
hidden: boolean,
|
hidden: boolean,
|
||||||
acMsg?: string,
|
|
||||||
html?: boolean,
|
html?: boolean,
|
||||||
difficulty?: number,
|
difficulty?: number,
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
@@ -546,7 +545,6 @@ export interface Lib extends Record<string, any> {
|
|||||||
rank: typeof import('./lib/rank'),
|
rank: typeof import('./lib/rank'),
|
||||||
rating: typeof import('./lib/rating'),
|
rating: typeof import('./lib/rating'),
|
||||||
testdataConfig: typeof import('./lib/testdataConfig'),
|
testdataConfig: typeof import('./lib/testdataConfig'),
|
||||||
'testdata.convert.ini': typeof import('./lib/testdata.convert.ini'),
|
|
||||||
useragent: typeof import('./lib/useragent'),
|
useragent: typeof import('./lib/useragent'),
|
||||||
validator: typeof import('./lib/validator'),
|
validator: typeof import('./lib/validator'),
|
||||||
template?: any,
|
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 { load } from 'js-yaml';
|
||||||
import type { ProblemConfig } from '../interface';
|
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 {
|
interface ParseResult {
|
||||||
count: number,
|
count: number,
|
||||||
memoryMax: number,
|
memoryMax: number,
|
||||||
@@ -37,24 +21,26 @@ export async function parseConfig(config: string | ProblemConfig) {
|
|||||||
let cfg: ProblemConfig = {};
|
let cfg: ProblemConfig = {};
|
||||||
if (typeof config === 'string') {
|
if (typeof config === 'string') {
|
||||||
// TODO should validate here?
|
// TODO should validate here?
|
||||||
cfg = load(config) as ProblemConfig;
|
cfg = await readYamlCases(load(config) as Record<string, any>) as ProblemConfig;
|
||||||
} else cfg = config;
|
} else cfg = config;
|
||||||
if (cfg.cases) {
|
if (cfg.subtasks.length) {
|
||||||
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) {
|
|
||||||
for (const subtask of cfg.subtasks) {
|
for (const subtask of cfg.subtasks) {
|
||||||
result.memoryMax = Math.max(result.memoryMax, subtask.memory);
|
result.memoryMax = Math.max(result.memoryMax, subtask.memory);
|
||||||
result.memoryMin = Math.min(result.memoryMin, subtask.memory);
|
result.memoryMin = Math.min(result.memoryMin, subtask.memory);
|
||||||
result.timeMax = Math.max(result.timeMax, subtask.time);
|
result.timeMax = Math.max(result.timeMax, subtask.time);
|
||||||
result.timeMin = Math.min(result.timeMin, 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 type { Readable } from 'stream';
|
||||||
import { ObjectID, FilterQuery } from 'mongodb';
|
import { ObjectID, FilterQuery } from 'mongodb';
|
||||||
import { Dictionary, pick } from 'lodash';
|
import { Dictionary, escapeRegExp, pick } from 'lodash';
|
||||||
import { STATUS } from './builtin';
|
import { STATUS } from './builtin';
|
||||||
import * as document from './document';
|
import * as document from './document';
|
||||||
import domain from './domain';
|
import domain from './domain';
|
||||||
@@ -28,8 +28,7 @@ export class ProblemModel {
|
|||||||
|
|
||||||
static PROJECTION_PUBLIC: Field[] = [
|
static PROJECTION_PUBLIC: Field[] = [
|
||||||
...ProblemModel.PROJECTION_LIST,
|
...ProblemModel.PROJECTION_LIST,
|
||||||
'content', 'html', 'data', 'config', 'acMsg',
|
'content', 'html', 'data', 'config', 'additional_file',
|
||||||
'additional_file',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
static extend(getter: Getter) {
|
static extend(getter: Getter) {
|
||||||
@@ -209,6 +208,13 @@ export class ProblemModel {
|
|||||||
return Object.assign(r, l);
|
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[]) {
|
static async getListStatus(domainId: string, uid: number, pids: number[]) {
|
||||||
const psdocs = await ProblemModel.getMultiStatus(
|
const psdocs = await ProblemModel.getMultiStatus(
|
||||||
domainId, { uid, docId: { $in: Array.from(new Set(pids)) } },
|
domainId, { uid, docId: { $in: Array.from(new Set(pids)) } },
|
||||||
@@ -258,7 +264,6 @@ ProblemModel.extend((docId, pid) => ({
|
|||||||
additional_file: [],
|
additional_file: [],
|
||||||
hidden: true,
|
hidden: true,
|
||||||
config: '',
|
config: '',
|
||||||
acMsg: '',
|
|
||||||
difficulty: 0,
|
difficulty: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export async function runConfig() {
|
|||||||
}
|
}
|
||||||
const config = await coll.find().toArray();
|
const config = await coll.find().toArray();
|
||||||
for (const i of config) cache[i._id] = i.value;
|
for (const i of config) cache[i._id] = i.value;
|
||||||
bus.emit('database/config');
|
await bus.emit('database/config');
|
||||||
}
|
}
|
||||||
|
|
||||||
bus.on('system/setting', (args) => {
|
bus.on('system/setting', (args) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Collection } from 'mongodb';
|
import { Collection } from 'mongodb';
|
||||||
|
import { escapeRegExp } from 'lodash';
|
||||||
import * as system from './system';
|
import * as system from './system';
|
||||||
import token from './token';
|
import token from './token';
|
||||||
import * as setting from './setting';
|
import * as setting from './setting';
|
||||||
@@ -224,7 +225,7 @@ class UserModel {
|
|||||||
@ArgMethod
|
@ArgMethod
|
||||||
static async getPrefixList(domainId: string, prefix: string, limit: number = 50) {
|
static async getPrefixList(domainId: string, prefix: string, limit: number = 50) {
|
||||||
prefix = prefix.toLowerCase();
|
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 } })
|
const uids = await coll.find({ unameLower: { $regex } })
|
||||||
.limit(limit).map((doc) => doc._id).toArray();
|
.limit(limit).map((doc) => doc._id).toArray();
|
||||||
const users = [];
|
const users = [];
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export interface EventMap extends Record<string, any> {
|
|||||||
'message/run': (command: string) => VoidReturn
|
'message/run': (command: string) => VoidReturn
|
||||||
|
|
||||||
'database/connect': (db: Db) => void
|
'database/connect': (db: Db) => void
|
||||||
'database/config': () => void
|
'database/config': () => VoidReturn
|
||||||
|
|
||||||
'system/setting': (args: Record<string, any>) => VoidReturn
|
'system/setting': (args: Record<string, any>) => VoidReturn
|
||||||
|
|
||||||
|
|||||||
@@ -79,9 +79,15 @@ class StorageService {
|
|||||||
private opts: StorageOptions;
|
private opts: StorageOptions;
|
||||||
private replaceWithAlternativeUrlFor: Record<'user' | 'judge', (originalUrl: string) => string>;
|
private replaceWithAlternativeUrlFor: Record<'user' | 'judge', (originalUrl: string) => string>;
|
||||||
|
|
||||||
async start(opts: StorageOptions) {
|
async start() {
|
||||||
try {
|
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({
|
this.client = new Client({
|
||||||
...parseMainEndpointUrl(this.opts.endPoint),
|
...parseMainEndpointUrl(this.opts.endPoint),
|
||||||
accessKey: this.opts.accessKey,
|
accessKey: this.opts.accessKey,
|
||||||
@@ -103,14 +109,7 @@ class StorageService {
|
|||||||
logger.warn('Storage init fail. will retry later.');
|
logger.warn('Storage init fail. will retry later.');
|
||||||
this.error = e.toString();
|
this.error = e.toString();
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = system.getMany([
|
await this.start();
|
||||||
'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);
|
|
||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import { FilterQuery, ObjectID } from 'mongodb';
|
|||||||
import AdmZip from 'adm-zip';
|
import AdmZip from 'adm-zip';
|
||||||
import Queue from 'p-queue';
|
import Queue from 'p-queue';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
|
import { convertIniConfig } from '@hydrooj/utils/lib/cases';
|
||||||
import { Progress } from './ui';
|
import { Progress } from './ui';
|
||||||
|
import { Rdoc } from './interface';
|
||||||
import { Logger } from './logger';
|
import { Logger } from './logger';
|
||||||
import { streamToBuffer } from './utils';
|
import { streamToBuffer } from './utils';
|
||||||
import { iterateAllDomain, iterateAllProblem } from './pipelineUtils';
|
import { iterateAllDomain, iterateAllProblem } from './pipelineUtils';
|
||||||
import gridfs from './service/gridfs';
|
import gridfs from './service/gridfs';
|
||||||
import storage from './service/storage';
|
import storage from './service/storage';
|
||||||
import db from './service/db';
|
import db from './service/db';
|
||||||
|
import difficultyAlgorithm from './lib/difficulty';
|
||||||
import problem from './model/problem';
|
import problem from './model/problem';
|
||||||
import domain from './model/domain';
|
import domain from './model/domain';
|
||||||
import * as document from './model/document';
|
import * as document from './model/document';
|
||||||
import * as system from './model/system';
|
import * as system from './model/system';
|
||||||
import difficultyAlgorithm from './lib/difficulty';
|
|
||||||
import { STATUS } from './model/builtin';
|
import { STATUS } from './model/builtin';
|
||||||
import RecordModel from './model/record';
|
|
||||||
import { Rdoc } from './interface';
|
|
||||||
|
|
||||||
const logger = new Logger('upgrade');
|
const logger = new Logger('upgrade');
|
||||||
type UpgradeScript = void | (() => Promise<boolean | void>);
|
type UpgradeScript = void | (() => Promise<boolean | void>);
|
||||||
@@ -262,6 +262,33 @@ const scripts: UpgradeScript[] = [
|
|||||||
}
|
}
|
||||||
return true;
|
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;
|
export default scripts;
|
||||||
@@ -11,7 +11,7 @@ export async function connect() {
|
|||||||
while (dbVer < expected) {
|
while (dbVer < expected) {
|
||||||
const func = scripts[dbVer];
|
const func = scripts[dbVer];
|
||||||
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
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await func();
|
await func();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/import-qduoj",
|
"name": "@hydrooj/import-qduoj",
|
||||||
"version": "1.1.6",
|
"version": "1.1.7",
|
||||||
"description": "Import QDUOJ problem export",
|
"description": "Import QDUOJ problem export",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/migrate-vijos",
|
"name": "@hydrooj/migrate-vijos",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ const tasks = {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ac_msg: 'acMsg',
|
|
||||||
parent_doc_id: {
|
parent_doc_id: {
|
||||||
field: 'parentId',
|
field: 'parentId',
|
||||||
processer: (parentId, doc) => {
|
processer: (parentId, doc) => {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class Loader extends nunjucks.Loader {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
let fullpath = null;
|
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);
|
const p = path.resolve(process.cwd(), argv.template, name);
|
||||||
if (fs.existsSync(p)) fullpath = p;
|
if (fs.existsSync(p)) fullpath = p;
|
||||||
if (!fullpath) {
|
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,
|
clearDefaultValue: true,
|
||||||
position: 'bottom left',
|
position: 'bottom left',
|
||||||
classes: '',
|
classes: '',
|
||||||
|
multi: false,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
this.clear(this.options.clearDefaultValue);
|
this.clear(this.options.clearDefaultValue);
|
||||||
@@ -46,9 +47,7 @@ export default class AutoComplete extends DOMAttachedObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear(clearValue = true) {
|
clear(clearValue = true) {
|
||||||
if (clearValue) {
|
if (clearValue) this.$dom.val('');
|
||||||
this.$dom.val('');
|
|
||||||
}
|
|
||||||
this._value = null;
|
this._value = null;
|
||||||
this.lastText = null;
|
this.lastText = null;
|
||||||
}
|
}
|
||||||
@@ -80,19 +79,14 @@ export default class AutoComplete extends DOMAttachedObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onKeyUp(ev) {
|
onKeyUp(ev) {
|
||||||
if (ev.which === 27) {
|
if (ev.which === 27 /* ESC */) {
|
||||||
// ESC
|
|
||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.$dom.val() === this.lastText) {
|
if (this.$dom.val() === this.lastText) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.lastText = this.$dom.val();
|
this.lastText = this.$dom.val();
|
||||||
this.updateOpenState();
|
this.updateOpenState();
|
||||||
if (this.isOpen) {
|
if (this.isOpen) this.renderList();
|
||||||
this.renderList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMenuClick(ev) {
|
onMenuClick(ev) {
|
||||||
@@ -109,7 +103,8 @@ export default class AutoComplete extends DOMAttachedObject {
|
|||||||
this._value = item;
|
this._value = item;
|
||||||
this.$dom.trigger('vjAutoCompleteSelect', item);
|
this.$dom.trigger('vjAutoCompleteSelect', item);
|
||||||
if (text != null) {
|
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();
|
this.close();
|
||||||
}
|
}
|
||||||
@@ -141,7 +136,11 @@ export default class AutoComplete extends DOMAttachedObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async renderList() {
|
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 items = await this.getItems(val);
|
||||||
const html = this.getHtml(items);
|
const html = this.getHtml(items);
|
||||||
this.currentItems = 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",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.5.14",
|
"version": "4.5.16",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "hydro.js",
|
"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 disabled = args.disabled or false %}
|
||||||
{% set required = args.required or false %}
|
{% set required = args.required or false %}
|
||||||
{% set autofocus = args.autofocus or false %}
|
{% set autofocus = args.autofocus or false %}
|
||||||
|
{% set noAutocomplete = args.noAutocomplete or false %}
|
||||||
{% set extra_style = args.extra_style or false %}
|
{% set extra_style = args.extra_style or false %}
|
||||||
{% set date = args.date or false %}
|
{% set date = args.date or false %}
|
||||||
{% set time = args.time or false %}
|
{% set time = args.time or false %}
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
{% if disabled %}disabled{% endif %}
|
{% if disabled %}disabled{% endif %}
|
||||||
{% if required %}required{% endif %}
|
{% if required %}required{% endif %}
|
||||||
{% if autofocus %}autofocus data-autofocus{% endif %}
|
{% if autofocus %}autofocus data-autofocus{% endif %}
|
||||||
|
{% if noAutocomplete %}autocomplete="off"{% endif %}
|
||||||
{% if date %}data-pick-date{% endif %}
|
{% if date %}data-pick-date{% endif %}
|
||||||
{% if time %}data-pick-time{% endif %}
|
{% if time %}data-pick-time{% endif %}
|
||||||
{% if extra_style %}style="{{extra_style}}"{% endif %}
|
{% if extra_style %}style="{{extra_style}}"{% endif %}
|
||||||
|
|||||||
@@ -62,7 +62,9 @@
|
|||||||
columns:null,
|
columns:null,
|
||||||
label:'Problems',
|
label:'Problems',
|
||||||
name:'pids',
|
name:'pids',
|
||||||
value:pids
|
value:pids,
|
||||||
|
noAutocomplete:true,
|
||||||
|
placeholder:_("Seperated with ','")
|
||||||
}) }}
|
}) }}
|
||||||
{{ form.form_textarea({
|
{{ form.form_textarea({
|
||||||
columns:null,
|
columns:null,
|
||||||
@@ -77,6 +79,11 @@
|
|||||||
<button type="submit" class="rounded primary button">
|
<button type="submit" class="rounded primary button">
|
||||||
{{ _('Update') }}
|
{{ _('Update') }}
|
||||||
</button>
|
</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 %}
|
{% else %}
|
||||||
<button type="submit" class="rounded primary button">
|
<button type="submit" class="rounded primary button">
|
||||||
{{ _('Create') }}
|
{{ _('Create') }}
|
||||||
|
|||||||
@@ -85,6 +85,11 @@
|
|||||||
<button type="submit" class="rounded primary button">
|
<button type="submit" class="rounded primary button">
|
||||||
{{ _('Update') }}
|
{{ _('Update') }}
|
||||||
</button>
|
</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 %}
|
{% else %}
|
||||||
<button type="submit" class="rounded primary button">
|
<button type="submit" class="rounded primary button">
|
||||||
{{ _('Create') }}
|
{{ _('Create') }}
|
||||||
|
|||||||
@@ -114,6 +114,10 @@
|
|||||||
<dl class="large horizontal">
|
<dl class="large horizontal">
|
||||||
<dt>{{ _('ID') }}</dt>
|
<dt>{{ _('ID') }}</dt>
|
||||||
<dd>{{ pdoc.docId }}</dd>
|
<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 page_name != 'discussion_node' and page_name != 'discussion_detail' %}
|
||||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||||
<dt>{{ _('Submission') }}</dt>
|
<dt>{{ _('Submission') }}</dt>
|
||||||
@@ -126,10 +130,6 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{{ _('(None)') }}
|
{{ _('(None)') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if pdoc['acMsg'] and pdoc['psdoc']['status'] == STATUS.STATUS_ACCEPTED %}
|
|
||||||
<dt>{{ _('AC Message') }}</dt>
|
|
||||||
<dd>{{ pdoc['acMsg'] }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -24,8 +24,18 @@
|
|||||||
<div class="medium-9 columns">
|
<div class="medium-9 columns">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="problem-content-container"><div class="problem-content" data-marker-enabled>
|
<div class="problem-content-container"><div class="problem-content" data-marker-enabled>
|
||||||
<div class="section__header non-scratchpad--hide">
|
<div class="section__header">
|
||||||
<h1>{{ pdoc.title }}</h1>
|
{% 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>
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
{% if tdoc %}
|
{% 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_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i;
|
||||||
const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 };
|
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);
|
const match = TIME_RE.exec(str);
|
||||||
if (!match) throw new Error(`${str} error parsing time`);
|
if (!match) throw new Error(`${str} error parsing time`);
|
||||||
return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]);
|
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);
|
const match = MEMORY_RE.exec(str);
|
||||||
if (!match) throw new Error(`${str} error parsing memory`);
|
if (!match) throw new Error(`${str} error parsing memory`);
|
||||||
return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2]]);
|
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(func: Function, ...args: any[]): Promise<any>;
|
||||||
export async function retry(times: number, func: Function, ...args: any[]): Promise<any>;
|
export async function retry(times: number, func: Function, ...args: any[]): Promise<any>;
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/utils",
|
"name": "@hydrooj/utils",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "hydrooj utils",
|
"description": "hydrooj utils",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
|||||||
Reference in new issue
Block a user