diff --git a/bin/hydrooj.js b/bin/hydrooj.js index f75ea4f0b..696233a2b 100644 --- a/bin/hydrooj.js +++ b/bin/hydrooj.js @@ -2,46 +2,55 @@ const os = require('os'); const path = require('path'); +const cluster = require('cluster'); const fs = require('fs-extra'); const { argv } = require('yargs'); const hydro = require('../dist/loader'); -fs.ensureDirSync(path.resolve(os.homedir(), '.hydro')); -const addonPath = path.resolve(os.homedir(), '.hydro', 'addon.json'); -if (!fs.existsSync(addonPath)) fs.writeFileSync(addonPath, '[]'); -let addons = JSON.parse(fs.readFileSync(addonPath).toString()); - -try { - const ui = argv.ui || '@hydrooj/ui-default'; - require.resolve(ui); - addons.push(ui); -} catch (e) { - console.error('Please also install @hydrooj/ui-default'); -} - -if (argv._[0]) { - const operation = argv._[0]; - const arg1 = argv._[1]; - const arg2 = argv._[2]; - if (operation === 'addon') { - if (arg1 === 'add') addons.push(arg2); - else if (arg1 === 'remove') { - for (let i = 0; i < addons.length; i++) { - if (addons[i] === arg2) { - addons.splice(i, 1); - break; - } - } - } - addons = Array.from(new Set(addons)); - console.log('Current Addons: ', addons); - fs.writeFileSync(addonPath, JSON.stringify(addons, null, 2)); - } -} else { - addons = Array.from(new Set(addons)); - for (const addon of addons) hydro.addon(addon); +if (!cluster.isMaster) { + // Forked by hydro hydro.load().catch((e) => { console.error(e); process.exit(1); }); +} else { + fs.ensureDirSync(path.resolve(os.homedir(), '.hydro')); + const addonPath = path.resolve(os.homedir(), '.hydro', 'addon.json'); + if (!fs.existsSync(addonPath)) fs.writeFileSync(addonPath, '[]'); + let addons = JSON.parse(fs.readFileSync(addonPath).toString()); + + try { + const ui = argv.ui || '@hydrooj/ui-default'; + require.resolve(ui); + addons.push(ui); + } catch (e) { + console.error('Please also install @hydrooj/ui-default'); + } + + if (argv._[0] && argv._[0] !== 'cli') { + const operation = argv._[0]; + const arg1 = argv._[1]; + const arg2 = argv._[2]; + if (operation === 'addon') { + if (arg1 === 'add') addons.push(arg2); + else if (arg1 === 'remove') { + for (let i = 0; i < addons.length; i++) { + if (addons[i] === arg2) { + addons.splice(i, 1); + break; + } + } + } + addons = Array.from(new Set(addons)); + console.log('Current Addons: ', addons); + fs.writeFileSync(addonPath, JSON.stringify(addons, null, 2)); + } + } else { + addons = Array.from(new Set(addons)); + for (const addon of addons) hydro.addon(addon); + (argv._[0] === 'cli' ? hydro.loadCli : hydro.load)().catch((e) => { + console.error(e); + process.exit(1); + }); + } } diff --git a/hydro/entry/cli.ts b/hydro/entry/cli.ts index e69de29bb..839a787ee 100644 --- a/hydro/entry/cli.ts +++ b/hydro/entry/cli.ts @@ -0,0 +1,88 @@ +/* eslint-disable no-eval */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable import/no-dynamic-require */ +import { argv } from 'yargs'; +import { + lib, service, model, script, + builtinLib, builtinScript, builtinModel, +} from './common'; + +const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; +const ARR = /=>.*$/mg; +function parseParameters(fn: Function) { + const code = fn.toString() + .replace(COMMENTS, '') + .replace(ARR, ''); + const result = code.slice(code.indexOf('(') + 1, code.indexOf(')')) + .match(/([^,]+)/g).map((i) => i.trim()); + return result === null ? [] : result; +} + +async function cli() { + const [, modelName, func, ...args] = argv._; + if (!global.Hydro.model[modelName]) { + return console.error(`Model ${modelName} doesn't exist.`); + } + if (!global.Hydro.model[modelName][func]) { + return console.error(`Function ${func} doesn't exist in model ${modelName}.`); + } + if (typeof global.Hydro.model[modelName][func] !== 'function') { + return console.error(`${func} in model ${modelName} is not a function.`); + } + const parameterMin = global.Hydro.model[modelName][func].length; + const parameters = parseParameters(global.Hydro.model[modelName][func]); + const parameterMax = parameters.length; + if (args.length > parameterMax) { + console.error(`Too many arguments. Max ${parameterMax}`); + return console.error(parameters.join(', ')); + } + if (args.length < parameterMin) { + console.error(`Too few arguments. Min ${parameterMin}`); + return console.error(parameters.join(', ')); + } + let result = global.Hydro.model[modelName][func](...args); + if (result instanceof Promise) result = await result; + return console.log(result); +} + +export async function load(call, args) { + let pending = args; + const fail = []; + const active = []; + require('../lib/i18n'); + require('../utils'); + require('../error'); + require('../options'); + const bus = require('../service/bus'); + await new Promise((resolve) => { + const h = () => { + bus.unsubscribe(['system_database_connected'], h); + resolve(); + }; + bus.subscribe(['system_database_connected'], h); + require('../service/db'); + }); + for (const i of builtinLib) require(`../lib/${i}`); + await lib(pending, fail); + require('../service/gridfs'); + await service(pending, fail); + for (const i of builtinModel) require(`../model/${i}`); + await model(pending, fail); + for (const i in global.Hydro.service) { + if (global.Hydro.service[i].postInit) { + try { + await global.Hydro.service[i].postInit(); + } catch (e) { + console.error(e); + } + } + } + for (const i of builtinScript) require(`../script/${i}`); + await script(pending, fail, active); + for (const postInit of global.Hydro.postInit) { + await postInit(); + } + pending = []; + await cli(); + return { active, fail }; +} diff --git a/hydro/entry/master.ts b/hydro/entry/master.ts index c3acfb222..ade691147 100644 --- a/hydro/entry/master.ts +++ b/hydro/entry/master.ts @@ -21,12 +21,11 @@ export async function load(call) { bus.subscribe(['system_database_connected'], h); require('../service/db'); }); - require('../service/monitor'); for (const i of builtinModel) require(`../model/${i}`); const modelSystem = require('../model/system'); const dbVer = await modelSystem.get('db.ver'); if (dbVer !== 1) { - const ins = require('../script/install'); + const ins = require('../script/upgrade0_1'); await ins.run({ username: 'Root', password: 'rootroot' }); } for (const i in global.Hydro.service) { diff --git a/hydro/entry/setup.ts b/hydro/entry/setup.ts index e86baf538..e83f419cd 100644 --- a/hydro/entry/setup.ts +++ b/hydro/entry/setup.ts @@ -2,12 +2,14 @@ import os from 'os'; import path from 'path'; import http from 'http'; import fs from 'fs-extra'; -import Koa from 'koa'; +import Koa, { Context } from 'koa'; import Body from 'koa-body'; -import Router from 'koa-router'; import cache from 'koa-static-cache'; import nunjucks from 'nunjucks'; import mongodb from 'mongodb'; +import { argv } from 'yargs'; + +const listenPort = argv.port || 8888; class Loader { getSource(name: string) { // eslint-disable-line class-methods-use-this @@ -83,7 +85,7 @@ class Loader { const env = new nunjucks.Environment(new Loader(), { autoescape: true, trimBlocks: true }); -function render(name) { +function render(name: string) { return new Promise((resolve, reject) => { env.render(name, { _: (str: string) => str, @@ -94,70 +96,74 @@ function render(name) { }); } +async function get(ctx: Context) { + ctx.body = await render('setup.html'); + ctx.response.type = 'text/html'; +} + +async function post(ctx: Context) { + const { + host, port, name, username, password, + } = ctx.request.body; + let mongourl = 'mongodb://'; + if (username) mongourl += `${username}:${password}@`; + mongourl += `${host}:${port}/${name}`; + try { + const Database = await mongodb.MongoClient.connect(mongourl, { + useNewUrlParser: true, useUnifiedTopology: true, + }); + const db = Database.db(name); + await Promise.all([ + db.collection('system').updateOne( + { _id: 'server.host' }, + { $set: { value: ctx.request.host } }, + { upsert: true }, + ), + db.collection('system').updateOne( + { _id: 'server.hostname' }, + { $set: { value: ctx.request.hostname } }, + { upsert: true }, + ), + db.collection('system').updateOne( + { _id: 'server.url' }, + { $set: { value: ctx.request.href } }, + { upsert: true }, + ), + db.collection('system').updateOne( + { _id: 'server.port' }, + { $set: { value: parseInt(listenPort as string, 10) } }, + { upsert: true }, + ), + ]); + fs.ensureDirSync(path.resolve(os.homedir(), '.hydro')); + fs.writeFileSync(path.resolve(os.homedir(), '.hydro', 'config.json'), JSON.stringify({ + host, port, name, username, password, + })); + ctx.body = `

This page will reload in 3 secs.

+ `; + setTimeout(() => process.exit(0), 500); + } catch (e) { + ctx.body = `Error connecting to database: ${e.message}\n${e.stack}`; + } +} + export function load() { const app = new Koa(); const server = http.createServer(app.callback()); - const router = new Router(); app.keys = ['Hydro']; - app.use(cache(path.join(os.tmpdir(), 'hydro', 'public'), { - maxAge: 365 * 24 * 60 * 60, - })); - app.use(Body()); - router.get('/', async (ctx) => { - ctx.body = await render('setup.html'); - ctx.response.type = 'text/html'; - }); - const listenPort = global.argv.port || 8888; - router.post('/', async (ctx) => { - const { - host, port, name, username, password, - } = ctx.request.body; - let mongourl = 'mongodb://'; - if (username) mongourl += `${username}:${password}@`; - mongourl += `${host}:${port}/${name}`; - try { - const Database = await mongodb.MongoClient.connect(mongourl, { - useNewUrlParser: true, useUnifiedTopology: true, - }); - const db = Database.db(name); - await Promise.all([ - db.collection('system').updateOne( - { _id: 'server.host' }, - { $set: { value: ctx.request.host } }, - { upsert: true }, - ), - db.collection('system').updateOne( - { _id: 'server.hostname' }, - { $set: { value: ctx.request.hostname } }, - { upsert: true }, - ), - db.collection('system').updateOne( - { _id: 'server.url' }, - { $set: { value: ctx.request.href } }, - { upsert: true }, - ), - db.collection('system').updateOne( - { _id: 'server.port' }, - { $set: { value: parseInt(listenPort, 10) } }, - { upsert: true }, - ), - ]); - fs.ensureDirSync(path.resolve(os.homedir(), '.hydro')); - fs.writeFileSync(path.resolve(os.homedir(), '.hydro', 'config.json'), JSON.stringify({ - host, port, name, username, password, - })); - ctx.body = `

This page will reload in 3 secs.

- `; - setTimeout(() => process.exit(0), 500); - } catch (e) { - ctx.body = `Error connecting to database: ${e.message}\n${e.stack}`; - } - }); - app.use(router.routes()).use(router.allowedMethods()); + app + .use(cache(path.join(os.tmpdir(), 'hydro', 'public'), { + maxAge: 365 * 24 * 60 * 60, + })) + .use(Body()) + .use((ctx) => { + if (ctx.request.method.toLowerCase() === 'post') return post(ctx); + return get(ctx); + }); server.listen(listenPort); console.log('Server listening at:', listenPort); } diff --git a/hydro/interface.ts b/hydro/interface.ts index 7436618b8..18356183e 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -300,7 +300,6 @@ declare global { }, onDestory: Function[], addons: string[], - argv: any, } } } diff --git a/hydro/loader.ts b/hydro/loader.ts index e7a574888..e42a00594 100644 --- a/hydro/loader.ts +++ b/hydro/loader.ts @@ -2,7 +2,6 @@ /* eslint-disable import/no-dynamic-require */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-eval */ - import './interface'; import os from 'os'; import path from 'path'; @@ -11,7 +10,6 @@ import fs from 'fs-extra'; import { argv } from 'yargs'; import AdmZip from 'adm-zip'; -global.argv = argv; global.Hydro = { stat: { reqCount: 0 }, handler: {}, @@ -35,15 +33,16 @@ global.Hydro = { global.onDestory = []; global.addons = []; -if (global.argv.debug) { +if (argv.debug) { console.log(process.argv); - process.env.debug = 'enable'; + console.log(argv); } async function terminate() { - for (const task of global.onDestory) { - // eslint-disable-next-line no-await-in-loop - await task(); + try { + for (const task of global.onDestory) await task(); + } catch (e) { + process.exit(1); } process.exit(0); } @@ -80,9 +79,8 @@ async function stopWorker() { } async function startWorker(cnt: number) { - const sargv = [`--args=${Buffer.from(JSON.stringify(global.addons)).toString('base64')}`]; - await fork(['--firstWorker', ...sargv]); - for (let i = 1; i < cnt; i++) await fork(sargv); + await fork(['--firstWorker']); + for (let i = 1; i < cnt; i++) await fork(); } async function executeCommand(input: string) { @@ -163,6 +161,9 @@ export function addon(addonPath: string) { } else throw new Error(`Addon not found: ${addonPath}`); } +process.on('unhandledRejection', (e) => console.error(e)); +process.on('SIGINT', terminate); + export async function load() { while (!global.addons) { await new Promise((resolve) => { @@ -171,11 +172,9 @@ export async function load() { } addon(path.resolve(__dirname, '..')); Error.stackTraceLimit = 50; - process.on('unhandledRejection', (e) => console.error(e)); - process.on('SIGINT', terminate); process.on('message', messageHandler); cluster.on('message', messageHandler); - if (cluster.isMaster || global.argv.startAsMaster) { + if (cluster.isMaster || argv.startAsMaster) { console.log(`Master ${process.pid} Starting`); process.stdin.setEncoding('utf8'); process.stdin.on('data', (buf) => { @@ -205,12 +204,12 @@ export async function load() { worker.send({ event: 'setAddon', addons: global.addons }); }); await startWorker(cnt); - } else if (global.argv.entry) { - console.log(`Worker ${process.pid} Starting as ${global.argv.entry}`); - await entry({ entry: global.argv.entry }); - console.log(`Worker ${process.pid} Started as ${global.argv.entry}`); + } else if (argv.entry) { + console.log(`Worker ${process.pid} Starting as ${argv.entry}`); + await entry({ entry: argv.entry }); + console.log(`Worker ${process.pid} Started as ${argv.entry}`); } else { - if (global.argv.firstWorker) cluster.isFirstWorker = true; + if (argv.firstWorker) cluster.isFirstWorker = true; else cluster.isFirstWorker = false; console.log(`Worker ${process.pid} Starting`); await entry({ entry: 'worker' }); @@ -219,8 +218,14 @@ export async function load() { if (global.gc) global.gc(); } -if (global.argv.pandora || !module.parent) { - load().catch((e) => { +export async function loadCli() { + await entry({ entry: 'cli' }); + return terminate(); +} + +if (argv.pandora || !module.parent) { + const func = argv._[0] === 'cli' ? load : loadCli; + func().catch((e) => { console.error(e); process.exit(1); }); diff --git a/hydro/model/domain.ts b/hydro/model/domain.ts index f18eb785e..fd1540364 100644 --- a/hydro/model/domain.ts +++ b/hydro/model/domain.ts @@ -1,6 +1,6 @@ import { Dictionary } from 'lodash'; import { BUILTIN_ROLES, PRIV } from './builtin'; -import { Udoc, DomainDoc } from '../interface'; +import { DomainDoc } from '../interface'; import * as db from '../service/db'; const coll = db.collection('domain'); diff --git a/hydro/script/install.ts b/hydro/script/install.ts deleted file mode 100644 index 7ee980698..000000000 --- a/hydro/script/install.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PRIV } from '../model/builtin'; -import * as user from '../model/user'; -import * as domain from '../model/domain'; -import pwhash from '../lib/hash.hydro'; - -export const description = 'Install'; - -export async function run({ username = '', password = '' } = {}) { - const ddoc = await domain.get('system'); - if (!ddoc) await domain.add('system', 0, 'Hydro', 'Hydro System'); - if (username && password) { - const udoc = await user.getById('system', -1); - if (!udoc) { - await user.create('root@hydro.local', username, password, -1, '127.0.0.1', PRIV.PRIV_ALL); - } else { - const salt = String.random(); - await user.setById(-1, { - uname: username, - unameLower: username.trim().toLowerCase(), - salt, - hash: pwhash(password, salt), - hashType: 'hydro', - }); - } - } -} - -export const validate = { - $or: [ - { username: 'string', password: 'string' }, - { username: 'undefined', password: 'undefined' }, - ], -}; - -global.Hydro.script.install = { run, description, validate }; diff --git a/hydro/script/upgrade0_1.ts b/hydro/script/upgrade0_1.ts new file mode 100644 index 000000000..ebd7872b6 --- /dev/null +++ b/hydro/script/upgrade0_1.ts @@ -0,0 +1,12 @@ +import * as domain from '../model/domain'; + +export const description = 'Upgrade database from 0 to 1'; + +export async function run() { + const ddoc = await domain.get('system'); + if (!ddoc) await domain.add('system', 0, 'Hydro', 'Hydro System'); +} + +export const validate = {}; + +global.Hydro.script.setSuperadmin = { run, description, validate }; diff --git a/hydro/service/server.ts b/hydro/service/server.ts index 2d19e45e6..cf6038cfc 100644 --- a/hydro/service/server.ts +++ b/hydro/service/server.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import path from 'path'; import os from 'os'; -import http, { Server } from 'http'; +import http from 'http'; import moment from 'moment-timezone'; import { isSafeInteger, Dictionary } from 'lodash'; import { ObjectID } from 'mongodb'; @@ -776,8 +776,8 @@ export async function start() { } app.use(router.routes()).use(router.allowedMethods()); Route('notfound_handler', '*', Handler); - server.listen(global.argv.port || port); - console.log('Server listening at: %s', global.argv.port || port); + server.listen(argv.port || port); + console.log('Server listening at: %s', argv.port || port); } global.Hydro.service.server = { diff --git a/package.json b/package.json index 54545720b..84e4d8be8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.9.10", + "version": "2.9.11", "main": "dist/loader.js", "bin": "bin/hydrooj.js", "repository": "https://github.com/hydro-dev/Hydro.git",