diff --git a/packages/hydrooj/src/context.ts b/packages/hydrooj/src/context.ts index 7f9329b65..0dc36d7e3 100644 --- a/packages/hydrooj/src/context.ts +++ b/packages/hydrooj/src/context.ts @@ -1,6 +1,6 @@ import * as cordis from 'cordis'; import Schema from 'schemastery'; -import { GeoIP, ModuleInterfaces } from './interface'; +import type { DomainDoc, GeoIP, ModuleInterfaces } from './interface'; import { inject } from './lib/ui'; import { Loader } from './loader'; import { EventMap } from './service/bus'; @@ -33,14 +33,16 @@ export interface Context { setImmediate: typeof setImmediate; addScript: typeof addScript; provideModule: typeof provideModule; + /** @deprecated use injectUI instead */ inject: typeof inject; + injectUI: typeof inject; api: ApiMixin; broadcast: Context['emit']; geoip?: GeoIP; } export class Context extends cordis.Context { - static readonly session = Symbol('session'); + domain?: DomainDoc; } export namespace Context { @@ -64,13 +66,14 @@ const T = any>(origFunc: F, disposeFunc?) => this.caller?.on('dispose', () => (disposeFunc ? disposeFunc(res) : res())); }; export class ApiMixin extends Service { - static readonly methods = ['addScript', 'setInterval', 'setTimeout', 'setImmediate', 'provideModule', 'inject', 'broadcast']; + static readonly methods = ['addScript', 'setInterval', 'setTimeout', 'setImmediate', 'provideModule', 'inject', 'injectUI', 'broadcast']; addScript = T(addScript); setInterval = T(setInterval, clearInterval); setTimeout = T(setTimeout, clearTimeout); setImmediate = T(setImmediate, clearImmediate); provideModule = T(provideModule); inject = T(inject); + injectUI = T(inject); broadcast = (event: keyof EventMap, ...payload) => this.ctx.emit('bus/broadcast', event, payload); constructor(ctx) { super(ctx, 'api', true); diff --git a/packages/hydrooj/src/handler/home.ts b/packages/hydrooj/src/handler/home.ts index 63258c3bd..9800026d8 100644 --- a/packages/hydrooj/src/handler/home.ts +++ b/packages/hydrooj/src/handler/home.ts @@ -146,7 +146,7 @@ export class HomeHandler extends Handler { } async get({ domainId }) { - const homepageConfig = this.domain.homepage || system.get('hydrooj.homepage'); + const homepageConfig = this.ctx.setting.get('hydrooj.homepage'); const info = yaml.load(homepageConfig) as any; const contents = []; for (const column of info) { diff --git a/packages/hydrooj/src/loader.ts b/packages/hydrooj/src/loader.ts index 86460fb76..c4534a775 100644 --- a/packages/hydrooj/src/loader.ts +++ b/packages/hydrooj/src/loader.ts @@ -170,13 +170,16 @@ export async function load() { console.warn('DISCLAIMER:'); console.warn(' You are under development mode.'); console.warn(' The Hydro project is licensed under AGPL3,'); - console.warn(' which means you have to open source your modifications,'); + console.warn(' which means you have to open source all your modifications'); + console.warn(' and keep all copyright notice'); console.warn(' unless you have got another license from the original author.'); console.warn(''); console.warn('声明:'); console.warn(' 你正在运行开发者模式。'); console.warn(' Hydro 项目基于 AGPL3 协议开源,'); - console.warn(' 这意味着除非你获得了原作者的其他授权,你需要同样以 AGPL3 协议开源所有的修改。'); + console.warn(' 这意味着除非你获得了原作者的其他授权,'); + console.warn(' 你需要同样以 AGPL3 协议开源所有的修改,'); + console.warn(' 并保留所有的版权声明。'); console.warn('\x1b[39m'); console.log(''); console.log('Hydro will start in 5s.'); diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 135aa7c81..699c3132b 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -315,6 +315,10 @@ export class SettingService extends Service { constructor(ctx: Context) { super(ctx, 'setting', true); } + + get(key: string) { + return (this.caller ? this.caller.domain?.config?.[key.replace(/\./g, '$')] : null) ?? global.Hydro.model.system.get(key); + } } export async function apply(ctx: Context) { diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index d93f50a8d..c401810ec 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -23,6 +23,7 @@ import { Types } from '../lib/validator'; import { Logger } from '../logger'; import { PERM, PRIV } from '../model/builtin'; import * as opcount from '../model/opcount'; +import * as OplogModel from '../model/oplog'; import * as system from '../model/system'; import { User } from '../model/user'; import { builtinConfig } from '../settings'; @@ -119,9 +120,9 @@ export class HandlerCommon { url: (name: string, args?: any) => string; translate: (key: string) => string; session: Record; + ctx: Context; /** @deprecated */ domainId: string; - ctx: Context = global.app; constructor( public context: KoaContext, public readonly args: Record, @@ -134,6 +135,9 @@ export class HandlerCommon { this.translate = context.translate.bind(context); this.session = context.session; this.domainId = args.domainId; + this.ctx = global.app.extend({ + domain: this.domain, + }); } async limitRate(op: string, periodSecs: number, maxOperations: number, withUserId = system.get('limit.by_user')) { @@ -243,10 +247,11 @@ async function handle(ctx: KoaContext, HandlerClass, checker) { args, request, response, user, domain, UiContext, } = ctx.HydroContext; Object.assign(args, ctx.params); + const init = Date.now(); const h = new HandlerClass(ctx, args, request, response, user, domain, UiContext); ctx.handler = h; + const method = ctx.method.toLowerCase(); try { - const method = ctx.method.toLowerCase(); const operation = (method === 'post' && ctx.request.body?.operation) ? `_${ctx.request.body.operation}`.replace(/_([a-z])/gm, (s) => s[1].toUpperCase()) : ''; @@ -307,8 +312,16 @@ async function handle(ctx: KoaContext, HandlerClass, checker) { await h.onerror(e); } catch (err) { h.response.code = 500; + h.response.type = 'text/plain'; h.response.body = `${err.message}\n${err.stack}`; } + } finally { + const finish = Date.now(); + if (finish - init > 5000) { + const id = await OplogModel.log(h, 'slow_request', { method, processtime: finish - init }); + logger.warn(`Slow handler: ID=${id}, `, method, ctx.path, `${finish - init}ms`); + } + // TODO metrics: calc avg response time } } @@ -532,12 +545,15 @@ export async function apply(pluginContext: Context) { if (process.env.DEV) { app.use(async (ctx: Koa.Context, next: Function) => { const startTime = Date.now(); - await next(); - const endTime = Date.now(); - if (ctx.nolog || ctx.response.headers.nolog) return; - ctx._remoteAddress = ctx.request.ip; - logger.debug(`${ctx.request.method} /${ctx.domainId || 'system'}${ctx.request.path} \ + try { + await next(); + } finally { + const endTime = Date.now(); + if (!(ctx.nolog || ctx.response.headers.nolog)) { + logger.debug(`${ctx.request.method} /${ctx.domainId || 'system'}${ctx.request.path} \ ${ctx.response.status} ${endTime - startTime}ms ${ctx.response.length}`); + } + } }); } const uploadDir = join(tmpdir(), 'hydro', 'upload', process.env.NODE_APP_INSTANCE || '0'); diff --git a/packages/ui-default/backendlib/builder.ts b/packages/ui-default/backendlib/builder.ts index 844d7ef57..c3acbcc5a 100644 --- a/packages/ui-default/backendlib/builder.ts +++ b/packages/ui-default/backendlib/builder.ts @@ -1,7 +1,7 @@ import esbuild from 'esbuild'; import { Context, fs, Handler, Logger, NotFoundError, param, SettingModel, sha1, - size, SystemModel, Types, UiContextBase, + size, Types, UiContextBase, } from 'hydrooj'; import { debounce } from 'lodash'; import { tmpdir } from 'os'; @@ -17,15 +17,10 @@ declare module 'hydrooj' { 'ui-default.nav_logo_dark': string; } interface UiContextBase { - nav_logo_dark?: string; constantVersion?: string; } } -function updateLogo() { - UiContextBase.nav_logo_dark = SystemModel.get('ui-default.nav_logo_dark'); -} - const vfs: Record = {}; const hashes: Record = {}; const logger = new Logger('ui'); @@ -140,13 +135,11 @@ export async function apply(ctx: Context) { ctx.Route('constant', '/constant/:version', UiConstantsHandler); ctx.Route('constant', '/lazy/:version/:name', UiConstantsHandler); ctx.Route('constant', '/resource/:version/:name', UiConstantsHandler); - ctx.on('app/started', updateLogo); ctx.on('app/started', buildUI); const debouncedBuildUI = debounce(buildUI, 2000, { trailing: true }); const triggerHotUpdate = (path?: string) => { if (path && !path.includes('/ui-default/') && !path.includes('/public/')) return; debouncedBuildUI(); - updateLogo(); }; ctx.on('system/setting', () => triggerHotUpdate()); ctx.on('app/watch/change', triggerHotUpdate); diff --git a/packages/ui-default/backendlib/template.ts b/packages/ui-default/backendlib/template.ts index f9663a352..87330a9f0 100644 --- a/packages/ui-default/backendlib/template.ts +++ b/packages/ui-default/backendlib/template.ts @@ -170,6 +170,7 @@ const render = (name: string, state: any) => new Promise((resolve, rejec return state._(text.message).format(...text.params || []) + ((process.env.DEV && text.stack) ? `\n${text.stack}` : ''); }).join('\n'), datetimeSpan: (arg0, arg1, arg2) => misc.datetimeSpan(arg0, arg1, arg2, state.handler.user?.timeZone), + ctx: state.handler?.ctx, perm: PERM, PRIV, STATUS, diff --git a/packages/ui-default/templates/partials/header_mobile.html b/packages/ui-default/templates/partials/header_mobile.html index 951364dc0..978adc355 100644 --- a/packages/ui-default/templates/partials/header_mobile.html +++ b/packages/ui-default/templates/partials/header_mobile.html @@ -2,7 +2,7 @@
diff --git a/packages/ui-default/templates/partials/nav.html b/packages/ui-default/templates/partials/nav.html index 5b85ceffb..4fa67c793 100644 --- a/packages/ui-default/templates/partials/nav.html +++ b/packages/ui-default/templates/partials/nav.html @@ -10,7 +10,7 @@