mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
Merge branch 'master' into contest-problemlist
This commit is contained in:
commit
58c0cade0e
28 files changed
+304
-187
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/hydrojudge",
|
"name": "@hydrooj/hydrojudge",
|
||||||
"bin": "bin/hydrojudge.js",
|
"bin": "bin/hydrojudge.js",
|
||||||
"version": "3.1.0",
|
"version": "3.1.1",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import type { JudgeResultBody } from 'hydrooj';
|
|
||||||
import {
|
import {
|
||||||
SettingModel, StorageModel, SystemModel, TaskModel,
|
JudgeResultBody, RecordModel, SettingModel,
|
||||||
|
StorageModel, SystemModel, TaskModel,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
import { end, next } from 'hydrooj/src/handler/judge';
|
import { end, next } from 'hydrooj/src/handler/judge';
|
||||||
import { processTestdata } from '../cases';
|
import { processTestdata } from '../cases';
|
||||||
@@ -75,7 +75,10 @@ const session = {
|
|||||||
|
|
||||||
export async function postInit() {
|
export async function postInit() {
|
||||||
if (SystemModel.get('hydrojudge.disable')) return;
|
if (SystemModel.get('hydrojudge.disable')) return;
|
||||||
const handle = (t) => (new JudgeTask(session, t)).handle().catch(logger.error);
|
const handle = async (t) => {
|
||||||
|
const rdoc = await RecordModel.get(t.domainId, t.rid);
|
||||||
|
(new JudgeTask(session, Object.assign(rdoc, t))).handle().catch(logger.error);
|
||||||
|
};
|
||||||
TaskModel.consume({ type: 'judge' }, handle);
|
TaskModel.consume({ type: 'judge' }, handle);
|
||||||
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "4.0.18",
|
"version": "4.1.1",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/plugin-api",
|
"main": "src/plugin-api",
|
||||||
"module": "src/plugin-api",
|
"module": "src/plugin-api",
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import message from '../model/message';
|
|||||||
import * as oplog from '../model/oplog';
|
import * as oplog from '../model/oplog';
|
||||||
import problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
|
import ScheduleModel from '../model/schedule';
|
||||||
import storage from '../model/storage';
|
import storage from '../model/storage';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import TaskModel from '../model/task';
|
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
import {
|
import {
|
||||||
Handler, param, post, Types,
|
Handler, param, post, Types,
|
||||||
@@ -52,7 +52,7 @@ registerResolver(
|
|||||||
'Get a contest by ID',
|
'Get a contest by ID',
|
||||||
);
|
);
|
||||||
|
|
||||||
TaskModel.Worker.addHandler('contest', async (doc) => {
|
ScheduleModel.Worker.addHandler('contest', async (doc) => {
|
||||||
const tdoc = await contest.get(doc.domainId, doc.tid);
|
const tdoc = await contest.get(doc.domainId, doc.tid);
|
||||||
if (!tdoc) return;
|
if (!tdoc) return;
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
@@ -199,7 +199,7 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
|||||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||||
await contest.del(domainId, tid);
|
await contest.del(domainId, tid);
|
||||||
await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' });
|
await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' });
|
||||||
await TaskModel.deleteMany({
|
await ScheduleModel.deleteMany({
|
||||||
type: 'schedule', subType: 'contest', domainId, tid,
|
type: 'schedule', subType: 'contest', domainId, tid,
|
||||||
});
|
});
|
||||||
this.response.redirect = this.url('contest_main');
|
this.response.redirect = this.url('contest_main');
|
||||||
@@ -346,20 +346,18 @@ export class ContestEditHandler extends Handler {
|
|||||||
const task = {
|
const task = {
|
||||||
type: 'schedule', subType: 'contest', domainId, tid,
|
type: 'schedule', subType: 'contest', domainId, tid,
|
||||||
};
|
};
|
||||||
await TaskModel.deleteMany(task);
|
await ScheduleModel.deleteMany(task);
|
||||||
|
const operation = [];
|
||||||
if (Date.now() <= endAt.getTime() && autoHide) {
|
if (Date.now() <= endAt.getTime() && autoHide) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await Promise.all(pids.map((pid) => problem.edit(domainId, pid, { hidden: true })));
|
await Promise.all(pids.map((pid) => problem.edit(domainId, pid, { hidden: true })));
|
||||||
await TaskModel.add({
|
operation.push('unhide');
|
||||||
...task,
|
|
||||||
operation: ['unhide'],
|
|
||||||
executeAfter: endAt,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (lock && lockAt <= endAt) {
|
if (lock && lockAt <= endAt) operation.push('unlock');
|
||||||
await TaskModel.add({
|
if (operation.length) {
|
||||||
|
await ScheduleModel.add({
|
||||||
...task,
|
...task,
|
||||||
operation: ['unlock'],
|
operation,
|
||||||
executeAfter: endAt,
|
executeAfter: endAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,9 +103,7 @@ export async function postJudge(rdoc: RecordDoc) {
|
|||||||
contest: { $ne: new ObjectID('0'.repeat(24)) },
|
contest: { $ne: new ObjectID('0'.repeat(24)) },
|
||||||
}).project({ _id: 1, contest: 1 }).toArray();
|
}).project({ _id: 1, contest: 1 }).toArray();
|
||||||
const priority = await record.submissionPriority(rdoc.uid, -5000 - rdocs.length * 5 - 50);
|
const priority = await record.submissionPriority(rdoc.uid, -5000 - rdocs.length * 5 - 50);
|
||||||
await Promise.all(rdocs.map(
|
await record.judge(rdoc.domainId, rdocs.map((r) => r._id), priority, {}, { hackRejudge: input });
|
||||||
(r) => record.judge(rdoc.domainId, r._id, priority, r.contest ? { detail: false } : {}, { hackRejudge: input }),
|
|
||||||
));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({
|
next({
|
||||||
rid: rdoc._id, domainId: rdoc.domainId, key: 'next', message: { message: 'Unable to apply hack: {0}', params: [e.message] },
|
rid: rdoc._id, domainId: rdoc.domainId, key: 'next', message: { message: 'Unable to apply hack: {0}', params: [e.message] },
|
||||||
@@ -202,10 +200,11 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
|||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
if (!t) await sleep(500);
|
if (!t) await sleep(500);
|
||||||
}
|
}
|
||||||
this.send({ task: t });
|
let rdoc = await record.get(t.domainId, t.rid);
|
||||||
|
this.send({ task: { ...rdoc, ...t } });
|
||||||
this.processing = t;
|
this.processing = t;
|
||||||
const $set = { status: builtin.STATUS.STATUS_FETCHED };
|
const $set = { status: builtin.STATUS.STATUS_FETCHED };
|
||||||
const rdoc = await record.update(t.domainId, t.rid, $set, {});
|
rdoc = await record.update(t.domainId, t.rid, $set, {});
|
||||||
bus.broadcast('record/change', rdoc, $set, {});
|
bus.broadcast('record/change', rdoc, $set, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,10 +286,11 @@ class SystemUserPrivHandler extends SystemHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@requireSudo
|
@requireSudo
|
||||||
@param('uid', Types.Int, true)
|
@param('uid', Types.Int)
|
||||||
@param('priv', Types.UnsignedInt)
|
@param('priv', Types.UnsignedInt)
|
||||||
async post(domainId: string, uid: number, priv: number) {
|
@param('system', Types.Boolean, true)
|
||||||
if (typeof uid === 'number') {
|
async post(domainId: string, uid: number, priv: number, editSystem: boolean) {
|
||||||
|
if (!editSystem) {
|
||||||
const udoc = await user.getById(domainId, uid);
|
const udoc = await user.getById(domainId, uid);
|
||||||
if (!udoc) throw new UserNotFoundError(uid);
|
if (!udoc) throw new UserNotFoundError(uid);
|
||||||
if (udoc.priv === -1 || priv === -1 || priv === allPriv) throw new ForbiddenError('you can not edit user as SU in web.');
|
if (udoc.priv === -1 || priv === -1 || priv === allPriv) throw new ForbiddenError('you can not edit user as SU in web.');
|
||||||
|
|||||||
@@ -443,17 +443,17 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
|
|||||||
@param('pid', Types.UnsignedInt)
|
@param('pid', Types.UnsignedInt)
|
||||||
async postRejudge(domainId: string, pid: number) {
|
async postRejudge(domainId: string, pid: number) {
|
||||||
this.checkPerm(PERM.PERM_REJUDGE_PROBLEM);
|
this.checkPerm(PERM.PERM_REJUDGE_PROBLEM);
|
||||||
// TODO maybe async?
|
|
||||||
const rdocs = await record.getMulti(domainId, {
|
const rdocs = await record.getMulti(domainId, {
|
||||||
pid,
|
pid,
|
||||||
contest: { $ne: new ObjectID('0'.repeat(24)) },
|
contest: { $ne: new ObjectID('0'.repeat(24)) },
|
||||||
'files.hack': { $exists: false },
|
'files.hack': { $exists: false },
|
||||||
}).project({ _id: 1, contest: 1 }).toArray();
|
}).project({ _id: 1, contest: 1 }).toArray();
|
||||||
const priority = await record.submissionPriority(this.user._id, -10000 - rdocs.length * 5 - 50);
|
const priority = await record.submissionPriority(this.user._id, -10000 - rdocs.length * 5 - 50);
|
||||||
await Promise.all(rdocs.map(
|
await record.reset(domainId, rdocs.map((rdoc) => rdoc._id), true);
|
||||||
(doc) => record.reset(domainId, doc._id, true)
|
await Promise.all([
|
||||||
.then(() => record.judge(domainId, doc._id, priority, doc.contest ? { detail: false } : {})),
|
record.judge(domainId, rdocs.filter((i) => i.contest).map((i) => i._id), priority, { detail: false }),
|
||||||
));
|
record.judge(domainId, rdocs.filter((i) => !i.contest).map((i) => i._id), priority, {}),
|
||||||
|
]);
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,10 +100,7 @@ class RecordListHandler extends ContestDetailBaseHandler {
|
|||||||
filterStatus: status,
|
filterStatus: status,
|
||||||
};
|
};
|
||||||
if (this.user.hasPriv(PRIV.PRIV_VIEW_JUDGE_STATISTICS) && !full) {
|
if (this.user.hasPriv(PRIV.PRIV_VIEW_JUDGE_STATISTICS) && !full) {
|
||||||
this.response.body.statistics = {
|
this.response.body.statistics = await record.stat(all ? undefined : domainId);
|
||||||
...await record.stat(all ? undefined : domainId),
|
|
||||||
delay: await TaskModel.getDelay({ type: 'judge' }),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import domain from '../model/domain';
|
|||||||
import oauth from '../model/oauth';
|
import oauth from '../model/oauth';
|
||||||
import * as oplog from '../model/oplog';
|
import * as oplog from '../model/oplog';
|
||||||
import problem, { ProblemDoc } from '../model/problem';
|
import problem, { ProblemDoc } from '../model/problem';
|
||||||
|
import ScheduleModel from '../model/schedule';
|
||||||
import SolutionModel from '../model/solution';
|
import SolutionModel from '../model/solution';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import task from '../model/task';
|
|
||||||
import token from '../model/token';
|
import token from '../model/token';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
import {
|
import {
|
||||||
@@ -376,7 +376,7 @@ class UserDetailHandler extends Handler {
|
|||||||
class UserDeleteHandler extends Handler {
|
class UserDeleteHandler extends Handler {
|
||||||
async post({ password }) {
|
async post({ password }) {
|
||||||
this.user.checkPassword(password);
|
this.user.checkPassword(password);
|
||||||
const tid = await task.add({
|
const tid = await ScheduleModel.add({
|
||||||
executeAfter: moment().add(7, 'days').toDate(),
|
executeAfter: moment().add(7, 'days').toDate(),
|
||||||
type: 'script',
|
type: 'script',
|
||||||
id: 'deleteUser',
|
id: 'deleteUser',
|
||||||
|
|||||||
@@ -551,11 +551,18 @@ export interface Task {
|
|||||||
_id: ObjectID;
|
_id: ObjectID;
|
||||||
type: string;
|
type: string;
|
||||||
subType?: string;
|
subType?: string;
|
||||||
executeAfter: Date;
|
|
||||||
priority: number;
|
priority: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Schedule {
|
||||||
|
_id: ObjectID;
|
||||||
|
type: string;
|
||||||
|
subType?: string;
|
||||||
|
executeAfter: Date;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileNode {
|
export interface FileNode {
|
||||||
/** File Path In S3 */
|
/** File Path In S3 */
|
||||||
_id: string;
|
_id: string;
|
||||||
@@ -618,6 +625,7 @@ declare module './service/db' {
|
|||||||
'oplog': OplogDoc;
|
'oplog': OplogDoc;
|
||||||
'event': EventDoc;
|
'event': EventDoc;
|
||||||
'opcount': OpCountDoc;
|
'opcount': OpCountDoc;
|
||||||
|
'schedule': Schedule;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,6 +645,7 @@ export interface Model {
|
|||||||
solution: typeof import('./model/solution').default,
|
solution: typeof import('./model/solution').default,
|
||||||
system: typeof import('./model/system'),
|
system: typeof import('./model/system'),
|
||||||
task: typeof import('./model/task').default,
|
task: typeof import('./model/task').default,
|
||||||
|
schedule: typeof import('./model/schedule').default;
|
||||||
oplog: typeof import('./model/oplog'),
|
oplog: typeof import('./model/oplog'),
|
||||||
token: typeof import('./model/token').default,
|
token: typeof import('./model/token').default,
|
||||||
training: typeof import('./model/training'),
|
training: typeof import('./model/training'),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { buildProjection } from '../utils';
|
|||||||
import * as contest from './contest';
|
import * as contest from './contest';
|
||||||
import * as document from './document';
|
import * as document from './document';
|
||||||
import problem from './problem';
|
import problem from './problem';
|
||||||
import TaskModel from './task';
|
import ScheduleModel from './schedule';
|
||||||
import * as training from './training';
|
import * as training from './training';
|
||||||
|
|
||||||
export interface DiscussionDoc extends Document { }
|
export interface DiscussionDoc extends Document { }
|
||||||
@@ -294,8 +294,8 @@ async function updateSort() {
|
|||||||
export function apply(ctx: Context) {
|
export function apply(ctx: Context) {
|
||||||
ctx.using(['worker'], async ({ worker }) => {
|
ctx.using(['worker'], async ({ worker }) => {
|
||||||
worker.addHandler('discussion.sort', updateSort);
|
worker.addHandler('discussion.sort', updateSort);
|
||||||
if (!await TaskModel.count({ type: 'schedule', subType: 'discussion.sort' })) {
|
if (!await ScheduleModel.count({ type: 'schedule', subType: 'discussion.sort' })) {
|
||||||
await TaskModel.add({
|
await ScheduleModel.add({
|
||||||
type: 'schedule',
|
type: 'schedule',
|
||||||
subType: 'discussion.sort',
|
subType: 'discussion.sort',
|
||||||
executeAfter: moment().minute(0).second(0).millisecond(0).toDate(),
|
executeAfter: moment().minute(0).second(0).millisecond(0).toDate(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable object-curly-newline */
|
/* eslint-disable object-curly-newline */
|
||||||
import { omit, sum } from 'lodash';
|
import { sum } from 'lodash';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import {
|
import {
|
||||||
FilterQuery, MatchKeysAndValues,
|
FilterQuery, MatchKeysAndValues,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { Context } from '../context';
|
import { Context } from '../context';
|
||||||
import { ProblemNotFoundError } from '../error';
|
import { ProblemNotFoundError } from '../error';
|
||||||
import {
|
import {
|
||||||
FileInfo, JudgeMeta, JudgeRequest, ProblemConfigFile, RecordDoc,
|
FileInfo, JudgeMeta, ProblemConfigFile, RecordDoc,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import { MaybeArray, NumberKeys } from '../typeutils';
|
import { MaybeArray, NumberKeys } from '../typeutils';
|
||||||
@@ -63,8 +63,10 @@ export default class RecordModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async judge(domainId: string, rid: ObjectID, priority = 0, config: ProblemConfigFile = {}, meta: Partial<JudgeMeta> = {}) {
|
static async judge(domainId: string, rids: MaybeArray<ObjectID>, priority = 0, config: ProblemConfigFile = {}, meta: Partial<JudgeMeta> = {}) {
|
||||||
const rdoc = await RecordModel.get(domainId, rid);
|
rids = rids instanceof Array ? rids : [rids];
|
||||||
|
if (!rids.length) return null;
|
||||||
|
const rdoc = await RecordModel.get(domainId, rids[0]);
|
||||||
if (!rdoc) return null;
|
if (!rdoc) return null;
|
||||||
let data: FileInfo[] = [];
|
let data: FileInfo[] = [];
|
||||||
let source = `${domainId}/${rdoc.pid}`;
|
let source = `${domainId}/${rdoc.pid}`;
|
||||||
@@ -81,23 +83,20 @@ export default class RecordModel {
|
|||||||
data = pdoc.data;
|
data = pdoc.data;
|
||||||
if (typeof pdoc.config === 'string') throw new Error(pdoc.config);
|
if (typeof pdoc.config === 'string') throw new Error(pdoc.config);
|
||||||
if (pdoc.config.type === 'remote_judge') {
|
if (pdoc.config.type === 'remote_judge') {
|
||||||
if (rdoc.contest?.toHexString() !== '0'.repeat(24)) {
|
await task.deleteMany({ rid: { $in: rids } });
|
||||||
return await task.add({
|
return await task.addMany(rids.map((rid) => ({
|
||||||
...omit(rdoc, ['_id']),
|
...(pdoc.config as any),
|
||||||
priority,
|
priority,
|
||||||
type: 'remotejudge',
|
type: 'remotejudge',
|
||||||
subType: pdoc.config.subType,
|
rid,
|
||||||
target: pdoc.config.target,
|
domainId,
|
||||||
rid,
|
config,
|
||||||
domainId,
|
data,
|
||||||
config,
|
} as any)));
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await task.add({
|
await task.deleteMany({ rid: { $in: rids } });
|
||||||
...omit(rdoc, ['_id']),
|
return await task.addMany(rids.map((rid) => ({
|
||||||
priority,
|
priority,
|
||||||
type: 'judge',
|
type: 'judge',
|
||||||
rid,
|
rid,
|
||||||
@@ -106,7 +105,7 @@ export default class RecordModel {
|
|||||||
data,
|
data,
|
||||||
source,
|
source,
|
||||||
meta,
|
meta,
|
||||||
} as unknown as JudgeRequest);
|
} as any)));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async add(
|
static async add(
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import moment from 'moment-timezone';
|
||||||
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
|
import { sleep } from '@hydrooj/utils/lib/utils';
|
||||||
|
import { Context, Service } from '../context';
|
||||||
|
import { Schedule } from '../interface';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
import * as bus from '../service/bus';
|
||||||
|
import db from '../service/db';
|
||||||
|
|
||||||
|
const logger = new Logger('model/schedule');
|
||||||
|
const coll = db.collection('schedule');
|
||||||
|
|
||||||
|
async function getFirst(query: FilterQuery<Schedule>) {
|
||||||
|
if (process.env.CI) return null;
|
||||||
|
const q = { ...query };
|
||||||
|
q.executeAfter = q.executeAfter || { $lt: new Date() };
|
||||||
|
const res = await coll.findOneAndDelete(q);
|
||||||
|
if (res.value) {
|
||||||
|
logger.debug('%o', res.value);
|
||||||
|
if (res.value.interval) {
|
||||||
|
const executeAfter = moment(res.value.executeAfter).add(...res.value.interval).toDate();
|
||||||
|
await coll.insertOne({ ...res.value, executeAfter });
|
||||||
|
}
|
||||||
|
return res.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Consumer {
|
||||||
|
consuming: boolean;
|
||||||
|
running?: any;
|
||||||
|
|
||||||
|
constructor(public filter: any, public func: Function, public destoryOnError = true) {
|
||||||
|
this.consuming = true;
|
||||||
|
this.consume();
|
||||||
|
bus.on('app/exit', this.destory);
|
||||||
|
}
|
||||||
|
|
||||||
|
async consume() {
|
||||||
|
while (this.consuming) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const res = await getFirst(this.filter);
|
||||||
|
if (res) {
|
||||||
|
this.running = res;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.func(res);
|
||||||
|
this.running = null;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
} else await sleep(1000);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
if (this.destoryOnError) this.destory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async destory() {
|
||||||
|
this.consuming = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WorkerService extends Service {
|
||||||
|
private handlers: Record<string, Function> = {};
|
||||||
|
public consumer = new Consumer(
|
||||||
|
{ type: 'schedule', subType: { $in: Object.keys(this.handlers) } },
|
||||||
|
async (doc) => {
|
||||||
|
try {
|
||||||
|
logger.debug('Worker task: %o', doc);
|
||||||
|
const start = Date.now();
|
||||||
|
await Promise.race([
|
||||||
|
this.handlers[doc.subType](doc),
|
||||||
|
sleep(300000),
|
||||||
|
]);
|
||||||
|
const spent = Date.now() - start;
|
||||||
|
if (spent > 500) logger.warn('Slow worker task (%d ms): %s', spent, doc);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Worker task fail: ', e);
|
||||||
|
logger.error('%o', doc);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
public addHandler(type: string, handler: Function) {
|
||||||
|
this.handlers[type] = handler;
|
||||||
|
this.consumer.filter = { type: 'schedule', subType: { $in: Object.keys(this.handlers) } };
|
||||||
|
this.caller?.on('dispose', () => {
|
||||||
|
delete this.handlers[type];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Worker = new WorkerService(app, 'worker', false);
|
||||||
|
|
||||||
|
class ScheduleModel {
|
||||||
|
static coll = coll;
|
||||||
|
|
||||||
|
static async add(task: Partial<Schedule> & { type: string }) {
|
||||||
|
const res = await coll.insertOne({
|
||||||
|
...task,
|
||||||
|
executeAfter: task.executeAfter || new Date(),
|
||||||
|
_id: new ObjectID(),
|
||||||
|
});
|
||||||
|
return res.insertedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get(_id: ObjectID) {
|
||||||
|
return coll.findOne({ _id });
|
||||||
|
}
|
||||||
|
|
||||||
|
static count(query: FilterQuery<Schedule>) {
|
||||||
|
return coll.find(query).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
static del(_id: ObjectID) {
|
||||||
|
return coll.deleteOne({ _id });
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteMany(query: FilterQuery<Schedule>) {
|
||||||
|
return coll.deleteMany(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFirst = getFirst;
|
||||||
|
static Worker = Worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '../context' {
|
||||||
|
interface Context {
|
||||||
|
worker: WorkerService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apply(ctx: Context) {
|
||||||
|
Context.service('worker', WorkerService);
|
||||||
|
ctx.worker = Worker;
|
||||||
|
|
||||||
|
Worker.addHandler('task.daily', async () => {
|
||||||
|
await global.Hydro.model.record.coll.deleteMany({ contest: new ObjectID('000000000000000000000000') });
|
||||||
|
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
|
||||||
|
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
|
||||||
|
if (global.Hydro.model.system.get('server.checkUpdate') && !(new Date().getDay() % 3)) {
|
||||||
|
await global.Hydro.script.checkUpdate?.run({}, new Logger('task/checkUpdate').debug);
|
||||||
|
}
|
||||||
|
await ctx.parallel('task/daily');
|
||||||
|
});
|
||||||
|
ctx.on('domain/delete', (domainId) => coll.deleteMany({ domainId }));
|
||||||
|
|
||||||
|
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
||||||
|
if (!await ScheduleModel.count({ type: 'schedule', subType: 'task.daily' })) {
|
||||||
|
await ScheduleModel.add({
|
||||||
|
type: 'schedule',
|
||||||
|
subType: 'task.daily',
|
||||||
|
executeAfter: moment().add(1, 'day').hour(3).minute(0).second(0).millisecond(0).toDate(),
|
||||||
|
interval: [1, 'day'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await db.ensureIndexes(coll, { name: 'schedule', key: { type: 1, subType: 1, executeAfter: -1 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScheduleModel;
|
||||||
|
global.Hydro.model.schedule = ScheduleModel;
|
||||||
@@ -7,8 +7,8 @@ import type { Readable } from 'stream';
|
|||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import storage from '../service/storage';
|
import storage from '../service/storage';
|
||||||
|
import ScheduleModel from './schedule';
|
||||||
import * as system from './system';
|
import * as system from './system';
|
||||||
import TaskModel from './task';
|
|
||||||
|
|
||||||
export class StorageModel {
|
export class StorageModel {
|
||||||
static coll = db.collection('storage');
|
static coll = db.collection('storage');
|
||||||
@@ -135,15 +135,15 @@ async function cleanFiles() {
|
|||||||
res = await StorageModel.coll.findOneAndDelete({ autoDelete: { $lte: new Date() } });
|
res = await StorageModel.coll.findOneAndDelete({ autoDelete: { $lte: new Date() } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TaskModel.Worker.addHandler('storage.prune', cleanFiles);
|
ScheduleModel.Worker.addHandler('storage.prune', cleanFiles);
|
||||||
bus.on('ready', async () => {
|
bus.on('ready', async () => {
|
||||||
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
||||||
await db.ensureIndexes(
|
await db.ensureIndexes(
|
||||||
StorageModel.coll,
|
StorageModel.coll,
|
||||||
{ key: { path: 1, autoDelete: 1 }, sparse: true, name: 'autoDelete' },
|
{ key: { path: 1, autoDelete: 1 }, sparse: true, name: 'autoDelete' },
|
||||||
);
|
);
|
||||||
if (!await TaskModel.count({ type: 'schedule', subType: 'storage.prune' })) {
|
if (!await ScheduleModel.count({ type: 'schedule', subType: 'storage.prune' })) {
|
||||||
await TaskModel.add({
|
await ScheduleModel.add({
|
||||||
type: 'schedule',
|
type: 'schedule',
|
||||||
subType: 'storage.prune',
|
subType: 'storage.prune',
|
||||||
executeAfter: moment().startOf('hour').toDate(),
|
executeAfter: moment().startOf('hour').toDate(),
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { hostname } from 'os';
|
import { hostname } from 'os';
|
||||||
import moment from 'moment-timezone';
|
|
||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { sleep } from '@hydrooj/utils/lib/utils';
|
import { sleep } from '@hydrooj/utils/lib/utils';
|
||||||
import { Context, Service } from '../context';
|
import { Context } from '../context';
|
||||||
import { EventDoc, Task } from '../interface';
|
import { EventDoc, Task } from '../interface';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
@@ -16,14 +15,9 @@ const collEvent = db.collection('event');
|
|||||||
async function getFirst(query: FilterQuery<Task>) {
|
async function getFirst(query: FilterQuery<Task>) {
|
||||||
if (process.env.CI) return null;
|
if (process.env.CI) return null;
|
||||||
const q = { ...query };
|
const q = { ...query };
|
||||||
q.executeAfter = q.executeAfter || { $lt: new Date() };
|
|
||||||
const res = await coll.findOneAndDelete(q, { sort: { priority: -1 } });
|
const res = await coll.findOneAndDelete(q, { sort: { priority: -1 } });
|
||||||
if (res.value) {
|
if (res.value) {
|
||||||
logger.debug('%o', res.value);
|
logger.debug('%o', res.value);
|
||||||
if (res.value.interval) {
|
|
||||||
const executeAfter = moment(res.value.executeAfter).add(...res.value.interval).toDate();
|
|
||||||
await coll.insertOne({ ...res.value, executeAfter });
|
|
||||||
}
|
|
||||||
return res.value;
|
return res.value;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -63,51 +57,24 @@ class Consumer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WorkerService extends Service {
|
|
||||||
private handlers: Record<string, Function> = {};
|
|
||||||
public consumer = new Consumer(
|
|
||||||
{ type: 'schedule', subType: { $in: Object.keys(this.handlers) } },
|
|
||||||
async (doc) => {
|
|
||||||
try {
|
|
||||||
logger.debug('Worker task: %o', doc);
|
|
||||||
const start = Date.now();
|
|
||||||
await Promise.race([
|
|
||||||
this.handlers[doc.subType](doc),
|
|
||||||
sleep(300000),
|
|
||||||
]);
|
|
||||||
const spent = Date.now() - start;
|
|
||||||
if (spent > 500) logger.warn('Slow worker task (%d ms): %o', spent, doc);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Worker task fail: ', e);
|
|
||||||
logger.error('%o', doc);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
public addHandler(type: string, handler: Function) {
|
|
||||||
this.handlers[type] = handler;
|
|
||||||
this.consumer.filter = { type: 'schedule', subType: { $in: Object.keys(this.handlers) } };
|
|
||||||
this.caller?.on('dispose', () => {
|
|
||||||
delete this.handlers[type];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Worker = new WorkerService(app, 'worker', false);
|
|
||||||
|
|
||||||
class TaskModel {
|
class TaskModel {
|
||||||
|
static coll = coll;
|
||||||
|
|
||||||
static async add(task: Partial<Task> & { type: string }) {
|
static async add(task: Partial<Task> & { type: string }) {
|
||||||
const t: Task = {
|
const t: Task = {
|
||||||
...task,
|
...task,
|
||||||
priority: task.priority ?? 0,
|
priority: task.priority ?? 0,
|
||||||
executeAfter: task.executeAfter || new Date(),
|
|
||||||
_id: new ObjectID(),
|
_id: new ObjectID(),
|
||||||
};
|
};
|
||||||
const res = await coll.insertOne(t);
|
const res = await coll.insertOne(t);
|
||||||
return res.insertedId;
|
return res.insertedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async addMany(tasks: Task[]) {
|
||||||
|
const res = await coll.insertMany(tasks);
|
||||||
|
return res.insertedIds;
|
||||||
|
}
|
||||||
|
|
||||||
static get(_id: ObjectID) {
|
static get(_id: ObjectID) {
|
||||||
return coll.findOne({ _id });
|
return coll.findOne({ _id });
|
||||||
}
|
}
|
||||||
@@ -126,43 +93,14 @@ class TaskModel {
|
|||||||
|
|
||||||
static getFirst = getFirst;
|
static getFirst = getFirst;
|
||||||
|
|
||||||
static async getDelay(query?: FilterQuery<Task>): Promise<[number, Date]> {
|
|
||||||
const now = new Date();
|
|
||||||
const [res] = await coll.find(query).sort({ executeAfter: 1 }).limit(1).toArray();
|
|
||||||
if (res) return [Math.max(0, now.getTime() - res.executeAfter.getTime()), res.executeAfter];
|
|
||||||
return [0, now];
|
|
||||||
}
|
|
||||||
|
|
||||||
static async consume(query: any, cb: Function, destoryOnError = true) {
|
static async consume(query: any, cb: Function, destoryOnError = true) {
|
||||||
return new Consumer(query, cb, destoryOnError);
|
return new Consumer(query, cb, destoryOnError);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Consumer = Consumer;
|
|
||||||
static WorkerService = WorkerService;
|
|
||||||
static Worker = Worker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = process.env.exec_mode === 'cluster_mode' ? hostname() : nanoid();
|
const id = process.env.exec_mode === 'cluster_mode' ? hostname() : nanoid();
|
||||||
|
|
||||||
declare module '../context' {
|
|
||||||
interface Context {
|
|
||||||
worker: WorkerService;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
Context.service('worker', WorkerService);
|
|
||||||
ctx.worker = Worker;
|
|
||||||
|
|
||||||
Worker.addHandler('task.daily', async () => {
|
|
||||||
await global.Hydro.model.record.coll.deleteMany({ contest: new ObjectID('000000000000000000000000') });
|
|
||||||
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
|
|
||||||
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
|
|
||||||
if (global.Hydro.model.system.get('server.checkUpdate') && !(new Date().getDay() % 3)) {
|
|
||||||
await global.Hydro.script.checkUpdate?.run({}, new Logger('task/checkUpdate').debug);
|
|
||||||
}
|
|
||||||
await ctx.parallel('task/daily');
|
|
||||||
});
|
|
||||||
ctx.on('domain/delete', (domainId) => coll.deleteMany({ domainId }));
|
ctx.on('domain/delete', (domainId) => coll.deleteMany({ domainId }));
|
||||||
ctx.on('bus/broadcast', (event, payload) => {
|
ctx.on('bus/broadcast', (event, payload) => {
|
||||||
collEvent.insertOne({
|
collEvent.insertOne({
|
||||||
@@ -174,14 +112,6 @@ export async function apply(ctx: Context) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
||||||
if (!await TaskModel.count({ type: 'schedule', subType: 'task.daily' })) {
|
|
||||||
await TaskModel.add({
|
|
||||||
type: 'schedule',
|
|
||||||
subType: 'task.daily',
|
|
||||||
executeAfter: moment().add(1, 'day').hour(3).minute(0).second(0).millisecond(0).toDate(),
|
|
||||||
interval: [1, 'day'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await collEvent.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
await collEvent.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
||||||
const stream = collEvent.watch();
|
const stream = collEvent.watch();
|
||||||
const handleEvent = async (doc: EventDoc) => {
|
const handleEvent = async (doc: EventDoc) => {
|
||||||
@@ -208,6 +138,7 @@ export async function apply(ctx: Context) {
|
|||||||
await (res.value ? handleEvent(res.value) : sleep(500));
|
await (res.value ? handleEvent(res.value) : sleep(500));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await db.ensureIndexes(coll, { name: 'task', key: { type: 1, subType: 1, priority: -1 } });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TaskModel;
|
export default TaskModel;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import domain from './model/domain';
|
|||||||
import MessageModel from './model/message';
|
import MessageModel from './model/message';
|
||||||
import problem from './model/problem';
|
import problem from './model/problem';
|
||||||
import RecordModel from './model/record';
|
import RecordModel from './model/record';
|
||||||
|
import ScheduleModel from './model/schedule';
|
||||||
import * as system from './model/system';
|
import * as system from './model/system';
|
||||||
import TaskModel from './model/task';
|
import TaskModel from './model/task';
|
||||||
import user from './model/user';
|
import user from './model/user';
|
||||||
@@ -395,7 +396,7 @@ const scripts: UpgradeScript[] = [
|
|||||||
const _FRESH_INSTALL_IGNORE = 1;
|
const _FRESH_INSTALL_IGNORE = 1;
|
||||||
const tasks = await db.collection('task').find({ type: 'schedule', subType: 'contest.problemHide' }).toArray();
|
const tasks = await db.collection('task').find({ type: 'schedule', subType: 'contest.problemHide' }).toArray();
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
await TaskModel.add({ ...task, subType: 'contest', operation: ['unhide'] });
|
await ScheduleModel.add({ ...task, subType: 'contest', operation: ['unhide'] });
|
||||||
}
|
}
|
||||||
await TaskModel.deleteMany({ type: 'schedule', subType: 'contest.problemHide' });
|
await TaskModel.deleteMany({ type: 'schedule', subType: 'contest.problemHide' });
|
||||||
return true;
|
return true;
|
||||||
@@ -563,6 +564,12 @@ const scripts: UpgradeScript[] = [
|
|||||||
await db.collection('cache' as any).deleteMany({});
|
await db.collection('cache' as any).deleteMany({});
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
async function _69_70() {
|
||||||
|
const tasks = await TaskModel.coll.find({ type: 'schedule', subType: 'contest' }).toArray();
|
||||||
|
if (tasks.length) await ScheduleModel.coll.insertMany(tasks as any);
|
||||||
|
await TaskModel.coll.deleteMany({});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default scripts;
|
export default scripts;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/migrate",
|
"name": "@hydrooj/migrate",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"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>",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const langMap = {
|
|||||||
16: 'js',
|
16: 'js',
|
||||||
17: 'go',
|
17: 'go',
|
||||||
};
|
};
|
||||||
const nameMap = {
|
const nameMap: Record<string, string> = {
|
||||||
'sample.in': 'sample0.in',
|
'sample.in': 'sample0.in',
|
||||||
'sample.out': 'sample0.out',
|
'sample.out': 'sample0.out',
|
||||||
'test.in': 'test0.in',
|
'test.in': 'test0.in',
|
||||||
@@ -291,7 +291,7 @@ export async function run({
|
|||||||
report({ message: `Syncing testdata for ${file.name}` });
|
report({ message: `Syncing testdata for ${file.name}` });
|
||||||
for (const data of datas) {
|
for (const data of datas) {
|
||||||
if (data.isDirectory()) continue;
|
if (data.isDirectory()) continue;
|
||||||
const filename = nameMap[data.name] || data;
|
const filename = nameMap[data.name] || data.name;
|
||||||
await ProblemModel.addTestdata(domainId, pdoc.docId, filename, `${dataDir}/${file.name}/${data.name}`);
|
await ProblemModel.addTestdata(domainId, pdoc.docId, filename, `${dataDir}/${file.name}/${data.name}`);
|
||||||
}
|
}
|
||||||
await ProblemModel.addTestdata(domainId, pdoc.docId, 'config.yaml', Buffer.from(pdoc.config as string));
|
await ProblemModel.addTestdata(domainId, pdoc.docId, 'config.yaml', Buffer.from(pdoc.config as string));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { hostname } from 'os';
|
||||||
import { AggregatorRegistry, metric } from 'prom-client';
|
import { AggregatorRegistry, metric } from 'prom-client';
|
||||||
import { Context, Handler, SystemModel } from 'hydrooj';
|
import { Context, Handler, SystemModel } from 'hydrooj';
|
||||||
import { createRegistry } from './metrics';
|
import { createRegistry } from './metrics';
|
||||||
@@ -39,10 +40,11 @@ class MetricsHandler extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function apply(ctx: Context) {
|
export function apply(ctx: Context) {
|
||||||
|
if (process.env.HYDRO_CLI) return;
|
||||||
const registry = createRegistry(ctx);
|
const registry = createRegistry(ctx);
|
||||||
ctx.on('metrics', (id, metrics) => { instances[id] = metrics; });
|
ctx.on('metrics', (id, metrics) => { instances[id] = metrics; });
|
||||||
ctx.setInterval(async () => {
|
ctx.setInterval(async () => {
|
||||||
ctx.broadcast('metrics', process.env.NODE_APP_INSTANCE!, await registry.getMetricsAsJSON());
|
ctx.broadcast('metrics', `${hostname()}/${process.env.NODE_APP_INSTANCE}`, await registry.getMetricsAsJSON());
|
||||||
}, 5000 * (+SystemModel.get('prom-client.collect_rate') || 1));
|
}, 5000 * (+SystemModel.get('prom-client.collect_rate') || 1));
|
||||||
ctx.Route('metrics', '/metrics', MetricsHandler);
|
ctx.Route('metrics', '/metrics', MetricsHandler);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/prom-client",
|
"name": "@hydrooj/prom-client",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prom-client": "^14.1.0"
|
"prom-client": "^14.1.0"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default connect((state: any) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}))(class MonacoEditor extends React.PureComponent<ScratchpadOptions> {
|
}))(class MonacoEditor extends React.PureComponent<ScratchpadOptions> {
|
||||||
disposable = [];
|
disposable: monaco.IDisposable[] = [];
|
||||||
__prevent_trigger_change_event = false;
|
__prevent_trigger_change_event = false;
|
||||||
model: monaco.editor.ITextModel;
|
model: monaco.editor.ITextModel;
|
||||||
editor: monaco.editor.IStandaloneCodeEditor;
|
editor: monaco.editor.IStandaloneCodeEditor;
|
||||||
@@ -78,7 +78,7 @@ export default connect((state: any) => ({
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
range: model.getFullModelRange(),
|
range: model.getFullModelRange(),
|
||||||
text: value,
|
text: value!,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => null,
|
() => null,
|
||||||
|
|||||||
@@ -4,37 +4,36 @@ import i18n from 'vj/utils/i18n';
|
|||||||
import tpl from 'vj/utils/tpl';
|
import tpl from 'vj/utils/tpl';
|
||||||
import createHint from './hint';
|
import createHint from './hint';
|
||||||
|
|
||||||
let inserted = false;
|
let hintInserted = false;
|
||||||
|
$(tpl`<div style="display: none" class="dialog__body--user-select">
|
||||||
|
<div class="row"><div class="columns">
|
||||||
|
<h1 name="select_user_hint">${i18n('Select User')}</h1>
|
||||||
|
</div></div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="columns">
|
||||||
|
<label>${i18n('Username / UID')}
|
||||||
|
<input name="user" type="text" class="textbox" autocomplete="off" data-autofocus>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`).appendTo(document.body);
|
||||||
|
const userSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--user-select [name="user"]')) as any;
|
||||||
|
const userSelectDialog = new ActionDialog({
|
||||||
|
$body: $('.dialog__body--user-select > div'),
|
||||||
|
onDispatch(action) {
|
||||||
|
if (action === 'ok' && userSelector.value() === null) {
|
||||||
|
userSelector.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default async function selectUser(hint?: string) {
|
export default async function selectUser(hint?: string) {
|
||||||
if (!inserted) {
|
if (hint && !hintInserted) {
|
||||||
const el = $(tpl`<div style="display: none" class="dialog__body--user-select">
|
createHint(hint, $('[name="select_user_hint"]'));
|
||||||
<div class="row"><div class="columns">
|
hintInserted = true;
|
||||||
<h1 name="select_user_hint">${i18n('Select User')}</h1>
|
|
||||||
</div></div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="columns">
|
|
||||||
<label>${i18n('Username / UID')}
|
|
||||||
<input name="user" type="text" class="textbox" autocomplete="off" data-autofocus>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`);
|
|
||||||
el.appendTo(document.body);
|
|
||||||
if (hint) createHint(hint, $('[name="select_user_hint"]'));
|
|
||||||
inserted = true;
|
|
||||||
}
|
}
|
||||||
const userSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--user-select [name="user"]')) as any;
|
|
||||||
const userSelectDialog = new ActionDialog({
|
|
||||||
$body: $('.dialog__body--user-select > div'),
|
|
||||||
onDispatch(action) {
|
|
||||||
if (action === 'ok' && userSelector.value() === null) {
|
|
||||||
userSelector.focus();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
userSelector.clear();
|
userSelector.clear();
|
||||||
const action = await userSelectDialog.open();
|
const action = await userSelectDialog.open();
|
||||||
if (action !== 'ok') return null;
|
if (action !== 'ok') return null;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.41.14",
|
"version": "4.41.17",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ const page = new NamedPage('manage_user_priv', () => {
|
|||||||
|
|
||||||
async function changeUserPriv(uid, priv) {
|
async function changeUserPriv(uid, priv) {
|
||||||
try {
|
try {
|
||||||
const res = await request.post('', uid !== 'default' ? {
|
const res = await request.post('', {
|
||||||
uid,
|
uid: uid !== 'default' ? uid : 0,
|
||||||
priv,
|
priv,
|
||||||
} : { priv });
|
system: uid === 'default',
|
||||||
|
});
|
||||||
if (res.url && res.url !== window.location.href) window.location.href = res.url;
|
if (res.url && res.url !== window.location.href) window.location.href = res.url;
|
||||||
else {
|
else {
|
||||||
Notification.success(i18n('Priv has been updated to {0}.', priv));
|
Notification.success(i18n('Priv has been updated to {0}.', priv));
|
||||||
@@ -54,8 +55,8 @@ const page = new NamedPage('manage_user_priv', () => {
|
|||||||
handleOpenUserPrivDialog(user.udoc);
|
handleOpenUserPrivDialog(user.udoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('[name="select_user"]').on('click', () => handleClickSelectUser());
|
$(document).on('click', '[name="select_user"]', () => handleClickSelectUser());
|
||||||
$('[name="set_priv"]').on('click', (ev) => handleOpenUserPrivDialog(ev));
|
$(document).on('click', '[name="set_priv"]', (ev) => handleOpenUserPrivDialog(ev));
|
||||||
});
|
});
|
||||||
|
|
||||||
export default page;
|
export default page;
|
||||||
@@ -163,18 +163,26 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
|||||||
$('.loader-container').hide();
|
$('.loader-container').hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let progress = false;
|
||||||
|
|
||||||
async function enterScratchpadMode() {
|
async function enterScratchpadMode() {
|
||||||
|
if (progress) return;
|
||||||
|
progress = true;
|
||||||
await extender.extend();
|
await extender.extend();
|
||||||
await loadReact();
|
await loadReact();
|
||||||
renderReact();
|
renderReact();
|
||||||
await scratchpadFadeIn();
|
await scratchpadFadeIn();
|
||||||
|
progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function leaveScratchpadMode() {
|
async function leaveScratchpadMode() {
|
||||||
|
if (progress) return;
|
||||||
|
progress = true;
|
||||||
await scratchpadFadeOut();
|
await scratchpadFadeOut();
|
||||||
$('.problem-content-container').append($('.problem-content'));
|
$('.problem-content-container').append($('.problem-content'));
|
||||||
await extender.collapse();
|
await extender.collapse();
|
||||||
unmountReact();
|
unmountReact();
|
||||||
|
progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjective() {
|
async function loadObjective() {
|
||||||
|
|||||||
@@ -124,7 +124,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<dl class="large horizontal">
|
<dl class="large horizontal">
|
||||||
<dt>{{ _('Delay')}}</dt><dd>{{ statistics.delay[0]|int }}</dd>
|
|
||||||
<dt>{{ _('5 Min')}}</dt><dd>{{ statistics['d5min'] }}</dd>
|
<dt>{{ _('5 Min')}}</dt><dd>{{ statistics['d5min'] }}</dd>
|
||||||
<dt>{{ _('1 Hour')}}</dt><dd>{{ statistics['d1h'] }}</dd>
|
<dt>{{ _('1 Hour')}}</dt><dd>{{ statistics['d1h'] }}</dd>
|
||||||
<dt>{{ _('Day') }}</dt><dd>{{ statistics['day'] }}</dd>
|
<dt>{{ _('Day') }}</dt><dd>{{ statistics['day'] }}</dd>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/vjudge",
|
"name": "@hydrooj/vjudge",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"description": "Submit problems to remote oj",
|
"description": "Submit problems to remote oj",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import {
|
import {
|
||||||
Context, db, DomainModel, JudgeHandler, Logger,
|
Context, db, DomainModel, JudgeHandler, Logger,
|
||||||
ProblemModel, sleep, STATUS, TaskModel, Time,
|
ProblemModel, RecordModel, sleep, STATUS, TaskModel, Time,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
import { BasicProvider, IBasicProvider, RemoteAccount } from './interface';
|
import { BasicProvider, IBasicProvider, RemoteAccount } from './interface';
|
||||||
import { getDifficulty } from './providers/codeforces';
|
import { getDifficulty } from './providers/codeforces';
|
||||||
@@ -35,6 +35,8 @@ class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async judge(task) {
|
async judge(task) {
|
||||||
|
const rdoc = await RecordModel.get(task.domainId, task.rid);
|
||||||
|
task = Object.assign(rdoc, task);
|
||||||
const next = (payload) => JudgeHandler.next({ ...payload, rid: task.rid });
|
const next = (payload) => JudgeHandler.next({ ...payload, rid: task.rid });
|
||||||
const end = (payload) => JudgeHandler.end({ ...payload, rid: task.rid });
|
const end = (payload) => JudgeHandler.end({ ...payload, rid: task.rid });
|
||||||
await next({ status: STATUS.STATUS_FETCHED });
|
await next({ status: STATUS.STATUS_FETCHED });
|
||||||
|
|||||||
Reference in new issue
Block a user