From c26fae2edefd140a0f723238dfc669f763ed0839 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sat, 7 Oct 2017 22:44:35 -0400 Subject: [PATCH] feat: Kernel module --- package.json | 13 +++--- server/index.js | 39 ++--------------- server/init.js | 90 -------------------------------------- server/master.js | 18 +------- server/models/user.js | 11 ++++- server/modules/config.js | 5 ++- server/modules/db.js | 12 +++--- server/modules/kernel.js | 91 +++++++++++++++++++++++++++++++++++++++ server/modules/queue.js | 2 +- server/worker.js | 2 - tools/fuse_tasks.js | 21 --------- wiki.js | 78 +++++++++++++++++++++++++++------ yarn.lock | Bin 291320 -> 281438 bytes 13 files changed, 186 insertions(+), 196 deletions(-) delete mode 100644 server/init.js create mode 100644 server/modules/kernel.js diff --git a/package.json b/package.json index 0a36dd6e..f48d48e9 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "bcryptjs-then": "1.0.1", "bluebird": "3.5.1", "body-parser": "1.18.2", - "bull": "3.2.0", + "bull": "3.3.0", "bunyan": "1.8.12", "cheerio": "1.0.0-rc.2", "child-process-promise": "2.2.1", @@ -66,7 +66,7 @@ "graphql": "0.10.5", "graphql-tools": "2.2.1", "highlight.js": "9.12.0", - "i18next": "9.0.1", + "i18next": "9.1.0", "i18next-express-middleware": "1.0.7", "i18next-localstorage-cache": "1.1.1", "i18next-node-fs-backend": "1.0.0", @@ -76,7 +76,6 @@ "js-yaml": "3.10.0", "jsonwebtoken": "8.0.1", "klaw": "2.1.0", - "levelup": "1.3.9", "lodash": "4.17.4", "markdown-it": "8.4.0", "markdown-it-abbr": "1.0.4", @@ -89,7 +88,6 @@ "markdown-it-mathjax": "2.0.0", "markdown-it-task-lists": "2.0.1", "mathjax-node": "1.2.1", - "memdown": "1.4.1", "mime-types": "2.1.17", "moment": "2.18.1", "moment-timezone": "0.5.13", @@ -106,21 +104,19 @@ "passport-local": "1.0.0", "passport-slack": "0.0.7", "passport-windowslive": "1.0.2", - "pg": "7.3.0", + "pg": "6.4.2", "pg-hstore": "2.3.2", "pg-promise": "6.10.3", "pm2": "2.7.1", "pug": "2.0.0-rc.4", + "qr-image": "3.2.0", "read-chunk": "2.1.0", "remove-markdown": "0.2.2", "request": "2.83.0", - "search-index-adder": "0.3.9", - "search-index-searcher": "0.2.10", "semver": "5.4.1", "sequelize": "4.13.5", "serve-favicon": "2.4.5", "simplemde": "1.11.2", - "stopword": "0.1.8", "stream-to-promise": "2.2.0", "tar": "4.0.1", "through2": "2.0.3", @@ -135,6 +131,7 @@ "apollo-client": "^1.9.3", "autoprefixer": "7.1.5", "babel-cli": "6.26.0", + "babel-core": "6.26.0", "babel-jest": "21.2.0", "babel-preset-env": "1.6.0", "babel-preset-es2015": "6.24.1", diff --git a/server/index.js b/server/index.js index 30ea6e2c..e74f5c2b 100644 --- a/server/index.js +++ b/server/index.js @@ -1,5 +1,3 @@ -'use strict' - // =========================================== // Wiki.js // Licensed under AGPLv3 @@ -13,7 +11,8 @@ let wiki = { IS_MASTER: cluster.isMaster, ROOTPATH: process.cwd(), SERVERPATH: path.join(process.cwd(), 'server'), - configSvc: require('./modules/config') + configSvc: require('./modules/config'), + kernel: require('./modules/kernel') } global.wiki = wiki @@ -38,37 +37,7 @@ wiki.logger = require('./modules/logger').init() wiki.db = require('./modules/db').init() // ---------------------------------------- -// Start Cluster +// Start Kernel // ---------------------------------------- -const numCPUs = require('os').cpus().length -let numWorkers = (wiki.config.workers > 0) ? wiki.config.workers : numCPUs -if (numWorkers > numCPUs) { - numWorkers = numCPUs -} - -if (cluster.isMaster) { - wiki.logger.info('=======================================') - wiki.logger.info('= Wiki.js =============================') - wiki.logger.info('=======================================') - - require('./master').then(() => { - // -> Create background workers - for (let i = 0; i < numWorkers; i++) { - cluster.fork() - } - - // -> Queue post-init tasks - - wiki.queue.uplClearTemp.add({}, { - repeat: { cron: '*/15 * * * *' } - }) - }) - - cluster.on('exit', (worker, code, signal) => { - wiki.logger.info(`Background Worker #${worker.id} was terminated.`) - }) -} else { - wiki.logger.info(`Background Worker #${cluster.worker.id} is initializing...`) - require('./worker') -} +wiki.kernel.init() diff --git a/server/init.js b/server/init.js deleted file mode 100644 index 95b3ed52..00000000 --- a/server/init.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict' - -const Promise = require('bluebird') -const fs = Promise.promisifyAll(require('fs-extra')) -const pm2 = Promise.promisifyAll(require('pm2')) -const ora = require('ora') -const path = require('path') - -const ROOTPATH = process.cwd() - -module.exports = { - /** - * Detect the most appropriate start mode - */ - startDetect: function () { - if (process.env.WIKI_JS_HEROKU) { - return this.startInHerokuMode() - } else { - return this.startInBackgroundMode() - } - }, - /** - * Start in background mode - */ - startInBackgroundMode: function () { - let spinner = ora('Initializing...').start() - return fs.emptyDirAsync(path.join(ROOTPATH, './logs')).then(() => { - return pm2.connectAsync().then(() => { - return pm2.startAsync({ - name: 'wiki', - script: 'server', - cwd: ROOTPATH, - output: path.join(ROOTPATH, './logs/wiki-output.log'), - error: path.join(ROOTPATH, './logs/wiki-error.log'), - minUptime: 5000, - maxRestarts: 5 - }).then(() => { - spinner.succeed('Wiki.js has started successfully.') - }).finally(() => { - pm2.disconnect() - }) - }) - }).catch(err => { - spinner.fail(err) - process.exit(1) - }) - }, - /** - * Start in Heroku mode - */ - startInHerokuMode: function () { - console.warn('Incorrect command on Heroku, use instead: node server') - process.exit(1) - }, - /** - * Stop Wiki.js process(es) - */ - stop () { - let spinner = ora('Shutting down Wiki.js...').start() - return pm2.connectAsync().then(() => { - return pm2.stopAsync('wiki').then(() => { - spinner.succeed('Wiki.js has stopped successfully.') - }).finally(() => { - pm2.disconnect() - }) - }).catch(err => { - spinner.fail(err) - process.exit(1) - }) - }, - /** - * Restart Wiki.js process(es) - */ - restart: function () { - let self = this - return self.stop().delay(1000).then(() => { - self.startDetect() - }) - }, - /** - * Start the web-based configuration wizard - * - * @param {Number} port Port to bind the HTTP server on - */ - configure (port) { - port = port || 3000 - let spinner = ora('Initializing interactive setup...').start() - require('./configure')(port, spinner) - } -} diff --git a/server/master.js b/server/master.js index f4350edf..4d8a450c 100644 --- a/server/master.js +++ b/server/master.js @@ -1,17 +1,6 @@ -'use strict' - /* global wiki */ -const Promise = require('bluebird') - -wiki.redis = require('./modules/redis').init() -wiki.queue = require('./modules/queue').init() - -module.exports = Promise.join( - wiki.db.onReady, - wiki.configSvc.loadFromDb(), - wiki.queue.clean() -).then(() => { +module.exports = () => { // ---------------------------------------- // Load global modules // ---------------------------------------- @@ -194,7 +183,4 @@ module.exports = Promise.join( }) return true -}).catch(err => { - wiki.logger.error(err) - process.exit(1) -}) +} diff --git a/server/models/user.js b/server/models/user.js index 4d740b39..a73425d8 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -19,7 +19,7 @@ module.exports = (sequelize, DataTypes) => { } }, provider: { - type: DataTypes.ENUM(wiki.data.authProviders), + type: DataTypes.STRING, allowNull: false }, providerId: { @@ -37,6 +37,15 @@ module.exports = (sequelize, DataTypes) => { role: { type: DataTypes.ENUM('admin', 'user', 'guest'), allowNull: false + }, + tfaIsActive: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + tfaSecret: { + type: DataTypes.STRING, + allowNull: true } }, { timestamps: true, diff --git a/server/modules/config.js b/server/modules/config.js index 5884b8cf..d18da9fa 100644 --- a/server/modules/config.js +++ b/server/modules/config.js @@ -75,13 +75,14 @@ module.exports = { } } }).then(results => { - if (_.isArray(results) && results.length > 0) { + if (_.isArray(results) && results.length === subsets.length) { results.forEach(result => { wiki.config[result.key] = result.config }) return true } else { - return Promise.reject(new Error('Invalid DB Configuration result set')) + wiki.logger.warn('DB Configuration is empty or incomplete.') + return false } }) } diff --git a/server/modules/db.js b/server/modules/db.js index 17081b36..34fc02e3 100644 --- a/server/modules/db.js +++ b/server/modules/db.js @@ -75,7 +75,7 @@ module.exports = { min: 0, idle: 10000 }, - logging: false, + logging: log => { wiki.logger.log('verbose', log) }, operatorsAliases }) @@ -92,10 +92,10 @@ module.exports = { fs .readdirSync(dbModelsPath) - .filter(function (file) { + .filter(file => { return (file.indexOf('.') !== 0 && file.indexOf('_') !== 0) }) - .forEach(function (file) { + .forEach(file => { let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0])) self[modelName] = self.inst.import(path.join(dbModelsPath, file)) }) @@ -110,8 +110,8 @@ module.exports = { // -> Sync DB Schemas syncSchemas() { return self.inst.sync({ - force: false, - logging: false + force: true, + logging: log => { wiki.logger.log('verbose', log) } }) }, // -> Set Connection App Name @@ -129,7 +129,7 @@ module.exports = { // Perform init tasks - self.onReady = Promise.each(initTasksQueue, t => t()) + self.onReady = Promise.each(initTasksQueue, t => t()).return(true) return self } diff --git a/server/modules/kernel.js b/server/modules/kernel.js new file mode 100644 index 00000000..48e1659f --- /dev/null +++ b/server/modules/kernel.js @@ -0,0 +1,91 @@ +const cluster = require('cluster') +const Promise = require('bluebird') +const _ = require('lodash') + +/* global wiki */ + +module.exports = { + numWorkers: 1, + workers: [], + init() { + if (cluster.isMaster) { + wiki.logger.info('=======================================') + wiki.logger.info('= Wiki.js =============================') + wiki.logger.info('=======================================') + + wiki.redis = require('./redis').init() + wiki.queue = require('./queue').init() + + this.setWorkerLimit() + this.bootMaster() + } else { + this.bootWorker() + } + }, + /** + * Pre-Master Boot Sequence + */ + preBootMaster() { + return Promise.mapSeries([ + () => { return wiki.db.onReady }, + () => { return wiki.configSvc.loadFromDb() }, + () => { return wiki.queue.clean() } + ], fn => { return fn() }) + }, + /** + * Boot Master Process + */ + bootMaster() { + this.preBootMaster().then(sequenceResults => { + if (_.every(sequenceResults, rs => rs === true)) { + this.postBootMaster() + } else { + wiki.logger.info('Starting configuration manager...') + require('../configure')() + } + return true + }).catch(err => { + wiki.logger.error(err) + process.exit(1) + }) + }, + /** + * Post-Master Boot Sequence + */ + postBootMaster() { + require('../master')().then(() => { + _.times(this.numWorker, this.spawnWorker) + + wiki.queue.uplClearTemp.add({}, { + repeat: { cron: '*/15 * * * *' } + }) + }) + + cluster.on('exit', (worker, code, signal) => { + wiki.logger.info(`Background Worker #${worker.id} was terminated.`) + }) + }, + /** + * Boot Worker Process + */ + bootWorker() { + wiki.logger.info(`Background Worker #${cluster.worker.id} is initializing...`) + require('../worker') + }, + /** + * Spawn new Worker process + */ + spawnWorker() { + this.workers.push(cluster.fork()) + }, + /** + * Set Worker count based on config + system capabilities + */ + setWorkerLimit() { + const numCPUs = require('os').cpus().length + this.numWorkers = (wiki.config.workers > 0) ? wiki.config.workers : numCPUs + if (this.numWorkers > numCPUs) { + this.numWorkers = numCPUs + } + } +} diff --git a/server/modules/queue.js b/server/modules/queue.js index e7612ee3..4bbb6dc0 100644 --- a/server/modules/queue.js +++ b/server/modules/queue.js @@ -30,7 +30,7 @@ module.exports = { }) }).then(() => { wiki.logger.info('Purging old queue jobs: OK') - }).catch(err => { + }).return(true).catch(err => { wiki.logger.error(err) }) } diff --git a/server/worker.js b/server/worker.js index 0028f755..6ca90eba 100644 --- a/server/worker.js +++ b/server/worker.js @@ -1,5 +1,3 @@ -'use strict' - /* global wiki */ const Promise = require('bluebird') diff --git a/tools/fuse_tasks.js b/tools/fuse_tasks.js index e5160a6c..c9d0ee03 100644 --- a/tools/fuse_tasks.js +++ b/tools/fuse_tasks.js @@ -65,27 +65,6 @@ module.exports = Promise.mapSeries([ } }) }, - /** - * i18n - */ - () => { - console.info(colors.white(' └── ') + colors.green('Copying i18n client files...')) - return fs.ensureDirAsync('./assets/js/i18n').then(() => { - return fs.readJsonAsync('./server/locales/en/browser.json').then(enContent => { - return fs.readdirAsync('./server/locales').then(langs => { - return Promise.map(langs, lang => { - console.info(colors.white(' ' + lang + '.json')) - let outputPath = path.join('./assets/js/i18n', lang + '.json') - return fs.readJsonAsync(path.join('./server/locales', lang + 'browser.json'), 'utf8').then((content) => { - return fs.outputJsonAsync(outputPath, _.defaultsDeep(content, enContent)) - }).catch(err => { // eslint-disable-line handle-callback-err - return fs.outputJsonAsync(outputPath, enContent) - }) - }) - }) - }) - }) - }, /** * Delete Fusebox cache */ diff --git a/wiki.js b/wiki.js index faa38b0d..4f14f1bb 100644 --- a/wiki.js +++ b/wiki.js @@ -1,13 +1,72 @@ #!/usr/bin/env node -'use strict' // =========================================== // Wiki.js -// 1.0.1 +// 2.0 // Licensed under AGPLv3 // =========================================== -const init = require('./server/init') +const Promise = require('bluebird') +const fs = Promise.promisifyAll(require('fs-extra')) +const pm2 = Promise.promisifyAll(require('pm2')) +const ora = require('ora') +const path = require('path') + +const ROOTPATH = process.cwd() + +const init = { + /** + * Start in background mode + */ + start () { + let spinner = ora('Initializing...').start() + return fs.emptyDirAsync(path.join(ROOTPATH, './logs')).then(() => { + return pm2.connectAsync().then(() => { + return pm2.startAsync({ + name: 'wiki', + script: 'server', + cwd: ROOTPATH, + output: path.join(ROOTPATH, './logs/wiki-output.log'), + error: path.join(ROOTPATH, './logs/wiki-error.log'), + minUptime: 5000, + maxRestarts: 5 + }).then(() => { + spinner.succeed('Wiki.js has started successfully.') + }).finally(() => { + pm2.disconnect() + }) + }) + }).catch(err => { + spinner.fail(err) + process.exit(1) + }) + }, + /** + * Stop Wiki.js process(es) + */ + stop () { + let spinner = ora('Shutting down Wiki.js...').start() + return pm2.connectAsync().then(() => { + return pm2.stopAsync('wiki').then(() => { + spinner.succeed('Wiki.js has stopped successfully.') + }).finally(() => { + pm2.disconnect() + }) + }).catch(err => { + spinner.fail(err) + process.exit(1) + }) + }, + /** + * Restart Wiki.js process(es) + */ + restart: function () { + let self = this + return self.stop().delay(1000).then(() => { + self.startDetect() + }) + } +} require('yargs') // eslint-disable-line no-unused-expressions .usage('Usage: node $0 [args]') @@ -16,7 +75,7 @@ require('yargs') // eslint-disable-line no-unused-expressions alias: ['boot', 'init'], desc: 'Start Wiki.js process', handler: argv => { - init.startDetect() + init.start() } }) .command({ @@ -35,18 +94,9 @@ require('yargs') // eslint-disable-line no-unused-expressions init.restart() } }) - .command({ - command: 'configure [port]', - alias: ['config', 'conf', 'cfg', 'setup'], - desc: 'Configure Wiki.js using the web-based setup wizard', - builder: (yargs) => yargs.default('port', 3000), - handler: argv => { - init.configure(argv.port) - } - }) .recommendCommands() .demandCommand(1, 'You must provide one of the accepted commands above.') .help() .version() - .epilogue('Read the docs at https://wiki.requarks.io') + .epilogue('Read the docs at https://docs.requarks.io/wiki') .argv diff --git a/yarn.lock b/yarn.lock index 6f13419804f9ea17056cfe62ac051186708ca4f8..7e8baa6fdc3291660cb19632a4a56d36c0658c32 100644 GIT binary patch delta 1104 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