From 1438278ce45cfe37a1796843f6c79ed102a5cba2 Mon Sep 17 00:00:00 2001 From: Josh Vanderwillik Date: Tue, 19 Aug 2014 12:36:46 -0400 Subject: [PATCH] Extract starting functionality into an exported class closes #3789 - Create a GhostServer class to manage state - index.js now calls start on the exported server - Alter tests to expect a GhostServer instance --- core/index.js | 4 +- core/server/GhostServer.js | 153 ++++++++++++++++++ core/server/index.js | 91 +---------- core/test/functional/routes/admin_test.js | 14 +- .../routes/api/authentication_test.js | 8 +- core/test/functional/routes/api/db_test.js | 8 +- core/test/functional/routes/api/error_test.js | 10 +- .../routes/api/notifications_test.js | 8 +- core/test/functional/routes/api/posts_test.js | 8 +- .../functional/routes/api/settings_test.js | 10 +- core/test/functional/routes/api/slugs_test.js | 8 +- core/test/functional/routes/api/tags_test.js | 10 +- core/test/functional/routes/api/users_test.js | 8 +- core/test/functional/routes/frontend_test.js | 8 +- index.js | 6 +- 15 files changed, 213 insertions(+), 141 deletions(-) create mode 100644 core/server/GhostServer.js diff --git a/core/index.js b/core/index.js index 0d294fc2d3..3f2b8dddb0 100644 --- a/core/index.js +++ b/core/index.js @@ -8,7 +8,7 @@ var when = require('when'), process.env.NODE_ENV = process.env.NODE_ENV || 'development'; -function startGhost(options) { +function makeGhost(options) { var deferred = when.defer(); options = options || {}; @@ -31,4 +31,4 @@ function startGhost(options) { return deferred.promise; } -module.exports = startGhost; \ No newline at end of file +module.exports = makeGhost; diff --git a/core/server/GhostServer.js b/core/server/GhostServer.js new file mode 100644 index 0000000000..4866366cd9 --- /dev/null +++ b/core/server/GhostServer.js @@ -0,0 +1,153 @@ +var when = require('when'), + fs = require('fs'), + semver = require('semver'), + packageInfo = require('../../package.json'), + config = require('./config'); + +function GhostServer(app) { + this.app = app; + this.httpServer = null; + this.connections = []; + this.upgradeWarning = setTimeout(this.logUpgradeWarning.bind(this), 5000); +} + +GhostServer.prototype.connection = function (socket) { + this.connections.push(socket); +}; + +// Most browsers keep a persistant connection open to the server +// which prevents the close callback of httpServer from returning +// We need to destroy all connections manually +GhostServer.prototype.closeConnections = function () { + this.connections.forEach(function (socket) { + socket.destroy(); + }); +}; + +GhostServer.prototype.logStartMessages = function () { + // Tell users if their node version is not supported, and exit + if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) { + console.log( + "\nERROR: Unsupported version of Node".red, + "\nGhost needs Node version".red, + packageInfo.engines.node.yellow, + "you are using version".red, + process.versions.node.yellow, + "\nPlease go to http://nodejs.org to get a supported version".green + ); + + process.exit(0); + } + + // Startup & Shutdown messages + if (process.env.NODE_ENV === 'production') { + console.log( + "Ghost is running...".green, + "\nYour blog is now available on", + config.url, + "\nCtrl+C to shut down".grey + ); + + // ensure that Ghost exits correctly on Ctrl+C + process.removeAllListeners('SIGINT').on('SIGINT', function () { + console.log( + "\nGhost has shut down".red, + "\nYour blog is now offline" + ); + process.exit(0); + }); + } else { + console.log( + ("Ghost is running in " + process.env.NODE_ENV + "...").green, + "\nListening on", + config.getSocket() || config.server.host + ':' + config.server.port, + "\nUrl configured as:", + config.url, + "\nCtrl+C to shut down".grey + ); + // ensure that Ghost exits correctly on Ctrl+C + process.removeAllListeners('SIGINT').on('SIGINT', function () { + console.log( + "\nGhost has shutdown".red, + "\nGhost was running for", + Math.round(process.uptime()), + "seconds" + ); + process.exit(0); + }); + } +}; + +GhostServer.prototype.logShutdownMessages = function () { + console.log('Ghost is closing connections'.red); +}; + +GhostServer.prototype.logUpgradeWarning = function () { + console.log('Warning: Ghost will no longer start automatically when using it as an npm module. Please see the docs(http://tinyurl.com/npm-upgrade) for information on how to update your code.'.red); +}; + +// Starts the ghost server listening on the configured port +GhostServer.prototype.start = function () { + // ## Start Ghost App + var deferred = when.defer(); + if (config.getSocket()) { + // Make sure the socket is gone before trying to create another + try { + fs.unlinkSync(config.getSocket()); + } catch (e) { + // We can ignore this. + } + + this.httpServer = this.app.listen( + config.getSocket() + ); + fs.chmod(config.getSocket(), '0660'); + + } else { + this.httpServer = this.app.listen( + config.server.port, + config.server.host + ); + } + + this.httpServer.on('connection', this.connection.bind(this)); + this.httpServer.on('listening', function () { + this.logStartMessages(); + clearTimeout(this.upgradeWarning); + deferred.resolve(this); + }.bind(this)); + return deferred.promise; +}; + +// Returns a promise that will be fulfilled when the server stops. +// If the server has not been started, the promise will be fulfilled +// immediately +GhostServer.prototype.stop = function () { + var deferred = when.defer(); + + if (this.httpServer === null) { + deferred.resolve(this); + } else { + this.httpServer.close(function () { + this.httpServer = null; + this.logShutdownMessages(); + deferred.resolve(this); + }.bind(this)); + this.closeConnections(); + } + return deferred.promise; +}; + +// Restarts the ghost application +GhostServer.prototype.restart = function () { + return this.stop().then(this.start.bind(this)); +}; + +// To be called after `stop` +GhostServer.prototype.hammertime = function () { + console.log('Can\'t touch this'.green); + return this; +}; + +module.exports = GhostServer; + diff --git a/core/server/index.js b/core/server/index.js index fe456bc133..86832df547 100644 --- a/core/server/index.js +++ b/core/server/index.js @@ -5,7 +5,6 @@ var crypto = require('crypto'), compress = require('compression'), fs = require('fs'), uuid = require('node-uuid'), - semver = require('semver'), _ = require('lodash'), when = require('when'), @@ -20,9 +19,9 @@ var crypto = require('crypto'), permissions = require('./permissions'), apps = require('./apps'), packageInfo = require('../../package.json'), + GhostServer = require('./GhostServer'), // Variables - httpServer, dbHash; // If we're in development mode, require "when/console/monitor" @@ -107,61 +106,6 @@ function builtFilesExist() { return when.all(deferreds); } -function ghostStartMessages() { - // Tell users if their node version is not supported, and exit - if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) { - console.log( - "\nERROR: Unsupported version of Node".red, - "\nGhost needs Node version".red, - packageInfo.engines.node.yellow, - "you are using version".red, - process.versions.node.yellow, - "\nPlease go to http://nodejs.org to get a supported version".green - ); - - process.exit(0); - } - - // Startup & Shutdown messages - if (process.env.NODE_ENV === 'production') { - console.log( - "Ghost is running...".green, - "\nYour blog is now available on", - config.url, - "\nCtrl+C to shut down".grey - ); - - // ensure that Ghost exits correctly on Ctrl+C - process.removeAllListeners('SIGINT').on('SIGINT', function () { - console.log( - "\nGhost has shut down".red, - "\nYour blog is now offline" - ); - process.exit(0); - }); - } else { - console.log( - ("Ghost is running in " + process.env.NODE_ENV + "...").green, - "\nListening on", - config.getSocket() || config.server.host + ':' + config.server.port, - "\nUrl configured as:", - config.url, - "\nCtrl+C to shut down".grey - ); - // ensure that Ghost exits correctly on Ctrl+C - process.removeAllListeners('SIGINT').on('SIGINT', function () { - console.log( - "\nGhost has shutdown".red, - "\nGhost was running for", - Math.round(process.uptime()), - "seconds" - ); - process.exit(0); - }); - } -} - - // This is run after every initialization is done, right before starting server. // Its main purpose is to move adding notifications here, so none of the submodules // should need to include api, which previously resulted in circular dependencies. @@ -192,7 +136,7 @@ function initNotifications() { // ## Initializes the ghost application. // Sets up the express server instance. // Instantiates the ghost singleton, helpers, routes, middleware, and apps. -// Finally it starts the http server. +// Finally it returns an instance of GhostServer function init(server) { // create a hash for cache busting assets var assetHash = (crypto.createHash('md5').update(packageInfo.version + Date.now()).digest('hex')).substring(0, 10); @@ -239,8 +183,7 @@ function init(server) { apps.init() ); }).then(function () { - var adminHbs = hbs.create(), - deferred = when.defer(); + var adminHbs = hbs.create(); // Output necessary notifications on init initNotifications(); @@ -276,34 +219,8 @@ function init(server) { errors.logWarn(warn.message, warn.context, warn.help); }); - // ## Start Ghost App - if (config.getSocket()) { - // Make sure the socket is gone before trying to create another - try { - fs.unlinkSync(config.getSocket()); - } catch (e) { - // We can ignore this. - } - httpServer = server.listen( - config.getSocket() - ); - fs.chmod(config.getSocket(), '0660'); - - } else { - httpServer = server.listen( - config.server.port, - config.server.host - ); - } - - httpServer.on('listening', function () { - ghostStartMessages(); - deferred.resolve(httpServer); - }); - - - return deferred.promise; + return new GhostServer(server); }); } diff --git a/core/test/functional/routes/admin_test.js b/core/test/functional/routes/admin_test.js index b62620a9c9..17ccde3bfb 100644 --- a/core/test/functional/routes/admin_test.js +++ b/core/test/functional/routes/admin_test.js @@ -12,7 +12,7 @@ var request = require('supertest'), testUtils = require('../../utils'), ghost = require('../../../../core'), - httpServer, + ghostServer, agent = request.agent, cacheRules = { @@ -52,9 +52,9 @@ describe('Admin Routing', function () { before(function (done) { var app = express(); - ghost({app: app}).then(function (_httpServer) { + ghost({app: app}).then(function (_ghostServer) { // Setup the request object with the ghost express app - httpServer = _httpServer; + ghostServer = _ghostServer; request = request(app); testUtils.clearData().then(function () { // we initialise data, but not a user. No user should be required for navigating the frontend @@ -69,7 +69,7 @@ describe('Admin Routing', function () { }); after(function () { - httpServer.close(); + ghostServer.stop(); }); describe('Legacy Redirects', function () { @@ -286,8 +286,8 @@ describe('Admin Routing', function () { // before(function (done) { // var app = express(); -// ghost({app: app}).then(function (_httpServer) { -// httpServer = _httpServer; +// ghost({app: app}).then(function (_ghostServer) { +// ghostServer = _ghostServer; // request = agent(app); // testUtils.clearData() @@ -339,7 +339,7 @@ describe('Admin Routing', function () { // }); // after(function () { -// httpServer.close(); +// ghostServer.stop(); // }); // describe('Ghost Admin magic /view/ route', function () { diff --git a/core/test/functional/routes/api/authentication_test.js b/core/test/functional/routes/api/authentication_test.js index 8710071d08..3575f04c3d 100644 --- a/core/test/functional/routes/api/authentication_test.js +++ b/core/test/functional/routes/api/authentication_test.js @@ -6,7 +6,7 @@ var supertest = require('supertest'), testUtils = require('../../../utils'), user = testUtils.DataGenerator.forModel.users[0], ghost = require('../../../../../core'), - httpServer, + ghostServer, request; @@ -18,8 +18,8 @@ describe('Authentication API', function () { // starting ghost automatically populates the db // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; request = supertest.agent(app); }).then(function () { return testUtils.doAuth(request); @@ -34,7 +34,7 @@ describe('Authentication API', function () { after(function (done) { testUtils.clearData().then(function () { - httpServer.close(); + ghostServer.stop(); done(); }); }); diff --git a/core/test/functional/routes/api/db_test.js b/core/test/functional/routes/api/db_test.js index 1a4bbf099f..9fe133b2dc 100644 --- a/core/test/functional/routes/api/db_test.js +++ b/core/test/functional/routes/api/db_test.js @@ -7,7 +7,7 @@ var supertest = require('supertest'), ghost = require('../../../../../core'), - httpServer, + ghostServer, request; @@ -19,8 +19,8 @@ describe('DB API', function () { // starting ghost automatically populates the db // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; request = supertest.agent(app); }).then(function () { @@ -36,7 +36,7 @@ describe('DB API', function () { after(function (done) { testUtils.clearData().then(function () { - httpServer.close(); + ghostServer.stop(); done(); }); }); diff --git a/core/test/functional/routes/api/error_test.js b/core/test/functional/routes/api/error_test.js index 332965f843..86f7a62a66 100644 --- a/core/test/functional/routes/api/error_test.js +++ b/core/test/functional/routes/api/error_test.js @@ -13,7 +13,7 @@ var supertest = require('supertest'), ghost = require('../../../../../core'), - httpServer, + ghostServer, request, agent; @@ -22,8 +22,8 @@ describe('Unauthorized', function () { before(function (done) { var app = express(); - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; // request = supertest(app); request = supertest.agent(app); testUtils.clearData().then(function () { @@ -37,7 +37,7 @@ describe('Unauthorized', function () { }); after(function () { - httpServer.close(); + ghostServer.stop(); }); @@ -63,4 +63,4 @@ describe('Unauthorized', function () { }); -}); \ No newline at end of file +}); diff --git a/core/test/functional/routes/api/notifications_test.js b/core/test/functional/routes/api/notifications_test.js index fccfbabe6c..fa6ed988bd 100644 --- a/core/test/functional/routes/api/notifications_test.js +++ b/core/test/functional/routes/api/notifications_test.js @@ -7,7 +7,7 @@ var testUtils = require('../../../utils'), ghost = require('../../../../../core'), - httpServer, + ghostServer, request; describe('Notifications API', function () { @@ -18,8 +18,8 @@ describe('Notifications API', function () { // starting ghost automatically populates the db // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; request = supertest.agent(app); }).then(function () { @@ -34,7 +34,7 @@ describe('Notifications API', function () { }); after(function () { - httpServer.close(); + ghostServer.stop(); }); describe('Add', function () { diff --git a/core/test/functional/routes/api/posts_test.js b/core/test/functional/routes/api/posts_test.js index d237ad5bd3..6aca958810 100644 --- a/core/test/functional/routes/api/posts_test.js +++ b/core/test/functional/routes/api/posts_test.js @@ -8,7 +8,7 @@ var testUtils = require('../../../utils'), ghost = require('../../../../../core'), - httpServer, + ghostServer, request; @@ -20,8 +20,8 @@ describe('Post API', function () { // starting ghost automatically populates the db // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; request = supertest.agent(app); }).then(function () { @@ -37,7 +37,7 @@ describe('Post API', function () { after(function (done) { testUtils.clearData().then(function () { - httpServer.close(); + ghostServer.stop(); done(); }); }); diff --git a/core/test/functional/routes/api/settings_test.js b/core/test/functional/routes/api/settings_test.js index 178a5201cc..b9d29d33cf 100644 --- a/core/test/functional/routes/api/settings_test.js +++ b/core/test/functional/routes/api/settings_test.js @@ -7,7 +7,7 @@ var testUtils = require('../../../utils'), ghost = require('../../../../../core'), - httpServer, + ghostServer, request; describe('Settings API', function () { @@ -18,8 +18,8 @@ describe('Settings API', function () { // starting ghost automatically populates the db // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; request = supertest.agent(app); }).then(function () { @@ -35,7 +35,7 @@ describe('Settings API', function () { after(function (done) { testUtils.clearData().then(function () { - httpServer.close(); + ghostServer.stop(); done(); }); }); @@ -206,4 +206,4 @@ describe('Settings API', function () { }); -}); \ No newline at end of file +}); diff --git a/core/test/functional/routes/api/slugs_test.js b/core/test/functional/routes/api/slugs_test.js index 35a52c5911..2ad62488f5 100644 --- a/core/test/functional/routes/api/slugs_test.js +++ b/core/test/functional/routes/api/slugs_test.js @@ -7,7 +7,7 @@ var testUtils = require('../../../utils'), ghost = require('../../../../../core'), - httpServer, + ghostServer, request; describe('Slug API', function () { @@ -18,8 +18,8 @@ describe('Slug API', function () { // starting ghost automatically populates the db // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; request = supertest.agent(app); }).then(function () { @@ -35,7 +35,7 @@ describe('Slug API', function () { after(function (done) { testUtils.clearData().then(function () { - httpServer.close(); + ghostServer.stop(); done(); }); }); diff --git a/core/test/functional/routes/api/tags_test.js b/core/test/functional/routes/api/tags_test.js index 2848c3aa3e..daa015203c 100644 --- a/core/test/functional/routes/api/tags_test.js +++ b/core/test/functional/routes/api/tags_test.js @@ -7,7 +7,7 @@ var testUtils = require('../../../utils'), ghost = require('../../../../../core'), - httpServer, + ghostServer, request; describe('Tag API', function () { @@ -18,8 +18,8 @@ describe('Tag API', function () { // starting ghost automatically populates the db // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; request = supertest.agent(app); }).then(function () { @@ -35,7 +35,7 @@ describe('Tag API', function () { after(function (done) { testUtils.clearData().then(function () { - httpServer.close(); + ghostServer.stop(); done(); }); }); @@ -63,4 +63,4 @@ describe('Tag API', function () { }); -}); \ No newline at end of file +}); diff --git a/core/test/functional/routes/api/users_test.js b/core/test/functional/routes/api/users_test.js index d44b9ce0d7..6dbf9afb0f 100644 --- a/core/test/functional/routes/api/users_test.js +++ b/core/test/functional/routes/api/users_test.js @@ -7,7 +7,7 @@ var testUtils = require('../../../utils'), ghost = require('../../../../../core'), - httpServer, + ghostServer, request; describe('User API', function () { @@ -18,8 +18,8 @@ describe('User API', function () { // starting ghost automatically populates the db // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves - ghost({app: app}).then(function (_httpServer) { - httpServer = _httpServer; + ghost({app: app}).then(function (_ghostServer) { + ghostServer = _ghostServer; request = supertest.agent(app); }).then(function () { @@ -35,7 +35,7 @@ describe('User API', function () { after(function (done) { testUtils.clearData().then(function () { - httpServer.close(); + ghostServer.stop(); done(); }); }); diff --git a/core/test/functional/routes/frontend_test.js b/core/test/functional/routes/frontend_test.js index 0ea7bccffe..7d0f01a06c 100644 --- a/core/test/functional/routes/frontend_test.js +++ b/core/test/functional/routes/frontend_test.js @@ -13,7 +13,7 @@ var request = require('supertest'), testUtils = require('../../utils'), ghost = require('../../../../core'), - httpServer, + ghostServer, cacheRules = { 'public': 'public, max-age=0', @@ -41,9 +41,9 @@ describe('Frontend Routing', function () { before(function (done) { var app = express(); - ghost({app: app}).then(function (_httpServer) { + ghost({app: app}).then(function (_ghostServer) { // Setup the request object with the ghost express app - httpServer = _httpServer; + ghostServer = _ghostServer; request = request(app); testUtils.clearData().then(function () { // we initialise data, but not a user. No user should be required for navigating the frontend @@ -58,7 +58,7 @@ describe('Frontend Routing', function () { }); after(function () { - httpServer.close(); + ghostServer.stop(); }); describe('Home', function () { diff --git a/index.js b/index.js index 270f69b7cf..f031b4f4d3 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,8 @@ var ghost = require('./core'), errors = require('./core/server/errors'); -ghost().otherwise(function (err) { +ghost().then(function (app) { + app.start(); +}).catch(function (err) { errors.logErrorAndExit(err, err.context, err.help); -}); \ No newline at end of file +});