2015-05-28 10:16:09 -05:00
|
|
|
// # Ghost Server
|
|
|
|
// Handles the creation of an HTTP Server for Ghost
|
2021-06-15 11:01:22 -05:00
|
|
|
const debug = require('@tryghost/debug')('server');
|
2020-04-30 14:26:12 -05:00
|
|
|
const errors = require('@tryghost/errors');
|
2021-06-08 10:47:31 -05:00
|
|
|
const tpl = require('@tryghost/tpl');
|
2021-06-15 09:36:27 -05:00
|
|
|
const logging = require('@tryghost/logging');
|
2021-02-16 10:29:40 -05:00
|
|
|
const notify = require('./notify');
|
2020-04-29 10:44:27 -05:00
|
|
|
const moment = require('moment');
|
2020-08-10 05:42:31 -05:00
|
|
|
const stoppable = require('stoppable');
|
2014-08-19 11:36:46 -05:00
|
|
|
|
2021-06-08 10:47:31 -05:00
|
|
|
const messages = {
|
|
|
|
cantTouchThis: 'Can\'t touch this',
|
|
|
|
ghostIsRunning: 'Ghost is running...',
|
|
|
|
yourBlogIsAvailableOn: 'Your site is now available on {url}',
|
|
|
|
ctrlCToShutDown: 'Ctrl+C to shut down',
|
|
|
|
ghostIsRunningIn: 'Ghost is running in {env}...',
|
|
|
|
listeningOn: 'Listening on: {host}:{port}',
|
|
|
|
urlConfiguredAs: 'Url configured as: {url}',
|
|
|
|
ghostIsShuttingDown: 'Ghost is shutting down',
|
|
|
|
ghostHasShutdown: 'Ghost has shut down',
|
|
|
|
yourBlogIsNowOffline: 'Your site is now offline',
|
|
|
|
ghostWasRunningFor: 'Ghost was running for',
|
|
|
|
addressInUse: {
|
|
|
|
error: '(EADDRINUSE) Cannot start Ghost.',
|
|
|
|
context: 'Port {port} is already in use by another program.',
|
|
|
|
help: 'Is another Ghost instance already running?'
|
|
|
|
},
|
|
|
|
otherError: {
|
|
|
|
error: '(Code: {errorNumber})',
|
|
|
|
context: 'There was an error starting your server.',
|
|
|
|
help: 'Please use the error code above to search for a solution.'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-05-28 10:16:09 -05:00
|
|
|
/**
|
|
|
|
* ## GhostServer
|
|
|
|
*/
|
2020-08-09 12:55:41 -05:00
|
|
|
class GhostServer {
|
2022-04-20 06:47:25 -05:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {Object} options
|
|
|
|
* @param {String} options.url
|
|
|
|
* @param {String} options.env development|production|testing
|
|
|
|
* @param {Object} options.serverConfig
|
|
|
|
* @param {String} options.serverConfig.host
|
|
|
|
* @param {Number} options.serverConfig.port
|
|
|
|
* @param {Number} options.serverConfig.shutdownTimeout
|
|
|
|
* @param {Boolean} options.serverConfig.testmode
|
|
|
|
*/
|
|
|
|
constructor({url, env, serverConfig}) {
|
2021-05-05 08:24:59 -05:00
|
|
|
this.url = url;
|
2022-04-20 06:47:25 -05:00
|
|
|
this.env = env;
|
|
|
|
this.serverConfig = serverConfig;
|
|
|
|
|
2021-01-26 19:33:55 -05:00
|
|
|
this.rootApp = null;
|
2020-08-09 12:55:41 -05:00
|
|
|
this.httpServer = null;
|
|
|
|
|
2020-08-11 15:26:34 -05:00
|
|
|
// Tasks that should be run before the server exits
|
|
|
|
this.cleanupTasks = [];
|
2020-08-09 12:55:41 -05:00
|
|
|
}
|
2014-08-19 11:36:46 -05:00
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
/**
|
|
|
|
* ## Public API methods
|
|
|
|
*
|
|
|
|
* ### Start
|
|
|
|
* Starts the ghost server listening on the configured port.
|
2021-02-23 05:35:25 -05:00
|
|
|
* Requires an express app to be passed in
|
|
|
|
*
|
|
|
|
* @param {Object} rootApp - Required express app instance.
|
2020-08-09 12:55:41 -05:00
|
|
|
* @return {Promise} Resolves once Ghost has started
|
|
|
|
*/
|
2021-02-23 05:35:25 -05:00
|
|
|
start(rootApp) {
|
2020-08-09 12:55:41 -05:00
|
|
|
debug('Starting...');
|
2022-04-20 06:47:25 -05:00
|
|
|
this.rootApp = rootApp;
|
|
|
|
|
|
|
|
const {host, port, testmode, shutdownTimeout} = this.serverConfig;
|
2020-08-09 12:55:41 -05:00
|
|
|
const self = this;
|
|
|
|
|
|
|
|
return new Promise(function (resolve, reject) {
|
2022-04-19 14:04:07 -05:00
|
|
|
self.httpServer = rootApp.listen(
|
2022-04-20 06:47:25 -05:00
|
|
|
port,
|
|
|
|
host
|
2022-04-19 14:04:07 -05:00
|
|
|
);
|
2015-05-28 10:16:09 -05:00
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
self.httpServer.on('error', function (error) {
|
|
|
|
let ghostError;
|
|
|
|
|
2021-02-16 07:26:06 -05:00
|
|
|
if (error.code === 'EADDRINUSE') {
|
2021-12-01 05:22:01 -05:00
|
|
|
ghostError = new errors.InternalServerError({
|
2021-06-08 10:47:31 -05:00
|
|
|
message: tpl(messages.addressInUse.error),
|
2022-04-20 06:47:25 -05:00
|
|
|
context: tpl(messages.addressInUse.context, {port}),
|
2021-06-08 10:47:31 -05:00
|
|
|
help: tpl(messages.addressInUse.help)
|
2020-08-09 12:55:41 -05:00
|
|
|
});
|
|
|
|
} else {
|
2021-12-01 05:22:01 -05:00
|
|
|
ghostError = new errors.InternalServerError({
|
2021-06-08 10:47:31 -05:00
|
|
|
message: tpl(messages.otherError.error, {errorNumber: error.errno}),
|
|
|
|
context: tpl(messages.otherError.context),
|
|
|
|
help: tpl(messages.otherError.help)
|
2020-08-09 12:55:41 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-02-19 15:32:37 -05:00
|
|
|
debug('Notifying server started (error)');
|
|
|
|
return notify.notifyServerStarted()
|
|
|
|
.finally(() => {
|
|
|
|
reject(ghostError);
|
|
|
|
});
|
2020-08-09 12:55:41 -05:00
|
|
|
});
|
2015-05-28 10:16:09 -05:00
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
self.httpServer.on('listening', function () {
|
|
|
|
debug('...Started');
|
2020-08-11 07:28:04 -05:00
|
|
|
self._logStartMessages();
|
2015-05-28 10:16:09 -05:00
|
|
|
|
2020-08-18 09:36:21 -05:00
|
|
|
// Debug logs output in testmode only
|
2022-04-20 06:47:25 -05:00
|
|
|
if (testmode) {
|
2021-02-16 06:54:19 -05:00
|
|
|
self._startTestMode();
|
2020-08-18 09:36:21 -05:00
|
|
|
}
|
|
|
|
|
2021-02-19 15:32:37 -05:00
|
|
|
debug('Notifying server ready (success)');
|
2021-02-16 10:29:40 -05:00
|
|
|
return notify.notifyServerStarted()
|
2020-08-09 12:55:41 -05:00
|
|
|
.finally(() => {
|
|
|
|
resolve(self);
|
|
|
|
});
|
|
|
|
});
|
2016-11-07 09:25:29 -05:00
|
|
|
|
2022-04-20 06:47:25 -05:00
|
|
|
stoppable(self.httpServer, shutdownTimeout);
|
2020-08-10 05:42:31 -05:00
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
// ensure that Ghost exits correctly on Ctrl+C and SIGTERM
|
|
|
|
process
|
2020-08-11 07:28:04 -05:00
|
|
|
.removeAllListeners('SIGINT').on('SIGINT', self.shutdown.bind(self))
|
|
|
|
.removeAllListeners('SIGTERM').on('SIGTERM', self.shutdown.bind(self));
|
2015-05-28 10:16:09 -05:00
|
|
|
});
|
2020-08-09 12:55:41 -05:00
|
|
|
}
|
2020-08-07 13:50:09 -05:00
|
|
|
|
2020-08-11 07:28:04 -05:00
|
|
|
/**
|
|
|
|
* ### Shutdown
|
|
|
|
* Stops the server, handles cleanup and exits the process = a full shutdown
|
|
|
|
* Called on SIGINT or SIGTERM
|
|
|
|
*/
|
Added new, simpler, linear boot process
Background:
- Ghosts existing boot process is split across multiple files, has affordances for outdated ways of running Ghost and is generally non-linear making it nigh-impossible to follow
- The web of dependencies that are loaded on boot are also impossible to unpick, which makes it really hard to decouple Ghost
- With 4.0 we want to introduce a new, linear, simpler, clearer way to boot up Ghost to unlock decoupling Ghost into much smaller pieces
This commit:
- adds a new ghost.js file which switches between boot mode with `node index` or `node index old` so that if we find bugs we can work around them this week
- Note: the old boot process will go away very soon, but ghost.js will remain as the interface between the command to start Ghost and the application code
- reworks the database migration process into a standalone utility, so that the DB is handled as one simple step of the boot process, decoupled from everything else
- is missing tests for this new db utility
- leaves a lot of work to do around loading core code, services, express apps in a sensible order, as work to fix this would start to break the old boot process
- doesn't use the new maintenance app because we aren't restarting the server here, instead we have the concept of a "core app" that starts in maintenance mode - need to think about how apps will be decoupled in the near future
2021-02-02 09:47:16 -05:00
|
|
|
async shutdown(code = 0) {
|
2020-08-11 07:28:04 -05:00
|
|
|
try {
|
2021-06-08 10:47:31 -05:00
|
|
|
logging.warn(tpl(messages.ghostIsShuttingDown));
|
2020-08-11 07:28:04 -05:00
|
|
|
await this.stop();
|
Added new, simpler, linear boot process
Background:
- Ghosts existing boot process is split across multiple files, has affordances for outdated ways of running Ghost and is generally non-linear making it nigh-impossible to follow
- The web of dependencies that are loaded on boot are also impossible to unpick, which makes it really hard to decouple Ghost
- With 4.0 we want to introduce a new, linear, simpler, clearer way to boot up Ghost to unlock decoupling Ghost into much smaller pieces
This commit:
- adds a new ghost.js file which switches between boot mode with `node index` or `node index old` so that if we find bugs we can work around them this week
- Note: the old boot process will go away very soon, but ghost.js will remain as the interface between the command to start Ghost and the application code
- reworks the database migration process into a standalone utility, so that the DB is handled as one simple step of the boot process, decoupled from everything else
- is missing tests for this new db utility
- leaves a lot of work to do around loading core code, services, express apps in a sensible order, as work to fix this would start to break the old boot process
- doesn't use the new maintenance app because we aren't restarting the server here, instead we have the concept of a "core app" that starts in maintenance mode - need to think about how apps will be decoupled in the near future
2021-02-02 09:47:16 -05:00
|
|
|
setTimeout(() => {
|
|
|
|
process.exit(code);
|
|
|
|
}, 100);
|
2020-08-11 07:28:04 -05:00
|
|
|
} catch (error) {
|
|
|
|
logging.error(error);
|
Added new, simpler, linear boot process
Background:
- Ghosts existing boot process is split across multiple files, has affordances for outdated ways of running Ghost and is generally non-linear making it nigh-impossible to follow
- The web of dependencies that are loaded on boot are also impossible to unpick, which makes it really hard to decouple Ghost
- With 4.0 we want to introduce a new, linear, simpler, clearer way to boot up Ghost to unlock decoupling Ghost into much smaller pieces
This commit:
- adds a new ghost.js file which switches between boot mode with `node index` or `node index old` so that if we find bugs we can work around them this week
- Note: the old boot process will go away very soon, but ghost.js will remain as the interface between the command to start Ghost and the application code
- reworks the database migration process into a standalone utility, so that the DB is handled as one simple step of the boot process, decoupled from everything else
- is missing tests for this new db utility
- leaves a lot of work to do around loading core code, services, express apps in a sensible order, as work to fix this would start to break the old boot process
- doesn't use the new maintenance app because we aren't restarting the server here, instead we have the concept of a "core app" that starts in maintenance mode - need to think about how apps will be decoupled in the near future
2021-02-02 09:47:16 -05:00
|
|
|
setTimeout(() => {
|
|
|
|
process.exit(1);
|
|
|
|
}, 100);
|
2020-08-11 07:28:04 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
/**
|
|
|
|
* ### Stop
|
2020-08-11 07:28:04 -05:00
|
|
|
* Stops the server & handles cleanup, but does not exit the process
|
|
|
|
* Used in tests for quick start/stop actions
|
|
|
|
* Called by shutdown to handle server stop and cleanup before exiting
|
2022-04-20 06:47:25 -05:00
|
|
|
* @returns {Promise<any>} Resolves once Ghost has stopped
|
2020-08-09 12:55:41 -05:00
|
|
|
*/
|
2020-08-11 07:28:04 -05:00
|
|
|
async stop() {
|
2020-08-11 15:26:34 -05:00
|
|
|
try {
|
2021-02-15 14:14:48 -05:00
|
|
|
// If we never fully started, there's nothing to stop
|
|
|
|
if (this.httpServer && this.httpServer.listening) {
|
|
|
|
// We stop the server first so that no new long running requests or processes can be started
|
|
|
|
await this._stopServer();
|
|
|
|
}
|
2020-08-11 15:26:34 -05:00
|
|
|
// Do all of the cleanup tasks
|
|
|
|
await this._cleanup();
|
|
|
|
} finally {
|
|
|
|
// Wrap up
|
|
|
|
this.httpServer = null;
|
|
|
|
this._logStopMessages();
|
|
|
|
}
|
2020-08-09 12:55:41 -05:00
|
|
|
}
|
2020-08-07 14:14:31 -05:00
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
/**
|
|
|
|
* ### Hammertime
|
|
|
|
* To be called after `stop`
|
|
|
|
*/
|
2020-08-11 07:28:04 -05:00
|
|
|
async hammertime() {
|
2021-06-08 10:47:31 -05:00
|
|
|
logging.info(tpl(messages.cantTouchThis));
|
2020-08-11 07:28:04 -05:00
|
|
|
}
|
2020-08-07 14:14:31 -05:00
|
|
|
|
2021-02-16 06:54:19 -05:00
|
|
|
/**
|
|
|
|
* Add a task that should be called on shutdown
|
|
|
|
*/
|
2020-08-11 15:26:34 -05:00
|
|
|
registerCleanupTask(task) {
|
|
|
|
this.cleanupTasks.push(task);
|
|
|
|
}
|
|
|
|
|
2020-08-11 07:28:04 -05:00
|
|
|
/**
|
|
|
|
* ### Stop Server
|
|
|
|
* Does the work of stopping the server using stoppable
|
|
|
|
* This handles closing connections:
|
|
|
|
* - New connections are rejected
|
|
|
|
* - Idle connections are closed immediately
|
|
|
|
* - Active connections are allowed to complete in-flight requests before being closed
|
|
|
|
*
|
|
|
|
* If server.shutdownTimeout is reached, requests are terminated in-flight
|
|
|
|
*/
|
|
|
|
async _stopServer() {
|
2022-04-20 06:47:25 -05:00
|
|
|
const util = require('util');
|
|
|
|
return util.promisify(this.httpServer.stop)();
|
2020-08-09 12:55:41 -05:00
|
|
|
}
|
2015-05-28 10:16:09 -05:00
|
|
|
|
2020-08-11 15:26:34 -05:00
|
|
|
async _cleanup() {
|
|
|
|
// Wait for all cleanup tasks to finish
|
2022-04-20 06:47:25 -05:00
|
|
|
return Promise.all(this.cleanupTasks.map(task => task()));
|
2020-08-11 15:26:34 -05:00
|
|
|
}
|
|
|
|
|
2021-02-16 06:54:19 -05:00
|
|
|
/**
|
|
|
|
* Internal Method for TestMode.
|
|
|
|
*/
|
|
|
|
_startTestMode() {
|
|
|
|
// This is horrible and very temporary
|
|
|
|
const jobService = require('./services/jobs');
|
|
|
|
|
|
|
|
// Output how many connections are open every 5 seconds
|
|
|
|
const connectionInterval = setInterval(() => this.httpServer.getConnections(
|
|
|
|
(err, connections) => logging.warn(`${connections} connections currently open`)
|
|
|
|
), 5000);
|
|
|
|
|
|
|
|
// Output a notice when the server closes
|
|
|
|
this.httpServer.on('close', function () {
|
|
|
|
clearInterval(connectionInterval);
|
|
|
|
logging.warn('Server has fully closed');
|
|
|
|
});
|
|
|
|
|
|
|
|
// Output job queue length every 5 seconds
|
|
|
|
setInterval(() => {
|
|
|
|
logging.warn(`${jobService.queue.length()} jobs in the queue. Idle: ${jobService.queue.idle()}`);
|
|
|
|
|
|
|
|
const runningScheduledjobs = Object.keys(jobService.bree.workers);
|
|
|
|
if (Object.keys(jobService.bree.workers).length) {
|
|
|
|
logging.warn(`${Object.keys(jobService.bree.workers).length} jobs running: ${runningScheduledjobs}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const scheduledJobs = Object.keys(jobService.bree.intervals);
|
|
|
|
if (Object.keys(jobService.bree.intervals).length) {
|
|
|
|
logging.warn(`${Object.keys(jobService.bree.intervals).length} scheduled jobs: ${scheduledJobs}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (runningScheduledjobs.length === 0 && scheduledJobs.length === 0) {
|
|
|
|
logging.warn('No scheduled or running jobs');
|
|
|
|
}
|
|
|
|
}, 5000);
|
2020-08-11 15:26:34 -05:00
|
|
|
}
|
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
/**
|
2021-02-16 06:54:19 -05:00
|
|
|
* Log Start Messages
|
2020-08-09 12:55:41 -05:00
|
|
|
*/
|
2020-08-11 07:28:04 -05:00
|
|
|
_logStartMessages() {
|
2022-04-20 06:47:25 -05:00
|
|
|
logging.info(tpl(messages.ghostIsRunningIn, {env: this.env}));
|
2015-05-28 10:16:09 -05:00
|
|
|
|
2022-04-20 06:47:25 -05:00
|
|
|
if (this.env === 'production') {
|
2021-06-08 10:47:31 -05:00
|
|
|
logging.info(tpl(messages.yourBlogIsAvailableOn, {url: this.url}));
|
2015-05-28 10:16:09 -05:00
|
|
|
} else {
|
2021-06-08 10:47:31 -05:00
|
|
|
logging.info(tpl(messages.listeningOn, {
|
2022-04-20 06:47:25 -05:00
|
|
|
host: this.serverConfig.host,
|
|
|
|
port: this.serverConfig.port
|
2020-08-09 12:55:41 -05:00
|
|
|
}));
|
2021-06-08 10:47:31 -05:00
|
|
|
logging.info(tpl(messages.urlConfiguredAs, {url: this.url}));
|
2015-05-28 10:16:09 -05:00
|
|
|
}
|
|
|
|
|
2021-06-08 10:47:31 -05:00
|
|
|
logging.info(tpl(messages.ctrlCToShutDown));
|
2014-08-19 11:36:46 -05:00
|
|
|
}
|
2017-07-11 03:00:07 -05:00
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
/**
|
2021-02-16 06:54:19 -05:00
|
|
|
* Log Stop Messages
|
2020-08-09 12:55:41 -05:00
|
|
|
*/
|
2020-08-11 07:28:04 -05:00
|
|
|
_logStopMessages() {
|
2021-06-08 10:47:31 -05:00
|
|
|
logging.warn(tpl(messages.ghostHasShutdown));
|
2014-08-19 11:36:46 -05:00
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
// Extra clear message for production mode
|
2022-04-20 06:47:25 -05:00
|
|
|
if (this.env === 'production') {
|
2021-06-08 10:47:31 -05:00
|
|
|
logging.warn(tpl(messages.yourBlogIsNowOffline));
|
2020-08-09 12:55:41 -05:00
|
|
|
}
|
2020-08-07 13:45:22 -05:00
|
|
|
|
2020-08-09 12:55:41 -05:00
|
|
|
// Always output uptime
|
|
|
|
logging.warn(
|
2021-06-08 10:47:31 -05:00
|
|
|
tpl(messages.ghostWasRunningFor),
|
2020-08-09 12:55:41 -05:00
|
|
|
moment.duration(process.uptime(), 'seconds').humanize()
|
|
|
|
);
|
2020-08-07 13:45:22 -05:00
|
|
|
}
|
2020-08-09 12:55:41 -05:00
|
|
|
}
|
2014-08-19 11:36:46 -05:00
|
|
|
|
|
|
|
module.exports = GhostServer;
|