From 78178eb43dd2b751f876e97bf23759fcadc1b796 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 18 Nov 2015 11:44:22 +0000 Subject: [PATCH] load services --- boot/master.js | 4 ++ lib/api-server.js | 4 ++ lib/services-loader.js | 113 +++++++++++++++++++++++++++++++++++++++++ lib/worker.js | 64 +++++++++++++---------- package.json | 1 + walnut.js | 4 ++ 6 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 lib/services-loader.js diff --git a/boot/master.js b/boot/master.js index d2d2a05..e6d904b 100644 --- a/boot/master.js +++ b/boot/master.js @@ -78,6 +78,10 @@ cluster.on('online', function (worker) { info.conf.ipcKey = conf.ipcKey; info.conf.memstoreSock = conf.memstoreSock; info.conf.sqlite3Sock = conf.sqlite3Sock; + // TODO get this from db config instead + var config = require('../config'); + info.conf.primaryNameserver = config.primaryNameserver; + info.conf.nameservers = config.nameservers; worker.send(info); }); } diff --git a/lib/api-server.js b/lib/api-server.js index 50ae29c..2ab22ce 100644 --- a/lib/api-server.js +++ b/lib/api-server.js @@ -20,6 +20,10 @@ module.exports.create = function (conf, deps, app) { return new PromiseA(function (resolve, reject) { try { + // TODO dynamic requires are a no-no + // can we statically generate a require-er? on each install? + // module.exports = { {{pkgpath}}: function () { return require({{pkgpath}}) } } + // requirer[pkgpath]() route.route = require(pkgpath).create(conf, deps, app); } catch(e) { reject(e); diff --git a/lib/services-loader.js b/lib/services-loader.js new file mode 100644 index 0000000..ef61465 --- /dev/null +++ b/lib/services-loader.js @@ -0,0 +1,113 @@ +'use strict'; + +module.exports.create = function (conf, deps) { + var PromiseA = deps.Promise; + + function loadService(node) { + var path = require('path'); + + return new PromiseA(function (resolve) { + // process.nextTick runs at the end of the current event loop + // we actually want time to pass so that potential api traffic can be handled + setTimeout(function () { + var servicepath = path.join(conf.servicespath, node); + var pkg; + + try { + // TODO no package should be named package.json + pkg = require(servicepath + '/package.json'); + resolve({ + pkg: pkg + , name: node + , service: require(servicepath) + }); + return; + } catch(e) { + // TODO report errors to admin console + // TODO take sha256sum of e.stack and store in db with tick for updatedAt + console.error("[Service] could not require service '" + servicepath + "'"); + console.error(e.stack); + //services.push({ error: e }); + resolve(null); + return; + } + }, 1); + }); + } + + function loadServices() { + var fs = PromiseA.promisifyAll(require('fs')); + + // deps : { memstore, sqlstores, clientSqlFactory, systemSqlFactory } + + // XXX this is a no-no (file system access in a worker, cannot be statically analyzed) + // TODO regenerate a static file of all requires on each install + // TODO read system config db to find which services auto-start + // TODO allow certain apis access to certain services + return fs.readdirAsync(conf.servicespath).then(function (nodes) { + var promise = PromiseA.resolve(); + var services = []; + + nodes.forEach(function (node) { + promise = promise.then(function () { + return loadService(node).then(function (srv) { + if (!srv) { + return; + } + services.push(srv); + }); + }); + }); + + return promise.then(function () { + return services; + }); + }); + } + + function startService(srv) { + return new PromiseA(function (resolve) { + // process.nextTick runs at the end of the current event loop + // we actually want time to pass so that potential api traffic can be handled + setTimeout(function () { + try { + PromiseA.resolve(srv.service.create(conf, deps)).then(resolve, function (e) { + console.error("[Service] couldn't promise service"); + console.error(e.stack); + resolve(null); + }); + return; + } catch(e) { + console.error("[Service] couldn't start service"); + console.error(e.stack); + resolve(null); + return; + } + }, 1); + }); + } + + function startServices(services) { + var promise = PromiseA.resolve(); + var servicesMap = {}; + + services.forEach(function (srv) { + promise = promise.then(function () { + return startService(srv).then(function (service) { + if (!service) { + // TODO log + return null; + } + srv.service = service; + servicesMap[srv.name] = srv; + }); + }); + }); + + return promise.then(function () { + return servicesMap; + }); + } + + return loadServices().then(startServices); +}; diff --git a/lib/worker.js b/lib/worker.js index 738c8eb..305ef1b 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -11,6 +11,7 @@ module.exports.create = function (webserver, info, state) { var express = require('express-lazy'); var app = express(); var apiHandler; + var Services; var memstore; var sqlstores = {}; var models = {}; @@ -110,6 +111,8 @@ module.exports.create = function (webserver, info, state) { app.use('/', caddyBugfix); return PromiseA.all([ + // TODO security on memstore + // TODO memstoreFactory.create cstore.create({ sock: info.conf.memstoreSock , connect: info.conf.memstoreSock @@ -146,14 +149,28 @@ module.exports.create = function (webserver, info, state) { return require('../lib/schemes-config').create(sqlstores.config).then(function (tables) { models.Config = tables; return models.Config.Config.get().then(function (vhostsMap) { - - /* - // todo getDomainInfo - var utils = require('./utils'); - results.domains.forEach(function (domain) { - utils.getDomainInfo(domain.id); - }); - */ + // TODO the core needs to be replacable in one shot + // rm -rf /tmp/walnut/; tar xvf -C /tmp/walnut/; mv /srv/walnut /srv/walnut.{{version}}; mv /tmp/walnut /srv/ + // this means that any packages must be outside, perhaps /srv/walnut/{boot,core,packages} + var apiConf = { + apppath: '../packages/apps/' + , apipath: '../packages/apis/' + , servicespath: path.join(__dirname, '..', 'packages', 'services') + , vhostsMap: vhostsMap + , server: webserver + , externalPort: info.conf.externalPort + , primaryNameserver: info.conf.primaryNameserver + , nameservers: info.conf.nameservers + , apiPrefix: '/api' + }; + + Services = require('./services-loader').create(apiConf, { + memstore: memstore + , sqlstores: sqlstores + , clientSqlFactory: clientFactory + , systemSqlFactory: systemFactory + , Promise: PromiseA + }); function handleApi(req, res, next) { var myApp; @@ -183,29 +200,22 @@ module.exports.create = function (webserver, info, state) { return; } - // apiHandler = require('./vhost-server').create(info.localPort, vhostsdir).create(webserver, app) + // apiHandler = require('./vhost-server').create(info.conf.localPort, vhostsdir).create(webserver, app) myApp = express(); if (app.get('trust proxy')) { myApp.set('trust proxy', app.get('trust proxy')); } - apiHandler = require('./api-server').create( - { apppath: '../packages/apps/' - , apipath: '../packages/apis/' - , vhostsMap: vhostsMap - , server: webserver - , externalPort: info.externalPort - , apiPrefix: '/api' - } - , { app: myApp - , memstore: memstore - , sqlstores: sqlstores - , clientSqlFactory: clientFactory - , systemSqlFactory: systemFactory - //, handlePromise: require('./lib/common').promisableRequest; - //, handleRejection: require('./lib/common').rejectableRequest; - //, localPort: info.localPort - } - ).api; + apiHandler = require('./api-server').create(apiConf, { + app: myApp + , memstore: memstore + , sqlstores: sqlstores + , clientSqlFactory: clientFactory + , systemSqlFactory: systemFactory + //, handlePromise: require('./lib/common').promisableRequest; + //, handleRejection: require('./lib/common').rejectableRequest; + //, localPort: info.conf.localPort + , Promise: PromiseA + }, Services).api; apiHandler(req, res, next); } diff --git a/package.json b/package.json index c8f8227..c6cb246 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "mime-db": "^1.8.0", "mime-types": "^2.0.10", "ms": "^0.7.0", + "native-dns": "^0.7.0", "negotiator": "^0.5.1", "node-pre-gyp": "^0.6.4", "node-uuid": "^1.4.4", diff --git a/walnut.js b/walnut.js index 043c856..9aea314 100644 --- a/walnut.js +++ b/walnut.js @@ -22,5 +22,9 @@ Math.random = function () { if (cluster.isMaster) { require('./boot/master'); } else { + /* + alternately we could use this and then check require.main + cluster.setupMaster({ exec : "app.js", }); + */ require('./boot/worker').create(null); }