Merge pull request #24 from wistityhq/feature/restart

Restart the server in development and production
This commit is contained in:
Loïc Saint-Roch 2015-11-05 16:28:09 +01:00
commit a93db78104
15 changed files with 253 additions and 340 deletions

View File

@ -8,6 +8,7 @@
// Node.js core.
const REPL = require('repl');
const cluster = require('cluster');
// Public node modules.
const winston = require('winston');
@ -36,7 +37,12 @@ module.exports = function () {
// Now load up the Strapi framework for real.
const strapi = server();
strapi.log.info('Starting the application in interactive mode...');
// Only log if the process is a master.
if (cluster.isMaster) {
strapi.log.info('Starting the application in interactive mode...');
}
strapi.start({}, function (err) {
// Log and exit the REPL in case there is an error

View File

@ -41,7 +41,7 @@ function Strapi() {
this.load = _.bind(this.load, this);
this.start = _.bind(this.start, this);
this.stop = _.bind(this.stop, this);
this.rebuild = _.bind(this.rebuild, this);
this.restart = _.bind(this.restart, this);
this.initialize = _.bind(this.initialize, this);
this.exposeGlobals = _.bind(this.exposeGlobals, this);
this.runBootstrap = _.bind(this.runBootstrap, this);
@ -81,7 +81,7 @@ util.inherits(Strapi, events.EventEmitter);
Strapi.prototype.start = require('./start');
Strapi.prototype.stop = require('./stop');
Strapi.prototype.rebuild = require('./rebuild');
Strapi.prototype.restart = require('./restart');
Strapi.prototype.server = require('koa');
Strapi.prototype.app = require('koa')();

View File

@ -170,21 +170,6 @@ module.exports = function (strapi) {
});
cb();
},
/**
* Reload the hook
*/
reload: function () {
hook.initialize(function (err) {
if (err) {
strapi.log.error('Failed to reinitialize the API hook.');
strapi.stop();
} else {
strapi.emit('hook:_api:reloaded');
}
});
}
};

View File

@ -111,13 +111,6 @@ module.exports = function (strapi) {
});
}
// Make sure the ORM config are equals to the databases file
// (aiming to not have issue with adapters when rebuilding the dictionary).
// It's kind of messy, for now, but it works fine. If someone has a better
// solution we'd be glad to accept a Pull Request.
const ormConfig = JSON.parse(fs.readFileSync(path.resolve(strapi.config.appPath, strapi.config.paths.config, 'environments', strapi.config.environment, 'databases.json')));
strapi.config.orm = ormConfig.orm;
// Save different environments inside an array because we need it in the Strapi Studio.
strapi.config.environments = fs.readdirSync(path.resolve(strapi.config.appPath, strapi.config.paths.config, 'environments'));
@ -131,21 +124,6 @@ module.exports = function (strapi) {
});
cb();
},
/**
* Reload the hook
*/
reload: function () {
hook.initialize(function (err) {
if (err) {
strapi.log.error('Failed to reinitialize the config hook.');
strapi.stop();
} else {
strapi.emit('hook:_config:reloaded');
}
});
}
};

View File

@ -141,21 +141,6 @@ module.exports = function (strapi) {
});
cb();
},
/**
* Reload the hook
*/
reload: function () {
hook.initialize(function (err) {
if (err) {
strapi.log.error('Failed to reinitialize the router.');
strapi.stop();
} else {
strapi.emit('hook:router:reloaded');
}
});
}
};

View File

