diff --git a/core/bbs.js b/core/bbs.js index aff1bdb3..0bf8f8aa 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -69,15 +69,12 @@ function main() { const configOverridePath = argv.config; - return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath)); + return callback(null, configOverridePath || conf.Config.getDefaultPath(), _.isString(configOverridePath)); }, function initConfig(configPath, configPathSupplied, callback) { const configFile = configPath + 'config.hjson'; - conf.Config.create(configFile, {}, err => { - console.log(err); - }); - conf.init(resolvePath(configFile), function configInit(err) { + conf.Config.create(resolvePath(configFile), err => { // // If the user supplied a path and we can't read/parse it // then it's a fatal error diff --git a/core/config.js b/core/config.js index d352a568..927d7bbf 100644 --- a/core/config.js +++ b/core/config.js @@ -1,202 +1,61 @@ -/* jslint node: true */ -'use strict'; - // ENiGMA½ -const Errors = require('./enig_error.js').Errors; const DefaultConfig = require('./config_default'); const ConfigLoader = require('./config_loader'); -// deps -const paths = require('path'); -const async = require('async'); -const assert = require('assert'); - const _ = require('lodash'); -exports.init = init; -exports.getDefaultPath = getDefaultPath; +// Global system configuration instance; see Config.create() +let systemConfigInstance; -class Config extends ConfigLoader { +exports.Config = class Config extends ConfigLoader { constructor(options) { super(options); } - static create(basePath, options, cb) { + static create(baseConfigPath, cb) { const replacePaths = [ 'loginServers.ssh.algorithms.kex', 'loginServers.ssh.algorithms.cipher', 'loginServers.ssh.algorithms.hmac', 'loginServers.ssh.algorithms.compress', ]; + const replaceKeys = [ 'args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch', ]; - options.defaultConfig = DefaultConfig(); - - options.defaultsCustomizer = (defaultVal, configVal, key, path) => { - if (Array.isArray(defaultVal) && Array.isArray(configVal)) { - if (replacePaths.includes(path) || replaceKeys.includes(key)) { - // full replacement using user config value - return configVal; - } else { - // merge user config & default config; keep only unique - _.uniq(defaultVal.concat(configVal)); + const options = { + defaultConfig : DefaultConfig, + defaultsCustomizer : (defaultVal, configVal, key, path) => { + if (Array.isArray(defaultVal) && Array.isArray(configVal)) { + if (replacePaths.includes(path) || replaceKeys.includes(key)) { + // full replacement using user config value + return configVal; + } else { + // merge user config & default config; keep only unique + _.uniq(defaultVal.concat(configVal)); + } } - } + }, }; - return ConfigLoader.create(basePath, options, cb); - } -}; - -let currentConfiguration = {}; - -function hasMessageConferenceAndArea(config) { - assert(_.isObject(config.messageConferences)); // we create one ourself! - - const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => { - return 'system_internal' !== confTag; - }); - - if(0 === nonInternalConfs.length) { - return false; - } - - // :TODO: there is likely a better/cleaner way of doing this - - let result = false; - _.forEach(nonInternalConfs, confTag => { - if(_.has(config.messageConferences[confTag], 'areas') && - Object.keys(config.messageConferences[confTag].areas) > 0) - { - result = true; - return false; // stop iteration - } - }); - - return result; -} - -const ArrayReplaceKeyPaths = [ - 'loginServers.ssh.algorithms.kex', - 'loginServers.ssh.algorithms.cipher', - 'loginServers.ssh.algorithms.hmac', - 'loginServers.ssh.algorithms.compress', -]; - -const ArrayReplaceKeys = [ - 'args', - 'sendArgs', 'recvArgs', 'recvArgsNonBatch', -]; - -function mergeValidateAndFinalize(config, cb) { - const defaultConfig = DefaultConfig(); - - const arrayReplaceKeyPathsMutable = _.clone(ArrayReplaceKeyPaths); - const shouldReplaceArray = (arr, key) => { - if(ArrayReplaceKeys.includes(key)) { - return true; - } - for(let i = 0; i < arrayReplaceKeyPathsMutable.length; ++i) { - const o = _.get(defaultConfig, arrayReplaceKeyPathsMutable[i]); - if(_.isEqual(o, arr)) { - arrayReplaceKeyPathsMutable.splice(i, 1); - return true; - } - } - return false; - }; - - async.waterfall( - [ - function mergeWithDefaultConfig(callback) { - const mergedConfig = _.mergeWith( - defaultConfig, - config, - (defConfig, userConfig, key) => { - if(Array.isArray(defConfig) && Array.isArray(userConfig)) { - // - // Arrays are special: Some we merge, while others - // we simply replace. - // - if(shouldReplaceArray(defConfig, key)) { - return userConfig; - } else { - return _.uniq(defConfig.concat(userConfig)); - } - } - } - ); - - return callback(null, mergedConfig); - }, - function validate(mergedConfig, callback) { - // - // Various sections must now exist in config - // - // :TODO: Logic is broken here: - if(hasMessageConferenceAndArea(mergedConfig)) { - return callback(Errors.MissingConfig('Please create at least one message conference and area!')); - } - return callback(null, mergedConfig); - }, - function setIt(mergedConfig, callback) { - currentConfiguration = mergedConfig; - exports.get = () => currentConfiguration; - return callback(null); - } - ], - err => { - if(cb) { + systemConfigInstance = new Config(options); + systemConfigInstance.init(baseConfigPath, err => { + if (err) { + console.stdout(`Configuration ${baseConfigPath} error: ${err.message}`); // eslint-disable-line no-console return cb(err); } - } - ); -} -function init(configPath, options, cb) { - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } + // late bind an exported get method to the global Config + // instance we just created + exports.get = systemConfigInstance.get.bind(systemConfigInstance); - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - ConfigCache.getConfig(reCachedPath, (err, config) => { - if(!err) { - mergeValidateAndFinalize(config, err => { - if(!err) { - const Events = require('./events.js'); - Events.emit(Events.getSystemEvents().ConfigChanged); - } - }); - } else { - console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console - } - }); - }; - - const ConfigCache = require('./config_cache.js'); - const getConfigOptions = { - filePath : configPath, - hotReload : options.hotReload, - }; - if(options.hotReload) { - getConfigOptions.callback = changed; - } - ConfigCache.getConfigWithOptions(getConfigOptions, (err, config) => { - if(err) { return cb(err); - } + }); + } - return mergeValidateAndFinalize(config, cb); - }); -} - -function getDefaultPath() { - // e.g. /enigma-bbs-install-path/config/ - return './config/'; -} - -exports.Config = Config; \ No newline at end of file + static getDefaultPath() { + // e.g. /enigma-bbs-install-path/config/ + return './config/'; + } +}; diff --git a/core/config_cache.js b/core/config_cache.js index 631fb5f1..fc3ba878 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -35,7 +35,7 @@ module.exports = new class ConfigCache this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { if(!err) { if(options.callback) { - options.callback( { fileName, fileRoot } ); + options.callback( { fileName, fileRoot, configCache : this } ); } } }); diff --git a/core/config_loader.js b/core/config_loader.js index 50549c85..6df22bdb 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -6,26 +6,99 @@ const _ = require('lodash'); const reduceDeep = require('deepdash/getReduceDeep')(_); module.exports = class ConfigLoader { - constructor(options) { + constructor( + { hotReload = true, defaultConfig = {}, defaultsCustomizer = null } = { hotReload : true, defaultConfig : {}, defaultsCustomizer : null } ) + { this.current = {}; - this.hotReload = _.get(options, 'hotReload', true); + + this.hotReload = hotReload; + this.defaultConfig = defaultConfig; + this.defaultsCustomizer = defaultsCustomizer; } - static create(basePath, options, cb) { - const config = new ConfigLoader(options); - config._init( - basePath, - options, - err => { - return cb(err, config); - } - ); + init(baseConfigPath, cb) { + this.baseConfigPath = baseConfigPath; + return this._reload(baseConfigPath, cb); } get() { return this.current; } + _reload(baseConfigPath, cb) { + let defaultConfig; + if (_.isFunction(this.defaultConfig)) { + defaultConfig = this.defaultConfig(); + } else if (_.isObject(this.defaultConfig)) { + defaultConfig = this.defaultConfig; + } else { + defaultConfig = {}; + } + + // + // 1 - Fetch base configuration from |baseConfigPath| + // 2 - Merge with |defaultConfig| + // 3 - Resolve any includes + // 4 - Resolve @reference and @environment + // 5 - Perform any validation + // + async.waterfall( + [ + (callback) => { + return this._loadConfigFile(baseConfigPath, callback); + }, + (config, callback) => { + if (_.isFunction(this.defaultsCustomizer)) { + const stack = []; + const mergedConfig = _.mergeWith( + defaultConfig, + config, + (defaultVal, configVal, key, target, source) => { + var path; + while (true) { + if (!stack.length) { + stack.push({source, path : []}); + } + + const prev = stack[stack.length - 1]; + + if (source === prev.source) { + path = prev.path.concat(key); + stack.push({source : configVal, path}); + break; + } + + stack.pop(); + } + + path = path.join('.'); + return this.defaultsCustomizer(defaultVal, configVal, key, path); + } + ); + + return callback(null, mergedConfig); + } + + return callback(null, _.merge(defaultConfig, config)); + }, + (config, callback) => { + const configRoot = paths.dirname(baseConfigPath); + return this._resolveIncludes(configRoot, config, callback); + }, + (config, callback) => { + config = this._resolveAtSpecs(config); + return callback(null, config); + }, + ], + (err, config) => { + if (!err) { + this.current = config; + } + return cb(err); + } + ); + } + _convertTo(value, type) { switch (type) { case 'bool' : @@ -102,92 +175,24 @@ module.exports = class ConfigLoader { }); } - _configFileChanged({fileName, fileRoot}) { + _configFileChanged({fileName, fileRoot, configCache}) { const reCachedPath = paths.join(fileRoot, fileName); - ConfigCache.getConfig(reCachedPath, (err, config) => { - /* - if(!err) { - mergeValidateAndFinalize(config, err => { - if(!err) { + configCache.getConfig(reCachedPath, (err, config) => { + if (err) { + return console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console + } + + if (this.configPaths.includes(reCachedPath)) { + this._reload(this.baseConfigPath, err => { + if (!err) { const Events = require('./events.js'); Events.emit(Events.getSystemEvents().ConfigChanged); } }); - } else { - console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console } - */ }); } - _init(basePath, options, cb) { - options.defaultConfig = options.defaultConfig || {}; - - // - // 1 - Fetch base configuration from |basePath| - // 2 - Merge with |defaultConfig|, if any - // 3 - Resolve any includes - // 4 - Resolve @reference and @environment - // 5 - Perform any validation - // - async.waterfall( - [ - (callback) => { - return this._loadConfigFile(basePath, callback); - }, - (config, callback) => { - if (_.isFunction(options.defaultsCustomizer)) { - const stack = []; - const mergedConfig = _.mergeWith( - options.defaultConfig, - config, - (defaultVal, configVal, key, target, source) => { - var path; - while (true) { - if (!stack.length) { - stack.push({source, path : []}); - } - - const prev = stack[stack.length - 1]; - - if (source === prev.source) { - path = prev.path.concat(key); - stack.push({source : configVal, path}); - break; - } - - stack.pop(); - } - - path = path.join('.'); - return options.defaultsCustomizer(defaultVal, configVal, key, path); - } - ); - - return callback(null, mergedConfig); - } - - // :TODO: correct? - return callback(null, _.merge(options.defaultConfig, config)); - }, - (config, callback) => { - const configRoot = paths.dirname(basePath); - return this._resolveIncludes(configRoot, config, callback); - }, - (config, callback) => { - config = this._resolveAtSpecs(config); - return callback(null, config); - }, - ], - (err, config) => { - if (!err) { - this.current = config; - } - return cb(err); - } - ); - } - _resolveIncludes(configRoot, config, cb) { if (!Array.isArray(config.includes)) { return cb(null, config); @@ -207,6 +212,7 @@ module.exports = class ConfigLoader { }); }, err => { + this.configPaths = [ this.baseConfigPath, ...includePaths ]; return cb(err, config); }); }