Skip to content

Commit 06bc19c

Browse files
authored
Added handling for knex-migrator being executed in Ghost 2.0 (#765)
refs #759 - We have moved the execution of knex-migrator into Ghost 2.0.0 - This commit will ensure we skip the db migration when you: - migrate from ^1 to ^2 - you update from ^2 to ^2 - when you install ^2 - Added net socket server for Ghost 2.0 (alternative to simple port polling) - way better error handling between Ghost and the CLI - Ghost 2.0 executes knex-migrator - it will turn maintenance on if migrations need to be executed - the handling of receiving success or failure state requires a better communication between the CLI and Ghost, because the blog stays in maintenance mode and runs the migrations in background - Ghost will tell the CLI when it's ready by using an extension: write a socket url into the config and send the success/failure state - this is much better than using the http socket to communicate, because - A) port polling connects to the http port, it's impossible to send simple messages over this transport layer - B) the code is much simpler, CLI opens a socket port and Ghost pushes a notification if the notification is available - C) we receive any error from Ghost - even if the http server wasn't started yet - we don't communicate with Ghost, Ghost communicates with the CLI - port polling for v1 blogs is untouched, still works as expected - coverage has decreased a very little 0,2% - will try to add more tests when we merge the 1.9 branch into master
1 parent c9cf6d3 commit 06bc19c

File tree

10 files changed

+676
-171
lines changed

10 files changed

+676
-171
lines changed

extensions/systemd/systemd.js

+35-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,25 @@ class SystemdProcessManager extends cli.ProcessManager {
1818
start() {
1919
this._precheck();
2020

21-
return this.ui.sudo(`systemctl start ${this.systemdName}`)
21+
const portfinder = require('portfinder');
22+
const socketAddress = {
23+
port: null,
24+
host: 'localhost'
25+
};
26+
27+
return portfinder.getPortPromise()
28+
.then((port) => {
29+
socketAddress.port = port;
30+
this.instance.config.set('bootstrap-socket', socketAddress);
31+
return this.instance.config.save();
32+
})
33+
.then(() => {
34+
return this.ui.sudo(`systemctl start ${this.systemdName}`)
35+
})
2236
.then(() => {
2337
return this.ensureStarted({
24-
logSuggestion: this.logSuggestion
38+
logSuggestion: this.logSuggestion,
39+
socketAddress
2540
});
2641
})
2742
.catch((error) => {
@@ -43,10 +58,25 @@ class SystemdProcessManager extends cli.ProcessManager {
4358
restart() {
4459
this._precheck();
4560

46-
return this.ui.sudo(`systemctl restart ${this.systemdName}`)
61+
const portfinder = require('portfinder');
62+
const socketAddress = {
63+
port: null,
64+
host: 'localhost'
65+
};
66+
67+
return portfinder.getPortPromise()
68+
.then((port) => {
69+
socketAddress.port = port;
70+
this.instance.config.set('bootstrap-socket', socketAddress);
71+
return this.instance.config.save();
72+
})
73+
.then(() => {
74+
return this.ui.sudo(`systemctl restart ${this.systemdName}`)
75+
})
4776
.then(() => {
4877
return this.ensureStarted({
49-
logSuggestion: this.logSuggestion
78+
logSuggestion: this.logSuggestion,
79+
socketAddress
5080
});
5181
})
5282
.catch((error) => {
@@ -91,6 +121,7 @@ class SystemdProcessManager extends cli.ProcessManager {
91121
if (!error.message.match(/inactive|activating/)) {
92122
return Promise.reject(new cli.errors.ProcessError(error));
93123
}
124+
94125
return Promise.resolve(false);
95126
});
96127
}

extensions/systemd/test/systemd-spec.js

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const proxyquire = require('proxyquire').noCallThru();
55

66
const modulePath = '../systemd';
77
const errors = require('../../../lib/errors');
8+
const configStub = require('../../../test/utils/config-stub');
89
const Systemd = require(modulePath);
910

1011
const instance = {
@@ -30,6 +31,7 @@ describe('Unit: Systemd > Process Manager', function () {
3031
let ext, ui;
3132

3233
beforeEach(function () {
34+
instance.config = configStub();
3335
ui = {sudo: sinon.stub().resolves()},
3436
ext = new Systemd(ui, null, instance);
3537
ext.ensureStarted = sinon.stub().resolves();

lib/commands/setup.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class SetupCommand extends Command {
4141
const os = require('os');
4242
const url = require('url');
4343
const path = require('path');
44+
const semver = require('semver');
4445

4546
const linux = require('../tasks/linux');
4647
const migrate = require('../tasks/migrate');
@@ -171,10 +172,20 @@ class SetupCommand extends Command {
171172
}));
172173

173174
if (argv.migrate !== false) {
175+
const instance = this.system.getInstance();
176+
174177
// Tack on db migration task to the end
175178
tasks.push({
176179
title: 'Running database migrations',
177-
task: migrate
180+
task: migrate,
181+
// CASE: We are about to install Ghost 2.0. We moved the execution of knex-migrator into Ghost.
182+
enabled: () => {
183+
if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0')) {
184+
return false;
185+
}
186+
187+
return true;
188+
}
178189
});
179190
}
180191

lib/commands/update.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,17 @@ class UpdateCommand extends Command {
7373
}, {
7474
title: 'Running database migrations',
7575
skip: (ctx) => ctx.rollback,
76-
task: migrate
76+
task: migrate,
77+
// CASE: We have moved the execution of knex-migrator into Ghost 2.0.0.
78+
// If you are already on ^2 or you update from ^1 to ^2, then skip the task.
79+
enabled: () => {
80+
if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0') ||
81+
semver.satisfies(context.version, '^2.0.0')) {
82+
return false;
83+
}
84+
85+
return true;
86+
}
7787
}, {
7888
title: 'Restarting Ghost',
7989
skip: () => !argv.restart,

lib/process-manager.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ class ProcessManager {
7070
*/
7171
ensureStarted(options) {
7272
const portPolling = require('./utils/port-polling');
73+
const semver = require('semver');
7374

7475
options = Object.assign({
7576
stopOnError: true,
7677
port: this.instance.config.get('server.port'),
77-
host: this.instance.config.get('server.host', 'localhost')
78+
host: this.instance.config.get('server.host', 'localhost'),
79+
useNetServer: semver.satisfies(this.instance.cliConfig.get('active-version'), '^2.0.0')
7880
}, options || {});
7981

8082
return portPolling(options).catch((err) => {

lib/utils/port-polling.js

+94-13
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,79 @@
11
'use strict';
22

3-
const net = require('net');
43
const errors = require('../errors');
54

6-
module.exports = function portPolling(options) {
7-
options = Object.assign({
8-
timeoutInMS: 2000,
9-
maxTries: 20,
10-
delayOnConnectInMS: 3 * 2000,
11-
logSuggestion: 'ghost log',
12-
socketTimeoutInMS: 1000 * 60
13-
}, options || {});
5+
/**
6+
* @TODO: in theory it could happen that other clients connect, but tbh even with the port polling it was possible: you
7+
* could just start a server on the Ghost port
8+
*/
9+
const useNetServer = (options)=> {
10+
return new Promise((resolve, reject)=> {
11+
const net = require('net');
12+
let waitTimeout = null;
13+
let ghostSocket = null;
14+
15+
const server = net.createServer((socket)=> {
16+
ghostSocket = socket;
17+
18+
socket.on('data', (data) => {
19+
let message;
20+
21+
try {
22+
message = JSON.parse(data);
23+
} catch (err) {
24+
message = {started: false, error: err};
25+
}
26+
27+
if (waitTimeout) {
28+
clearTimeout(waitTimeout);
29+
}
30+
31+
socket.destroy();
32+
ghostSocket = null;
33+
34+
server.close(() => {
35+
if (message.started) {
36+
resolve();
37+
} else {
38+
reject(new errors.GhostError({
39+
message: message.error.message,
40+
help: message.error.help,
41+
suggestion: options.logSuggestion
42+
}));
43+
}
44+
});
45+
});
46+
});
47+
48+
waitTimeout = setTimeout(() => {
49+
if (ghostSocket) {
50+
ghostSocket.destroy();
51+
}
52+
53+
ghostSocket = null;
54+
55+
server.close(() => {
56+
reject(new errors.GhostError({
57+
message: 'Could not communicate with Ghost',
58+
suggestion: options.logSuggestion
59+
}));
60+
});
61+
}, options.netServerTimeoutInMS);
62+
63+
server.listen({host: options.socketAddress.host, port: options.socketAddress.port});
64+
});
65+
};
66+
67+
const usePortPolling = (options)=> {
68+
const net = require('net');
1469

1570
if (!options.port) {
1671
return Promise.reject(new errors.CliError({
1772
message: 'Port is required.'
1873
}));
1974
}
2075

21-
const connectToGhostSocket = (() => {
76+
const connectToGhostSocket = () => {
2277
return new Promise((resolve, reject) => {
2378
// if host is specified and is *not* 0.0.0.0 (listen on all ips), use the custom host
2479
const host = options.host && options.host !== '0.0.0.0' ? options.host : 'localhost';
@@ -35,7 +90,7 @@ module.exports = function portPolling(options) {
3590
ghostSocket.destroy();
3691

3792
// force retry
38-
const err = new Error();
93+
const err = new Error('Socket timed out.');
3994
err.retry = true;
4095
reject(err);
4196
}));
@@ -73,7 +128,7 @@ module.exports = function portPolling(options) {
73128
reject(err);
74129
}));
75130
});
76-
});
131+
};
77132

78133
const startPolling = (() => {
79134
return new Promise((resolve, reject) => {
@@ -87,10 +142,14 @@ module.exports = function portPolling(options) {
87142
.catch((err) => {
88143
if (err.retry && tries < options.maxTries) {
89144
tries = tries + 1;
90-
setTimeout(retry, options.timeoutInMS);
145+
setTimeout(retry, options.retryTimeoutInMS);
91146
return;
92147
}
93148

149+
if (err instanceof errors.CliError) {
150+
return reject(err);
151+
}
152+
94153
reject(new errors.GhostError({
95154
message: 'Ghost did not start.',
96155
suggestion: options.logSuggestion,
@@ -103,3 +162,25 @@ module.exports = function portPolling(options) {
103162

104163
return startPolling();
105164
};
165+
166+
module.exports = function portPolling(options) {
167+
options = Object.assign({
168+
retryTimeoutInMS: 2000,
169+
maxTries: 20,
170+
delayOnConnectInMS: 3 * 2000,
171+
logSuggestion: 'ghost log',
172+
socketTimeoutInMS: 1000 * 60,
173+
useNetServer: false,
174+
netServerTimeoutInMS: 5 * 60 * 1000,
175+
socketAddress: {
176+
port: 1212,
177+
host: 'localhost'
178+
}
179+
}, options || {});
180+
181+
if (options.useNetServer) {
182+
return useNetServer(options);
183+
}
184+
185+
return usePortPolling(options);
186+
};

0 commit comments

Comments
 (0)