2015-05-28 18:16:09 +03:00
|
|
|
// # Ghost Server
|
|
|
|
// Handles the creation of an HTTP Server for Ghost
|
2020-04-29 18:44:27 +03:00
|
|
|
const debug = require('ghost-ignition').debug('server');
|
|
|
|
|
|
|
|
const Promise = require('bluebird');
|
|
|
|
const fs = require('fs-extra');
|
|
|
|
const path = require('path');
|
|
|
|
const _ = require('lodash');
|
2020-05-27 20:47:53 +03:00
|
|
|
const config = require('../shared/config');
|
2020-05-28 13:57:02 +03:00
|
|
|
const urlUtils = require('./../shared/url-utils');
|
2020-04-30 22:26:12 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
2020-05-28 21:30:23 +03:00
|
|
|
const {events, i18n} = require('./lib/common');
|
|
|
|
const logging = require('../shared/logging');
|
2020-04-29 18:44:27 +03:00
|
|
|
const moment = require('moment');
|
2014-08-19 20:36:46 +04:00
|
|
|
|
2015-05-28 18:16:09 +03:00
|
|
|
/**
|
|
|
|
* ## GhostServer
|
|
|
|
* @constructor
|
|
|
|
* @param {Object} rootApp - parent express instance
|
|
|
|
*/
|
2014-09-19 20:17:58 +04:00
|
|
|
function GhostServer(rootApp) {
|
|
|
|
this.rootApp = rootApp;
|
2014-08-19 20:36:46 +04:00
|
|
|
this.httpServer = null;
|
2014-11-11 09:35:37 +03:00
|
|
|
this.connections = {};
|
|
|
|
this.connectionId = 0;
|
2014-08-24 00:42:44 +04:00
|
|
|
|
|
|
|
// Expose config module for use externally.
|
|
|
|
this.config = config;
|
2014-08-19 20:36:46 +04:00
|
|
|
}
|
|
|
|
|
2020-04-02 14:06:14 +03:00
|
|
|
const debugInfo = {
|
|
|
|
versions: process.versions,
|
|
|
|
platform: process.platform,
|
|
|
|
arch: process.arch,
|
|
|
|
release: process.release
|
|
|
|
};
|
|
|
|
|
2015-05-28 18:16:09 +03:00
|
|
|
/**
|
|
|
|
* ## 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
|
|
|
|
*/
|
|
|
|
GhostServer.prototype.start = function (externalApp) {
|
2016-10-03 11:33:14 +03:00
|
|
|
debug('Starting...');
|
2020-04-29 18:44:27 +03:00
|
|
|
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'
|
|
|
|
};
|
2015-05-28 18:16:09 +03:00
|
|
|
|
2016-11-07 17:25:29 +03:00
|
|
|
return new Promise(function (resolve, reject) {
|
2019-07-05 14:40:43 +03:00
|
|
|
if (Object.prototype.hasOwnProperty.call(config.get('server'), 'socket')) {
|
2016-09-13 21:30:47 +03:00
|
|
|
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;
|
|
|
|
}
|
2015-05-28 18:16:09 +03:00
|
|
|
|
|
|
|
// Make sure the socket is gone before trying to create another
|
|
|
|
try {
|
2016-09-13 21:30:47 +03:00
|
|
|
fs.unlinkSync(socketValues.path);
|
2015-05-28 18:16:09 +03:00
|
|
|
} catch (e) {
|
|
|
|
// We can ignore this.
|
|
|
|
}
|
|
|
|
|
2016-09-13 21:30:47 +03:00
|
|
|
self.httpServer = rootApp.listen(socketValues.path);
|
|
|
|
fs.chmod(socketValues.path, socketValues.permissions);
|
|
|
|
config.set('server:socket', socketValues);
|
2015-05-28 18:16:09 +03:00
|
|
|
} else {
|
|
|
|
self.httpServer = rootApp.listen(
|
2016-09-13 18:41:14 +03:00
|
|
|
config.get('server').port,
|
|
|
|
config.get('server').host
|
2015-05-28 18:16:09 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.httpServer.on('error', function (error) {
|
2020-04-29 18:44:27 +03:00
|
|
|
let ghostError;
|
2016-11-07 17:25:29 +03:00
|
|
|
|
2015-05-28 18:16:09 +03:00
|
|
|
if (error.errno === 'EADDRINUSE') {
|
2020-04-30 22:26:12 +03:00
|
|
|
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')
|
2016-11-07 17:25:29 +03:00
|
|
|
});
|
2015-05-28 18:16:09 +03:00
|
|
|
} else {
|
2020-04-30 22:26:12 +03:00
|
|
|
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')
|
2016-11-07 17:25:29 +03:00
|
|
|
});
|
2015-05-28 18:16:09 +03:00
|
|
|
}
|
2016-10-04 18:33:43 +03:00
|
|
|
|
2016-11-07 17:25:29 +03:00
|
|
|
reject(ghostError);
|
2015-05-28 18:16:09 +03:00
|
|
|
});
|
|
|
|
self.httpServer.on('connection', self.connection.bind(self));
|
|
|
|
self.httpServer.on('listening', function () {
|
2016-10-03 11:33:14 +03:00
|
|
|
debug('...Started');
|
2015-05-28 18:16:09 +03:00
|
|
|
self.logStartMessages();
|
2018-08-22 14:28:31 +03:00
|
|
|
|
|
|
|
return GhostServer.announceServerStart()
|
|
|
|
.finally(() => {
|
|
|
|
resolve(self);
|
|
|
|
});
|
2015-05-28 18:16:09 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Stop
|
|
|
|
* Returns a promise that will be fulfilled when the server stops. If the server has not been started,
|
|
|
|
* the promise will be fulfilled immediately
|
|
|
|
* @returns {Promise} Resolves once Ghost has stopped
|
|
|
|
*/
|
|
|
|
GhostServer.prototype.stop = function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
const self = this;
|
2015-05-28 18:16:09 +03:00
|
|
|
|
|
|
|
return new Promise(function (resolve) {
|
|
|
|
if (self.httpServer === null) {
|
|
|
|
resolve(self);
|
|
|
|
} else {
|
|
|
|
self.httpServer.close(function () {
|
2020-04-30 22:26:12 +03:00
|
|
|
events.emit('server.stop');
|
2015-05-28 18:16:09 +03:00
|
|
|
self.httpServer = null;
|
|
|
|
self.logShutdownMessages();
|
|
|
|
resolve(self);
|
|
|
|
});
|
|
|
|
|
|
|
|
self.closeConnections();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Restart
|
|
|
|
* Restarts the ghost application
|
|
|
|
* @returns {Promise} Resolves once Ghost has restarted
|
|
|
|
*/
|
|
|
|
GhostServer.prototype.restart = function () {
|
2017-01-10 02:18:23 +03:00
|
|
|
return this.stop().then(function (ghostServer) {
|
|
|
|
return ghostServer.start();
|
|
|
|
});
|
2015-05-28 18:16:09 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Hammertime
|
|
|
|
* To be called after `stop`
|
|
|
|
*/
|
|
|
|
GhostServer.prototype.hammertime = function () {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.info(i18n.t('notices.httpServer.cantTouchThis'));
|
2015-05-28 18:16:09 +03:00
|
|
|
|
|
|
|
return Promise.resolve(this);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ## Private (internal) methods
|
|
|
|
*
|
|
|
|
* ### Connection
|
|
|
|
* @param {Object} socket
|
|
|
|
*/
|
2014-08-19 20:36:46 +04:00
|
|
|
GhostServer.prototype.connection = function (socket) {
|
2020-04-29 18:44:27 +03:00
|
|
|
const self = this;
|
2014-11-11 09:35:37 +03:00
|
|
|
|
|
|
|
self.connectionId += 1;
|
|
|
|
socket._ghostId = self.connectionId;
|
|
|
|
|
|
|
|
socket.on('close', function () {
|
|
|
|
delete self.connections[this._ghostId];
|
|
|
|
});
|
|
|
|
|
|
|
|
self.connections[socket._ghostId] = socket;
|
2014-08-19 20:36:46 +04:00
|
|
|
};
|
|
|
|
|
2015-05-28 18:16:09 +03:00
|
|
|
/**
|
|
|
|
* ### Close Connections
|
|
|
|
* Most browsers keep a persistent connection open to the server, which prevents the close callback of
|
|
|
|
* httpServer from returning. We need to destroy all connections manually.
|
|
|
|
*/
|
2014-08-19 20:36:46 +04:00
|
|
|
GhostServer.prototype.closeConnections = function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
const self = this;
|
2014-11-11 09:35:37 +03:00
|
|
|
|
|
|
|
Object.keys(self.connections).forEach(function (socketId) {
|
2020-04-29 18:44:27 +03:00
|
|
|
const socket = self.connections[socketId];
|
2014-11-11 09:35:37 +03:00
|
|
|
|
|
|
|
if (socket) {
|
|
|
|
socket.destroy();
|
|
|
|
}
|
2014-08-19 20:36:46 +04:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-05-28 18:16:09 +03:00
|
|
|
/**
|
|
|
|
* ### Log Start Messages
|
|
|
|
*/
|
2014-08-19 20:36:46 +04:00
|
|
|
GhostServer.prototype.logStartMessages = function () {
|
|
|
|
// Startup & Shutdown messages
|
2017-02-03 21:25:39 +03:00
|
|
|
if (config.get('env') === 'production') {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.info(i18n.t('notices.httpServer.ghostIsRunningIn', {env: config.get('env')}));
|
|
|
|
logging.info(i18n.t('notices.httpServer.yourBlogIsAvailableOn', {url: urlUtils.urlFor('home', true)}));
|
|
|
|
logging.info(i18n.t('notices.httpServer.ctrlCToShutDown'));
|
2014-08-19 20:36:46 +04:00
|
|
|
} else {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.info(i18n.t('notices.httpServer.ghostIsRunningIn', {env: config.get('env')}));
|
|
|
|
logging.info(i18n.t('notices.httpServer.listeningOn', {
|
2017-07-11 11:00:07 +03:00
|
|
|
host: config.get('server').socket || config.get('server').host,
|
|
|
|
port: config.get('server').port
|
|
|
|
}));
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.info(i18n.t('notices.httpServer.urlConfiguredAs', {url: urlUtils.urlFor('home', true)}));
|
|
|
|
logging.info(i18n.t('notices.httpServer.ctrlCToShutDown'));
|
2014-11-05 15:20:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function shutdown() {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(i18n.t('notices.httpServer.ghostHasShutdown'));
|
2017-02-03 21:25:39 +03:00
|
|
|
|
|
|
|
if (config.get('env') === 'production') {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(i18n.t('notices.httpServer.yourBlogIsNowOffline'));
|
2014-11-05 15:20:10 +03:00
|
|
|
} else {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(
|
|
|
|
i18n.t('notices.httpServer.ghostWasRunningFor'),
|
2016-09-19 13:28:18 +03:00
|
|
|
moment.duration(process.uptime(), 'seconds').humanize()
|
2014-08-19 20:36:46 +04:00
|
|
|
);
|
2014-11-05 15:20:10 +03:00
|
|
|
}
|
2017-02-03 21:25:39 +03:00
|
|
|
|
2014-11-05 15:20:10 +03:00
|
|
|
process.exit(0);
|
2014-08-19 20:36:46 +04:00
|
|
|
}
|
2017-07-11 11:00:07 +03:00
|
|
|
|
2014-11-05 15:20:10 +03:00
|
|
|
// ensure that Ghost exits correctly on Ctrl+C and SIGTERM
|
2017-07-11 11:00:07 +03:00
|
|
|
process.removeAllListeners('SIGINT').on('SIGINT', shutdown).removeAllListeners('SIGTERM').on('SIGTERM', shutdown);
|
2014-08-19 20:36:46 +04:00
|
|
|
};
|
|
|
|
|
2015-05-28 18:16:09 +03:00
|
|
|
/**
|
|
|
|
* ### Log Shutdown Messages
|
|
|
|
*/
|
2014-08-19 20:36:46 +04:00
|
|
|
GhostServer.prototype.logShutdownMessages = function () {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(i18n.t('notices.httpServer.ghostIsClosingConnections'));
|
2014-08-19 20:36:46 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = GhostServer;
|
2018-07-27 16:19:39 +03:00
|
|
|
|
2018-08-14 20:04:25 +03:00
|
|
|
const connectToBootstrapSocket = (message) => {
|
|
|
|
const socketAddress = config.get('bootstrap-socket');
|
|
|
|
const net = require('net');
|
|
|
|
const client = new net.Socket();
|
2018-07-27 16:19:39 +03:00
|
|
|
|
2018-08-14 20:04:25 +03:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
const connect = (options = {}) => {
|
|
|
|
let wasResolved = false;
|
2018-07-27 16:19:39 +03:00
|
|
|
|
|
|
|
const waitTimeout = setTimeout(() => {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.info('Bootstrap socket timed out.');
|
2018-08-14 20:04:25 +03:00
|
|
|
|
|
|
|
if (!client.destroyed) {
|
|
|
|
client.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (wasResolved) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
wasResolved = true;
|
2018-07-27 16:19:39 +03:00
|
|
|
resolve();
|
|
|
|
}, 1000 * 5);
|
|
|
|
|
|
|
|
client.connect(socketAddress.port, socketAddress.host, () => {
|
|
|
|
if (waitTimeout) {
|
|
|
|
clearTimeout(waitTimeout);
|
|
|
|
}
|
|
|
|
|
2018-08-14 20:04:25 +03:00
|
|
|
client.write(JSON.stringify(message));
|
|
|
|
|
|
|
|
if (wasResolved) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
wasResolved = true;
|
2018-07-27 16:19:39 +03:00
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
|
|
|
|
client.on('close', () => {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.info('Bootstrap client was closed.');
|
2018-08-14 20:04:25 +03:00
|
|
|
|
2018-07-27 16:19:39 +03:00
|
|
|
if (waitTimeout) {
|
|
|
|
clearTimeout(waitTimeout);
|
|
|
|
}
|
|
|
|
});
|
2018-08-14 20:04:25 +03:00
|
|
|
|
|
|
|
client.on('error', (err) => {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(`Can't connect to the bootstrap socket (${socketAddress.host} ${socketAddress.port}) ${err.code}`);
|
2018-08-14 20:04:25 +03:00
|
|
|
|
|
|
|
client.removeAllListeners();
|
|
|
|
|
|
|
|
if (waitTimeout) {
|
|
|
|
clearTimeout(waitTimeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.tries < 3) {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(`Tries: ${options.tries}`);
|
2018-08-14 20:04:25 +03:00
|
|
|
|
|
|
|
// retry
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn('Retrying...');
|
2018-08-14 20:04:25 +03:00
|
|
|
|
|
|
|
options.tries = options.tries + 1;
|
|
|
|
const retryTimeout = setTimeout(() => {
|
|
|
|
clearTimeout(retryTimeout);
|
|
|
|
connect(options);
|
|
|
|
}, 150);
|
|
|
|
} else {
|
|
|
|
if (wasResolved) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
wasResolved = true;
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
connect({tries: 0});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2018-08-22 14:28:31 +03:00
|
|
|
/**
|
|
|
|
* @NOTE announceServerStartCalled:
|
|
|
|
*
|
|
|
|
* - backwards compatible logic, because people complained that not all themes were loaded when using Ghost as NPM module
|
|
|
|
* - we told them to call `announceServerStart`, which is not required anymore, because we restructured the code
|
|
|
|
*/
|
|
|
|
let announceServerStartCalled = false;
|
2018-08-14 20:04:25 +03:00
|
|
|
module.exports.announceServerStart = function announceServerStart() {
|
2018-08-22 14:28:31 +03:00
|
|
|
if (announceServerStartCalled || config.get('maintenance:enabled')) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
announceServerStartCalled = true;
|
|
|
|
|
2020-04-30 22:26:12 +03:00
|
|
|
events.emit('server.start');
|
2018-08-14 20:04:25 +03:00
|
|
|
|
|
|
|
// CASE: IPC communication to the CLI via child process.
|
|
|
|
if (process.send) {
|
|
|
|
process.send({
|
2020-04-02 14:06:14 +03:00
|
|
|
started: true,
|
|
|
|
debug: debugInfo
|
2018-08-14 20:04:25 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// CASE: Ghost extension - bootstrap sockets
|
|
|
|
if (config.get('bootstrap-socket')) {
|
|
|
|
return connectToBootstrapSocket({
|
|
|
|
started: true
|
2018-07-27 16:19:39 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
};
|
|
|
|
|
2018-08-22 14:28:31 +03:00
|
|
|
/**
|
|
|
|
* @NOTE announceServerStopCalled:
|
|
|
|
*
|
|
|
|
* - backwards compatible logic, because people complained that not all themes were loaded when using Ghost as NPM module
|
|
|
|
* - we told them to call `announceServerStart`, which is not required anymore, because we restructured code
|
|
|
|
*/
|
|
|
|
let announceServerStopCalled = false;
|
2018-07-27 16:19:39 +03:00
|
|
|
module.exports.announceServerStopped = function announceServerStopped(error) {
|
2018-08-22 14:28:31 +03:00
|
|
|
if (announceServerStopCalled) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
announceServerStopCalled = true;
|
|
|
|
|
2018-07-27 16:19:39 +03:00
|
|
|
// CASE: IPC communication to the CLI via child process.
|
|
|
|
if (process.send) {
|
|
|
|
process.send({
|
|
|
|
started: false,
|
2020-04-02 14:06:14 +03:00
|
|
|
error: error,
|
|
|
|
debug: debugInfo
|
2018-07-27 16:19:39 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// CASE: Ghost extension - bootstrap sockets
|
|
|
|
if (config.get('bootstrap-socket')) {
|
2018-08-14 20:04:25 +03:00
|
|
|
return connectToBootstrapSocket({
|
|
|
|
started: false,
|
2020-04-02 14:06:14 +03:00
|
|
|
error: error,
|
|
|
|
debug: debugInfo
|
2018-07-27 16:19:39 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
};
|