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",