mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +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",
|
||||
"bin": "bin/hydrojudge.js",
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.1",
|
||||
"main": "package.json",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import type { JudgeResultBody } from 'hydrooj';
|
||||
import {
|
||||
SettingModel, StorageModel, SystemModel, TaskModel,
|
||||
JudgeResultBody, RecordModel, SettingModel,
|
||||
StorageModel, SystemModel, TaskModel,
|
||||
} from 'hydrooj';
|
||||
import { end, next } from 'hydrooj/src/handler/judge';
|
||||
import { processTestdata } from '../cases';
|
||||
@@ -75,7 +75,10 @@ const session = {
|
||||
|
||||
export async function postInit() {
|
||||
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', priority: { $gt: -50 } }, handle);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "4.0.18",
|
||||
"version": "4.1.1",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/plugin-api",
|
||||
"module": "src/plugin-api",
|
||||
|
||||
@@ -17,9 +17,9 @@ import message from '../model/message';
|
||||
import * as oplog from '../model/oplog';
|
||||
import problem from '../model/problem';
|
||||
import record from '../model/record';
|
||||
import ScheduleModel from '../model/schedule';
|
||||
import storage from '../model/storage';
|
||||
import * as system from '../model/system';
|
||||
import TaskModel from '../model/task';
|
||||
import user from '../model/user';
|
||||
import {
|
||||
Handler, param, post, Types,
|
||||
@@ -52,7 +52,7 @@ registerResolver(
|
||||
'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);
|
||||
if (!tdoc) return;
|
||||
const tasks = [];
|
||||
@@ -199,7 +199,7 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||
await contest.del(domainId, tid);
|
||||
await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' });
|
||||
await TaskModel.deleteMany({
|
||||
await ScheduleModel.deleteMany({
|
||||
type: 'schedule', subType: 'contest', domainId, tid,
|
||||
});
|
||||
this.response.redirect = this.url('contest_main');
|
||||
@@ -346,20 +346,18 @@ export class ContestEditHandler extends Handler {
|
||||
const task = {
|
||||
type: 'schedule', subType: 'contest', domainId, tid,
|
||||
};
|
||||
await TaskModel.deleteMany(task);
|
||||
await ScheduleModel.deleteMany(task);
|
||||
const operation = [];
|
||||
if (Date.now() <= endAt.getTime() && autoHide) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(pids.map((pid) => problem.edit(domainId, pid, { hidden: true })));
|
||||
await TaskModel.add({
|
||||
...task,
|
||||
operation: ['unhide'],
|
||||
executeAfter: endAt,
|
||||
});
|
||||
operation.push('unhide');
|
||||
}
|
||||
if (lock && lockAt <= endAt) {
|
||||
await TaskModel.add({
|
||||
if (lock && lockAt <= endAt) operation.push('unlock');
|
||||
if (operation.length) {
|
||||
await ScheduleModel.add({
|
||||
...task,
|
||||
operation: ['unlock'],
|
||||
operation,
|
||||
executeAfter: endAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,9 +103,7 @@ export async function postJudge(rdoc: RecordDoc) {
|
||||
contest: { $ne: new ObjectID('0'.repeat(24)) },
|
||||
}).project({ _id: 1, contest: 1 }).toArray();
|
||||
const priority = await record.submissionPriority(rdoc.uid, -5000 - rdocs.length * 5 - 50);
|
||||
await Promise.all(rdocs.map(
|
||||
(r) => record.judge(rdoc.domainId, r._id, priority, r.contest ? { detail: false } : {}, { hackRejudge: input }),
|
||||
));
|
||||
await record.judge(rdoc.domainId, rdocs.map((r) => r._id), priority, {}, { hackRejudge: input });
|
||||
} catch (e) {
|
||||
next({
|
||||
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
|
||||
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;
|
||||
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, {});
|
||||
}
|
||||
|
||||
|
||||
@@ -286,10 +286,11 @@ class SystemUserPrivHandler extends SystemHandler {
|
||||
}
|
||||
|
||||
@requireSudo
|
||||
@param('uid', Types.Int, true)
|
||||
@param('uid', Types.Int)
|
||||
@param('priv', Types.UnsignedInt)
|
||||
async post(domainId: string, uid: number, priv: number) {
|
||||
if (typeof uid === 'number') {
|
||||
@param('system', Types.Boolean, true)
|
||||
async post(domainId: string, uid: number, priv: number, editSystem: boolean) {
|
||||
if (!editSystem) {
|
||||
const udoc = await user.getById(domainId, 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.');
|
||||
|
||||
@@ -443,17 +443,17 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
|
||||
@param('pid', Types.UnsignedInt)
|
||||
async postRejudge(domainId: string, pid: number) {
|
||||
this.checkPerm(PERM.PERM_REJUDGE_PROBLEM);
|
||||
// TODO maybe async?
|
||||
const rdocs = await record.getMulti(domainId, {
|
||||
pid,
|
||||
contest: { $ne: new ObjectID('0'.repeat(24)) },
|
||||
'files.hack': { $exists: false },
|
||||
}).project({ _id: 1, contest: 1 }).toArray();
|
||||
const priority = await record.submissionPriority(this.user._id, -10000 - rdocs.length * 5 - 50);
|
||||
await Promise.all(rdocs.map(
|
||||
(doc) => record.reset(domainId, doc._id, true)
|
||||
.then(() => record.judge(domainId, doc._id, priority, doc.contest ? { detail: false } : {})),
|
||||
));
|
||||
await record.reset(domainId, rdocs.map((rdoc) => rdoc._id), true);
|
||||
await Promise.all([
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -100,10 +100,7 @@ class RecordListHandler extends ContestDetailBaseHandler {
|
||||
filterStatus: status,
|
||||
};
|
||||
if (this.user.hasPriv(PRIV.PRIV_VIEW_JUDGE_STATISTICS) && !full) {
|
||||
this.response.body.statistics = {
|
||||
...await record.stat(all ? undefined : domainId),
|
||||
delay: await TaskModel.getDelay({ type: 'judge' }),
|
||||
};
|
||||
this.response.body.statistics = await record.stat(all ? undefined : domainId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ import domain from '../model/domain';
|
||||
import oauth from '../model/oauth';
|
||||
import * as oplog from '../model/oplog';
|
||||
import problem, { ProblemDoc } from '../model/problem';
|
||||
import ScheduleModel from '../model/schedule';
|
||||
import SolutionModel from '../model/solution';
|
||||
import * as system from '../model/system';
|
||||
import task from '../model/task';
|
||||
import token from '../model/token';
|
||||
import user from '../model/user';
|
||||
import {
|
||||
@@ -376,7 +376,7 @@ class UserDetailHandler extends Handler {
|
||||
class UserDeleteHandler extends Handler {
|
||||
async post({ password }) {
|
||||
this.user.checkPassword(password);
|
||||
const tid = await task.add({
|
||||
const tid = await ScheduleModel.add({
|
||||
executeAfter: moment().add(7, 'days').toDate(),
|
||||
type: 'script',
|
||||
id: 'deleteUser',
|
||||
|
||||
@@ -551,11 +551,18 @@ export interface Task {
|
||||
_id: ObjectID;
|
||||
type: string;
|
||||
subType?: string;
|
||||
executeAfter: Date;
|
||||
priority: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
_id: ObjectID;
|
||||
type: string;
|
||||
subType?: string;
|
||||
executeAfter: Date;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FileNode {
|
||||
/** File Path In S3 */
|
||||
_id: string;
|
||||
@@ -618,6 +625,7 @@ declare module './service/db' {
|
||||
'oplog': OplogDoc;
|
||||
'event': EventDoc;
|
||||
'opcount': OpCountDoc;
|
||||
'schedule': Schedule;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,6 +645,7 @@ export interface Model {
|
||||
solution: typeof import('./model/solution').default,
|
||||
system: typeof import('./model/system'),
|
||||
task: typeof import('./model/task').default,
|
||||
schedule: typeof import('./model/schedule').default;
|
||||
oplog: typeof import('./model/oplog'),
|
||||
token: typeof import('./model/token').default,
|
||||
training: typeof import('./model/training'),
|
||||
|
||||
@@ -10,7 +10,7 @@ import { buildProjection } from '../utils';
|
||||
import * as contest from './contest';
|
||||
import * as document from './document';
|
||||
import problem from './problem';
|
||||
import TaskModel from './task';
|
||||
import ScheduleModel from './schedule';
|
||||
import * as training from './training';
|
||||
|
||||
export interface DiscussionDoc extends Document { }
|
||||
@@ -294,8 +294,8 @@ async function updateSort() {
|
||||
export function apply(ctx: Context) {
|
||||
ctx.using(['worker'], async ({ worker }) => {
|
||||
worker.addHandler('discussion.sort', updateSort);
|
||||
if (!await TaskModel.count({ type: 'schedule', subType: 'discussion.sort' })) {
|
||||
await TaskModel.add({
|
||||
if (!await ScheduleModel.count({ type: 'schedule', subType: 'discussion.sort' })) {
|
||||
await ScheduleModel.add({
|
||||
type: 'schedule',
|
||||
subType: 'discussion.sort',
|
||||
executeAfter: moment().minute(0).second(0).millisecond(0).toDate(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable object-curly-newline */
|
||||
import { omit, sum } from 'lodash';
|
||||
import { sum } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
FilterQuery, MatchKeysAndValues,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { Context } from '../context';
|
||||
import { ProblemNotFoundError } from '../error';
|
||||
import {
|
||||
FileInfo, JudgeMeta, JudgeRequest, ProblemConfigFile, RecordDoc,
|
||||
FileInfo, JudgeMeta, ProblemConfigFile, RecordDoc,
|
||||
} from '../interface';
|
||||
import db from '../service/db';
|
||||
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> = {}) {
|
||||
const rdoc = await RecordModel.get(domainId, rid);
|
||||
static async judge(domainId: string, rids: MaybeArray<ObjectID>, priority = 0, config: ProblemConfigFile = {}, meta: Partial<JudgeMeta> = {}) {
|
||||
rids = rids instanceof Array ? rids : [rids];
|
||||
if (!rids.length) return null;
|
||||
const rdoc = await RecordModel.get(domainId, rids[0]);
|
||||
if (!rdoc) return null;
|
||||
let data: FileInfo[] = [];
|
||||
let source = `${domainId}/${rdoc.pid}`;
|
||||
@@ -81,23 +83,20 @@ export default class RecordModel {
|
||||
data = pdoc.data;
|
||||
if (typeof pdoc.config === 'string') throw new Error(pdoc.config);
|
||||
if (pdoc.config.type === 'remote_judge') {
|
||||
if (rdoc.contest?.toHexString() !== '0'.repeat(24)) {
|
||||
return await task.add({
|
||||
...omit(rdoc, ['_id']),
|
||||
priority,
|
||||
type: 'remotejudge',
|
||||
subType: pdoc.config.subType,
|
||||
target: pdoc.config.target,
|
||||
rid,
|
||||
domainId,
|
||||
config,
|
||||
data,
|
||||
});
|
||||
}
|
||||
await task.deleteMany({ rid: { $in: rids } });
|
||||
return await task.addMany(rids.map((rid) => ({
|
||||
...(pdoc.config as any),
|
||||
priority,
|
||||
type: 'remotejudge',
|
||||
rid,
|
||||
domainId,
|
||||
config,
|
||||
data,
|
||||
} as any)));
|
||||
}
|
||||
}
|
||||
return await task.add({
|
||||
...omit(rdoc, ['_id']),
|
||||
await task.deleteMany({ rid: { $in: rids } });
|
||||
return await task.addMany(rids.map((rid) => ({
|
||||
priority,
|
||||
type: 'judge',
|
||||
rid,
|
||||
@@ -106,7 +105,7 @@ export default class RecordModel {
|
||||
data,
|
||||
source,
|
||||
meta,
|
||||
} as unknown as JudgeRequest);
|
||||
} as any)));
|
||||
}
|
||||
|
||||
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 db from '../service/db';
|
||||
import storage from '../service/storage';
|
||||
import ScheduleModel from './schedule';
|
||||
import * as system from './system';
|
||||
import TaskModel from './task';
|
||||
|
||||
export class StorageModel {
|
||||
static coll = db.collection('storage');
|
||||
@@ -135,15 +135,15 @@ async function cleanFiles() {
|
||||
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 () => {
|
||||
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
||||
await db.ensureIndexes(
|
||||
StorageModel.coll,
|
||||
{ key: { path: 1, autoDelete: 1 }, sparse: true, name: 'autoDelete' },
|
||||
);
|
||||
if (!await TaskModel.count({ type: 'schedule', subType: 'storage.prune' })) {
|
||||
await TaskModel.add({
|
||||
if (!await ScheduleModel.count({ type: 'schedule', subType: 'storage.prune' })) {
|
||||
await ScheduleModel.add({
|
||||
type: 'schedule',
|
||||
subType: 'storage.prune',
|
||||
executeAfter: moment().startOf('hour').toDate(),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { hostname } from 'os';
|
||||
import moment from 'moment-timezone';
|
||||
import { FilterQuery, ObjectID } from 'mongodb';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { sleep } from '@hydrooj/utils/lib/utils';
|
||||
import { Context, Service } from '../context';
|
||||
import { Context } from '../context';
|
||||
import { EventDoc, Task } from '../interface';
|
||||
import { Logger } from '../logger';
|
||||
import * as bus from '../service/bus';
|
||||
@@ -16,14 +15,9 @@ const collEvent = db.collection('event');
|
||||
async function getFirst(query: FilterQuery<Task>) {
|
||||
if (process.env.CI) return null;
|
||||
const q = { ...query };
|
||||
q.executeAfter = q.executeAfter || { $lt: new Date() };
|
||||
const res = await coll.findOneAndDelete(q, { sort: { priority: -1 } });
|
||||
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;
|
||||
@@ -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 {
|
||||
static coll = coll;
|
||||
|
||||
static async add(task: Partial<Task> & { type: string }) {
|
||||
const t: Task = {
|
||||
...task,
|
||||
priority: task.priority ?? 0,
|
||||
executeAfter: task.executeAfter || new Date(),
|
||||
_id: new ObjectID(),
|
||||
};
|
||||
const res = await coll.insertOne(t);
|
||||
return res.insertedId;
|
||||
}
|
||||
|
||||
static async addMany(tasks: Task[]) {
|
||||
const res = await coll.insertMany(tasks);
|
||||
return res.insertedIds;
|
||||
}
|
||||
|
||||
static get(_id: ObjectID) {
|
||||
return coll.findOne({ _id });
|
||||
}
|
||||
@@ -126,43 +93,14 @@ class TaskModel {
|
||||
|
||||
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) {
|
||||
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();
|
||||
|
||||
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 }));
|
||||
ctx.on('bus/broadcast', (event, payload) => {
|
||||
collEvent.insertOne({
|
||||
@@ -174,14 +112,6 @@ export async function apply(ctx: Context) {
|
||||
});
|
||||
|
||||
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 });
|
||||
const stream = collEvent.watch();
|
||||
const handleEvent = async (doc: EventDoc) => {
|
||||
@@ -208,6 +138,7 @@ export async function apply(ctx: Context) {
|
||||
await (res.value ? handleEvent(res.value) : sleep(500));
|
||||
}
|
||||
});
|
||||
await db.ensureIndexes(coll, { name: 'task', key: { type: 1, subType: 1, priority: -1 } });
|
||||
}
|
||||
|
||||
export default TaskModel;
|
||||
|
||||
@@ -15,6 +15,7 @@ import domain from './model/domain';
|
||||
import MessageModel from './model/message';
|
||||
import problem from './model/problem';
|
||||
import RecordModel from './model/record';
|
||||
import ScheduleModel from './model/schedule';
|
||||
import * as system from './model/system';
|
||||
import TaskModel from './model/task';
|
||||
import user from './model/user';
|
||||
@@ -395,7 +396,7 @@ const scripts: UpgradeScript[] = [
|
||||
const _FRESH_INSTALL_IGNORE = 1;
|
||||
const tasks = await db.collection('task').find({ type: 'schedule', subType: 'contest.problemHide' }).toArray();
|
||||
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' });
|
||||
return true;
|
||||
@@ -563,6 +564,12 @@ const scripts: UpgradeScript[] = [
|
||||
await db.collection('cache' as any).deleteMany({});
|
||||
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;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/migrate",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"main": "index.ts",
|
||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
|
||||
@@ -42,7 +42,7 @@ const langMap = {
|
||||
16: 'js',
|
||||
17: 'go',
|
||||
};
|
||||
const nameMap = {
|
||||
const nameMap: Record<string, string> = {
|
||||
'sample.in': 'sample0.in',
|
||||
'sample.out': 'sample0.out',
|
||||
'test.in': 'test0.in',
|
||||
@@ -291,7 +291,7 @@ export async function run({
|
||||
report({ message: `Syncing testdata for ${file.name}` });
|
||||
for (const data of datas) {
|
||||
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, 'config.yaml', Buffer.from(pdoc.config as string));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { hostname } from 'os';
|
||||
import { AggregatorRegistry, metric } from 'prom-client';
|
||||
import { Context, Handler, SystemModel } from 'hydrooj';
|
||||
import { createRegistry } from './metrics';
|
||||
@@ -39,10 +40,11 @@ class MetricsHandler extends Handler {
|
||||
}
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
if (process.env.HYDRO_CLI) return;
|
||||
const registry = createRegistry(ctx);
|
||||
ctx.on('metrics', (id, metrics) => { instances[id] = metrics; });
|
||||
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));
|
||||
ctx.Route('metrics', '/metrics', MetricsHandler);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/prom-client",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"prom-client": "^14.1.0"
|
||||
|
||||
@@ -26,7 +26,7 @@ export default connect((state: any) => ({
|
||||
});
|
||||
},
|
||||
}))(class MonacoEditor extends React.PureComponent<ScratchpadOptions> {
|
||||
disposable = [];
|
||||
disposable: monaco.IDisposable[] = [];
|
||||
__prevent_trigger_change_event = false;
|
||||
model: monaco.editor.ITextModel;
|
||||
editor: monaco.editor.IStandaloneCodeEditor;
|
||||
@@ -78,7 +78,7 @@ export default connect((state: any) => ({
|
||||
[
|
||||
{
|
||||
range: model.getFullModelRange(),
|
||||
text: value,
|
||||
text: value!,
|
||||
},
|
||||
],
|
||||
() => null,
|
||||
|
||||
@@ -4,37 +4,36 @@ import i18n from 'vj/utils/i18n';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
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) {
|
||||
if (!inserted) {
|
||||
const el = $(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>`);
|
||||
el.appendTo(document.body);
|
||||
if (hint) createHint(hint, $('[name="select_user_hint"]'));
|
||||
inserted = true;
|
||||
if (hint && !hintInserted) {
|
||||
createHint(hint, $('[name="select_user_hint"]'));
|
||||
hintInserted = 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();
|
||||
const action = await userSelectDialog.open();
|
||||
if (action !== 'ok') return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.41.14",
|
||||
"version": "4.41.17",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "index.ts",
|
||||
|
||||
@@ -20,10 +20,11 @@ const page = new NamedPage('manage_user_priv', () => {
|
||||
|
||||
async function changeUserPriv(uid, priv) {
|
||||
try {
|
||||
const res = await request.post('', uid !== 'default' ? {
|
||||
uid,
|
||||
const res = await request.post('', {
|
||||
uid: uid !== 'default' ? uid : 0,
|
||||
priv,
|
||||
} : { priv });
|
||||
system: uid === 'default',
|
||||
});
|
||||
if (res.url && res.url !== window.location.href) window.location.href = res.url;
|
||||
else {
|
||||
Notification.success(i18n('Priv has been updated to {0}.', priv));
|
||||
@@ -54,8 +55,8 @@ const page = new NamedPage('manage_user_priv', () => {
|
||||
handleOpenUserPrivDialog(user.udoc);
|
||||
}
|
||||
|
||||
$('[name="select_user"]').on('click', () => handleClickSelectUser());
|
||||
$('[name="set_priv"]').on('click', (ev) => handleOpenUserPrivDialog(ev));
|
||||
$(document).on('click', '[name="select_user"]', () => handleClickSelectUser());
|
||||
$(document).on('click', '[name="set_priv"]', (ev) => handleOpenUserPrivDialog(ev));
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -163,18 +163,26 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
$('.loader-container').hide();
|
||||
}
|
||||
|
||||
let progress = false;
|
||||
|
||||
async function enterScratchpadMode() {
|
||||
if (progress) return;
|
||||
progress = true;
|
||||
await extender.extend();
|
||||
await loadReact();
|
||||
renderReact();
|
||||
await scratchpadFadeIn();
|
||||
progress = false;
|
||||
}
|
||||
|
||||
async function leaveScratchpadMode() {
|
||||
if (progress) return;
|
||||
progress = true;
|
||||
await scratchpadFadeOut();
|
||||
$('.problem-content-container').append($('.problem-content'));
|
||||
await extender.collapse();
|
||||
unmountReact();
|
||||
progress = false;
|
||||
}
|
||||
|
||||
async function loadObjective() {
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
</div>
|
||||
<div class="section__body">
|
||||
<dl class="large horizontal">
|
||||
<dt>{{ _('Delay')}}</dt><dd>{{ statistics.delay[0]|int }}</dd>
|
||||
<dt>{{ _('5 Min')}}</dt><dd>{{ statistics['d5min'] }}</dd>
|
||||
<dt>{{ _('1 Hour')}}</dt><dd>{{ statistics['d1h'] }}</dd>
|
||||
<dt>{{ _('Day') }}</dt><dd>{{ statistics['day'] }}</dd>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/vjudge",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"description": "Submit problems to remote oj",
|
||||
"main": "package.json",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import os from 'os';
|
||||
import {
|
||||
Context, db, DomainModel, JudgeHandler, Logger,
|
||||
ProblemModel, sleep, STATUS, TaskModel, Time,
|
||||
ProblemModel, RecordModel, sleep, STATUS, TaskModel, Time,
|
||||
} from 'hydrooj';
|
||||
import { BasicProvider, IBasicProvider, RemoteAccount } from './interface';
|
||||
import { getDifficulty } from './providers/codeforces';
|
||||
@@ -35,6 +35,8 @@ class Service {
|
||||
}
|
||||
|
||||
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 end = (payload) => JudgeHandler.end({ ...payload, rid: task.rid });
|
||||
await next({ status: STATUS.STATUS_FETCHED });
|
||||
|
||||
Reference in new issue
Block a user