diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 81a18460d..129ddd887 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "3.1.0", + "version": "3.1.1", "main": "package.json", "author": "undefined ", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/hydrojudge/src/hosts/builtin.ts b/packages/hydrojudge/src/hosts/builtin.ts index 2b872625c..99e2d61af 100644 --- a/packages/hydrojudge/src/hosts/builtin.ts +++ b/packages/hydrojudge/src/hosts/builtin.ts @@ -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); } diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 2a086d07a..6c1f555fc 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -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", diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index c85b67dca..6229662c3 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -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, }); } diff --git a/packages/hydrooj/src/handler/judge.ts b/packages/hydrooj/src/handler/judge.ts index 716d1572e..3188f93e6 100644 --- a/packages/hydrooj/src/handler/judge.ts +++ b/packages/hydrooj/src/handler/judge.ts @@ -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, {}); } diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index e5ce1bd0b..931e9efff 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -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.'); diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 7e671dd9a..b303d4ece 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -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(); } diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index d1fd47a17..0adb85cfb 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -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); } } } diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index 8f7fd04ea..1795bc3f6 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -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', diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 46374ab07..376b3fc05 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -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'), diff --git a/packages/hydrooj/src/model/discussion.ts b/packages/hydrooj/src/model/discussion.ts index cb6602c0c..e182f035f 100644 --- a/packages/hydrooj/src/model/discussion.ts +++ b/packages/hydrooj/src/model/discussion.ts @@ -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(), diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index dde34d8c4..bb1ab8627 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -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 = {}) { - const rdoc = await RecordModel.get(domainId, rid); + static async judge(domainId: string, rids: MaybeArray, priority = 0, config: ProblemConfigFile = {}, meta: Partial = {}) { + 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( diff --git a/packages/hydrooj/src/model/schedule.ts b/packages/hydrooj/src/model/schedule.ts new file mode 100644 index 000000000..39534aae9 --- /dev/null +++ b/packages/hydrooj/src/model/schedule.ts @@ -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) { + 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 = {}; + 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 & { 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) { + return coll.find(query).count(); + } + + static del(_id: ObjectID) { + return coll.deleteOne({ _id }); + } + + static deleteMany(query: FilterQuery) { + 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; diff --git a/packages/hydrooj/src/model/storage.ts b/packages/hydrooj/src/model/storage.ts index 35d9c7576..9e87b6487 100644 --- a/packages/hydrooj/src/model/storage.ts +++ b/packages/hydrooj/src/model/storage.ts @@ -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(), diff --git a/packages/hydrooj/src/model/task.ts b/packages/hydrooj/src/model/task.ts index 88db45e97..4706306b2 100644 --- a/packages/hydrooj/src/model/task.ts +++ b/packages/hydrooj/src/model/task.ts @@ -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) { 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 = {}; - 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 & { 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): 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; diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index d30b1fcaf..63fcbacc4 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -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; diff --git a/packages/migrate/package.json b/packages/migrate/package.json index 1f8c354c7..9a049f4f7 100644 --- a/packages/migrate/package.json +++ b/packages/migrate/package.json @@ -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 ", diff --git a/packages/migrate/scripts/hustoj.ts b/packages/migrate/scripts/hustoj.ts index cb9232b18..484548f4c 100644 --- a/packages/migrate/scripts/hustoj.ts +++ b/packages/migrate/scripts/hustoj.ts @@ -42,7 +42,7 @@ const langMap = { 16: 'js', 17: 'go', }; -const nameMap = { +const nameMap: Record = { '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)); diff --git a/packages/prom-client/index.ts b/packages/prom-client/index.ts index b243ca6d0..15c5aacf6 100644 --- a/packages/prom-client/index.ts +++ b/packages/prom-client/index.ts @@ -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); } diff --git a/packages/prom-client/package.json b/packages/prom-client/package.json index 9cc703db4..ded73a611 100644 --- a/packages/prom-client/package.json +++ b/packages/prom-client/package.json @@ -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" diff --git a/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx b/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx index 1ccbecd59..0b6bf7382 100644 --- a/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx +++ b/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx @@ -26,7 +26,7 @@ export default connect((state: any) => ({ }); }, }))(class MonacoEditor extends React.PureComponent { - 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, diff --git a/packages/ui-default/components/selectUser.ts b/packages/ui-default/components/selectUser.ts index 49c384f27..d6e29a163 100644 --- a/packages/ui-default/components/selectUser.ts +++ b/packages/ui-default/components/selectUser.ts @@ -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``).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``); - 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; diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index bd13ed628..3221ca503 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.41.14", + "version": "4.41.17", "author": "undefined ", "license": "AGPL-3.0", "main": "index.ts", diff --git a/packages/ui-default/pages/manage_user_priv.page.js b/packages/ui-default/pages/manage_user_priv.page.js index aa5f132fd..b64a65507 100644 --- a/packages/ui-default/pages/manage_user_priv.page.js +++ b/packages/ui-default/pages/manage_user_priv.page.js @@ -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; diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index 7a62b0055..544481c71 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -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() { diff --git a/packages/ui-default/templates/record_main.html b/packages/ui-default/templates/record_main.html index df72c12b2..2be37ce54 100644 --- a/packages/ui-default/templates/record_main.html +++ b/packages/ui-default/templates/record_main.html @@ -124,7 +124,6 @@
-
{{ _('Delay')}}
{{ statistics.delay[0]|int }}
{{ _('5 Min')}}
{{ statistics['d5min'] }}
{{ _('1 Hour')}}
{{ statistics['d1h'] }}
{{ _('Day') }}
{{ statistics['day'] }}
diff --git a/packages/vjudge/package.json b/packages/vjudge/package.json index f3db54adc..28f979a49 100644 --- a/packages/vjudge/package.json +++ b/packages/vjudge/package.json @@ -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", diff --git a/packages/vjudge/src/model.ts b/packages/vjudge/src/model.ts index ef36b8428..735668742 100644 --- a/packages/vjudge/src/model.ts +++ b/packages/vjudge/src/model.ts @@ -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 });