@ -62,188 +62,189 @@ module.exports = function (strapi) {
]
});
manager.on('connect_failed', function () {
if (firstConnectionAttempt) {
strapi.log.warn('Connection to the Studio server failed!');
}
});
manager.on('reconnect_failed', function () {
strapi.log.warn('Reconnection to the Studio server failed!');
});
manager.on('reconnect', function () {
strapi.log.info('Connection to the Studio server found, please wait a few seconds...');
});
manager.on('reconnecting', function (number) {
strapi.log.warn('Connection error with the Studio server, new attempt in progress... (' + number + ')');
});
socket.on('connect', function () {
strapi.log.info('Connection with the Studio server found, please wait a few seconds...');
firstConnectionAttempt = false;
process.nextTick(_self.connectWithStudio(socket));
});
socket.on('error', function (err) {
strapi.log.warn(err);
});
socket.on('disconnect', function () {
strapi.log.info('Disconnected from the Studio server.');
});
socket.on('authorized', function (data) {
const decryptedData = strapi.rsa.decryptPublic(data, 'json');
if (decryptedData.status === 'ok') {
if (strapi.config.environment === 'development') {
socket.emit('testEncryption', {
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
secretKey: strapi.config.studio.secretKey,
data: 'ok'
})
}, function (err) {
if (err) {
strapi.log.warn(err);
}
strapi.log.info('Connected with the Studio server.');
});
} else {
strapi.log.warn('The use of the Studio is restricted to development environment.');
process.nextTick(function () {
manager.on('connect_failed', function () {
if (firstConnectionAttempt) {
strapi.log.warn('Connection to the Studio server failed!');
}
}
});
});
socket.on('todo', function (data, fn) {
if (!data.hasOwnProperty('from') || !data.hasOwnProperty('to')) {
fn(stringify('Some required attributes are missing', null, 2), null);
} else if (data.from === strapi.token) {
if (data.hasOwnProperty('files')) {
const syncPromise = function (file, index) {
const deferred = Promise.defer();
manager.on('reconnect_failed', function () {
strapi.log.warn('Reconnection to the Studio server failed!');
});
_self.unzipper(file)
.then(function () {
if (!_.isEmpty(data.files[index + 1])) {
return syncPromise(data.files[index + 1], index + 1);
} else {
manager.on('reconnect', function () {
strapi.log.info('Connection to the Studio server found, please wait a few seconds...');
});
manager.on('reconnecting', function (number) {
strapi.log.warn('Connection error with the Studio server, new attempt in progress... (' + number + ')');
});
socket.on('connect', function () {
firstConnectionAttempt = false;
_self.connectWithStudio(socket);
});
socket.on('error', function (err) {
strapi.log.warn(err);
});
socket.on('disconnect', function () {
strapi.log.info('Disconnected from the Studio server.');
});
socket.on('authorized', function (data) {
const decryptedData = strapi.rsa.decryptPublic(data, 'json');
if (decryptedData.status === 'ok') {
if (strapi.config.environment === 'development') {
socket.emit('testEncryption', {
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
secretKey: strapi.config.studio.secretKey,
data: 'ok'
})
}, function (err) {
if (err) {
strapi.log.warn(err);
}
strapi.log.info('Connected with the Studio server.');
});
} else {
strapi.log.warn('The use of the Studio is restricted to development environment.');
}
}
});
socket.on('todo', function (data, fn) {
if (!data.hasOwnProperty('from') || !data.hasOwnProperty('to')) {
fn(stringify('Some required attributes are missing', null, 2), null);
} else if (data.from === strapi.token) {
if (data.hasOwnProperty('files')) {
const syncPromise = function (file, index) {
const deferred = Promise.defer();
_self.unzipper(file)
.then(function () {
if (!_.isEmpty(data.files[index + 1])) {
return syncPromise(data.files[index + 1], index + 1);
} else {
deferred.resolve();
}
})
.then(function () {
deferred.resolve();
}
})
})
.catch(function (err) {
deferred.reject(err);
});
return deferred.promise;
};
syncPromise(_.first(data.files), 0)
.then(function () {
deferred.resolve();
})
.catch(function (err) {
deferred.reject(err);
});
if (data.hasOwnProperty('action') && _.isFunction(_self[data.action])) {
_self[data.action](data, function (err, obj) {
if (err) {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: stringify(err, null, 2),
data: null
})
});
return deferred.promise;
};
return false;
}
syncPromise(_.first(data.files), 0)
.then(function () {
if (data.hasOwnProperty('action') && _.isFunction(_self[data.action])) {
_self[data.action](data, function (err, obj) {
if (err) {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: stringify(err, null, 2),
data: null
err: null,
data: stringify(obj, null, 2)
})
});
return false;
}
});
} else if (!data.hasOwnProperty('action')) {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: null,
data: stringify(obj, null, 2)
data: stringify(true, null, 2)
})
});
});
} else if (!data.hasOwnProperty('action')) {
} else {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: stringify('Unknow action', null, 2),
data: null
})
});
}
})
.catch(function (err) {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: null,
data: stringify(true, null, 2)
})
});
} else {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: stringify('Unknow action', null, 2),
err: err,
data: null
})
});
});
} else if (!data.hasOwnProperty('action')) {
fn(strapi.rsa.encrypt(stringify('`action` attribute is missing', null, 2)), strapi.rsa.encryptPrivate(null));
} else if (_.isFunction(_self[data.action])) {
_self[data.action](data, function (err, obj) {
if (err) {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: err,
data: null
})
});
return false;
}
})
.catch(function (err) {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: err,
data: null
err: null,
data: stringify(obj, null, 2)
})
});
});
} else if (!data.hasOwnProperty('action')) {
fn(strapi.rsa.encrypt(stringify('`action` attribute is missing', null, 2)), strapi.rsa.encryptPrivate(null));
} else if (_.isFunction(_self[data.action])) {
_self[data.action](data, function (err, obj) {
if (err) {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: err,
data: null
})
});
return false;
}
} else {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: null,
data: stringify(obj, null, 2)
err: stringify('Unknow action', null, 2),
data: null
})
});
});
}
} else {
fn({
appId: strapi.config.studio.appId,
token: strapi.token,
encrypted: strapi.rsa.encrypt({
err: stringify('Unknow action', null, 2),
data: null
})
});
fn(stringify('Bad user token', null, 2), null);
}
} else {
fn(stringify('Bad user token', null, 2), null);
}
});
});
socket.on('err', function (data) {
strapi.log.warn(data.text);
socket.on('err', function (data) {
strapi.log.warn(data.text);
});
});
cb();
@ -263,28 +264,27 @@ module.exports = function (strapi) {
if (err) {
strapi.log.warn('Continuing without credentials.');
} else {
strapi.log.info('Connection with the Studio server found, please wait a few seconds...');
config = JSON.parse(config);
strapi.token = config.token;
socket.emit('getPublicKey', null, function (publicKey) {
if (publicKey) {
if (publicKey && strapi.config.environment === 'development') {
const key = new RSA();
let object;
key.importKey(publicKey, 'public');
if (strapi.config.environment === 'development') {
object = {
appId: strapi.config.studio.appId,
appName: strapi.config.name,
publicKey: strapi.rsa.exportKey('private'),
secretKey: strapi.config.studio.secretKey,
dashboardToken: strapi.config.dashboard.token,
token: strapi.token,
env: strapi.config.environment
};
const object = {
appId: strapi.config.studio.appId,
appName: strapi.config.name,
publicKey: strapi.rsa.exportKey('private'),
secretKey: strapi.config.studio.secretKey,
dashboardToken: strapi.config.dashboard.token,
token: strapi.token,
env: strapi.config.environment
};
socket.emit('check', key.encrypt(object));
}
socket.emit('check', key.encrypt(object));
}
});
}
@ -334,7 +334,7 @@ module.exports = function (strapi) {
*/
rebuild: function (data, cb) {
strapi.rebuild();
strapi.restart();
cb(null, true);
},

View File

@ -10,7 +10,6 @@ const spawn = require('child_process').spawnSync;
// Public node modules.
const _ = require('lodash');
const async = require('async');
const Waterline = require('waterline');
const WaterlineGraphQL = require('waterline-graphql');
@ -199,43 +198,6 @@ module.exports = function (strapi) {
}
cb();
},
/**
* Reload the hook
*/
reload: function () {
hook.teardown(function () {
hook.initialize(function (err) {
if (err) {
strapi.log.error('Failed to reinitialize the ORM hook.');
strapi.stop();
} else {
strapi.emit('hook:waterline:reloaded');
}
});
});
},
/**
* Teardown adapters
*/
teardown: function (cb) {
cb = cb || function (err) {
if (err) {
strapi.log.error('Failed to teardown ORM adapters.');
strapi.stop();
}
};
async.forEach(Object.keys(strapi.adapters || {}), function (name, next) {
if (strapi.adapters[name].teardown) {
strapi.adapters[name].teardown(null, next);
} else {
next();
}
}, cb);
}
};

View File

@ -55,6 +55,12 @@ module.exports = function (strapi) {
// Make the environment in config match the server one.
environment: strapi.app.env || process.env.NODE_ENV,
// Default reload config.
reload: {
timeout: 1000,
workers: 1
},
// Default paths.
paths: {
tmp: '.tmp',
@ -74,8 +80,9 @@ module.exports = function (strapi) {
url: 'http://studio.strapi.io'
},
// Start off needed empty objects.
routes: {}
// Start off needed empty objects and strings.
routes: {},
frontendUrl: ''
};
};

View File

@ -7,6 +7,7 @@
// Public node modules.
const _ = require('lodash');
const async = require('async');
const herd = require('herd');
// Local dependencies.
const __Configuration = require('./configuration');
@ -140,7 +141,18 @@ module.exports = function (strapi) {
}
// We can finally make the server listen on the configured port.
strapi.app.listen(strapi.config.port);
// Use of the `herd` node module to herd the child processes with
// zero downtime reloads.
if (_.isPlainObject(strapi.config.reload) && !_.isEmpty(strapi.config.reload) && strapi.config.reload.workers > 0) {
herd(strapi.config.name)
.timeout(strapi.config.reload.timeout)
.size(strapi.config.reload.workers)
.run(function () {
strapi.app.listen(strapi.config.port);
});
} else {
strapi.app.listen(strapi.config.port);
}
cb && cb(null, strapi);
};

