diff --git a/bin/goldilocks.js b/bin/goldilocks.js index 0896544..173c951 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -8,67 +8,136 @@ if (!cluster.isMaster) { return; } -function run(config) { - // TODO spin up multiple workers - // TODO use greenlock-cluster - function work() { - var worker = cluster.fork(); - worker.on('exit', work).on('online', function () { - console.log('[worker]', worker.id, 'online'); - // Worker is listening - worker.send(config); - }); - } - console.log('config.tcp.bind', config.tcp.bind); - work(); -} - -function readConfigAndRun(args) { - var fs = require('fs'); - var path = require('path'); - var cwd = args.cwd || process.cwd(); - var text; - var filename; - var config; - - if (args.config) { - filename = path.resolve(cwd, args.config); - text = fs.readFileSync(filename, 'utf8'); - } - else { - filename = path.resolve(cwd, 'goldilocks.yml'); - - if (fs.existsSync(filename)) { - text = fs.readFileSync(filename, 'utf8'); +var PromiseA = require('bluebird'); +var fs = PromiseA.promisifyAll(require('fs')); +var configStorage; +function mergeSettings(orig, changes) { + Object.keys(changes).forEach(function (key) { + // TODO: use an API that can properly handle updating arrays. + if (!changes[key] || (typeof changes[key] !== 'object') || Array.isArray(changes[key])) { + orig[key] = changes[key]; + } + else if (!orig[key] || typeof orig[key] !== 'object') { + orig[key] = changes[key]; } else { - filename = path.resolve(cwd, 'goldilocks.json'); - if (fs.existsSync(filename)) { - text = fs.readFileSync(filename, 'utf8'); - } else { - text = '{}'; - } + mergeSettings(orig[key], changes[key]); } - } - - try { - config = JSON.parse(text); - } catch(e) { - try { - config = require('js-yaml').safeLoad(text); - // blank config file - if ('undefined' === typeof config) { - config = {}; - } - } catch(e) { - throw new Error( - "Could not load '" + filename + "' as JSON nor YAML" - ); - } - } - + }); +} +function createStorage(filename, filetype) { var recase = require('recase').create({}); - config = recase.camelCopy(config); + var snakeCopy = recase.snakeCopy.bind(recase); + var camelCopy = recase.camelCopy.bind(recase); + + var parse, dump; + if (filetype === 'json') { + parse = JSON.parse; + dump = function (arg) { return JSON.stringify(arg, null, ' '); }; + } else { + var yaml = require('js-yaml'); + parse = function (text) { return yaml.safeLoad(text) || {}; }; + dump = yaml.safeDump; + } + + function read() { + return fs.readFileAsync(filename) + .catch(function (err) { + if (err.code === 'ENOENT') { + return ''; + } + return PromiseA.reject(err); + }) + .then(parse) + ; + } + + var result = { + read: function () { + return read().then(camelCopy); + } + , save: function (changes) { + if (!changes || typeof changes !== 'object' || Array.isArray(changes)) { + return PromiseA.reject(new Error('invalid config')); + } + changes = snakeCopy(changes); + return read() + .then(snakeCopy) + .then(function (current) { + mergeSettings(current, changes); + // TODO: validate/lint the config before we actually write it. + return dump(current); + }) + .then(function (newText) { + return fs.writeFileAsync(filename, newText); + }) + .then(function () { + return result.read(); + }) + ; + } + }; + return result; +} +function checkConfigLocation(cwd, configFile) { + cwd = cwd || process.cwd(); + var path = require('path'); + var filename; + + var prom; + if (configFile) { + filename = path.resolve(cwd, configFile); + prom = fs.readFileAsync(filename); + } else { + prom = PromiseA.reject('blah') + .catch(function () { + filename = path.resolve(cwd, 'goldilocks.yml'); + return fs.readFileAsync(filename); + }) + .catch(function () { + filename = path.resolve(cwd, 'goldilocks.json'); + return fs.readFileAsync(filename); + }) + .catch(function () { + filename = path.resolve(cwd, 'etc/goldilocks/goldilocks.yml'); + return fs.readFileAsync(filename); + }) + .catch(function () { + filename = '/etc/goldilocks/goldilocks.yml'; + return fs.readFileAsync(filename); + }) + .catch(function () { + filename = path.resolve(cwd, 'goldilocks.yml'); + return ''; + }) + ; + } + + return prom.then(function (text) { + try { + JSON.parse(text); + return { name: filename, type: 'json' }; + } catch (err) {} + + try { + require('js-yaml').safeLoad(text); + return { name: filename, type: 'yaml' }; + } catch (err) {} + + throw new Error('Could not load "' + filename + '" as JSON nor YAML'); + }); +} +function createConfigStorage(args) { + return checkConfigLocation(args.cwd, args.config) + .then(function (result) { + console.log('config file', result.name, 'is of type', result.type); + configStorage = createStorage(result.name, result.type); + return configStorage.read(); + }) + ; +} + +function fillConfig(config, args) { config.debug = config.debug || args.debug; if (!config.dns) { @@ -77,7 +146,7 @@ function readConfigAndRun(args) { // Use Object.assign to add any properties needed but not defined in the mdns config. // It will first copy the defaults into an empty object, then copy any real config over that. var mdnsDefaults = { port: 5353, broadcast: '224.0.0.251', ttl: 300 }; - config.mdns = Object.assign({}, mdnsDefaults, config.mdns || {}); + config.mdns = Object.assign({}, mdnsDefaults, config.mdns); if (!config.tcp) { config.tcp = {}; @@ -100,12 +169,7 @@ function readConfigAndRun(args) { } // maybe this should not go in config... but be ephemeral in some way? - if (args.cwd) { - config.cwd = args.cwd; - } - if (!config.cwd) { - config.cwd = process.cwd(); - } + config.cwd = args.cwd || config.cwd || process.cwd(); var ipaddr = require('ipaddr.js'); var addresses = []; @@ -135,11 +199,10 @@ function readConfigAndRun(args) { // TODO maybe move to config.state.addresses (?) config.addresses = addresses; - config.device = { hostname: 'daplien-pod' }; + config.device = { hostname: require('os').hostname() }; config.tunnel = args.tunnel || config.tunnel; - var PromiseA = require('bluebird'); var tcpProm, dnsProm; if (config.tcp.bind) { @@ -186,18 +249,63 @@ function readConfigAndRun(args) { }); } - PromiseA.all([tcpProm, dnsProm]) - .then(function () { - run(config); - }) + return PromiseA.all([tcpProm, dnsProm]) + .then(function () { return config; }) .catch(function (failed) { - console.warn("could not bind to the desired ports"); Object.keys(failed).forEach(function (key) { console.log('[error bind]', key, failed[key].code); }); + return PromiseA.reject(new Error("could not bind to the desired ports")); }); } +function run(args) { + var workers = {}; + var cachedConfig; + + cluster.on('message', function (worker, message) { + if (message.type !== 'com.daplie.goldilocks.config-change') { + return; + } + configStorage.save(message.changes) + .then(function (config) { + return fillConfig(config, args); + }) + .then(function (config) { + cachedConfig = config; + }) + .catch(function (err) { + console.error('error changing config', err); + }) + ; + }); + + cluster.on('online', function (worker) { + console.log('[worker]', worker.id, 'online'); + workers[worker.id] = worker; + // Worker is listening + worker.send(cachedConfig); + }); + + cluster.on('exit', function (worker) { + delete workers[worker.id]; + cluster.fork(); + }); + + createConfigStorage(args) + .then(function (config) { + return fillConfig(config, args); + }) + .then(function (config) { + console.log('config.tcp.bind', config.tcp.bind); + cachedConfig = config; + // TODO spin up multiple workers + // TODO use greenlock-cluster + cluster.fork(); + }) + ; +} + function readEnv(args) { // TODO try { @@ -213,7 +321,7 @@ function readEnv(args) { , debug: process.env.GOLDILOCKS_DEBUG && true }; - readConfigAndRun(Object.assign({}, env, args)); + run(Object.assign({}, env, args)); } var program = require('commander'); diff --git a/lib/app.js b/lib/app.js index 602a7fa..7db8d67 100644 --- a/lib/app.js +++ b/lib/app.js @@ -17,29 +17,6 @@ module.exports = function (myDeps, conf, overrideHttp) { var app; var request; - /* - function _reloadWrite(data, enc, cb) { - // /*jshint validthis: true */ /* - if (this.headersSent) { - this.__write(data, enc, cb); - return; - } - - if (!/html/i.test(this.getHeader('Content-Type'))) { - this.__write(data, enc, cb); - return; - } - - if (this.getHeader('Content-Length')) { - this.setHeader('Content-Length', this.getHeader('Content-Length') + this.__my_addLen); - } - - this.__write(this.__my_livereload); - this.__write(data, enc, cb); - } - */ - - function createServeInit() { var PromiseA = require('bluebird'); var OAUTH3 = require('../packages/assets/org.oauth3'); @@ -106,7 +83,7 @@ module.exports = function (myDeps, conf, overrideHttp) { myDeps.PromiseA = PromiseA; myDeps.OAUTH3 = OAUTH3; - myDeps.storage = { owners: owners }; + myDeps.storage = Object.assign({ owners: owners }, myDeps.storage); myDeps.recase = require('recase').create({}); myDeps.request = request; myDeps.api = { diff --git a/lib/worker.js b/lib/worker.js index 23724cb..3318b83 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -10,6 +10,16 @@ process.on('message', function (conf) { , net: require('net') }; deps.proxy = require('./proxy-conn').create(deps, conf); + deps.storage = { + config: { + save: function (changes) { + process.send({ + type: 'com.daplie.goldilocks.config-change', + changes: changes + }); + } + } + }; require('./goldilocks.js').create(deps, conf); }); diff --git a/packages/apis/com.daplie.goldilocks/index.js b/packages/apis/com.daplie.goldilocks/index.js index b5ed9dc..33d2f51 100644 --- a/packages/apis/com.daplie.goldilocks/index.js +++ b/packages/apis/com.daplie.goldilocks/index.js @@ -152,11 +152,13 @@ module.exports.create = function (deps, conf) { } jsonParser(req, res, function () { + console.log('config POST body', req.body); - console.log('req.body', req.body); - - deps.storage.config.merge(req.body); - deps.storage.config.save(); + // Since we are sending the changes to another process we don't really + // have a good way of seeing if it worked, so always report success + deps.storage.config.save(req.body); + res.setHeader('Content-Type', 'application/json;'); + res.end('{"success":true}'); }); }); }