Ghost/core/boot.js
Hannah Wolfe 0b79abf5b2 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-08 11:56:44 +00:00

185 lines
6.4 KiB
JavaScript

// # The Ghost Boot Sequence
// IMPORTANT: The only global requires here should be debug + overrides
const debug = require('ghost-ignition').debug('boot');
require('./server/overrides');
// END OF GLOBAL REQUIRES
const initCore = async ({ghostServer}) => {
const settings = require('./server/services/settings');
const jobService = require('./server/services/jobs');
const models = require('./server/models');
const {events, i18n} = require('./server/lib/common');
ghostServer.registerCleanupTask(async () => {
await jobService.shutdown();
});
// Initialize Ghost core internationalization
i18n.init();
debug('Default i18n done for core');
models.init();
debug('Models done');
await settings.init();
// @TODO: fix this - has to happen before db.ready is emitted
debug('Begin: Url Service');
require('./frontend/services/url');
debug('End: Url Service');
// @TODO: fix this location
events.emit('db.ready');
};
const initExpressApps = async () => {
debug('Begin: initExpressApps');
const themeService = require('./frontend/services/themes');
const frontendSettings = require('./frontend/services/settings');
await frontendSettings.init();
debug('Frontend settings done');
await themeService.init();
debug('Themes done');
const parentApp = require('./server/web/parent/app')();
debug('End: initExpressApps');
return parentApp;
};
const initServices = async ({config}) => {
debug('Begin: initialiseServices');
const themeService = require('./frontend/services/themes');
const frontendSettings = require('./frontend/services/settings');
const appService = require('./frontend/services/apps');
const urlUtils = require('./shared/url-utils');
// CASE: When Ghost is ready with bootstrapping (db migrations etc.), we can trigger the router creation.
// Reason is that the routers access the routes.yaml, which shouldn't and doesn't have to be validated to
// start Ghost in maintenance mode.
// Routing is a bridge between the frontend and API
const routing = require('./frontend/services/routing');
// We pass the themeService API version here, so that the frontend services are less tightly-coupled
routing.bootstrap.start(themeService.getApiVersion());
const settings = require('./server/services/settings');
const permissions = require('./server/services/permissions');
const xmlrpc = require('./server/services/xmlrpc');
const slack = require('./server/services/slack');
const {mega} = require('./server/services/mega');
const webhooks = require('./server/services/webhooks');
const scheduling = require('./server/adapters/scheduling');
const getRoutesHash = () => frontendSettings.getCurrentHash('routes');
await Promise.all([
// Initialize the permissions actions and objects
permissions.init(),
xmlrpc.listen(),
slack.listen(),
mega.listen(),
webhooks.listen(),
settings.syncRoutesHash(getRoutesHash),
appService.init(),
scheduling.init({
// NOTE: When changing API version need to consider how to migrate custom scheduling adapters
// that rely on URL to lookup persisted scheduled records (jobs, etc.). Ref: https://github.com/TryGhost/Ghost/pull/10726#issuecomment-489557162
apiUrl: urlUtils.urlFor('api', {version: 'v3', versionType: 'admin'}, true)
})
]);
debug('XMLRPC, Slack, MEGA, Webhooks, Scheduling, Permissions done');
// Initialise analytics events
if (config.get('segment:key')) {
require('./analytics-events').init();
}
debug('End: initialiseServices');
};
const mountGhost = (rootApp, ghostApp) => {
const urlService = require('./frontend/services/url');
rootApp.disable('maintenance');
rootApp.use(urlService.utils.getSubdir(), ghostApp);
};
const bootGhost = async () => {
// Metrics & debugging
const startTime = Date.now();
let ghostServer;
try {
// Config is the absolute first thing to do!
debug('Begin: Load config');
const config = require('./shared/config');
debug('End: Load config');
debug('Begin: Load version info');
const version = require('./server/lib/ghost-version');
config.set('version', version);
debug('End: Load version info');
debug('Begin: load server + minimal app');
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
// Get minimal application in maintenance mode
const rootApp = require('./app');
// Start server with minimal App
const GhostServer = require('./server/ghost-server');
ghostServer = new GhostServer();
await ghostServer.start(rootApp);
const logging = require('./shared/logging');
logging.info('Ghost server start', (Date.now() - startTime) / 1000 + 's');
debug('End: load server + minimal app');
debug('Begin: Get DB ready');
// Get the DB ready
await require('./db').ready();
debug('End: Get DB ready');
// Load Ghost with all its services
debug('Begin: Load Ghost Core Services');
await initCore({ghostServer});
const ghostApp = await initExpressApps({});
await initServices({config});
debug('End: Load Ghost Core Services');
// Mount the full Ghost app onto the minimal root app & disable maintenance mode
mountGhost(rootApp, ghostApp);
// Announce Server Readiness
logging.info('Ghost boot', (Date.now() - startTime) / 1000 + 's');
GhostServer.announceServerReadiness();
} catch (error) {
const errors = require('@tryghost/errors');
// @TODO: fix these extra requires
const GhostServer = require('./server/ghost-server');
const logging = require('./shared/logging');
let serverStartError = error;
if (!errors.utils.isIgnitionError(serverStartError)) {
serverStartError = new errors.GhostError({message: serverStartError.message, err: serverStartError});
}
logging.error(serverStartError);
GhostServer.announceServerReadiness(serverStartError);
if (ghostServer) {
ghostServer.shutdown(2);
} else {
setTimeout(() => {
process.exit(2);
}, 100);
}
}
};
module.exports = bootGhost;