View File

@ -1,5 +1,12 @@
'use strict';
/**
* Module dependencies
*/
// Node.js core.
const cluster = require('cluster');
/**
* `strapi.prototype.initialize()`
*
@ -52,16 +59,19 @@ module.exports = function initialize(cb) {
}
});
// Run the application bootstrap.
self.runBootstrap(function afterBootstrap(err) {
if (err) {
self.log.error('Bootstrap encountered an error.');
return cb(self.log.error(err));
}
// Only run the application bootstrap if
// we are in a master cluster.
if (cluster.isMaster) {
self.runBootstrap(function afterBootstrap(err) {
if (err) {
self.log.error('Bootstrap encountered an error.');
return cb(self.log.error(err));
}
});
}
// And fire the `ready` event.
// This is listened to by attached servers, etc.
self.emit('ready');
cb(null, self);
});
// And fire the `ready` event.
// This is listened to by attached servers, etc.
self.emit('ready');
cb(null, self);
};

View File

@ -7,6 +7,7 @@
// Public node modules.
const _ = require('lodash');
const async = require('async');
const cluster = require('cluster');
// Local dependencies.
const __hooks = require('../configuration/hooks');
@ -30,6 +31,12 @@ module.exports = function (strapi) {
return;
}
// Do not load the `studio` hook if the
// cluster is not the master.
if (!cluster.isMaster) {
delete hooks.studio;
}
// Handle folder-defined modules (default to `lib/index.js`)
// Since a hook definition must be a function.
if (_.isObject(hookPrototype) && !_.isArray(hookPrototype) && !_.isFunction(hookPrototype)) {

View File

@ -1,74 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const async = require('async');
/**
* Load the application dictionary
*/
module.exports = function () {
const self = this;
console.log();
self.log.debug('Rebuilding application dictionary...');
// Update the Strapi status (might be used
// by the core or some hooks).
self.reloading = true;
// Async module loader to rebuild a
// dictionary of the application.
async.auto({
// Rebuild the dictionaries.
dictionaries: function (cb) {
self.hooks._config.reload();
self.hooks._api.reload();
cb();
},
// Make sure to delete the router stack.
router: function (cb) {
delete self.router;
cb();
}
},
// Callback.
function (err) {
// Just in case there is an error.
if (err) {
self.log.error('Impossible to reload the server');
self.log.error('Please restart the server manually');
self.stop();
}
// Tell the application the framework is reloading
// (might be used by some hooks).
self.reloading = true;
// Teardown Waterline adapters and
// reload the Waterline ORM.
self.log.debug('Reloading the ORM...');
self.hooks.waterline.reload();
// Reloading the router.
self.log.debug('Reloading the router...');
self.hooks.router.reload();
// Update `strapi` status.
self.reloaded = true;
self.reloading = false;
// Finally inform the developer everything seems ok.
self.log.info('Dictionary successfully rebuilt');
self.log.warn('You still need to restart your server to fully enjoy changes...');
console.log();
});
};

