diff --git a/packages/hydrojudge/src/check.ts b/packages/hydrojudge/src/check.ts index 8ac5d357..531a4d8c 100644 --- a/packages/hydrojudge/src/check.ts +++ b/packages/hydrojudge/src/check.ts @@ -3,11 +3,24 @@ import { findFileSync } from '@hydrooj/utils/lib/utils'; import checkers from './checkers'; import compile from './compile'; import { SystemError } from './error'; +import { Execute } from './interface'; +import { CopyInFile } from './sandbox/interface'; import { parseFilename } from './utils'; const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'); -export async function check(config): Promise<[number, number, string]> { +interface CheckConfig { + checker_type: string; + stdin: string, + stdout: string, + user_stdout: string, + user_stderr: string, + score: number, + copyIn?: Record, + detail: boolean, +} + +export async function check(config: CheckConfig): Promise<[number, number, string]> { if (!checkers[config.checker_type]) throw new SystemError('Unknown checker type {0}', [config.checker_type]); const { code, status, score, message, @@ -24,7 +37,7 @@ export async function check(config): Promise<[number, number, string]> { return [status, score, message]; } -export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any) { +export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise { if (!checkers[checkerType]) throw new SystemError('Unknown checker type {0}.', [checkerType]); if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc }; const file = await fs.readFile(checker); diff --git a/packages/hydrojudge/src/checkers.ts b/packages/hydrojudge/src/checkers.ts index 9477db01..add33411 100644 --- a/packages/hydrojudge/src/checkers.ts +++ b/packages/hydrojudge/src/checkers.ts @@ -2,8 +2,19 @@ import { STATUS } from '@hydrooj/utils/lib/status'; import { SystemError } from './error'; import { run } from './sandbox'; +import { CopyInFile } from './sandbox/interface'; import { parse } from './testlib'; +interface CheckConfig { + input: string, + output: string, + user_stdout: string, + user_stderr: string, + score: number, + copyIn: Record, + detail: boolean, +} + interface CheckResult { status: number, score: number, @@ -11,7 +22,7 @@ interface CheckResult { code?: number, } -type Checker = (config: any) => Promise; +type Checker = (config: CheckConfig) => Promise; const checkers: Record = { async default(config) { @@ -22,7 +33,7 @@ const checkers: Record = { ...config.copyIn, }, }); - let status; + let status: number; let message: any = ''; if (stdout) { status = STATUS.STATUS_WRONG_ANSWER; diff --git a/packages/hydrojudge/src/daemon.ts b/packages/hydrojudge/src/daemon.ts index 97ffdf4e..858f12fe 100644 --- a/packages/hydrojudge/src/daemon.ts +++ b/packages/hydrojudge/src/daemon.ts @@ -23,19 +23,19 @@ import { Queue, sleep } from './utils'; declare global { namespace NodeJS { interface Global { - onDestory: Function[] + onDestroy: Function[] hosts: any } } } -if (!global.onDestory) global.onDestory = []; +if (!global.onDestroy) global.onDestroy = []; if (!global.hosts) global.hosts = []; let exit = false; const terminate = async () => { log.info('正在保存数据'); try { - await Promise.all(global.onDestory.map((f) => f())); + await Promise.all(global.onDestroy.map((f: Function) => f())); process.exit(1); } catch (e) { if (exit) process.exit(1); diff --git a/packages/hydrojudge/src/hosts/hydro.ts b/packages/hydrojudge/src/hosts/hydro.ts index dc8f79e8..13784927 100644 --- a/packages/hydrojudge/src/hosts/hydro.ts +++ b/packages/hydrojudge/src/hosts/hydro.ts @@ -63,7 +63,7 @@ class JudgeTask { this.source = this.request.source; this.tmpdir = path.resolve(getConfig('tmp_dir'), this.host, this.rid); this.clean = []; - await Lock.aquire(`${this.host}/${this.source}/${this.rid}`); + await Lock.acquire(`${this.host}/${this.source}/${this.rid}`); fs.ensureDirSync(this.tmpdir); tmpfs.mount(this.tmpdir, getConfig('tmpfs_size')); log.info('Submission: %s/%s/%s', this.host, this.source, this.rid); @@ -170,7 +170,7 @@ export default class Hydro { } async cacheOpen(source: string, files: any[], next?) { - await Lock.aquire(`${this.config.host}/${source}`); + await Lock.acquire(`${this.config.host}/${source}`); try { return this._cacheOpen(source, files, next); } finally { @@ -245,7 +245,7 @@ export default class Hydro { this.ws = new WebSocket(`${this.config.server_url.replace(/^http/i, 'ws')}judge/conn/websocket?t=${res.data.entropy}`); this.ws.on('open', () => { this.ws.send(this.config.cookie); - global.onDestory.push(() => this.ws.close()); + global.onDestroy.push(() => this.ws.close()); const content = this.config.minPriority !== undefined ? `{"key":"prio","prio":${this.config.minPriority}}` : '{"key":"ping"}'; diff --git a/packages/hydrojudge/src/interface.ts b/packages/hydrojudge/src/interface.ts index 00fa83ac..99c7fc4e 100644 --- a/packages/hydrojudge/src/interface.ts +++ b/packages/hydrojudge/src/interface.ts @@ -1,12 +1,8 @@ -export interface SFile { - src?: string, - content?: string, - fileId?: string, -} +import { CopyInFile } from './sandbox/interface'; export interface Execute { execute: string, clean: Function, - copyIn: Record, + copyIn: Record, time?: number, } export interface CompileErrorInfo { diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index a3f6b9cd..d3a863a0 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -9,6 +9,9 @@ import { CompileError, FormatError } from '../error'; import { run } from '../sandbox'; import signals from '../signals'; import { parseFilename } from '../utils'; +import { + Case, Context, ContextSubTask, SubTask, +} from './interface'; const Score = { sum: (a: number, b: number) => (a + b), @@ -16,8 +19,8 @@ const Score = { min: Math.min, }; -function judgeCase(c, sid: string) { - return async (ctx, ctxSubtask, runner?: Function) => { +function judgeCase(c: Case, sid: string) { + return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => { if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min') || (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score) || ((ctxSubtask.subtask.if || []).filter((i: string) => ctx.failed[i]).length) @@ -72,7 +75,6 @@ function judgeCase(c, sid: string) { stdout: c.output, user_stdout: stdout, user_stderr: stderr, - checker: ctx.config.checker, checker_type: ctx.config.checker_type, score: ctxSubtask.subtask.score, detail: ctx.config.detail ?? true, @@ -109,8 +111,8 @@ function judgeCase(c, sid: string) { }; } -function judgeSubtask(subtask, sid: string) { - return async (ctx) => { +function judgeSubtask(subtask: SubTask, sid: string) { + return async (ctx: Context) => { subtask.type = subtask.type || 'min'; const ctxSubtask = { subtask, @@ -133,7 +135,7 @@ function judgeSubtask(subtask, sid: string) { }; } -export const judge = async (ctx) => { +export const judge = async (ctx: Context) => { if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.'); ctx.next({ status: STATUS.STATUS_COMPILING }); if (ctx.config.template) { diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts index cc39baac..1bd07402 100644 --- a/packages/hydrojudge/src/judge/hack.ts +++ b/packages/hydrojudge/src/judge/hack.ts @@ -8,8 +8,9 @@ import { CompileError } from '../error'; import { run } from '../sandbox'; import signals from '../signals'; import { copyInDir, parseFilename } from '../utils'; +import { Context } from './interface'; -export const judge = async (ctx) => { +export const judge = async (ctx: Context) => { if (ctx.config.template) { if (ctx.config.template[ctx.lang]) { const tpl = ctx.config.template[ctx.lang]; @@ -100,7 +101,6 @@ export const judge = async (ctx) => { stdout, user_stdout: stdout, user_stderr: stderr, - checker: ctx.config.checker, checker_type: ctx.config.checker_type, score: 100, detail: ctx.config.detail, diff --git a/packages/hydrojudge/src/judge/index.ts b/packages/hydrojudge/src/judge/index.ts index 76622f43..2d0af9ed 100644 --- a/packages/hydrojudge/src/judge/index.ts +++ b/packages/hydrojudge/src/judge/index.ts @@ -1,9 +1,10 @@ import * as def from './default'; import * as hack from './hack'; import * as interactive from './interactive'; +import { Context } from './interface'; import * as run from './run'; import * as submit_answer from './submit_answer'; export = { default: def, interactive, run, submit_answer, hack, objective: submit_answer, -}; +} as Record }>; diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index 42d33547..f5e72c82 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -7,16 +7,19 @@ import { run } from '../sandbox'; import signals from '../signals'; import { parse } from '../testlib'; import { findFileSync, parseFilename } from '../utils'; +import { + Case, Context, ContextSubTask, SubTask, +} from './interface'; const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'); const Score = { - sum: (a, b) => (a + b), + sum: (a: number, b: number) => (a + b), max: Math.max, min: Math.min, }; -function judgeCase(c) { - return async (ctx, ctxSubtask) => { +function judgeCase(c: Case) { + return async (ctx: Context, ctxSubtask: ContextSubTask) => { ctx.executeInteractor.copyIn.in = c.input ? { src: c.input } : { content: '' }; ctx.executeInteractor.copyIn.out = c.output ? { src: c.output } : { content: '' }; const [{ code, time_usage_ms, memory_usage_kb }, resInteractor] = await run([ @@ -34,7 +37,7 @@ function judgeCase(c) { }, ]); // TODO handle tout (maybe pass to checker?) - let status; + let status: number; let score = 0; let message: any = ''; if (time_usage_ms > ctxSubtask.subtask.time * ctx.executeUser.time) { @@ -70,8 +73,8 @@ function judgeCase(c) { }; } -function judgeSubtask(subtask) { - return async (ctx) => { +function judgeSubtask(subtask: SubTask) { + return async (ctx: Context) => { subtask.type = subtask.type || 'min'; const ctxSubtask = { subtask, @@ -90,7 +93,7 @@ function judgeSubtask(subtask) { }; } -export const judge = async (ctx) => { +export const judge = async (ctx: Context) => { ctx.next({ status: STATUS.STATUS_COMPILING }); [ctx.executeUser, ctx.executeInteractor] = await Promise.all([ (() => { diff --git a/packages/hydrojudge/src/judge/interface.ts b/packages/hydrojudge/src/judge/interface.ts new file mode 100644 index 00000000..e1f54ee9 --- /dev/null +++ b/packages/hydrojudge/src/judge/interface.ts @@ -0,0 +1,101 @@ +import PQueue from 'p-queue'; +import { LangConfig } from '@hydrooj/utils/lib/lang'; +import { Execute } from '../interface'; + +export type Context = JudgeTaskInterface & RuntimeContext; + +export interface JudgeTaskInterface { + next(arg0: { + status?: number; + message?: string; + progress?: number; + case?: { + status: number; + score?: number; + time_ms: number; + memory_kb: number; + message: string; + }; + }, arg1?: number): void; + end(arg0: { + status: number; + score: number; + time_ms: number; + memory_kb: number; + }): void; + getLang: (name: string) => LangConfig; + + stat: Record; + lang: string; + code: string; + tmpdir: string; + input?: string; + clean: Function[]; + config: Config; +} + +export interface RuntimeContext { + total_score?: number; + total_status?: number; + total_time_usage_ms?: number; + total_memory_usage_kb?: number; + + queue?: PQueue; + errored?: boolean; + rerun?: number; + failed?: Record; + + execute?: Execute; + checker?: Execute; + executeInteractor?: Execute; + executeUser?: Execute; +} + +type ExtraFile = string[]; + +export interface Config { + time: string; + memory: string; + + subtasks?: SubTask[]; + count?: number; + checker_type?: string; + detail?: boolean; + filename?: string; + + judge_extra_files?: ExtraFile; + user_extra_files?: ExtraFile; + template?: Record; + + checker?: string; + validator?: string; + std?: string; + hack?: string; + interactor?: string; + + outputs?: { + output: string, + score: number, + }[]; +} + +export interface SubTask { + time: number; + memory: number; + type: 'sum' | 'max' | 'min'; + score: number; + cases: Record; + if: string[]; +} + +export interface ContextSubTask { + subtask: SubTask; + score: number; + status: number; +} + +export interface Case { + id: number; + input: string; + output: string; +} diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts index fba3ca8b..f5bde47f 100644 --- a/packages/hydrojudge/src/judge/run.ts +++ b/packages/hydrojudge/src/judge/run.ts @@ -6,8 +6,9 @@ import { CompileError } from '../error'; import { run } from '../sandbox'; import signals from '../signals'; import { compilerText, parseMemoryMB, parseTimeMS } from '../utils'; +import { Context } from './interface'; -export const judge = async (ctx) => { +export const judge = async (ctx: Context) => { ctx.stat.judge = new Date(); ctx.next({ status: STATUS.STATUS_COMPILING }); try { diff --git a/packages/hydrojudge/src/judge/submit_answer.ts b/packages/hydrojudge/src/judge/submit_answer.ts index 7a724fbe..1e87800f 100644 --- a/packages/hydrojudge/src/judge/submit_answer.ts +++ b/packages/hydrojudge/src/judge/submit_answer.ts @@ -1,8 +1,9 @@ import { STATUS } from '@hydrooj/utils/lib/status'; +import { Context } from './interface'; export async function judge({ next, end, config, code, -}) { +}: Context) { next({ status: STATUS.STATUS_JUDGING, progress: 0 }); code = code.replace(/\r/g, ''); const outputs = code.split('\n'); diff --git a/packages/hydrojudge/src/log.ts b/packages/hydrojudge/src/log.ts index dbc9a0bf..193f384d 100644 --- a/packages/hydrojudge/src/log.ts +++ b/packages/hydrojudge/src/log.ts @@ -91,7 +91,7 @@ export class Logger { private createMethod(name: LogType, prefix: string, minLevel: number) { this[name] = (...args: [any, ...any[]]) => { if (this.level < minLevel) return; - this.stream.write(`${prefix + this.displayName + this.format(...args)}\n`); + this.stream.write(`${prefix} ${this.displayName} ${this.format(...args)}\n`); }; } diff --git a/packages/hydrojudge/src/sandbox.ts b/packages/hydrojudge/src/sandbox.ts index 906cfdb6..f022904f 100644 --- a/packages/hydrojudge/src/sandbox.ts +++ b/packages/hydrojudge/src/sandbox.ts @@ -1,10 +1,14 @@ -import Axios from 'axios'; import cac from 'cac'; import fs from 'fs-extra'; +import { ParseEntry } from 'shell-quote'; import { STATUS } from '@hydrooj/utils/lib/status'; import { getConfig } from './config'; import { FormatError, SystemError } from './error'; import { Logger } from './log'; +import { SandboxClient } from './sandbox/client'; +import { + Cmd, CopyInFile, SandboxResult, SandboxStatus, +} from './sandbox/interface'; import { cmd, parseMemoryMB } from './utils'; const argv = cac().parse(); @@ -12,16 +16,41 @@ const logger = new Logger('sandbox'); let callId = 0; let supportOptional = false; -const statusMap = { - 'Time Limit Exceeded': STATUS.STATUS_TIME_LIMIT_EXCEEDED, - 'Memory Limit Exceeded': STATUS.STATUS_MEMORY_LIMIT_EXCEEDED, - 'Output Limit Exceeded': STATUS.STATUS_RUNTIME_ERROR, - Accepted: STATUS.STATUS_ACCEPTED, - 'Nonzero Exit Status': STATUS.STATUS_RUNTIME_ERROR, - 'Internal Error': STATUS.STATUS_SYSTEM_ERROR, - 'File Error': STATUS.STATUS_SYSTEM_ERROR, - Signalled: STATUS.STATUS_RUNTIME_ERROR, -}; +const statusMap: Map = new Map([ + [SandboxStatus.TimeLimitExceeded, STATUS.STATUS_TIME_LIMIT_EXCEEDED], + [SandboxStatus.MemoryLimitExceeded, STATUS.STATUS_MEMORY_LIMIT_EXCEEDED], + [SandboxStatus.OutputLimitExceeded, STATUS.STATUS_RUNTIME_ERROR], + [SandboxStatus.Accepted, STATUS.STATUS_ACCEPTED], + [SandboxStatus.NonzeroExitStatus, STATUS.STATUS_RUNTIME_ERROR], + [SandboxStatus.InternalError, STATUS.STATUS_SYSTEM_ERROR], + [SandboxStatus.FileError, STATUS.STATUS_SYSTEM_ERROR], + [SandboxStatus.Signalled, STATUS.STATUS_RUNTIME_ERROR], +]); + +interface Parameter { + time?: number; + stdin?: string; + stdout?: string; + stderr?: string; + execute?: string; + memory?: number; + processLimit?: number; + copyIn?: Record; + copyOut?: string[]; + copyOutCached?: string[]; +} + +function checkStringArray(args: ParseEntry[]): args is string[] { + return args.every((arg: ParseEntry) => typeof arg === 'string'); +} + +function parseArgs(execute: string): string[] { + const args = cmd(execute.replace(/\$\{dir\}/g, '/w')); + if (!checkStringArray(args)) { + throw new SystemError(`${execute} contains invalid operator`); + } + return args; +} function proc({ execute = '', @@ -29,14 +58,14 @@ function proc({ memory = parseMemoryMB(getConfig('memoryMax')), processLimit = getConfig('processLimit'), stdin = '', copyIn = {}, copyOut = [], copyOutCached = [], -} = {}) { +}: Parameter = {}): Cmd { if (!supportOptional) { copyOut = (copyOut as string[]).map((i) => (i.endsWith('?') ? i.substr(0, i.length - 1) : i)); } const size = parseMemoryMB(getConfig('stdio_size')); const rate = getConfig('rate'); return { - args: cmd(execute.replace(/\$\{dir\}/g, '/w')), + args: parseArgs(execute), env: getConfig('env').split('\n'), files: [ stdin ? { src: stdin } : { content: '' }, @@ -44,7 +73,7 @@ function proc({ { name: 'stderr', max: Math.floor(1024 * 1024 * size) }, ], cpuLimit: Math.floor(time * 1000 * 1000 * rate), - realCpuLimit: Math.floor(time * 3000 * 1000 * rate), + clockLimit: Math.floor(time * 3000 * 1000 * rate), memoryLimit: Math.floor(memory * 1024 * 1024), strictMemoryLimit: getConfig('strict_memory'), // stackLimit: memory * 1024 * 1024, @@ -55,11 +84,11 @@ function proc({ }; } -async function adaptResult(result, params) { +async function adaptResult(result: SandboxResult, params: Parameter) { const rate = getConfig('rate'); // FIXME: Signalled? const ret: any = { - status: statusMap[result.status] || STATUS.STATUS_ACCEPTED, + status: statusMap.get(result.status) || STATUS.STATUS_ACCEPTED, time_usage_ms: result.time / 1000000 / rate, memory_usage_kb: result.memory / 1024, files: result.files, @@ -78,8 +107,8 @@ async function adaptResult(result, params) { return ret; } -export async function runMultiple(execute) { - let res; +export async function runMultiple(execute: Parameter[]) { + let res: SandboxResult[]; const size = parseMemoryMB(getConfig('stdio_size')); try { const body = { @@ -107,35 +136,35 @@ export async function runMultiple(execute) { body.cmd[1].files[1] = null; const id = callId++; if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(body)); - res = await Axios.create({ baseURL: getConfig('sandbox_host') }).post('/run', body); - if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res.data)); + res = await new SandboxClient(getConfig('sandbox_host')).run(body); + if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res)); } catch (e) { if (e instanceof FormatError) throw e; throw new SystemError('Sandbox Error', [e]); } - return await Promise.all(res.data.map((i) => adaptResult(i, {}))); + return await Promise.all(res.map((r) => adaptResult(r, {}))); } export async function del(fileId: string) { - const res = await Axios.create({ baseURL: getConfig('sandbox_host') }).delete(`/file/${fileId}`); - return res.data; + await new SandboxClient(getConfig('sandbox_host')).deleteFile(fileId); } -export async function run(execute, params?) { - let result; +export async function run(execute: string | Parameter[], params?: Parameter) { + let result: SandboxResult; if (typeof execute === 'object') return await runMultiple(execute); try { + const client = new SandboxClient(getConfig('sandbox_host')); if (!supportOptional) { - const res = await Axios.create({ baseURL: getConfig('sandbox_host') }).get('/version'); - supportOptional = res.data.copyOutOptional; + const res = await client.version(); + supportOptional = res.copyOutOptional; if (!supportOptional) logger.warn('Sandbox version tooooooo low! Please upgrade to at least 1.2.0'); } const body = { cmd: [proc({ execute, ...params })] }; const id = callId++; if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(body)); - const res = await Axios.create({ baseURL: getConfig('sandbox_host') }).post('/run', body); - if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res.data)); - [result] = res.data; + const res = await client.run(body); + if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res)); + [result] = res; } catch (e) { if (e instanceof FormatError) throw e; // FIXME request body larger than maxBodyLength limit diff --git a/packages/hydrojudge/src/sandbox/client.ts b/packages/hydrojudge/src/sandbox/client.ts new file mode 100644 index 00000000..0bfc51bb --- /dev/null +++ b/packages/hydrojudge/src/sandbox/client.ts @@ -0,0 +1,30 @@ +import axios, { AxiosInstance } from 'axios'; +import { SandboxRequest, SandboxResult, SandboxVersion } from './interface'; + +export class SandboxClient { + private client: AxiosInstance; + + constructor(baseURL: string) { + this.client = axios.create({ baseURL }); + } + + public run(req: SandboxRequest): Promise { + return this.client.post('/run', req).then((res) => res.data); + } + + public getFile(fileId: string): Promise { + return this.client.get(`/file/${fileId}`).then((res) => res.data); + } + + public deleteFile(fileId: string): Promise { + return this.client.delete(`/file/${fileId}`); + } + + public listFiles(): Promise> { + return this.client.get('/file').then((res) => res.data); + } + + public version(): Promise { + return this.client.get('/version').then((res) => res.data); + } +} diff --git a/packages/hydrojudge/src/sandbox/interface.ts b/packages/hydrojudge/src/sandbox/interface.ts new file mode 100644 index 00000000..095ebb51 --- /dev/null +++ b/packages/hydrojudge/src/sandbox/interface.ts @@ -0,0 +1,143 @@ +export interface SandboxVersion { + buildVersion: string; + goVersion: string; + platform: string; + os: string; + copyOutOptional?: boolean; + pipeProxy?: boolean; +} + +export interface LocalFile { + src: string; +} + +export interface MemoryFile { + content: string | Buffer; +} + +export interface PreparedFile { + fileId: string; +} + +export interface Collector { + name: string; + max: number; + pipe?: boolean; +} + +export type CopyInFile = LocalFile | MemoryFile | PreparedFile; +export type CmdFile = LocalFile | MemoryFile | PreparedFile | Collector | null; + +/** + * Cmd defines a single command to be executed by sandbox server + */ +export interface Cmd { + args: string[]; + env?: string[]; + /** files defines open file descriptor for the command */ + files?: CmdFile[]; + tty?: boolean; + + /** cpuLimit and clockLimit defines time limitations in ns */ + cpuLimit?: number; + clockLimit?: number; + /** memoryLimit and stackLimit defines memory limitation in bytes */ + memoryLimit?: number; + stackLimit?: number; + procLimit?: number; + /** cpuRateLimit defines cpu share limits in 1/1000 cpus if enabled in sandbox server */ + cpuRateLimit?: number; + /** cpuSetLimit defines cpu set limit if enabled in sandbox server */ + cpuSetLimit?: string; + /** strictMemoryLimit set rlimit_data limit for memoryLimit */ + strictMemoryLimit?: boolean; + + /** files to be copied into sandbox before execution */ + copyIn?: Record; + + /** + * files to be copied out from sandbox after execution. + * append '?' to make the file optional and do not cause FileError when missing + */ + copyOut?: string[]; + /** similar to copyOut but fileId returned instead */ + copyOutCached?: string[]; + /** copyOut limit in byte */ + copyOutMax?: number; +} + +/** + * SandboxRequest defines a single request to sandbox server + */ +export interface SandboxRequest { + requestId?: string; + cmd: Cmd[]; + pipeMapping?: PipeMap[]; +} + +export enum SandboxStatus { + Accepted = 'Accepted', + MemoryLimitExceeded = 'Memory Limit Exceeded', + TimeLimitExceeded = 'Time Limit Exceeded', + OutputLimitExceeded = 'Output Limit Exceeded', + FileError = 'File Error', + NonzeroExitStatus = 'Nonzero Exit Status', + Signalled = 'Signalled', + InternalError = 'Internal Error', +} + +export interface PipeIndex { + index: number; + fd: number; +} + +export interface PipeMap { + in: PipeIndex; + out: PipeIndex; + /** enable pipe proxy */ + proxy?: boolean; + /** if proxy enabled, save transmitted content */ + name?: string; + max?: number; +} + +export enum FileErrorType { + CopyInOpenFile = 'CopyInOpenFile', + CopyInCreateFile = 'CopyInCreateFile', + CopyInCopyContent = 'CopyInCopyContent', + CopyOutOpen = 'CopyOutOpen', + CopyOutNotRegularFile = 'CopyOutNotRegularFile', + CopyOutSizeExceeded = 'CopyOutSizeExceeded', + CopyOutCreateFile = 'CopyOutCreateFile', + CopyOutCopyContent = 'CopyOutCopyContent', + CollectSizeExceeded = 'CollectSizeExceeded', +} + +export interface FileError { + name: string; + type: FileErrorType; + message?: string; +} + +/** + * SandboxResult defines result from sandbox server + */ +export interface SandboxResult { + status: SandboxStatus; + /** contains error message if status is not Accept */ + error?: string; + /** signal number if status is Signalled, otherwise exit status */ + exitStatus: number; + /** cpu time in ns */ + time: number; + /** peak memory in byte */ + memory: number; + /** wall clock time in ns */ + runTime: number; + /** copyOut file name to content (UTF-8 encoded and invalid character replaced) */ + files?: Record; + /** copyOutCached file name to fileId */ + fileIds?: Record; + /** contains detailed error if status is FileError */ + fileError?: FileError[]; +} diff --git a/packages/hydrojudge/src/sysinfo.ts b/packages/hydrojudge/src/sysinfo.ts index 42699f77..90147e18 100644 --- a/packages/hydrojudge/src/sysinfo.ts +++ b/packages/hydrojudge/src/sysinfo.ts @@ -3,6 +3,7 @@ import fs from 'fs-extra'; import { noop } from 'lodash'; import { get as _get } from '@hydrooj/utils/lib/sysinfo'; import { getConfig } from './config'; +import { Context } from './judge/interface'; import { judge } from './judge/run'; import * as tmpfs from './tmpfs'; @@ -11,7 +12,7 @@ export { update } from '@hydrooj/utils/lib/sysinfo'; async function stackSize() { let output = ''; try { - const context: any = { + const context: Context = { lang: 'cc', code: `#include using namespace std; @@ -33,8 +34,19 @@ int main(){ if (data.case) output = data.case.message; }, end: () => { }, + getLang: () => ({ + compile: '/usr/bin/g++ -Wall -std=c++14 -o foo.cc foo.cc -lm', + execute: 'foo', + code_file: 'foo.cc', + highlight: 'cpp astyle-c', + monaco: 'cpp', + display: 'C++', + time_limit_rate: 1, + domain: [], + key: '', + }), + tmpdir: path.resolve(getConfig('tmp_dir'), 'sysinfo'), }; - context.tmpdir = path.resolve(getConfig('tmp_dir'), 'sysinfo'); fs.ensureDirSync(context.tmpdir); tmpfs.mount(context.tmpdir, '32m'); await judge(context).catch((e) => console.error(e)); diff --git a/packages/hydrojudge/src/tmpfs.ts b/packages/hydrojudge/src/tmpfs.ts index 5c05ea6a..4b732652 100644 --- a/packages/hydrojudge/src/tmpfs.ts +++ b/packages/hydrojudge/src/tmpfs.ts @@ -6,11 +6,15 @@ import log from './log'; const linux = os.platform() === 'linux'; if (!linux) log.warn('Not running on linux. tmpfs disabled.'); +const userInfo = os.userInfo(); +const uid = userInfo.uid; +if (uid !== 0) log.warn('Not running by root. tmpfs disabled.'); + export function mount(path: string, size = '32m') { fs.ensureDirSync(path); - if (linux) child.execSync(`mount tmpfs ${path} -t tmpfs -o size=${size}`); + if (linux && uid === 0) child.execSync(`mount tmpfs ${path} -t tmpfs -o size=${size}`); } export function umount(path: string) { - if (linux) child.execSync(`umount ${path}`); + if (linux && uid === 0) child.execSync(`umount ${path}`); } diff --git a/packages/hydrojudge/src/utils.ts b/packages/hydrojudge/src/utils.ts index 53e1e0e5..f84378d4 100644 --- a/packages/hydrojudge/src/utils.ts +++ b/packages/hydrojudge/src/utils.ts @@ -16,7 +16,7 @@ export function parseFilename(filePath: string) { return t[t.length - 1]; } -const encrypt = (algorithm, content) => { +const encrypt = (algorithm: string, content: crypto.BinaryLike) => { const hash = crypto.createHash(algorithm); hash.update(content); return hash.digest('hex'); @@ -56,7 +56,7 @@ export class Queue extends EventEmitter { export namespace Lock { const data = {}; - export async function aquire(key: string) { + export async function acquire(key: string) { // eslint-disable-next-line no-await-in-loop while (data[key]) await sleep(100); data[key] = true;