Skip to content

Commit 079b2a5

Browse files
committed
feat(core): Ensure that Ghost was started
closes TryGhost#472 - added port polling utility - general process manager class offers `ensureStarted` function - systemd extension makes use of `ensureStarted`
1 parent 76829e9 commit 079b2a5

File tree

7 files changed

+327
-6
lines changed

7 files changed

+327
-6
lines changed

extensions/systemd/systemd.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,18 @@ class SystemdProcessManager extends cli.ProcessManager {
1414
this._precheck();
1515

1616
return this.ui.sudo(`systemctl start ${this.systemdName}`)
17-
.catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
17+
.then(() => {
18+
return this.ensureStarted({
19+
logSuggestion: `journalctl -u ${this.systemdName} -n 50`
20+
});
21+
})
22+
.catch((error) => {
23+
if (error instanceof cli.errors.CliError) {
24+
throw error;
25+
}
26+
27+
throw new cli.errors.ProcessError(error);
28+
});
1829
}
1930

2031
stop() {
@@ -28,7 +39,16 @@ class SystemdProcessManager extends cli.ProcessManager {
2839
this._precheck();
2940

3041
return this.ui.sudo(`systemctl restart ${this.systemdName}`)
31-
.catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
42+
.then(() => {
43+
return this.ensureStarted();
44+
})
45+
.catch((error) => {
46+
if (error instanceof cli.errors.CliError) {
47+
throw error;
48+
}
49+
50+
throw new cli.errors.ProcessError(error);
51+
});
3252
}
3353

3454
isEnabled() {

extensions/systemd/test/systemd-spec.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function makeSystemd(options, ui) {
1919
return new LocalSystem(ui, null, instance);
2020
}
2121

22-
describe('Unit: Systemd > Process Manager', function () {
22+
describe.only('Unit: Systemd > Process Manager', function () {
2323
it('Returns proper systemd name', function () {
2424
const ext = new Systemd(null, null, instance);
2525

@@ -32,6 +32,7 @@ describe('Unit: Systemd > Process Manager', function () {
3232
beforeEach(function () {
3333
ui = {sudo: sinon.stub().resolves()},
3434
ext = new Systemd(ui, null, instance);
35+
ext.ensureStarted = sinon.stub().resolves();
3536
ext._precheck = () => true;
3637
});
3738

@@ -68,6 +69,7 @@ describe('Unit: Systemd > Process Manager', function () {
6869
beforeEach(function () {
6970
ui = {sudo: sinon.stub().resolves()},
7071
ext = new Systemd(ui, null, instance);
72+
ext.ensureStarted = sinon.stub().resolves();
7173
ext._precheck = () => true;
7274
});
7375

@@ -104,6 +106,7 @@ describe('Unit: Systemd > Process Manager', function () {
104106
beforeEach(function () {
105107
ui = {sudo: sinon.stub().resolves()},
106108
ext = new Systemd(ui, null, instance);
109+
ext.ensureStarted = sinon.stub().resolves();
107110
ext._precheck = () => true;
108111
});
109112

lib/commands/run.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22
const spawn = require('child_process').spawn;
3+
const merge = require('lodash/merge');
34
const Command = require('../command');
45

56
class RunCommand extends Command {
@@ -54,7 +55,9 @@ class RunCommand extends Command {
5455
this.sudo = true;
5556
}
5657

57-
useDirect(instance) {
58+
useDirect(instance, options) {
59+
options = merge({delayErrorChaining: true}, options || {});
60+
5861
this.child = spawn(process.execPath, ['current/index.js'], {
5962
cwd: instance.dir,
6063
stdio: [0, 1, 2, 'ipc']
@@ -71,7 +74,12 @@ class RunCommand extends Command {
7174
return;
7275
}
7376

74-
instance.process.error(message.error);
77+
if (!options.delayErrorChaining) {
78+
instance.process.error(message.error);
79+
} else {
80+
// NOTE: Backwards compatibility of https://github.com/TryGhost/Ghost/pull/9440
81+
setTimeout(() => {instance.process.error(message.error);}, 1000);
82+
}
7583
});
7684
}
7785

lib/process-manager.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use strict';
2+
23
const every = require('lodash/every');
4+
const merge = require('lodash/merge');
5+
const portPolling = require('./utils/port-polling');
36
const requiredMethods = [
47
'start',
58
'stop',
@@ -60,6 +63,33 @@ class ProcessManager {
6063
// Base Implementation
6164
}
6265

66+
/**
67+
* General implementation of figuring out if the Ghost blog has started successfully.
68+
*
69+
* @returns {Promise<any>}
70+
*/
71+
ensureStarted(options) {
72+
options = merge({
73+
stopOnError: true,
74+
port: this.instance.config.get('server.port')
75+
}, options || {});
76+
77+
return portPolling(options)
78+
.catch((err) => {
79+
if (options.stopOnError) {
80+
return this.stop()
81+
.then(() => {
82+
throw err;
83+
})
84+
.catch(() => {
85+
throw err;
86+
});
87+
}
88+
89+
throw err;
90+
});
91+
}
92+
6393
/**
6494
* This function checks if this process manager can be used on this system
6595
*

lib/utils/port-polling.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
const net = require('net');
4+
const merge = require('lodash/merge');
5+
const errors = require('../errors');
6+
7+
module.exports = function portPolling(options) {
8+
options = merge({
9+
timeoutInMS: 1000,
10+
maxTries: 20,
11+
delayOnConnectInMS: 3 * 1000,
12+
logSuggestion: 'ghost log',
13+
socketTimeoutInMS: 1000 * 30
14+
}, options || {});
15+
16+
if (!options.port) {
17+
return Promise.reject(new errors.CliError({
18+
message: 'Port is required.'
19+
}));
20+
}
21+
22+
const connectToGhostSocket = (() => {
23+
return new Promise((resolve, reject) => {
24+
const ghostSocket = net.connect(options.port);
25+
26+
// inactivity timeout
27+
ghostSocket.setTimeout(options.socketTimeoutInMS);
28+
ghostSocket.on('timeout', (() => {
29+
ghostSocket.destroy();
30+
31+
// force retry
32+
const err = new Error();
33+
err.retry = true;
34+
reject(err);
35+
}));
36+
37+
ghostSocket.on('connect', (() => {
38+
if (options.delayOnConnectInMS) {
39+
let ghostDied = false;
40+
41+
// CASE: client closes socket
42+
ghostSocket.on('close', (() => {
43+
ghostDied = true;
44+
}));
45+
46+
setTimeout(() => {
47+
ghostSocket.destroy();
48+
49+
if (ghostDied) {
50+
reject(new Error('Ghost died.'));
51+
} else {
52+
resolve();
53+
}
54+
}, options.delayOnConnectInMS);
55+
56+
return;
57+
}
58+
59+
ghostSocket.destroy();
60+
resolve();
61+
}));
62+
63+
ghostSocket.on('error', ((err) => {
64+
ghostSocket.destroy();
65+
66+
err.retry = true;
67+
reject(err);
68+
}));
69+
});
70+
});
71+
72+
const startPolling = (() => {
73+
return new Promise((resolve, reject) => {
74+
let tries = 0;
75+
76+
(function retry() {
77+
connectToGhostSocket()
78+
.then(() => {
79+
resolve();
80+
})
81+
.catch((err) => {
82+
if (err.retry && tries < options.maxTries) {
83+
tries = tries + 1;
84+
setTimeout(retry, options.timeoutInMS);
85+
return;
86+
}
87+
88+
reject(new errors.GhostError({
89+
message: 'Ghost did not start.',
90+
suggestion: options.logSuggestion,
91+
err: err
92+
}));
93+
});
94+
}());
95+
});
96+
});
97+
98+
return startPolling();
99+
};

test/unit/commands/run-spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe('Unit: Commands > Run', function () {
166166
const errorStub = sinon.stub();
167167
const exitStub = sinon.stub(process, 'exit');
168168

169-
instance.useDirect({dir: '/var/www/ghost', process: {success: successStub, error: errorStub}});
169+
instance.useDirect({dir: '/var/www/ghost', process: {success: successStub, error: errorStub}}, {delayErrorChaining: false});
170170

171171
expect(spawnStub.calledOnce).to.be.true;
172172
expect(spawnStub.calledWithExactly(process.execPath, ['current/index.js'], {

0 commit comments

Comments
 (0)