22
lib/restart.js Normal file
View File

@ -0,0 +1,22 @@
'use strict';
/**
* Module dependencies
*/
// Node.js core.
const cluster = require('cluster');
// Public node modules.
const _ = require('lodash');
/**
* Programmatically restart the server
* (useful for the Studio)
*/
module.exports = function () {
_.forEach(cluster.worker, function () {
process.kill(process.pid, 'SIGHUP');
});
};

View File

@ -4,6 +4,9 @@
* Module dependencies
*/
// Node.js core.
const cluster = require('cluster');
// Public node modules.
const async = require('async');
@ -44,12 +47,21 @@ module.exports = function start(configOverride, cb) {
}
// Log some server info.
self.log.info('Server started in ' + self.config.appPath);
self.log.info('Your server is running at ' + self.config.url);
self.log.info('Time: ' + new Date());
self.log.info('Environment: ' + self.config.environment);
self.log.info('Process PID: ' + process.pid);
self.log.info('To shut down your server, press <CTRL> + C at any time');
if (cluster.isMaster) {
self.log.info('Server started in ' + self.config.appPath);
self.log.info('Your server is running at ' + self.config.url);
self.log.debug('Time: ' + new Date());
self.log.debug('Environment: ' + self.config.environment);
self.log.debug('Process PID: ' + process.pid);
self.log.debug('Cluster: master');
self.log.info('To shut down your server, press <CTRL> + C at any time');
} else {
self.log.warn('New worker starting...');
self.log.debug('Process PID: ' + process.pid);
self.log.debug('Cluster: worker #' + cluster.worker.id);
}
// Blank log to give some space.
console.log();
// Emit an event when Strapi has started.

View File

@ -37,6 +37,7 @@
"consolidate": "^0.13.1",
"fs-extra": "^0.24.0",
"graphql": "^0.4.9",
"herd": "^1.0.0",
"include-all": "^0.1.6",
"json-stringify-safe": "^5.0.1",
"koa": "^1.1.0",