move delarations to @hydrooj/common

This commit is contained in:
undefined committed 2025-02-09 15:17:10 +00:00
1 parent 100ccf718f
commit 4a33816a37
49 files changed
+496 -465

No files matched your search

+6 -4
View File
@@ -8,7 +8,7 @@ import Koa from 'koa';
import Body from 'koa-body';
import Compress from 'koa-compress';
import { Shorty } from 'shorty.js';
import { WebSocketServer } from 'ws';
import { WebSocket, WebSocketServer } from 'ws';
import {
Counter, errorMessage, isClass, Logger, parseMemoryMB,
} from '@hydrooj/utils/lib/utils';
@@ -71,6 +71,7 @@ export interface HydroResponse {
etag?: string;
attachment: (name: string, stream?: any) => void;
addHeader: (name: string, value: string) => void;
serializer: typeof serializer;
}
export type RendererFunction = (name: string, context: Record<string, any>) => string | Promise<string>;
type HydroContext = {
@@ -170,7 +171,7 @@ export class HandlerCommon {
renderHTML(templateName: string, args: Record<string, any>) {
const type = templateName.split('.')[1];
const engine = this.ctx.server.renderers[type] || (() => JSON.stringify(args, serializer(false, this)));
const engine = this.ctx.server.renderers[type] || (() => JSON.stringify(args, this.response.serializer(false, this)));
return engine(templateName, {
handler: this,
UserContext: this.user,
@@ -265,7 +266,7 @@ export class ConnectionHandler extends HandlerCommon {
}
send(data: any) {
let payload = JSON.stringify(data, serializer(false, this));
let payload = JSON.stringify(data, this.response.serializer(false, this));
if (this.compression) {
if (this.counter > 1000) this.resetCompression();
payload = this.compression.deflate(payload);
@@ -327,6 +328,7 @@ export interface WebServiceConfig {
host?: string;
xff?: string;
xhost?: string;
serializer?: typeof serializer;
}
export class WebService extends Service {
@@ -441,7 +443,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
func: (t) => this.handleHttp(t, NotFoundHandler, () => true),
},
]));
this.addLayer('base', base(logger, this.config.xff, this.config.xhost));
this.addLayer('base', base(logger, this.config.xff, this.config.xhost, this.config.serializer));
wsServer.on('connection', async (socket, request) => {
socket.on('error', (err) => {
logger.warn('Websocket Error: %s', err.message);
+216
View File
@@ -0,0 +1,216 @@
import { findFileSync, parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (s: string, errMsg: string) => s) {
const config: any = {
checker_type: cfg.checker_type || 'default',
judge_extra_files: [],
user_extra_files: [],
};
if (cfg.type === 'objective') {
if (cfg.checker || cfg.interactor || cfg.validator) {
throw new Error('You cannot use checker, interactor or validator for objective questions');
}
} else {
if (cfg.checker) {
if (!cfg.checker.includes('.')) {
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
}
config.checker ||= checkFile(cfg.checker, 'Cannot find checker {0}.');
}
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
if (cfg.manager) config.manager = checkFile(cfg.manager, 'Cannot find Manager {0}.');
if (cfg.validator) config.validator = checkFile(cfg.validator, 'Cannot find validator {0}.');
for (const n of ['judge', 'user']) {
const conf = cfg[`${n}_extra_files`];
if (!conf) continue;
if (conf instanceof Array) {
config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`));
} else throw new Error(`Invalid ${n}_extra_files config.`);
}
}
if (cfg.cases?.length) {
config.subtasks = [{
cases: cfg.cases,
type: 'sum',
}];
}
if (cfg.time) config.time = parseTimeMS(cfg.time);
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
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]?.trim()) throw new Error('Testdata count incorrect.');
const [input, output, time, score, memory] = f[i].split('|');
const cur = {
cases: [{ input, output }],
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;
}
interface MatchRule {
regex: RegExp;
output: (a: RegExpExecArray) => string[];
id: (a: RegExpExecArray) => number;
subtask: (a: RegExpExecArray) => number;
preferredScorerType: (a: RegExpExecArray) => 'min' | 'max' | 'sum';
}
const SubtaskMatcher: MatchRule[] = [
{
regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|IN|txt|TXT|in\.txt|IN\.TXT)$/,
output: (a) => ['out', 'ans']
.flatMap((i) => [i, i.toUpperCase(), `${i}.txt`, `${i.toUpperCase()}.TXT`])
.flatMap((i) => [`${a[1]}.${i}`, `${a[1]}.${i}`.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')])
.concat(a[1].includes('input') ? `${a[1]}.txt`.replace(/input/g, 'output') : null),
id: (a) => +a[4],
subtask: (a) => +(a[3] || 1),
preferredScorerType: (a) => (a[3] ? 'min' : 'sum'),
},
{
regex: /^([^\d]*)\.(in|IN)(\d+)$/,
output: (a) => [
`${a[1]}.${a[2] === 'in' ? 'ou' : 'OU'}${a[3]}`,
`${a[1]}.${a[2] === 'in' ? 'out' : 'OUT'}${a[3]}`,
].flatMap((i) => [i, i.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]),
id: (a) => +a[2],
subtask: () => 1,
preferredScorerType: () => 'sum',
},
{
regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.(in|IN)$/,
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}${a[2]}${a[3]}${a[4]}.${i}`),
id: (a) => +a[4],
subtask: (a) => +a[2],
preferredScorerType: () => 'min',
},
{
regex: /^(([0-9]+)[-_](?:.*))\.(in|IN)$/,
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}.${i}`),
id: (a) => +a[2],
subtask: () => 1,
preferredScorerType: () => 'sum',
},
];
function* getScore(totalScore: number, count: number) {
const base = Math.floor(totalScore / count);
const extra = count - (totalScore % count);
for (let i = 0; i < count; i++) {
if (i >= extra) yield base + 1;
else yield base;
}
}
interface ParsedCase {
id?: number;
time?: number | string;
memory?: number | string;
score?: number;
input?: string;
output?: string;
}
interface ParsedSubtask {
cases: ParsedCase[];
type: 'min' | 'max' | 'sum';
time?: number | string;
memory?: number | string;
score?: number;
id?: number;
if?: number[];
}
export function readSubtasksFromFiles(files: string[], config) {
const subtask: Record<number, ParsedSubtask> = {};
for (const s of config.subtasks || []) if (s.id) subtask[s.id] = s;
for (const file of files) {
let match = false;
for (const rule of SubtaskMatcher) {
const data = rule.regex.exec(file);
if (!data) continue;
const sid = rule.subtask(data);
const c = { input: file, output: '', id: rule.id(data) };
const type = rule.preferredScorerType(data);
const outputs = (config.noOutputFile ? ['/dev/null'] : rule.output(data)).filter((i) => i);
for (const output of outputs) {
if (output === file) continue;
if (output === '/dev/null' || files.includes(output)) {
match = true;
c.output = output;
if (!subtask[sid]) {
subtask[sid] = {
time: config.time,
memory: config.memory,
type,
cases: [c],
id: sid,
};
} else if (!subtask[sid].cases) subtask[sid].cases = [c];
else subtask[sid].cases.push(c);
break;
}
}
if (match) break;
}
}
return Object.values(subtask);
}
export interface NormalizedCase extends Required<ParsedCase> {
time: number;
memory: number;
}
export interface NormalizedSubtask extends Required<ParsedSubtask> {
cases: NormalizedCase[];
time: number;
memory: number;
}
export function normalizeSubtasks(
subtasks: ParsedSubtask[], checkFile: (name: string, errMsg: string) => string,
time: number | string = '1000ms', memory: number | string = '256m', ignoreParseError = false,
timeRate = 1, memoryRate = 1,
): NormalizedSubtask[] {
subtasks.sort((a, b) => (a.id - b.id));
const subtaskScore = getScore(
Math.max(100 - Math.sum(subtasks.map((i) => i.score || 0)), 0),
subtasks.filter((i) => !i.score).length,
);
return subtasks.map((s, id) => {
s.cases.sort((a, b) => (a.id - b.id));
const score = s.score || subtaskScore.next().value as number;
const caseScore = getScore(
Math.max(score - Math.sum(s.cases.map((i) => i.score || 0)), 0),
s.cases.filter((i) => !i.score).length,
);
return {
id: id + 1,
type: 'min',
if: [],
...s,
score,
time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate,
memory: parseMemoryMB(s.memory || memory, !ignoreParseError) * memoryRate,
cases: s.cases.map((c, index) => ({
id: index + 1,
...c,
score: c.score || (s.type === 'sum' ? caseScore.next().value as number : score),
time: parseTimeMS(c.time || s.time || time, !ignoreParseError) * timeRate,
memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError) * memoryRate,
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',
})) as NormalizedCase[],
};
});
}
+4
View File
@@ -0,0 +1,4 @@
export * from './lang';
export * from './status';
export * from './types';
export * from './cases';
File renamed without changes.
+8
View File
@@ -0,0 +1,8 @@
{
"name": "@hydrooj/common",
"main": "./index.ts",
"version": "0.0.1",
"dependencies": {
"@hydrooj/utils": "workspace:^"
}
}
@@ -79,6 +79,16 @@ export const STATUS_CODES: Record<STATUS, string> = {
[STATUS.STATUS_HACK_UNSUCCESSFUL]: 'fail',
};
export const NORMAL_STATUS: STATUS[] = [
STATUS.STATUS_ACCEPTED,
STATUS.STATUS_WRONG_ANSWER,
STATUS.STATUS_TIME_LIMIT_EXCEEDED,
STATUS.STATUS_MEMORY_LIMIT_EXCEEDED,
STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED,
STATUS.STATUS_RUNTIME_ERROR,
STATUS.STATUS_COMPILE_ERROR,
];
export function getScoreColor(score: number | string): string {
if (score === null || score === undefined || !Number.isFinite(+score)) return '#000000';
return [
@@ -1,6 +1,6 @@
import { expect } from 'chai';
import { describe, it } from 'node:test';
import { readSubtasksFromFiles } from '../lib/common';
import { readSubtasksFromFiles } from '../cases';
interface Info {
subtask: number;
+166
View File
@@ -0,0 +1,166 @@
export type CompilableSource = string | {
file: string;
lang: string;
};
export enum ProblemType {
Default = 'default',
SubmitAnswer = 'submit_answer',
Interactive = 'interactive',
Communication = 'communication',
Objective = 'objective',
Remote = 'remote_judge',
}
export interface TestCaseConfig {
input: string;
output: string;
time?: string;
memory?: string;
score?: number;
}
export enum SubtaskType {
min = 'min',
max = 'max',
sum = 'sum',
}
export interface SubtaskConfig {
time?: string;
memory?: string;
score?: number;
if?: number[];
id?: number;
type?: SubtaskType;
cases?: TestCaseConfig[];
}
export interface ProblemConfigFile {
type?: ProblemType;
subType?: string;
target?: string;
score?: number;
time?: string;
memory?: string;
filename?: string;
checker_type?: string;
num_processes?: number;
user_extra_files?: string[];
judge_extra_files?: string[];
detail?: boolean;
answers?: Record<string, [string | string[], number]>;
redirect?: string;
cases?: TestCaseConfig[];
subtasks?: SubtaskConfig[];
langs?: string[];
checker?: CompilableSource;
interactor?: CompilableSource;
manager?: CompilableSource;
validator?: CompilableSource;
time_limit_rate?: Record<string, number>;
memory_limit_rate?: Record<string, number>;
}
export interface FileInfo {
/** storage path */
_id: string,
/** filename */
name: string,
/** file size (in bytes) */
size: number,
etag: string,
lastModified: Date,
}
export interface JudgeMeta {
problemOwner: number;
hackRejudge?: string;
rejudge?: boolean;
// FIXME stricter types
type?: string;
}
export interface RecordPayload {
domainId: string;
pid: number;
uid: number;
lang: string;
code: string;
score: number;
memory: number;
time: number;
judgeTexts: (string | JudgeMessage)[];
compilerTexts: string[];
testCases: Required<TestCase>[];
rejudged: boolean;
source?: string;
/** judge uid */
judger: number;
judgeAt: Date;
status: number;
progress?: number;
/** pretest */
input?: string;
/** hack target rid */
hackTarget?: string;
/** 0 if pretest&script */
contest?: string;
files?: Record<string, string>
subtasks?: Record<number, SubtaskResult>;
}
export interface JudgeRequest extends Omit<RecordPayload, 'testCases'> {
priority: number;
type: 'judge' | 'generate';
rid: string;
config: ProblemConfigFile;
meta: JudgeMeta;
data: FileInfo[];
source: string;
trusted: boolean;
}
export interface TestCase {
id?: number;
subtaskId?: number;
score?: number;
time: number;
memory: number;
status: number;
message: string;
}
export interface JudgeMessage {
message: string;
params?: string[];
stack?: string;
}
export interface SubtaskResult {
type: SubtaskType;
score: number;
status: number;
}
export interface JudgeResultBody {
key: string;
domainId: string;
rid: string;
judger?: number;
progress?: number;
addProgress?: number;
case?: TestCase;
cases?: TestCase[];
status?: number;
score?: number;
/** in miliseconds */
time?: number;
/** in kilobytes */
memory?: number;
message?: string | JudgeMessage;
compilerText?: string;
nop?: boolean;
subtasks?: Record<number, SubtaskResult>;
}
+5 -4
View File
@@ -1,11 +1,12 @@
/* eslint-disable no-await-in-loop */
import path from 'path';
import {
changeErrorType, fs, normalizeSubtasks, readSubtasksFromFiles, yaml,
convertIniConfig, LangConfig,
normalizeSubtasks, ProblemConfigFile, readSubtasksFromFiles, readYamlCases,
} from '@hydrooj/common';
import {
changeErrorType, fs, yaml,
} from '@hydrooj/utils';
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { ProblemConfigFile } from 'hydrooj';
import { getConfig } from './config';
import { FormatError, SystemError } from './error';
import { NextFunction, ParsedConfig } from './interface';
+1 -1
View File
@@ -1,5 +1,5 @@
/* eslint-disable no-template-curly-in-string */
import { STATUS } from '@hydrooj/utils/lib/status';
import { STATUS } from '@hydrooj/common';
import { FormatError, SystemError } from './error';
import { CopyInFile, runQueued } from './sandbox';
import { parse } from './testlib';
+1 -2
View File
@@ -1,5 +1,4 @@
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { STATUS } from '@hydrooj/utils/lib/status';
import { LangConfig, STATUS } from '@hydrooj/common';
import { CompileError } from './error';
import { Execute } from './interface';
import {
+1 -1
View File
@@ -1,4 +1,4 @@
import { STATUS_TEXTS } from '@hydrooj/utils/lib/status';
import { STATUS_TEXTS } from '@hydrooj/common';
interface CompileErrorInfo {
stdout?: string,
+3 -3
View File
@@ -1,10 +1,10 @@
import Queue from 'p-queue';
import { STATUS } from '@hydrooj/utils/lib/status';
import type { JudgeResultBody } from 'hydrooj';
import {
JudgeResultBody, NormalizedCase, NormalizedSubtask, STATUS,
} from '@hydrooj/common';
import { getConfig } from './config';
import { FormatError } from './error';
import { Context, ContextSubTask } from './judge/interface';
import { NormalizedCase, NormalizedSubtask } from './utils';
interface Task {
compile: () => Promise<void>;
+2 -4
View File
@@ -36,18 +36,16 @@ const session: Session = {
t._callbackAwait ||= Promise.resolve();
return (data: Partial<JudgeResultBody>) => {
logger.debug('Next: %o', data);
data.rid = new ObjectId(t.rid);
if (data.case) data.case.message ||= '';
t._callbackAwait = t._callbackAwait.then(() => JudgeHandler.next({ ...data, domainId: t.request.domainId }));
t._callbackAwait = t._callbackAwait.then(() => JudgeHandler.next({ ...data, rid: t.rid, domainId: t.request.domainId }));
};
},
getEnd(t: Context) {
t._callbackAwait ||= Promise.resolve();
return (data: Partial<JudgeResultBody>) => {
data.key = 'end';
data.rid = new ObjectId(t.rid);
logger.info('End: status=%d score=%d time=%dms memory=%dkb', data.status, data.score, data.time, data.memory);
t._callbackAwait = t._callbackAwait.then(() => JudgeHandler.end({ ...data, domainId: t.request.domainId }));
t._callbackAwait = t._callbackAwait.then(() => JudgeHandler.end({ ...data, rid: t.rid, domainId: t.request.domainId }));
};
},
getLang(lang: string, doThrow = true) {
+1 -3
View File
@@ -4,10 +4,8 @@ import AdmZip from 'adm-zip';
import PQueue from 'p-queue';
import superagent from 'superagent';
import WebSocket from 'ws';
import { JudgeResultBody, parseLang, STATUS } from '@hydrooj/common';
import { fs, noop } from '@hydrooj/utils';
import { parseLang } from '@hydrooj/utils/lib/lang';
import { STATUS } from '@hydrooj/utils/lib/status';
import type { JudgeResultBody } from 'hydrooj';
import { getConfig } from '../config';
import { FormatError, SystemError } from '../error';
import { Session } from '../interface';
+1 -1
View File
@@ -1,4 +1,4 @@
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { LangConfig } from '@hydrooj/common';
import { runQueued } from './sandbox';
export async function compilerVersions(langs: Record<string, LangConfig>) {
+4 -8
View File
@@ -1,7 +1,6 @@
import type { LangConfig } from '@hydrooj/utils/lib/lang';
import { NormalizedSubtask } from '@hydrooj/utils/lib/utils';
import type { JudgeRequest as OrigJudgeRequest, ObjectId } from 'hydrooj';
import { JudgeResultBody, ProblemConfigFile } from 'hydrooj';
import {
JudgeResultBody, type LangConfig, NormalizedSubtask, ProblemConfigFile,
} from '@hydrooj/common';
import { CopyInFile } from './sandbox';
import type { JudgeTask } from './task';
@@ -20,10 +19,7 @@ export interface ParsedConfig extends Omit<ProblemConfigFile, 'time' | 'memory'
subtasks: NormalizedSubtask[];
}
// replace ObjectId to string
export type JudgeRequest = {
[K in keyof OrigJudgeRequest]: OrigJudgeRequest[K] extends ObjectId ? string : OrigJudgeRequest[K];
};
export { JudgeRequest } from '@hydrooj/common';
export interface Session {
getLang: (name: string) => LangConfig;
@@ -1,8 +1,7 @@
import { STATUS } from '@hydrooj/utils/lib/status';
import { NormalizedCase, STATUS } from '@hydrooj/common';
import { runFlow } from '../flow';
import { Parameter, runPiped } from '../sandbox';
import signals from '../signals';
import { NormalizedCase } from '../utils';
import { Context, ContextSubTask } from './interface';
function judgeCase(c: NormalizedCase) {
+1 -2
View File
@@ -1,9 +1,8 @@
import { STATUS } from '@hydrooj/utils/lib/status';
import { NormalizedCase, STATUS } from '@hydrooj/common';
import checkers from '../checkers';
import { runFlow } from '../flow';
import { del, runQueued } from '../sandbox';
import signals from '../signals';
import { NormalizedCase } from '../utils';
import { Context, ContextSubTask } from './interface';
function judgeCase(c: NormalizedCase) {
+1 -1
View File
@@ -2,7 +2,7 @@
import { tmpdir } from 'os';
import path from 'path';
import fs from 'fs-extra';
import { STATUS } from '@hydrooj/utils/lib/status';
import { STATUS } from '@hydrooj/common';
import { SystemError } from '../error';
import { CopyInFile, runQueued } from '../sandbox';
import client from '../sandbox/client';
+3 -4
View File
@@ -1,10 +1,9 @@
/* eslint-disable no-sequences */
import { fs } from '@hydrooj/utils';
import { STATUS } from '@hydrooj/utils/lib/status';
import { STATUS } from '@hydrooj/common';
import { fs, parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
import checkers from '../checkers';
import { del, runQueued } from '../sandbox';
import signals from '../signals';
import { parseMemoryMB, parseTimeMS } from '../utils';
import { Context } from './interface';
export async function judge(ctx: Context) {
@@ -13,7 +12,7 @@ export async function judge(ctx: Context) {
ctx.compile(ctx.lang, ctx.code),
ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type),
ctx.compileLocalFile('validator', ctx.config.validator),
ctx.session.fetchFile(ctx.files.hack),
ctx.session.fetchFile(null, { [ctx.files.hack]: '' }),
]);
ctx.clean.push(() => fs.unlink(input));
ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 });
+1 -2
View File
@@ -1,9 +1,8 @@
import { STATUS } from '@hydrooj/utils/lib/status';
import { NormalizedCase, STATUS } from '@hydrooj/common';
import { runFlow } from '../flow';
import { runPiped } from '../sandbox';
import signals from '../signals';
import { parse } from '../testlib';
import { NormalizedCase } from '../utils';
import { Context, ContextSubTask } from './interface';
function judgeCase(c: NormalizedCase) {
+1 -1
View File
@@ -1,5 +1,5 @@
import PQueue from 'p-queue';
import type { NormalizedSubtask } from '@hydrooj/utils/lib/common';
import { NormalizedSubtask } from '@hydrooj/common';
import { Execute } from '../interface';
import { JudgeTask } from '../task';
+1 -1
View File
@@ -1,6 +1,6 @@
import assert from 'assert';
import { STATUS } from '@hydrooj/common';
import { fs, yaml } from '@hydrooj/utils';
import { STATUS } from '@hydrooj/utils/lib/status';
import { FormatError } from '../error';
import { Context } from './interface';
+2 -2
View File
@@ -1,8 +1,8 @@
import { STATUS } from '@hydrooj/utils/lib/status';
import { STATUS } from '@hydrooj/common';
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
import { runQueued } from '../sandbox';
import signals from '../signals';
import { JudgeTask } from '../task';
import { parseMemoryMB, parseTimeMS } from '../utils';
export const judge = async (ctx: JudgeTask) => {
ctx.stat.judge = new Date();
@@ -1,9 +1,8 @@
import { NormalizedCase, STATUS } from '@hydrooj/common';
import { fs } from '@hydrooj/utils';
import { STATUS } from '@hydrooj/utils/lib/status';
import checkers from '../checkers';
import { runFlow } from '../flow';
import { del, runQueued } from '../sandbox';
import { NormalizedCase } from '../utils';
import { Context } from './interface';
function judgeCase(c: NormalizedCase) {
+1 -1
View File
@@ -2,7 +2,7 @@ import cac from 'cac';
import PQueue from 'p-queue';
import { gte } from 'semver';
import { ParseEntry } from 'shell-quote';
import { STATUS } from '@hydrooj/utils/lib/status';
import { STATUS } from '@hydrooj/common';
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
import { getConfig } from './config';
import { FormatError, SystemError } from './error';
+3 -4
View File
@@ -1,9 +1,8 @@
import { basename, join } from 'path';
import {
CompilableSource, FileInfo, JudgeMeta, JudgeResultBody, STATUS, TestCase,
} from '@hydrooj/common';
import { findFileSync, fs } from '@hydrooj/utils';
import { STATUS } from '@hydrooj/utils/lib/status';
import type {
CompilableSource, FileInfo, JudgeMeta, JudgeResultBody, TestCase,
} from 'hydrooj';
import readCases from './cases';
import checkers from './checkers';
import compile from './compile';
+1 -1
View File
@@ -1,4 +1,4 @@
import { STATUS } from '@hydrooj/utils/lib/status';
import { STATUS } from '@hydrooj/common';
const operation = /^\s*(status|score)\((\d+)\)\s*(.*)$/m;
+4 -7
View File
@@ -5,13 +5,12 @@ import { omit } from 'lodash';
import { ObjectId } from 'mongodb';
import PQueue from 'p-queue';
import sanitize from 'sanitize-filename';
import { JudgeResultBody, ProblemConfigFile, TestCase } from '@hydrooj/common';
import { Context } from '../context';
import {
FileLimitExceededError, ForbiddenError, ProblemIsReferencedError, ValidationError,
} from '../error';
import {
JudgeResultBody, ProblemConfigFile, RecordDoc, Task, TestCase,
} from '../interface';
import { RecordDoc, Task } from '../interface';
import { Logger } from '../logger';
import * as builtin from '../model/builtin';
import { PERM, STATUS } from '../model/builtin';
@@ -73,11 +72,10 @@ function processPayload(body: Partial<JudgeResultBody>) {
}
export async function next(body: Partial<JudgeResultBody>) {
body.rid = new ObjectId(body.rid); // TODO remove this
const {
$set, $push, $unset, $inc,
} = processPayload(body);
const rdoc = await record.update(body.domainId, body.rid, $set, $push, $unset, $inc);
const rdoc = await record.update(body.domainId, new ObjectId(body.rid), $set, $push, $unset, $inc);
if (rdoc) bus.broadcast('record/change', rdoc, $set, $push, body);
return rdoc;
}
@@ -139,12 +137,11 @@ bus.on('record/judge', async (rdoc, updated, pdoc) => {
});
export async function end(body: Partial<JudgeResultBody>) {
body.rid = new ObjectId(body.rid); // TODO remove this
const { $set, $push } = processPayload(body);
const $unset: any = { progress: '' };
$set.judgeAt = new Date();
$set.judger = body.judger ?? 1;
const rdoc = await record.update(body.domainId, body.rid, $set, $push, $unset);
const rdoc = await record.update(body.domainId, new ObjectId(body.rid), $set, $push, $unset);
if (rdoc) bus.broadcast('record/change', rdoc, null, null, body); // trigger a full update
await postJudge(rdoc);
return rdoc;
+2 -2
View File
@@ -115,7 +115,7 @@ class SystemScriptHandler extends SystemHandler {
const time = new Date().getTime() - start;
judge.end({
domainId,
rid,
rid: rid.toHexString(),
status: STATUS.STATUS_ACCEPTED,
message: inspect(ret, false, 10, true),
judger: 1,
@@ -128,7 +128,7 @@ class SystemScriptHandler extends SystemHandler {
logger.error(err);
judge.end({
domainId,
rid,
rid: rid.toHexString(),
status: STATUS.STATUS_SYSTEM_ERROR,
message: `${err.message} \n${(err as any).params || []} \n${err.stack} `,
judger: 1,
+17 -160
View File
@@ -4,11 +4,16 @@ import { CredentialDeviceType } from '@simplewebauthn/types';
import type fs from 'fs';
import type { Dictionary, NumericDictionary } from 'lodash';
import type { Binary, FindCursor, ObjectId } from 'mongodb';
import type {
FileInfo, RecordPayload,
} from '@hydrooj/common/types';
import type { Context } from './context';
import type { DocStatusType } from './model/document';
import type { ProblemDoc } from './model/problem';
import type { Handler } from './service/server';
export * from '@hydrooj/common/types';
type document = typeof import('./model/document');
export interface System {
@@ -142,76 +147,6 @@ export interface BaseUser {
}
export type BaseUserDict = Record<number, BaseUser>;
export interface FileInfo {
/** storage path */
_id: string,
/** filename */
name: string,
/** file size (in bytes) */
size: number,
etag: string,
lastModified: Date,
}
export interface TestCaseConfig {
input: string;
output: string;
time?: string;
memory?: string;
score?: number;
}
export enum ProblemType {
Default = 'default',
SubmitAnswer = 'submit_answer',
Interactive = 'interactive',
Communication = 'communication',
Objective = 'objective',
Remote = 'remote_judge',
}
export enum SubtaskType {
min = 'min',
max = 'max',
sum = 'sum',
}
export interface SubtaskConfig {
time?: string;
memory?: string;
score?: number;
if?: number[];
id?: number;
type?: SubtaskType;
cases?: TestCaseConfig[];
}
export interface ProblemConfigFile {
type?: ProblemType;
subType?: string;
target?: string;
score?: number;
time?: string;
memory?: string;
filename?: string;
checker_type?: string;
checker?: string;
interactor?: string;
manager?: string;
num_processes?: number;
user_extra_files?: string[];
judge_extra_files?: string[];
detail?: boolean;
answers?: Record<string, [string | string[], number]>;
redirect?: string;
cases?: TestCaseConfig[];
subtasks?: SubtaskConfig[];
langs?: string[];
validator?: string;
time_limit_rate?: Record<string, number>;
memory_limit_rate?: Record<string, number>;
}
export interface ProblemConfig {
redirect?: [string, string];
count: number;
@@ -284,46 +219,11 @@ export interface ProblemStatusDoc extends StatusDocBase {
star?: boolean;
}
export interface TestCase {
id?: number;
subtaskId?: number;
score?: number;
time: number;
memory: number;
status: number;
message: string;
}
export interface RecordDoc {
export type RecordDoc = {
[K in keyof RecordPayload]: K extends 'hackTarget' | 'contest' ? ObjectId : RecordPayload[K];
} & {
_id: ObjectId;
domainId: string;
pid: number;
uid: number;
lang: string;
code: string;
score: number;
memory: number;
time: number;
judgeTexts: (string | JudgeMessage)[];
compilerTexts: string[];
testCases: Required<TestCase>[];
rejudged: boolean;
source?: string;
/** judge uid */
judger: number;
judgeAt: Date;
status: number;
progress?: number;
/** pretest */
input?: string;
/** hack target rid */
hackTarget?: ObjectId;
/** 0 if pretest&script */
contest?: ObjectId;
files?: Record<string, string>
subtasks?: Record<number, SubtaskResult>;
}
};
export interface RecordStatDoc {
_id: ObjectId;
@@ -335,24 +235,6 @@ export interface RecordStatDoc {
length: number;
lang: string;
}
export interface JudgeMeta {
problemOwner: number;
hackRejudge?: string;
rejudge?: boolean;
// FIXME stricter types
type?: string;
}
export interface JudgeRequest extends Omit<RecordDoc, '_id' | 'testCases'> {
priority: number;
type: 'judge' | 'generate';
rid: ObjectId;
config: ProblemConfigFile;
meta: JudgeMeta;
data: FileInfo[];
source: string;
trusted: boolean;
}
export interface ScoreboardNode {
type: 'string' | 'rank' | 'user' | 'email' | 'record' | 'records' | 'problem' | 'solved' | 'time' | 'total_score';
@@ -569,39 +451,6 @@ export interface Script {
validate: any,
}
export interface JudgeMessage {
message: string;
params?: string[];
stack?: string;
}
export interface SubtaskResult {
type: SubtaskType;
score: number;
status: number;
}
export interface JudgeResultBody {
key: string;
domainId: string;
rid: ObjectId;
judger?: number;
progress?: number;
addProgress?: number;
case?: TestCase;
cases?: TestCase[];
status?: number;
score?: number;
/** in miliseconds */
time?: number;
/** in kilobytes */
memory?: number;
message?: string | JudgeMessage;
compilerText?: string;
nop?: boolean;
subtasks?: Record<number, SubtaskResult>;
}
export interface Task {
_id: ObjectId;
type: string;
@@ -683,6 +532,13 @@ export interface ContestBalloonDoc {
sentAt?: Date;
}
export interface LockDoc {
_id: ObjectId;
key: string;
lockAt: Date;
daemonId: string;
}
declare module './service/db' {
interface Collections {
'blacklist': BlacklistDoc;
@@ -712,6 +568,7 @@ declare module './service/db' {
'opcount': OpCountDoc;
'schedule': Schedule;
'contest.balloon': ContestBalloonDoc;
'lock': LockDoc;
}
}
+5 -3
View File
@@ -1,7 +1,9 @@
import { load } from 'js-yaml';
import readYamlCases from '@hydrooj/utils/lib/cases';
import type { ProblemConfig, ProblemConfigFile } from '../interface';
import { normalizeSubtasks, parseMemoryMB, parseTimeMS } from '../utils';
import {
normalizeSubtasks, ProblemConfigFile, readYamlCases,
} from '@hydrooj/common';
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
import type { ProblemConfig } from '../interface';
export async function parseConfig(config: string | ProblemConfigFile = {}) {
const cfg: ProblemConfigFile = typeof config === 'string'
+2 -2
View File
@@ -2,9 +2,9 @@ import {
getScoreColor, STATUS, STATUS_CODES, STATUS_SHORT_TEXTS,
STATUS_TEXTS, USER_GENDER_FEMALE, USER_GENDER_ICONS, USER_GENDER_MALE,
USER_GENDER_OTHER, USER_GENDER_RANGE, USER_GENDERS,
} from '@hydrooj/utils/lib/status';
} from '@hydrooj/common';
export * from '@hydrooj/utils/lib/status';
export * from '@hydrooj/common/status';
export const PERM = {
PERM_NONE: 0n,
+2 -1
View File
@@ -8,11 +8,12 @@ import yaml from 'js-yaml';
import { pick } from 'lodash';
import { Filter, ObjectId } from 'mongodb';
import type { Readable } from 'stream';
import { ProblemConfigFile } from '@hydrooj/common';
import { Logger, size, streamToBuffer } from '@hydrooj/utils/lib/utils';
import { Context } from '../context';
import { FileUploadError, ProblemNotFoundError, ValidationError } from '../error';
import type {
Document, ProblemConfigFile, ProblemDict, ProblemStatusDoc, User,
Document, ProblemDict, ProblemStatusDoc, User,
} from '../interface';
import { parseConfig } from '../lib/testdataConfig';
import * as bus from '../service/bus';
+2 -3
View File
@@ -5,11 +5,10 @@ import {
Filter, FindOptions, MatchKeysAndValues,
ObjectId, OnlyFieldsOfType, PushOperator, UpdateFilter,
} from 'mongodb';
import { ProblemConfigFile } from '@hydrooj/common';
import { Context } from '../context';
import { ProblemNotFoundError } from '../error';
import {
JudgeMeta, ProblemConfigFile, RecordDoc,
} from '../interface';
import { JudgeMeta, RecordDoc } from '../interface';
import db from '../service/db';
import { MaybeArray, NumberKeys } from '../typeutils';
import { ArgMethod, buildProjection, Time } from '../utils';
+2 -2
View File
@@ -4,8 +4,8 @@ import * as cordis from 'cordis';
import yaml from 'js-yaml';
import { Dictionary } from 'lodash';
import moment from 'moment-timezone';
import { LangConfig, parseLang } from '@hydrooj/utils/lib/lang';
import { retry } from '@hydrooj/utils/lib/utils';
import { LangConfig, parseLang } from '@hydrooj/common';
import { retry } from '@hydrooj/utils';
import { Context, Service } from '../context';
import { Setting as _Setting } from '../interface';
import { Logger } from '../logger';
+1 -1
View File
@@ -1,4 +1,4 @@
import * as status from '@hydrooj/utils/lib/status';
import * as status from '@hydrooj/common/status';
import { findFileSync } from '@hydrooj/utils/lib/utils';
import {
avatar, Context, fs, PERM, PRIV, STATUS, yaml,
@@ -1,7 +1,7 @@
import {
Button, Classes, Icon, Popover, TreeNode,
} from '@blueprintjs/core';
import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/common';
import { TestCaseConfig } from 'hydrooj';
import { isEqual } from 'lodash';
import React from 'react';
@@ -1,7 +1,7 @@
import {
Button, Classes, Dialog, DialogBody, DialogFooter, Icon,
} from '@blueprintjs/core';
import { readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
import { readSubtasksFromFiles } from '@hydrooj/common';
import React, { useEffect, useRef } from 'react';
import { useSelector, useStore } from 'react-redux';
import { i18n } from 'vj/utils';
@@ -1,4 +1,4 @@
import { STATUS_TEXTS } from '@hydrooj/utils/lib/status';
import { STATUS_TEXTS } from '@hydrooj/common';
export default function reducer(state = {
input: '',
@@ -1,5 +1,4 @@
import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
import type { SubtaskType } from 'hydrooj/src/interface';
import { normalizeSubtasks, readSubtasksFromFiles, SubtaskType } from '@hydrooj/common';
import $ from 'jquery';
import yaml from 'js-yaml';
import React from 'react';
@@ -1,4 +1,4 @@
import { STATUS } from '@hydrooj/utils/lib/status';
import { STATUS } from '@hydrooj/common';
import $ from 'jquery';
import { map } from 'lodash';
import React from 'react';
@@ -1,4 +1,4 @@
import { getScoreColor } from '@hydrooj/utils/lib/status';
import { getScoreColor } from '@hydrooj/common';
import $ from 'jquery';
import { NamedPage } from 'vj/misc/Page';
import { getAvailableLangs, tpl } from 'vj/utils';
-60
View File
@@ -1,60 +0,0 @@
import { findFileSync, parseMemoryMB, parseTimeMS } 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: cfg.checker_type || 'default',
judge_extra_files: [],
user_extra_files: [],
};
if (cfg.type === 'objective') {
if (cfg.checker || cfg.interactor || cfg.validator) {
throw new Error('You cannot use checker, interactor or validator for objective questions');
}
} else {
if (cfg.checker) {
if (!cfg.checker.includes('.')) {
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
}
config.checker ||= checkFile(cfg.checker, 'Cannot find checker {0}.');
}
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
if (cfg.manager) config.manager = checkFile(cfg.manager, 'Cannot find Manager {0}.');
if (cfg.validator) config.validator = checkFile(cfg.validator, 'Cannot find validator {0}.');
for (const n of ['judge', 'user']) {
const conf = cfg[`${n}_extra_files`];
if (!conf) continue;
if (conf instanceof Array) {
config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`));
} else throw new Error(`Invalid ${n}_extra_files config.`);
}
}
if (cfg.cases?.length) {
config.subtasks = [{
cases: cfg.cases,
type: 'sum',
}];
}
if (cfg.time) config.time = parseTimeMS(cfg.time);
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
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]?.trim()) throw new Error('Testdata count incorrect.');
const [input, output, time, score, memory] = f[i].split('|');
const cur = {
cases: [{ input, output }],
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;
}
-156
View File
@@ -236,159 +236,3 @@ export function sortFiles(files: Record<string, any>[] | string[], key = '_id')
});
return result.map((x) => (isString ? x.name : (delete x._weights && x)));
}
interface MatchRule {
regex: RegExp;
output: (a: RegExpExecArray) => string[];
id: (a: RegExpExecArray) => number;
subtask: (a: RegExpExecArray) => number;
preferredScorerType: (a: RegExpExecArray) => 'min' | 'max' | 'sum';
}
const SubtaskMatcher: MatchRule[] = [
{
regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|IN|txt|TXT|in\.txt|IN\.TXT)$/,
output: (a) => ['out', 'ans']
.flatMap((i) => [i, i.toUpperCase(), `${i}.txt`, `${i.toUpperCase()}.TXT`])
.flatMap((i) => [`${a[1]}.${i}`, `${a[1]}.${i}`.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')])
.concat(a[1].includes('input') ? `${a[1]}.txt`.replace(/input/g, 'output') : null),
id: (a) => +a[4],
subtask: (a) => +(a[3] || 1),
preferredScorerType: (a) => (a[3] ? 'min' : 'sum'),
},
{
regex: /^([^\d]*)\.(in|IN)(\d+)$/,
output: (a) => [
`${a[1]}.${a[2] === 'in' ? 'ou' : 'OU'}${a[3]}`,
`${a[1]}.${a[2] === 'in' ? 'out' : 'OUT'}${a[3]}`,
].flatMap((i) => [i, i.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]),
id: (a) => +a[2],
subtask: () => 1,
preferredScorerType: () => 'sum',
},
{
regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.(in|IN)$/,
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}${a[2]}${a[3]}${a[4]}.${i}`),
id: (a) => +a[4],
subtask: (a) => +a[2],
preferredScorerType: () => 'min',
},
{
regex: /^(([0-9]+)[-_](?:.*))\.(in|IN)$/,
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}.${i}`),
id: (a) => +a[2],
subtask: () => 1,
preferredScorerType: () => 'sum',
},
];
function* getScore(totalScore: number, count: number) {
const base = Math.floor(totalScore / count);
const extra = count - (totalScore % count);
for (let i = 0; i < count; i++) {
if (i >= extra) yield base + 1;
else yield base;
}
}
interface ParsedCase {
id?: number;
time?: number | string;
memory?: number | string;
score?: number;
input?: string;
output?: string;
}
interface ParsedSubtask {
cases: ParsedCase[];
type: 'min' | 'max' | 'sum';
time?: number | string;
memory?: number | string;
score?: number;
id?: number;
if?: number[];
}
export function readSubtasksFromFiles(files: string[], config) {
const subtask: Record<number, ParsedSubtask> = {};
for (const s of config.subtasks || []) if (s.id) subtask[s.id] = s;
for (const file of files) {
let match = false;
for (const rule of SubtaskMatcher) {
const data = rule.regex.exec(file);
if (!data) continue;
const sid = rule.subtask(data);
const c = { input: file, output: '', id: rule.id(data) };
const type = rule.preferredScorerType(data);
const outputs = (config.noOutputFile ? ['/dev/null'] : rule.output(data)).filter((i) => i);
for (const output of outputs) {
if (output === file) continue;
if (output === '/dev/null' || files.includes(output)) {
match = true;
c.output = output;
if (!subtask[sid]) {
subtask[sid] = {
time: config.time,
memory: config.memory,
type,
cases: [c],
id: sid,
};
} else if (!subtask[sid].cases) subtask[sid].cases = [c];
else subtask[sid].cases.push(c);
break;
}
}
if (match) break;
}
}
return Object.values(subtask);
}
export interface NormalizedCase extends Required<ParsedCase> {
time: number;
memory: number;
}
export interface NormalizedSubtask extends Required<ParsedSubtask> {
cases: NormalizedCase[];
time: number;
memory: number;
}
export function normalizeSubtasks(
subtasks: ParsedSubtask[], checkFile: (name: string, errMsg: string) => string,
time: number | string = '1000ms', memory: number | string = '256m', ignoreParseError = false,
timeRate = 1, memoryRate = 1,
): NormalizedSubtask[] {
subtasks.sort((a, b) => (a.id - b.id));
const subtaskScore = getScore(
Math.max(100 - Math.sum(subtasks.map((i) => i.score || 0)), 0),
subtasks.filter((i) => !i.score).length,
);
return subtasks.map((s, id) => {
s.cases.sort((a, b) => (a.id - b.id));
const score = s.score || subtaskScore.next().value as number;
const caseScore = getScore(
Math.max(score - Math.sum(s.cases.map((i) => i.score || 0)), 0),
s.cases.filter((i) => !i.score).length,
);
return {
id: id + 1,
type: 'min',
if: [],
...s,
score,
time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate,
memory: parseMemoryMB(s.memory || memory, !ignoreParseError) * memoryRate,
cases: s.cases.map((c, index) => ({
id: index + 1,
...c,
score: c.score || (s.type === 'sum' ? caseScore.next().value as number : score),
time: parseTimeMS(c.time || s.time || time, !ignoreParseError) * timeRate,
memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError) * memoryRate,
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',
})) as NormalizedCase[],
};
});
}
+4 -3
View File
@@ -1,9 +1,10 @@
/* eslint-disable no-await-in-loop */
import os from 'os';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { LangConfig, STATUS } from '@hydrooj/common';
import {
Context, db, DomainModel, JudgeHandler, Logger, ProblemModel, RecordModel, Service, SettingModel,
sleep, STATUS, SystemModel, TaskModel, Time, yaml,
Context, db, DomainModel, JudgeHandler, Logger,
ProblemModel, RecordModel, Service, SettingModel,
sleep, SystemModel, TaskModel, Time, yaml,
} from 'hydrooj';
import { BasicProvider, IBasicProvider, RemoteAccount } from './interface';
import providers from './providers/index';
+1 -2
View File
@@ -1,5 +1,4 @@
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { JudgeResultBody } from 'hydrooj';
import { JudgeResultBody, LangConfig } from '@hydrooj/common';
export interface RemoteAccount {
_id: string;
+1 -1
View File
@@ -1,4 +1,4 @@
import { STATUS } from '@hydrooj/utils/lib/status';
import { STATUS } from '@hydrooj/common';
export function normalize(key: string) {
return key.toUpperCase().replace(/ /g, '_');