Skip to content

Commit

Permalink
feat(core): Ensure that Ghost was started
Browse files Browse the repository at this point in the history
closes TryGhost#472

- added port polling utility
- general process manager class offers `ensureStarted` function
- systemd extension makes use of `ensureStarted`
  • Loading branch information
kirrg001 committed Feb 4, 2018
1 parent 76829e9 commit 079b2a5
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 6 deletions.
24 changes: 22 additions & 2 deletions extensions/systemd/systemd.js
Expand Up @@ -14,7 +14,18 @@ class SystemdProcessManager extends cli.ProcessManager {
this._precheck();

return this.ui.sudo(`systemctl start ${this.systemdName}`)
.catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
.then(() => {
return this.ensureStarted({
logSuggestion: `journalctl -u ${this.systemdName} -n 50`
});
})
.catch((error) => {
if (error instanceof cli.errors.CliError) {
throw error;
}

throw new cli.errors.ProcessError(error);
});
}

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

return this.ui.sudo(`systemctl restart ${this.systemdName}`)
.catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
.then(() => {
return this.ensureStarted();
})
.catch((error) => {
if (error instanceof cli.errors.CliError) {
throw error;
}

throw new cli.errors.ProcessError(error);
});
}

isEnabled() {
Expand Down
5 changes: 4 additions & 1 deletion extensions/systemd/test/systemd-spec.js
Expand Up @@ -19,7 +19,7 @@ function makeSystemd(options, ui) {
return new LocalSystem(ui, null, instance);
}

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

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

Expand Down Expand Up @@ -68,6 +69,7 @@ describe('Unit: Systemd > Process Manager', function () {
beforeEach(function () {
ui = {sudo: sinon.stub().resolves()},
ext = new Systemd(ui, null, instance);
ext.ensureStarted = sinon.stub().resolves();
ext._precheck = () => true;
});

Expand Down Expand Up @@ -104,6 +106,7 @@ describe('Unit: Systemd > Process Manager', function () {
beforeEach(function () {
ui = {sudo: sinon.stub().resolves()},
ext = new Systemd(ui, null, instance);
ext.ensureStarted = sinon.stub().resolves();
ext._precheck = () => true;
});

Expand Down
12 changes: 10 additions & 2 deletions lib/commands/run.js
@@ -1,5 +1,6 @@
'use strict';
const spawn = require('child_process').spawn;
const merge = require('lodash/merge');
const Command = require('../command');

class RunCommand extends Command {
Expand Down Expand Up @@ -54,7 +55,9 @@ class RunCommand extends Command {
this.sudo = true;
}

useDirect(instance) {
useDirect(instance, options) {
options = merge({delayErrorChaining: true}, options || {});

this.child = spawn(process.execPath, ['current/index.js'], {
cwd: instance.dir,
stdio: [0, 1, 2, 'ipc']
Expand All @@ -71,7 +74,12 @@ class RunCommand extends Command {
return;
}

instance.process.error(message.error);
if (!options.delayErrorChaining) {
instance.process.error(message.error);
} else {
// NOTE: Backwards compatibility of https://github.com/TryGhost/Ghost/pull/9440
setTimeout(() => {instance.process.error(message.error);}, 1000);
}
});
}

Expand Down
30 changes: 30 additions & 0 deletions lib/process-manager.js
@@ -1,5 +1,8 @@
'use strict';

const every = require('lodash/every');
const merge = require('lodash/merge');
const portPolling = require('./utils/port-polling');
const requiredMethods = [
'start',
'stop',
Expand Down Expand Up @@ -60,6 +63,33 @@ class ProcessManager {
// Base Implementation
}

/**
* General implementation of figuring out if the Ghost blog has started successfully.
*
* @returns {Promise<any>}
*/
ensureStarted(options) {
options = merge({
stopOnError: true,
port: this.instance.config.get('server.port')
}, options || {});

return portPolling(options)
.catch((err) => {
if (options.stopOnError) {
return this.stop()
.then(() => {
throw err;
})
.catch(() => {
throw err;
});
}

throw err;
});
}

/**
* This function checks if this process manager can be used on this system
*
Expand Down
99 changes: 99 additions & 0 deletions lib/utils/port-polling.js
@@ -0,0 +1,99 @@
'use strict';

const net = require('net');
const merge = require('lodash/merge');
const errors = require('../errors');

module.exports = function portPolling(options) {
options = merge({
timeoutInMS: 1000,
maxTries: 20,
delayOnConnectInMS: 3 * 1000,
logSuggestion: 'ghost log',
socketTimeoutInMS: 1000 * 30
}, options || {});

if (!options.port) {
return Promise.reject(new errors.CliError({
message: 'Port is required.'
}));
}

const connectToGhostSocket = (() => {
return new Promise((resolve, reject) => {
const ghostSocket = net.connect(options.port);

// inactivity timeout
ghostSocket.setTimeout(options.socketTimeoutInMS);
ghostSocket.on('timeout', (() => {
ghostSocket.destroy();

// force retry
const err = new Error();
err.retry = true;
reject(err);
}));

ghostSocket.on('connect', (() => {
if (options.delayOnConnectInMS) {
let ghostDied = false;

// CASE: client closes socket
ghostSocket.on('close', (() => {
ghostDied = true;
}));

setTimeout(() => {
ghostSocket.destroy();

if (ghostDied) {
reject(new Error('Ghost died.'));
} else {
resolve();
}
}, options.delayOnConnectInMS);

return;
}

ghostSocket.destroy();
resolve();
}));

ghostSocket.on('error', ((err) => {
ghostSocket.destroy();

err.retry = true;
reject(err);
}));
});
});

const startPolling = (() => {
return new Promise((resolve, reject) => {
let tries = 0;

(function retry() {
connectToGhostSocket()
.then(() => {
resolve();
})
.catch((err) => {
if (err.retry && tries < options.maxTries) {
tries = tries + 1;
setTimeout(retry, options.timeoutInMS);
return;
}

reject(new errors.GhostError({
message: 'Ghost did not start.',
suggestion: options.logSuggestion,
err: err
}));
});
}());
});
});

return startPolling();
};
2 changes: 1 addition & 1 deletion test/unit/commands/run-spec.js
Expand Up @@ -166,7 +166,7 @@ describe('Unit: Commands > Run', function () {
const errorStub = sinon.stub();
const exitStub = sinon.stub(process, 'exit');

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

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

0 comments on commit 079b2a5

Please sign in to comment.