mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
5c4e884070
refs #12402 - With bumped version of job-manager it offloads job procesing into separate worker thread. Having jobs run out of main Ghost process even loop allows for safe job execution, which does not block Ghost from serving requests or performing other functions without a delay - Added experimental data access to 'testmode' jobs. This should serve as an illustration of how to access data from the job layer
324 lines
11 KiB
JavaScript
324 lines
11 KiB
JavaScript
// # Ghost Server
|
|
// Handles the creation of an HTTP Server for Ghost
|
|
const debug = require('ghost-ignition').debug('server');
|
|
|
|
const Promise = require('bluebird');
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
const _ = require('lodash');
|
|
const config = require('../shared/config');
|
|
const urlUtils = require('./../shared/url-utils');
|
|
const errors = require('@tryghost/errors');
|
|
const {events, i18n} = require('./lib/common');
|
|
const logging = require('../shared/logging');
|
|
const moment = require('moment');
|
|
const bootstrapSocket = require('@tryghost/bootstrap-socket');
|
|
const stoppable = require('stoppable');
|
|
|
|
/**
|
|
* ## GhostServer
|
|
*/
|
|
class GhostServer {
|
|
/**
|
|
* @constructor
|
|
* @param {Object} rootApp - parent express instance
|
|
*/
|
|
constructor(rootApp) {
|
|
this.rootApp = rootApp;
|
|
this.httpServer = null;
|
|
|
|
// Expose config module for use externally.
|
|
this.config = config;
|
|
|
|
// Tasks that should be run before the server exits
|
|
this.cleanupTasks = [];
|
|
}
|
|
|
|
/**
|
|
* ## Public API methods
|
|
*
|
|
* ### Start
|
|
* Starts the ghost server listening on the configured port.
|
|
* Alternatively you can pass in your own express instance and let Ghost
|
|
* start listening for you.
|
|
* @param {Object} externalApp - Optional express app instance.
|
|
* @return {Promise} Resolves once Ghost has started
|
|
*/
|
|
start(externalApp) {
|
|
debug('Starting...');
|
|
const self = this;
|
|
const rootApp = externalApp ? externalApp : self.rootApp;
|
|
let socketConfig;
|
|
|
|
const socketValues = {
|
|
path: path.join(config.get('paths').contentPath, config.get('env') + '.socket'),
|
|
permissions: '660'
|
|
};
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
if (Object.prototype.hasOwnProperty.call(config.get('server'), 'socket')) {
|
|
socketConfig = config.get('server').socket;
|
|
|
|
if (_.isString(socketConfig)) {
|
|
socketValues.path = socketConfig;
|
|
} else if (_.isObject(socketConfig)) {
|
|
socketValues.path = socketConfig.path || socketValues.path;
|
|
socketValues.permissions = socketConfig.permissions || socketValues.permissions;
|
|
}
|
|
|
|
// Make sure the socket is gone before trying to create another
|
|
try {
|
|
fs.unlinkSync(socketValues.path);
|
|
} catch (e) {
|
|
// We can ignore this.
|
|
}
|
|
|
|
self.httpServer = rootApp.listen(socketValues.path);
|
|
fs.chmod(socketValues.path, socketValues.permissions);
|
|
config.set('server:socket', socketValues);
|
|
} else {
|
|
self.httpServer = rootApp.listen(
|
|
config.get('server').port,
|
|
config.get('server').host
|
|
);
|
|
}
|
|
|
|
self.httpServer.on('error', function (error) {
|
|
let ghostError;
|
|
|
|
if (error.errno === 'EADDRINUSE') {
|
|
ghostError = new errors.GhostError({
|
|
message: i18n.t('errors.httpServer.addressInUse.error'),
|
|
context: i18n.t('errors.httpServer.addressInUse.context', {port: config.get('server').port}),
|
|
help: i18n.t('errors.httpServer.addressInUse.help')
|
|
});
|
|
} else {
|
|
ghostError = new errors.GhostError({
|
|
message: i18n.t('errors.httpServer.otherError.error', {errorNumber: error.errno}),
|
|
context: i18n.t('errors.httpServer.otherError.context'),
|
|
help: i18n.t('errors.httpServer.otherError.help')
|
|
});
|
|
}
|
|
|
|
reject(ghostError);
|
|
});
|
|
|
|
self.httpServer.on('listening', function () {
|
|
debug('...Started');
|
|
self._logStartMessages();
|
|
|
|
// Debug logs output in testmode only
|
|
if (config.get('server:testmode')) {
|
|
// This is horrible and very temporary
|
|
const jobService = require('./services/jobs');
|
|
|
|
// Output how many connections are open every 5 seconds
|
|
const connectionInterval = setInterval(() => self.httpServer.getConnections(
|
|
(err, connections) => logging.warn(`${connections} connections currently open`)
|
|
), 5000);
|
|
|
|
// Output a notice when the server closes
|
|
self.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()}`);
|
|
logging.warn(`${Object.keys(jobService.bree.workers).length} workers registered. Scheduled jobs: ${Object.keys(jobService.bree.intervals).length}`);
|
|
}, 5000);
|
|
}
|
|
|
|
return GhostServer.announceServerReadiness()
|
|
.finally(() => {
|
|
resolve(self);
|
|
});
|
|
});
|
|
|
|
stoppable(self.httpServer, config.get('server:shutdownTimeout'));
|
|
|
|
// ensure that Ghost exits correctly on Ctrl+C and SIGTERM
|
|
process
|
|
.removeAllListeners('SIGINT').on('SIGINT', self.shutdown.bind(self))
|
|
.removeAllListeners('SIGTERM').on('SIGTERM', self.shutdown.bind(self));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ### Shutdown
|
|
* Stops the server, handles cleanup and exits the process = a full shutdown
|
|
* Called on SIGINT or SIGTERM
|
|
*/
|
|
async shutdown() {
|
|
try {
|
|
logging.warn(i18n.t('notices.httpServer.ghostIsShuttingDown'));
|
|
await this.stop();
|
|
process.exit(0);
|
|
} catch (error) {
|
|
logging.error(error);
|
|
process.exit(-1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ### Stop
|
|
* 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
|
|
* @returns {Promise} Resolves once Ghost has stopped
|
|
*/
|
|
async stop() {
|
|
// If we never fully started, there's nothing to stop
|
|
if (this.httpServer === null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// We stop the server first so that no new long running requests or processes can be started
|
|
await this._stopServer();
|
|
// Do all of the cleanup tasks
|
|
await this._cleanup();
|
|
} finally {
|
|
// Wrap up
|
|
events.emit('server.stop');
|
|
this.httpServer = null;
|
|
this._logStopMessages();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ### Hammertime
|
|
* To be called after `stop`
|
|
*/
|
|
async hammertime() {
|
|
logging.info(i18n.t('notices.httpServer.cantTouchThis'));
|
|
}
|
|
|
|
registerCleanupTask(task) {
|
|
this.cleanupTasks.push(task);
|
|
}
|
|
|
|
/**
|
|
* ### 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() {
|
|
return new Promise((resolve, reject) => {
|
|
this.httpServer.stop((err, status) => (err ? reject(err) : resolve(status)));
|
|
});
|
|
}
|
|
|
|
async _cleanup() {
|
|
// Wait for all cleanup tasks to finish
|
|
await Promise
|
|
.all(this.cleanupTasks.map(task => task()));
|
|
}
|
|
|
|
_onShutdownComplete() {
|
|
// Wrap up
|
|
events.emit('server.stop');
|
|
this.httpServer = null;
|
|
this._logStopMessages();
|
|
}
|
|
|
|
/**
|
|
* ### Log Start Messages
|
|
*/
|
|
_logStartMessages() {
|
|
logging.info(i18n.t('notices.httpServer.ghostIsRunningIn', {env: config.get('env')}));
|
|
|
|
if (config.get('env') === 'production') {
|
|
logging.info(i18n.t('notices.httpServer.yourBlogIsAvailableOn', {url: urlUtils.urlFor('home', true)}));
|
|
} else {
|
|
logging.info(i18n.t('notices.httpServer.listeningOn', {
|
|
host: config.get('server').socket || config.get('server').host,
|
|
port: config.get('server').port
|
|
}));
|
|
logging.info(i18n.t('notices.httpServer.urlConfiguredAs', {url: urlUtils.urlFor('home', true)}));
|
|
}
|
|
|
|
logging.info(i18n.t('notices.httpServer.ctrlCToShutDown'));
|
|
}
|
|
|
|
/**
|
|
* ### Log Stop Messages
|
|
* Private / internal API
|
|
*/
|
|
_logStopMessages() {
|
|
logging.warn(i18n.t('notices.httpServer.ghostHasShutdown'));
|
|
|
|
// Extra clear message for production mode
|
|
if (config.get('env') === 'production') {
|
|
logging.warn(i18n.t('notices.httpServer.yourBlogIsNowOffline'));
|
|
}
|
|
|
|
// Always output uptime
|
|
logging.warn(
|
|
i18n.t('notices.httpServer.ghostWasRunningFor'),
|
|
moment.duration(process.uptime(), 'seconds').humanize()
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = GhostServer;
|
|
|
|
/**
|
|
* We call announce server readiness when the server is ready
|
|
* When the server is started, but not ready, it is only able to serve 503s
|
|
*
|
|
* If the server isn't able to reach readiness, announceServerReadiness is called with an error
|
|
* A status message, any error, and debug info are all passed to managing processes via IPC and the bootstrap socket
|
|
*/
|
|
let announceServerReadinessCalled = false;
|
|
|
|
const debugInfo = {
|
|
versions: process.versions,
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
release: process.release
|
|
};
|
|
|
|
module.exports.announceServerReadiness = function (error = null) {
|
|
// If we already announced readiness, we should not do it again
|
|
if (announceServerReadinessCalled) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// Mark this function as called
|
|
announceServerReadinessCalled = true;
|
|
|
|
// Build our message
|
|
// - if there's no error then the server is ready
|
|
let message = {
|
|
started: true,
|
|
debug: debugInfo
|
|
};
|
|
|
|
// - if there's an error then the server is not ready, include the errors
|
|
if (error) {
|
|
message.started = false;
|
|
message.error = error;
|
|
} else {
|
|
events.emit('server.start');
|
|
}
|
|
|
|
// CASE: IPC communication to the CLI for local process manager
|
|
if (process.send) {
|
|
process.send(message);
|
|
}
|
|
|
|
// CASE: use bootstrap socket to communicate with CLI for systemd
|
|
let socketAddress = config.get('bootstrap-socket');
|
|
if (socketAddress) {
|
|
return bootstrapSocket.connectAndSend(socketAddress, logging, message);
|
|
}
|
|
|
|
return Promise.resolve();
|
|
};
|