Skip to content

Commit

Permalink
feat(process): ensure that Ghost is started (#612)
Browse files Browse the repository at this point in the history
closes #472
- added port polling utility
- general process manager class offers `ensureStarted` function
- systemd extension makes use of `ensureStarted`
  • Loading branch information
kirrg001 authored and acburdine committed Feb 4, 2018
1 parent 0ca80a7 commit 8c68889
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 34 deletions.
30 changes: 28 additions & 2 deletions extensions/systemd/systemd.js
Expand Up @@ -10,11 +10,26 @@ class SystemdProcessManager extends cli.ProcessManager {
return `ghost_${this.instance.name}`;
}

get logSuggestion() {
return `journalctl -u ${this.systemdName} -n 50`;
}

start() {
this._precheck();

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

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

stop() {
Expand All @@ -28,7 +43,18 @@ 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({
logSuggestion: this.logSuggestion
});
})
.catch((error) => {
if (error instanceof cli.errors.CliError) {
throw error;
}

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

isEnabled() {
Expand Down
3 changes: 3 additions & 0 deletions extensions/systemd/test/systemd-spec.js
Expand Up @@ -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
3 changes: 2 additions & 1 deletion lib/commands/run.js
Expand Up @@ -71,7 +71,8 @@ class RunCommand extends Command {
return;
}

instance.process.error(message.error);
// 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,4 +1,5 @@
'use strict';

const every = require('lodash/every');
const requiredMethods = [
'start',
Expand Down Expand Up @@ -60,6 +61,35 @@ class ProcessManager {
// Base Implementation
}

/**
* General implementation of figuring out if the Ghost blog has started successfully.
*
* @returns {Promise<any>}
*/
ensureStarted(options) {
const portPolling = require('./utils/port-polling');

options = Object.assign({
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
103 changes: 103 additions & 0 deletions lib/utils/port-polling.js
@@ -0,0 +1,103 @@
'use strict';

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

module.exports = function portPolling(options) {
options = Object.assign({
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);
let delayOnConnectTimeout;

// inactivity timeout
ghostSocket.setTimeout(options.socketTimeoutInMS);
ghostSocket.on('timeout', (() => {
if (delayOnConnectTimeout) {
clearTimeout(delayOnConnectTimeout);
}

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;
}));

delayOnConnectTimeout = 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();
};

0 comments on commit 8c68889

Please sign in to comment.