Skip to content

Commit db99f6b

Browse files
committed
✨ 🔥 Services
refs TryGhost#146 - add ServiceManager and Service base classes - convert ProcessManager classes and related utilities to use the ServiceManager - move process manager default to advanced config options
1 parent 34fb804 commit db99f6b

File tree

15 files changed

+605
-110
lines changed

15 files changed

+605
-110
lines changed

lib/commands/config/advanced.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ module.exports = [{
5353
}
5454
}, {
5555
name: 'process',
56-
description: 'Type of process manager to run Ghost with'
56+
description: 'Type of process manager to run Ghost with',
57+
default: config => config.environment === 'production' ? 'systemd' : 'local'
5758
}, {
5859
name: 'db',
5960
description: 'Type of database Ghost should use',

lib/commands/config/index.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ module.exports.execute = function execute(key, value, options) {
8383
// ensure we are within a valid Ghost install
8484
checkValidInstall('config');
8585

86-
let configFile = `config.${this.environment}.json`;
87-
let config = Config.load(configFile);
86+
let config = Config.load(this.environment);
8887

8988
if (key && !value) {
9089
// getter

lib/commands/run.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ const KnexMigrator = require('knex-migrator');
44

55
const Config = require('../utils/config');
66
const Instance = require('../utils/instance');
7+
const ServiceManager = require('../services');
78
const checkValidInstall = require('../utils/check-valid-install');
8-
const resolveProcessManager = require('../utils/resolve-process');
99

1010
let instance;
1111

@@ -15,11 +15,11 @@ module.exports.execute = function execute() {
1515
process.env.NODE_ENV = this.environment;
1616
process.env.paths__contentPath = path.join(process.cwd(), 'content');
1717

18-
let config = Config.load(`config.${this.environment}.json`);
18+
let config = Config.load(this.environment);
1919
let knexMigrator = new KnexMigrator({
2020
knexMigratorFilePath: path.join(process.cwd(), 'current')
2121
});
22-
let processManager = resolveProcessManager(config, this.ui);
22+
let sm = ServiceManager.load(config, this.ui);
2323

2424
return knexMigrator.isDatabaseOK().catch((error) => {
2525
if (error.code === 'DB_NOT_INITIALISED' ||
@@ -29,9 +29,9 @@ module.exports.execute = function execute() {
2929
return knexMigrator.migrate();
3030
}
3131
}).then(() => {
32-
instance = new Instance(this.ui, processManager);
32+
instance = new Instance(this.ui, sm.process);
3333
}).catch((error) => {
34-
processManager.error(error.message);
34+
sm.process.error(error.message);
3535
});
3636
};
3737

lib/commands/setup.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ const Listr = require('listr');
77
const setupChecks = require('./doctor/checks/setup');
88
const startCommand = require('./start');
99
const configCommand = require('./config');
10+
const ServiceManager = require('../services');
1011
const dedupeProcessName = require('../utils/dedupe-process-name');
1112
const checkValidInstall = require('../utils/check-valid-install');
12-
const resolveProcessManager = require('../utils/resolve-process');
1313

1414
module.exports.execute = function execute(options) {
1515
checkValidInstall('setup');
@@ -78,9 +78,9 @@ module.exports.execute = function execute(options) {
7878
// De-duplicate process name before setting up the process manager
7979
dedupeProcessName(context.config);
8080

81-
let processManager = resolveProcessManager(context.config, this.ui);
81+
this.serviceManager = ServiceManager.load(context.config, this.ui);
8282

83-
return this.ui.run(processManager.setup(this.environment), 'Finishing setup');
83+
return this.ui.run(this.serviceManager.callHook('setup', context), 'Finishing setup');
8484
}).then(() => {
8585
if (!context.start) {return;}
8686

lib/commands/start.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ const fs = require('fs');
33
const path = require('path');
44

55
const Config = require('../utils/config');
6+
const ServiceManager = require('../services');
67
const checkValidInstall = require('../utils/check-valid-install');
7-
const resolveProcessManager = require('../utils/resolve-process');
88

99
function register(config, environment) {
1010
let systemConfig = Config.load('system');
@@ -34,18 +34,18 @@ module.exports.execute = function execute(options) {
3434
process.NODE_ENV = this.environment = 'development';
3535
}
3636

37-
let config = Config.load(`config.${this.environment}.json`);
37+
let config = Config.load(this.environment);
3838
let cliConfig = Config.load('.ghost-cli');
3939

4040
if (cliConfig.has('running')) {
4141
return Promise.reject(new Error('Ghost is already running.'));
4242
}
4343

44-
let processManager = resolveProcessManager(config, this.ui);
45-
let start = Promise.resolve(processManager.start(process.cwd(), this.environment)).then(() => {
44+
this.serviceManager = this.serviceManager || ServiceManager.load(config, this.ui);
45+
46+
let start = Promise.resolve(this.serviceManager.process.start(process.cwd(), this.environment)).then(() => {
4647
register(config, this.environment);
4748
cliConfig.set('running', this.environment).save();
48-
// TODO: add process info to global cli file so Ghost-CLI is aware of this instance
4949
return Promise.resolve();
5050
});
5151

lib/commands/stop.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
const Promise = require('bluebird');
33

44
const Config = require('../utils/config');
5+
const ServiceManager = require('../services');
56
const checkValidInstall = require('../utils/check-valid-install');
6-
const resolveProcessManager = require('../utils/resolve-process');
77

88
function stopAll() {
99
let systemConfig = Config.load('system');
@@ -43,9 +43,9 @@ function execute(options) {
4343
return Promise.reject(new Error('No running Ghost instance found here.'));
4444
}
4545

46-
let config = Config.load(`config.${cliConfig.get('running')}.json`);
47-
let processManager = resolveProcessManager(config, this.ui);
48-
let stop = Promise.resolve(processManager.stop(process.cwd())).then(() => {
46+
let config = Config.load(cliConfig.get('running'));
47+
this.serviceManager = this.serviceManager || ServiceManager.load(config, this.ui);
48+
let stop = Promise.resolve(this.serviceManager.process.stop(process.cwd())).then(() => {
4949
deregister(config);
5050
cliConfig.set('running', null).save();
5151
return Promise.resolve();

lib/commands/update.js

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const symlinkSync = require('symlink-or-copy').sync;
66

77
// Utils
88
const Config = require('../utils/config');
9+
const ServiceManager = require('../services');
910
const resolveVersion = require('../utils/resolve-version');
1011
const checkValidInstall = require('../utils/check-valid-install');
1112

@@ -52,6 +53,8 @@ module.exports.execute = function execute(version, options) {
5253

5354
context.environment = config.get('running', null);
5455

56+
this.serviceManager = ServiceManager.load(Config.load(context.environment), this.ui);
57+
5558
return new Listr([{
5659
title: 'Checking for latest Ghost version',
5760
skip: (ctx) => ctx.rollback,

lib/services/base.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
class BaseService {
4+
constructor(serviceManager) {
5+
this.serviceManager = serviceManager;
6+
}
7+
8+
init() {
9+
// default implementation - noop
10+
}
11+
12+
on(hook, fn) {
13+
if (typeof fn !== 'function') {
14+
fn = this[fn];
15+
}
16+
17+
this.serviceManager.registerHook(hook, fn, this.name);
18+
}
19+
20+
get config() {
21+
return this.serviceManager.config;
22+
}
23+
24+
get ui() {
25+
return this.serviceManager.ui;
26+
}
27+
};
28+
29+
module.exports = BaseService;

lib/services/index.js

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
'use strict';
2+
const each = require('lodash/each');
3+
const toArray = require('lodash/toArray');
4+
const includes = require('lodash/includes');
5+
const isFunction = require('lodash/isFunction');
6+
const Promise = require('bluebird');
7+
8+
const BaseService = require('./base');
9+
const BaseProcess = require('./process/base');
10+
const localService = require('./process/local');
11+
12+
function hasRequiredFns(pm) {
13+
let unimplementedMethods = [];
14+
15+
each(BaseProcess.requiredMethods, function (method) {
16+
if (!pm[method] || !pm.hasOwnProperty(method) || !isFunction(pm[method])) {
17+
unimplementedMethods.push(method);
18+
}
19+
});
20+
21+
return unimplementedMethods;
22+
}
23+
24+
class ServiceManager {
25+
constructor(config, ui) {
26+
this.config = config;
27+
this.ui = ui;
28+
29+
this.hooks = {};
30+
this.services = {};
31+
this.process = null;
32+
}
33+
34+
registerHook(hook, fn, serviceName) {
35+
if (!includes(ServiceManager.allowedHooks, hook)) {
36+
throw new Error(`Hook ${hook} does not exist.`);
37+
}
38+
39+
if (!this.services[serviceName]) {
40+
throw new Error(`Service ${serviceName} does not exist`);
41+
}
42+
43+
if (!this.hooks[hook]) {
44+
this.hooks[hook] = {};
45+
}
46+
47+
this.hooks[hook][serviceName] = fn;
48+
}
49+
50+
callHook(hook) {
51+
if (!includes(ServiceManager.allowedHooks, hook)) {
52+
throw new Error(`Hook ${hook} does not exist.`);
53+
}
54+
55+
let args = toArray(arguments).slice(1);
56+
let hooks = this.hooks[hook] || {};
57+
58+
return Promise.each(Object.keys(hooks), (serviceName) => {
59+
let fn = hooks[serviceName];
60+
return fn.apply(this.services[serviceName], args);
61+
});
62+
}
63+
64+
_loadProcess(name, Process) {
65+
if (this.process || name !== this.config.get('process')) {
66+
// Process manager has already been loaded, or the name does not
67+
// match the configured process manager
68+
return;
69+
}
70+
71+
if (!(Process.prototype instanceof BaseProcess)) {
72+
throw new Error(`Configured process manager ${name} does not extend BaseProcess!`);
73+
}
74+
75+
let missingFns = hasRequiredFns(Process.prototype);
76+
77+
if (missingFns.length) {
78+
throw new Error(`Configured manager ${name} is missing the following required methods: ${missingFns.join(', ')}`);
79+
}
80+
81+
if (!Process.willRun()) {
82+
this.ui.log(`The '${name}' process manager will not run on this system, defaulting to 'local'`, 'yellow');
83+
this.config.set('process', 'local');
84+
return this._loadProcess('local', localService.class);
85+
}
86+
87+
this.process = this._loadService(name, Process);
88+
}
89+
90+
_loadService(name, ServiceClass) {
91+
if (this.services[name]) {
92+
throw new Error('Service already exists!');
93+
}
94+
95+
let service = new ServiceClass(this);
96+
// Inject name into the service instance so it's accessible by the methods
97+
service.name = name;
98+
service.init();
99+
this.services[name] = service;
100+
101+
return service;
102+
}
103+
104+
static load(config, ui) {
105+
let sm = new ServiceManager(config, ui);
106+
107+
each(ServiceManager.knownServices, (service) => {
108+
let ServiceClass = service.class;
109+
110+
if (!(ServiceClass.prototype instanceof BaseService)) {
111+
throw new Error('Service does not inherit from BaseService');
112+
}
113+
114+
let name = service.name;
115+
116+
if (service.type === 'process') {
117+
sm._loadProcess(name, ServiceClass);
118+
return;
119+
}
120+
121+
sm._loadService(name, ServiceClass);
122+
});
123+
124+
return sm;
125+
}
126+
};
127+
128+
ServiceManager.allowedHooks = ['setup', 'start', 'stop', 'run'];
129+
ServiceManager.knownServices = [
130+
// TODO: we only know about the nginx & built in process manager services
131+
// for now, in the future make this load globally installed services
132+
require('./nginx'),
133+
require('./process/systemd'),
134+
localService
135+
];
136+
137+
module.exports = ServiceManager;

lib/process/index.js lib/services/process/base.js

+8-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
'use strict';
2+
const BaseService = require('../base');
23

3-
class BaseProcess {
4-
constructor(config) {
5-
this.config = config;
6-
}
7-
4+
class BaseProcess extends BaseService {
85
start() {
96
// Base implementation - noop
107
}
@@ -23,19 +20,20 @@ class BaseProcess {
2320
throw error;
2421
}
2522

26-
setup() {
27-
// Base implementation - noop
28-
}
29-
3023
/**
3124
* This function checks if this process manager can be used on this system
3225
*
3326
* @return {Boolean} whether or not the process manager can be used
3427
*/
35-
checkUsability() {
28+
static willRun() {
3629
// Base implementation - return true
3730
return true;
3831
}
3932
};
4033

34+
BaseProcess.requiredMethods = [
35+
'start',
36+
'stop'
37+
];
38+
4139
module.exports = BaseProcess;

lib/process/local.js lib/services/process/local.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict';
2-
const BaseProcess = require('./index');
2+
const BaseProcess = require('./base');
33
const fs = require('fs-extra');
44
const path = require('path');
55
const fkill = require('fkill');
@@ -9,13 +9,13 @@ const assign = require('lodash/assign');
99
const PID_FILE = '.ghostpid';
1010

1111
class LocalProcess extends BaseProcess {
12-
start(cwd, environment) {
12+
start(cwd) {
1313
return new Promise((resolve, reject) => {
1414
let cp = spawn('ghost', ['run'], {
1515
cwd: cwd,
1616
detached: true,
1717
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
18-
env: assign({}, process.env, {NODE_ENV: environment})
18+
env: assign({}, process.env, {NODE_ENV: this.config.environment})
1919
});
2020

2121
fs.writeFileSync(path.join(cwd, PID_FILE), cp.pid);
@@ -73,6 +73,14 @@ class LocalProcess extends BaseProcess {
7373
error(error) {
7474
if (process.send) {process.send({error: true, message: error.message});}
7575
}
76+
77+
static willRun() {
78+
return true;
79+
}
7680
};
7781

78-
module.exports = LocalProcess;
82+
module.exports = {
83+
name: 'local',
84+
type: 'process',
85+
class: LocalProcess
86+
};

0 commit comments

Comments
 (0)