core: experimental hot reload support

This commit is contained in:
undefined committed 2022-09-20 22:34:36 +08:00
1 parent 632ae27b88
commit fd7c9cbeb1
26 files changed
+429 -148

No files matched your search

+2 -4
View File
@@ -63,8 +63,6 @@ class DataReportHandler extends Handler {
}
}
export function apply() {
Route('data_report', '/center/report', DataReportHandler);
export function apply(ctx) {
ctx.Route('data_report', '/center/report', DataReportHandler);
}
global.Hydro.handler.center = apply;
+30 -27
View File
@@ -1,6 +1,6 @@
import { Client } from '@elastic/elasticsearch';
import { omit } from 'lodash';
import { addScript, Schema } from 'hydrooj';
import { Schema } from 'hydrooj';
import DomainModel from 'hydrooj/src/model/domain';
import ProblemModel, { ProblemDoc } from 'hydrooj/src/model/problem';
import * as system from 'hydrooj/src/model/system';
@@ -24,27 +24,6 @@ const processDocument = (doc: Partial<ProblemDoc>) => {
return omit(doc, indexOmit);
};
bus.on('problem/add', async (doc, docId) => {
await client.index({
index: 'problem',
id: `${doc.domainId}/${docId}`,
document: processDocument(doc),
});
});
bus.on('problem/edit', async (pdoc) => {
await client.index({
index: 'problem',
id: `${pdoc.domainId}/${pdoc.docId}`,
document: processDocument(pdoc),
});
});
bus.on('problem/del', async (domainId, docId) => {
await client.delete({
index: 'problem',
id: `${domainId}/${docId}`,
});
});
global.Hydro.lib.problemSearch = async (domainId, q, opts) => {
const allowedSize = system.get('elasic-search.indexSize') || 10000;
const size = opts?.limit || system.get('pagination.problem');
@@ -98,8 +77,32 @@ async function run({ domainId }, report) {
return true;
}
addScript('ensureElasticSearch', 'Elastic problem search re-index')
.args(Schema.object({
domainId: Schema.string(),
}))
.action(run);
export const apply = (ctx) => {
bus.on('problem/add', async (doc, docId) => {
await client.index({
index: 'problem',
id: `${doc.domainId}/${docId}`,
document: processDocument(doc),
});
});
bus.on('problem/edit', async (pdoc) => {
await client.index({
index: 'problem',
id: `${pdoc.domainId}/${pdoc.docId}`,
document: processDocument(pdoc),
});
});
bus.on('problem/del', async (domainId, docId) => {
await client.delete({
index: 'problem',
id: `${domainId}/${docId}`,
});
});
ctx.addScript(
'ensureElasticSearch', 'Elastic problem search re-index',
Schema.object({
domainId: Schema.string(),
}),
run,
);
};
+3 -4
View File
@@ -129,9 +129,8 @@ class FpsProblemImportHandler extends Handler {
}
}
export async function apply() {
Route('problem_import_fps', '/problem/import/fps', FpsProblemImportHandler, PERM.PERM_CREATE_PROBLEM);
export const sideEffect = true;
export async function apply(ctx) {
ctx.Route('problem_import_fps', '/problem/import/fps', FpsProblemImportHandler, PERM.PERM_CREATE_PROBLEM);
ProblemAdd('problem_import_fps', {}, 'copy', 'From FPS File');
}
global.Hydro.handler.fpsImport = apply;
+6 -1
View File
@@ -8,6 +8,7 @@ import yaml from 'js-yaml';
import i18n from '../lib/i18n';
import { Logger } from '../logger';
import * as bus from '../service/bus';
import { Runtime } from '../service/module';
const logger = new Logger('common');
@@ -36,7 +37,11 @@ const getLoader = (type: string, filename: string) => async function loader(pend
if (p && !fail.includes(i)) {
try {
logger.info(`${type.replace(/^(.)/, (t) => t.toUpperCase())} init: %s`, i);
require(p);
const res = require(p);
if (res.apply) {
const runtime = new Runtime(p);
runtime.load(res);
}
} catch (e) {
fail.push(i);
logger.info(`${type.replace(/^(.)/, (t) => t.toUpperCase())} load fail: %s`, i);
+11 -4
View File
@@ -8,12 +8,13 @@ import { Logger } from '../logger';
import options from '../options';
import * as bus from '../service/bus';
import db from '../service/db';
import { Runtime } from '../service/module';
import {
handler, lib, locale, model, script, service, setting, template,
} from './common';
const logger = new Logger('worker');
const detail = cac().parse().options.loaderDetail;
const { loaderDetail: detail, watch } = cac().parse().options;
const tmpdir = path.resolve(os.tmpdir(), 'hydro');
export async function load() {
@@ -53,11 +54,18 @@ export async function load() {
const server = require('../service/server');
await server.prepare();
if (detail) logger.info('finish: server');
if (watch) require('../service/watcher');
await service(pending, fail);
if (detail) logger.info('finish: service.extra');
require('../model/index');
if (detail) logger.info('finish: model.builtin');
require('../handler/index');
const handlerDir = path.resolve(__dirname, '..', 'handler');
const handlers = await fs.readdir(handlerDir);
for (const h of handlers) {
const f = path.resolve(handlerDir, h);
const m = require(f);
if (m.apply) new Runtime(f).load(m);
}
if (detail) logger.info('finish: handler.builtin');
await model(pending, fail);
if (detail) logger.info('finish: model.extra');
@@ -68,8 +76,6 @@ export async function load() {
if (detail) logger.info('finish: handler.extra');
for (const i in global.Hydro.handler) await global.Hydro.handler[i]();
if (detail) logger.info('finish: handler.apply');
const notfound = require('../handler/notfound');
await notfound.apply();
require('../script/index');
if (detail) logger.info('finish: script.builtin');
await script(pending, fail);
@@ -98,5 +104,6 @@ export async function load() {
}
logger.success('Server started');
process.send?.('ready');
await bus.serial('app/ready');
return { fail };
}
+4 -5
View File
@@ -2,7 +2,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema';
import { graphql, GraphQLSchema } from 'graphql';
import { resolvers, typeDefs } from 'graphql-scalars';
import * as bus from '../service/bus';
import { Handler, Route } from '../service/server';
import { Handler } from '../service/server';
const types: Record<string, Record<string, string>> = {};
const unions: Record<string, string> = {};
@@ -123,8 +123,7 @@ class ApiHandler extends Handler {
}
}
export function apply() {
Route('api', '/api', ApiHandler);
export const sideEffect = true;
export function apply(ctx) {
ctx.Route('api', '/api', ApiHandler);
}
global.Hydro.handler.api = apply;
+5 -7
View File
@@ -119,11 +119,9 @@ class BlogEditHandler extends BlogHandler {
}
}
export async function apply() {
Route('blog_main', '/blog/:uid', BlogUserHandler);
Route('blog_create', '/blog/:uid/create', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
Route('blog_detail', '/blog/:uid/:did', BlogDetailHandler);
Route('blog_edit', '/blog/:uid/:did/edit', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
export async function apply(ctx) {
ctx.Route('blog_main', '/blog/:uid', BlogUserHandler);
ctx.Route('blog_create', '/blog/:uid/create', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
ctx.Route('blog_detail', '/blog/:uid/:did', BlogDetailHandler);
ctx.Route('blog_edit', '/blog/:uid/:did/edit', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
}
global.Hydro.handler.blog = apply;
+3 -3
View File
@@ -1,4 +1,4 @@
import { Handler, Route } from '../service/server';
import { Handler } from '../service/server';
class ProblemCategoryCompatHandler extends Handler {
async get({ category }) {
@@ -6,6 +6,6 @@ class ProblemCategoryCompatHandler extends Handler {
}
}
global.Hydro.handler.compat = () => {
Route('problem_category_compat', '/p/category/:category', ProblemCategoryCompatHandler);
export const apply = (ctx) => {
ctx.Route('problem_category_compat', '/p/category/:category', ProblemCategoryCompatHandler);
};
-17
View File
@@ -1,17 +0,0 @@
import './home';
import './problem';
import './record';
import './judge';
import './user';
import './contest';
import './training';
import './discussion';
import './manage';
import './import';
import './misc';
import './homework';
import './domain';
import './status';
import './api';
import './blog';
import './compat';
-26
View File
@@ -1,26 +0,0 @@
/* eslint-disable no-empty-function */
/* eslint-disable class-methods-use-this */
import { NotFoundError } from '../error';
import {
Handler, param, Route, Types,
} from '../service/server';
class NotFoundHandler extends Handler {
@param('bsod', Types.Boolean)
prepare(domainId: string, bsod: boolean) {
if (bsod) throw new Error(this.request.path);
throw new NotFoundError(this.request.path);
}
get() { }
post() { }
head() { }
put() { }
delete() { }
}
export async function apply() {
Route('notfound', '(/.*)+', NotFoundHandler);
}
global.Hydro.handler.notfound = apply;
+1
View File
@@ -51,6 +51,7 @@ export function addon(addonPath: string, prepend = false) {
}
}
/** @deprecated */
export function addScript(name: string, description: string) {
if (global.Hydro.script[name]) throw new Error(`duplicate script ${name} registered.`);
return {
+7 -6
View File
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { ObjectID } from 'mongodb';
import Schema from 'schemastery';
import { addScript } from '../loader';
import blacklist from '../model/blacklist';
import * as discussion from '../model/discussion';
import * as document from '../model/document';
@@ -61,8 +60,9 @@ async function _user(
if (!dryrun) await UserModel.ban(uid);
}
addScript('blacklist', 'Add blacklist by ip, uid')
.args(Schema.object({
export const apply = (ctx) => ctx.addScript(
'blacklist', 'Add blacklist by ip, uid',
Schema.object({
address: Schema.string(),
discuss: Schema.object({
domainId: Schema.string(),
@@ -70,8 +70,8 @@ addScript('blacklist', 'Add blacklist by ip, uid')
}),
user: Schema.number(),
dryrun: Schema.boolean(),
}))
.action(async ({
}),
async ({
address = null, discuss = null, user = null, dryrun = true,
}, report) => {
if (address) await _address(address, new Set(), new Set(), new Set(), dryrun, report);
@@ -83,4 +83,5 @@ addScript('blacklist', 'Add blacklist by ip, uid')
}
if (user) await _user(user, new Set(), new Set(), new Set(), dryrun, report);
return true;
});
},
);
+1 -2
View File
@@ -2,7 +2,6 @@
import Schema from 'schemastery';
import * as semver from 'semver';
import superagent from 'superagent';
import { addScript } from '../loader';
import MessageModel from '../model/message';
import * as system from '../model/system';
@@ -38,4 +37,4 @@ export async function run(_: {}, report: Function) {
return true;
}
addScript('checkUpdate', 'Daily update check').args(Schema.any()).action(run);
export const apply = (ctx) => ctx.addScript('checkUpdate', 'Daily update check', Schema.any(), run);
+1 -2
View File
@@ -1,5 +1,4 @@
import Schema from 'schemastery';
import { addScript } from '../loader';
import { PRIV } from '../model/builtin';
import user from '../model/user';
import db from '../service/db';
@@ -9,7 +8,7 @@ const collDomainUser = db.collection('domain.user');
const collRecord = db.collection('record');
const collMessage = db.collection('message');
addScript('deleteUser', 'Delete a user')
export const apply = (ctx) => ctx.addScript('deleteUser', 'Delete a user')
.args(Schema.object({
uid: Schema.number(),
}))
+7 -6
View File
@@ -2,7 +2,6 @@
/* eslint-disable no-await-in-loop */
import { ObjectID } from 'mongodb';
import Schema from 'schemastery';
import { addScript } from '../loader';
import { STATUS } from '../model/builtin';
import * as document from '../model/document';
import db from '../service/db';
@@ -153,15 +152,17 @@ export async function pdoc(report) {
if (bulk.length) await bulk.execute();
}
addScript('problemStat', 'Recalculates nSubmit and nAccept in problem status.')
.args(Schema.object({
export const apply = (ctx) => ctx.addScript(
'problemStat', 'Recalculates nSubmit and nAccept in problem status.',
Schema.object({
udoc: Schema.boolean(),
pdoc: Schema.boolean(),
psdoc: Schema.boolean(),
}))
.action(async (arg, report) => {
}),
async (arg, report) => {
if (arg.pdoc === undefined || arg.pdoc) await pdoc(report);
if (arg.udoc === undefined || arg.udoc) await udoc(report);
if (arg.psdoc === undefined || arg.psdoc) await psdoc(report);
return true;
});
},
);
+4 -6
View File
@@ -6,7 +6,6 @@ import Schema from 'schemastery';
import { Tdoc, Udoc } from '../interface';
import difficultyAlgorithm from '../lib/difficulty';
import rating from '../lib/rating';
import { addScript } from '../loader';
import { PRIV, STATUS } from '../model/builtin';
import * as contest from '../model/contest';
import domain from '../model/domain';
@@ -184,8 +183,7 @@ export async function run({ domainId }, report: Function) {
return true;
}
addScript('rp', 'Calculate rp of a domain, or all domains')
.args(Schema.object({
domainId: Schema.string(),
}))
.action(run);
export const apply = (ctx) => ctx.addScript(
'rp', 'Calculate rp of a domain, or all domains',
Schema.object({ domainId: Schema.string() }), run,
);
+1 -4
View File
@@ -1,6 +1,5 @@
/* eslint-disable no-await-in-loop */
import Schema from 'schemastery';
import { addScript } from '../loader';
import { STATUS } from '../model/builtin';
import storage from '../model/storage';
@@ -37,6 +36,4 @@ export async function run(_, report) {
return true;
}
addScript('storageUsage', 'Calculate storage usage')
.args(Schema.any())
.action(run);
export const apply = (ctx) => ctx.addScript('storageUsage', 'Calculate storage usage', Schema.any(), run);
+56
View File
@@ -0,0 +1,56 @@
import Schema from 'schemastery';
import * as bus from './bus';
export const runtimes: Record<string, Runtime> = Object.create(null);
function addScript<K>(name: string, description: string, validate: Schema<K>, run: (args: K, report: any) => boolean | Promise<boolean>) {
if (global.Hydro.script[name]) throw new Error(`duplicate script ${name} registered.`);
global.Hydro.script[name] = { description, validate, run };
return () => delete global.Hydro.script[name];
}
export interface Context {
Route: typeof import('./server').Route;
Connection: typeof import('./server').Connection;
on: typeof bus['on'];
once: typeof bus['once'];
off: typeof bus['off'];
addScript: typeof addScript;
}
export class Runtime {
private disposables = [];
public sideEffect = false;
public loaded = false;
constructor(public filename: string) {
runtimes[filename] = this;
}
load(module: any) {
if (this.loaded) throw new Error(`Module ${this.filename} was already loaded`);
this.sideEffect = false;
if (typeof module.dispose === 'function') this.disposables.push(module.dispose);
const T = (origFunc) => (origFunc ? (...args) => {
const res = origFunc(...args);
this.disposables.push(res);
return res;
} : null);
module.apply({
Route: T(global.Hydro.service.server?.Route),
Connection: T(global.Hydro.service.server?.Connection),
on: T(bus.on),
once: T(bus.once),
off: bus.off,
addScript: T(addScript),
} as Context);
if (module.sideEffect) this.sideEffect = true;
this.loaded = true;
}
dispose() {
this.disposables.map((i) => i());
this.disposables = [];
this.loaded = false;
}
}
+7 -2
View File
@@ -6,6 +6,10 @@ import type WebSocket from 'ws';
import type { KoaContext } from './server';
type WebSocketCallback = (socket: WebSocket, request: IncomingMessage, ctx: KoaContext) => void;
function remove<T>(list: T[], item: T) {
const index = list.indexOf(item);
if (index >= 0) list.splice(index, 1);
}
export class WebSocketLayer {
clients = new Set<WebSocket>();
@@ -34,20 +38,21 @@ export class WebSocketLayer {
export class Router extends KoaRouter {
wsStack: WebSocketLayer[] = [];
disposeLastOp = () => null;
/**
* hack into router methods to make sure that koa middlewares are disposable
*/
register(...args: Parameters<KoaRouter['register']>) {
const layer = super.register(...args);
// TODO handle unregister
this.disposeLastOp = () => remove(this.stack, layer);
return layer;
}
ws(path: Parameters<typeof pathToRegexp>[0], callback?: WebSocketCallback) {
const layer = new WebSocketLayer(this, path, callback);
this.wsStack.push(layer);
// TODO handle unregistry
this.disposeLastOp = () => remove(this.wsStack, layer);
return layer;
}
}
+7 -1
View File
@@ -363,6 +363,7 @@ const Checker = (permPrivChecker) => {
export function Route(name: string, path: string, RouteHandler: any, ...permPrivChecker) {
router.all(name, path, (ctx) => handle(ctx as any, RouteHandler, Checker(permPrivChecker)));
return router.disposeLastOp;
}
export class ConnectionHandler extends HandlerCommon {
@@ -429,11 +430,16 @@ export function Connection(
let started = false;
class NotFoundHandler extends Handler {
prepare() { throw new NotFoundError(this.request.path); }
all() { }
}
// TODO use postInit?
export async function start() {
if (started) return;
const port = system.get('server.port');
app.use(router.routes()).use(router.allowedMethods());
app.use(router.routes()).use(router.allowedMethods()).use((ctx) => handle(ctx, NotFoundHandler, () => true));
await new Promise((resolve) => {
httpServer.listen(argv.options.port || port, () => {
logger.success('Server listening at: %d', argv.options.port || port);
+235
View File
@@ -0,0 +1,235 @@
/* eslint-disable consistent-return */
/* eslint-disable import/no-dynamic-require */
/* eslint-disable @typescript-eslint/no-shadow */
import { relative, resolve } from 'path';
import { FSWatcher, watch } from 'chokidar';
import { debounce } from 'lodash';
import { Logger } from '../logger';
import * as bus from './bus';
import { Runtime, runtimes } from './module';
function loadDependencies(filename: string, ignored: Set<string>) {
const dependencies = new Set<string>();
function traverse({ filename, children }: NodeJS.Module) {
if (ignored.has(filename) || dependencies.has(filename) || filename.includes('/node_modules/')) return;
dependencies.add(filename);
children.forEach(traverse);
}
traverse(require.cache[filename]);
return dependencies;
}
export function unwrapExports(module: any) {
return module?.default || module;
}
const logger = new Logger('watch');
export function coerce(val: any) {
// resolve error when stack is undefined, e.g. axios error with status code 401
const { message, stack } = val instanceof Error && val.stack ? val : new Error(val as any);
const lines = stack.split('\n');
const index = lines.findIndex((line) => line.endsWith(message));
return lines.slice(index).join('\n');
}
class Watcher {
private root: string;
private watcher: FSWatcher;
private externals: Set<string>;
private accepted: Set<string>;
private declined: Set<string>;
private stashed = new Set<string>();
constructor(private config: any) {
this.externals = new Set(Object.keys(require.cache));
bus.on('app/ready', () => this.start());
}
start() {
this.root = resolve(process.cwd());
this.watcher = watch(this.root, {
...this.config,
ignored: ['**/node_modules/**', '**/.git/**', '**/logs/**', '**/.cache/**'],
});
logger.info(`Start watching changes in ${this.root}`);
// files independent from any plugins will trigger a full reload
const triggerLocalReload = debounce(() => this.triggerLocalReload(), 1000);
this.watcher.on('change', (path) => {
logger.debug('change detected:', relative(this.root, path));
if (this.externals.has(path)) {
logger.warn('Require full reload');
} else if (require.cache[path]) {
this.stashed.add(path);
triggerLocalReload();
}
});
}
stop() {
return this.watcher.close();
}
private analyzeChanges() {
/** files pending classification */
const pending: string[] = [];
this.accepted = new Set(this.stashed);
this.declined = new Set(this.externals);
this.stashed.forEach((filename) => {
const { children } = require.cache[filename];
for (const { filename } of children) {
if (this.accepted.has(filename) || this.declined.has(filename) || filename.includes('/node_modules/')) continue;
pending.push(filename);
}
});
while (pending.length) {
let index = 0;
let hasUpdate = false;
while (index < pending.length) {
const filename = pending[index];
const { children } = require.cache[filename];
let isDeclined = true;
let isAccepted = false;
for (const { filename } of children) {
if (this.declined.has(filename) || filename.includes('/node_modules/')) continue;
if (this.accepted.has(filename)) {
isAccepted = true;
break;
} else {
isDeclined = false;
if (!pending.includes(filename)) {
hasUpdate = true;
pending.push(filename);
}
}
}
if (isAccepted || isDeclined) {
hasUpdate = true;
pending.splice(index, 1);
if (isAccepted) {
this.accepted.add(filename);
} else {
this.declined.add(filename);
}
} else {
index++;
}
}
// infinite loop
if (!hasUpdate) break;
}
for (const filename of pending) {
this.declined.add(filename);
}
}
private triggerLocalReload() {
const start = Date.now();
this.analyzeChanges();
console.log(this.accepted);
/** plugins pending classification */
const pending = new Map<string, Runtime>();
/** plugins that should be reloaded */
const reloads = new Map<Runtime, string>();
// we assume that plugin entry files are "atomic"
// that is, reloading them will not cause any other reloads
for (const filename in require.cache) {
const module = require.cache[filename];
const plugin = unwrapExports(module.exports);
const runtime = runtimes[filename];
if (!runtime || this.declined.has(filename)) continue;
pending.set(filename, runtime);
if (!runtimes[filename]?.sideEffect && !plugin?.['sideEffect']) this.declined.add(filename);
}
for (const [filename, runtime] of pending) {
// check if it is a dependent of the changed file
this.declined.delete(filename);
const dependencies = [...loadDependencies(filename, this.declined)];
// we only detect reloads at plugin level
// a plugin will be reloaded if any of its dependencies are accepted
if (!dependencies.some((dep) => this.accepted.has(dep))) continue;
dependencies.forEach((dep) => this.accepted.add(dep));
// prepare for reload
let isMarked = false;
const visited = new Set<Runtime>();
const queued = [runtime];
while (queued.length) {
const runtime = queued.shift();
if (visited.has(runtime)) continue;
visited.add(runtime);
if (reloads.has(runtime)) {
isMarked = true;
break;
}
}
if (!isMarked) reloads.set(runtime, filename);
}
const backup: Record<string, NodeJS.Module> = {};
for (const filename of this.accepted) {
backup[filename] = require.cache[filename];
delete require.cache[filename];
}
function rollback() {
for (const filename in backup) {
require.cache[filename] = backup[filename];
}
}
logger.debug('Will reload the following file(s): %o', this.accepted.keys());
const attempts = {};
try {
for (const [, filename] of reloads) {
attempts[filename] = unwrapExports(require(filename));
}
} catch (err) {
logger.warn(err);
return rollback();
}
try {
for (const [runtime, filename] of reloads) {
const path = relative(this.root, filename);
try {
runtime.dispose();
} catch (err) {
logger.warn(`failed to dispose plugin at %c\n${coerce(err)}`, path);
}
try {
runtime.load(attempts[filename]);
logger.info('reload plugin at %c', path);
} catch (err) {
logger.warn(`failed to reload plugin at %c\n${coerce(err)}`, path);
throw err;
}
}
} catch {
// rollback require.cache and plugin states
rollback();
logger.warn('Rolling back changes');
return;
}
// reset stashed files
this.stashed = new Set();
logger.success('Reload done in %d ms', Date.now() - start);
}
}
export default new Watcher({});
+9 -3
View File
@@ -1,3 +1,9 @@
import './scripts/hustoj';
import './scripts/vijos';
import './scripts/syzoj';
import { apply as hustoj } from './scripts/hustoj';
import { apply as syzoj } from './scripts/syzoj';
import { apply as vijos } from './scripts/vijos';
export function apply(ctx) {
hustoj(ctx);
vijos(ctx);
syzoj(ctx);
}
+7 -4
View File
@@ -6,7 +6,7 @@ import mysql from 'mysql';
import TurndownService from 'turndown';
import { STATUS } from '@hydrooj/utils/lib/status';
import { noop, Time } from '@hydrooj/utils/lib/utils';
import { addScript, Schema } from 'hydrooj';
import { Schema } from 'hydrooj';
import { NotFoundError } from 'hydrooj/src/error';
import { postJudge } from 'hydrooj/src/handler/judge';
import { RecordDoc } from 'hydrooj/src/interface';
@@ -311,8 +311,9 @@ export async function run({
return true;
}
addScript('migrateHustoj', 'migrate from hustoj')
.args(Schema.object({
export const apply = (ctx) => ctx.addScript(
'migrateHustoj', 'migrate from hustoj',
Schema.object({
host: Schema.string().required(),
port: Schema.number().required(),
name: Schema.string().required(),
@@ -321,4 +322,6 @@ addScript('migrateHustoj', 'migrate from hustoj')
domainId: Schema.string().required(),
contestType: Schema.string().required(),
dataDir: Schema.string().required(),
})).action(run);
}),
run,
);
+8 -5
View File
@@ -5,7 +5,7 @@ import mariadb from 'mariadb';
import { ObjectID } from 'mongodb';
import { STATUS } from '@hydrooj/utils/lib/status';
import { noop, Time } from '@hydrooj/utils/lib/utils';
import { addScript, Schema } from 'hydrooj';
import { Schema } from 'hydrooj';
import { NotFoundError } from 'hydrooj/src/error';
import { postJudge } from 'hydrooj/src/handler/judge';
import { DiscussionDoc, DiscussionReplyDoc, RecordDoc } from 'hydrooj/src/interface';
@@ -227,7 +227,7 @@ export async function run({
hint: pdoc.limit_and_hint,
});
for (const match of content.matchAll(fileReg)) {
const [,origialPath, pid, filename] = match;
const [, origialPath, pid, filename] = match;
if (!problemAdditionalFile[`P${pdoc.id}`]) problemAdditionalFile[`P${pdoc.id}`] = [{ fromPid: pid, filename }];
else problemAdditionalFile[`P${pdoc.id}`].push({ fromPid: pid, filename });
content = content.replace(origialPath, `file://${filename}`);
@@ -508,8 +508,9 @@ export async function run({
return true;
}
addScript('migrateSyzoj', 'migrate from syzoj')
.args(Schema.object({
export const apply = (ctx) => ctx.addScript(
'migrateSyzoj', 'migrate from syzoj',
Schema.object({
host: Schema.string().required(),
port: Schema.number().required(),
name: Schema.string().required(),
@@ -517,4 +518,6 @@ addScript('migrateSyzoj', 'migrate from syzoj')
password: Schema.string().required(),
domainId: Schema.string().default('system'),
dataDir: Schema.string().default('/opt/syzoj/web/uploads'),
})).action(run);
}),
run,
);
+7 -4
View File
@@ -2,7 +2,7 @@
/* eslint-disable no-await-in-loop */
import mongodb, { Cursor, Db } from 'mongodb';
import {
addScript, DiscussionTailReplyDoc, MessageDoc,
DiscussionTailReplyDoc, MessageDoc,
RecordDoc, Schema, TestCase, TrainingNode,
} from 'hydrooj';
@@ -507,11 +507,14 @@ export async function run({
return true;
}
addScript('migrateVijos', 'migrate from vijos')
.args(Schema.object({
export const apply = (ctx) => ctx.addScript(
'migrateVijos', 'migrate from vijos',
Schema.object({
host: Schema.string().required(),
port: Schema.number().required(),
name: Schema.string().required(),
username: Schema.string().required(),
password: Schema.string().required(),
})).action(run);
}),
run,
);
+7 -5
View File
@@ -1,4 +1,4 @@
import { addScript, Schema } from 'hydrooj';
import { Schema } from 'hydrooj';
import DomainModel from 'hydrooj/src/model/domain';
import { iterateAllProblem, iterateAllProblemInDomain } from 'hydrooj/src/pipelineUtils';
import sonic from './service';
@@ -28,8 +28,10 @@ async function run({ domainId }, report) {
return true;
}
addScript('ensureSonicSearch', 'Sonic problem search re-index')
.args(Schema.object({
export const apply = (ctx) => ctx.addScript(
'ensureSonicSearch', 'Sonic problem search re-index',
Schema.object({
domainId: Schema.string(),
}))
.action(run);
}),
run,
);