Skip to content

Commit 398fc27

Browse files
committed
fix(ui): improve sudo output suppression
no issue - improve ui flow by allowing spinner to run during sudo command - ui listens on stderr and prompts for password if sudo does
1 parent 7335a46 commit 398fc27

File tree

4 files changed

+69
-31
lines changed

4 files changed

+69
-31
lines changed

extensions/nginx/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ class NginxExtension extends cli.Extension {
152152
title: 'Generating Encryption Key (may take a few minutes)',
153153
skip: (ctx) => ctx.dnsfail || fs.existsSync(dhparamFile),
154154
task: () => {
155-
return this.ui.sudo(`openssl dhparam -out ${dhparamFile} 2048 > /dev/null`)
155+
return this.ui.sudo(`openssl dhparam -out ${dhparamFile} 2048`)
156156
.catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
157157
}
158158
}, {

lib/tasks/migrate.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ module.exports = function runMigrations(context) {
2525
// we should run sudo, otherwise run normally
2626
if (shouldUseGhostUser(contentDir)) {
2727
const knexMigratorPath = path.resolve(__dirname, '../../node_modules/.bin/knex-migrator-migrate');
28-
knexMigratorPromise = context.ui.sudo(`-E -u ghost ${knexMigratorPath} ${args.join(' ')}`, {
29-
stdio: ['inherit', 'inherit', 'pipe']
30-
});
28+
knexMigratorPromise = context.ui.sudo(`${knexMigratorPath} ${args.join(' ')}`, {sudoArgs: '-E -u ghost'});
3129
} else {
3230
knexMigratorPromise = execa('knex-migrator-migrate', args, {
3331
preferLocal: true,

lib/ui/index.js

+25-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
'use strict';
22
const ora = require('ora');
3+
const omit = require('lodash/omit');
34
const chalk = require('chalk');
45
const execa = require('execa');
56
const Listr = require('listr');
67
const Table = require('cli-table2');
78
const Promise = require('bluebird');
89
const inquirer = require('inquirer');
10+
const isObject = require('lodash/isObject');
911
const stripAnsi = require('strip-ansi');
1012
const logSymbols = require('log-symbols');
1113
const isFunction = require('lodash/isFunction');
12-
const isObject = require('lodash/isObject');
1314

1415
const errors = require('../errors');
1516
const CLIRenderer = require('./renderer');
@@ -166,7 +167,9 @@ class UI {
166167

167168
/**
168169
* Runs a sudo command on the system. Outputs the command
169-
* that it's running
170+
* that it's running.
171+
*
172+
* Code inspired by https://github.com/calmh/node-sudo/blob/master/lib/sudo.js
170173
*
171174
* @param {string} command Command to run
172175
* @param {Object} options Options to pass to execa
@@ -176,18 +179,29 @@ class UI {
176179
* @public
177180
*/
178181
sudo(command, options) {
182+
options = options || {};
179183
this.log(`Running sudo command: ${command}`, 'gray');
180184

181-
return this.noSpin(() => {
182-
const execaOptions = Object.assign({
183-
stdio: 'inherit'
184-
}, options || {});
185-
186-
return execa.shell(
187-
`sudo ${command.replace(/^ghost/, process.argv.slice(0, 2).join(' '))}`,
188-
execaOptions
189-
);
185+
const prompt = '#node-sudo-passwd#';
186+
const cmd = command.replace(/^ghost/, process.argv.slice(0, 2).join(' '));
187+
const cp = execa.shell(`sudo -S -p '${prompt}' ${options.sudoArgs || ''} ${cmd}`, omit(options, ['sudoArgs']));
188+
189+
cp.stderr.on('data', (data) => {
190+
const lines = data.toString('utf8').split('\n');
191+
192+
// If sudo has prompted for a password, then it will be the last line output
193+
if (lines[lines.length - 1] === prompt) {
194+
this.prompt({
195+
type: 'password',
196+
name: 'password',
197+
message: 'Password'
198+
}).then((answers) => {
199+
cp.stdin.write(`${answers.password}\n`);
200+
});
201+
}
190202
});
203+
204+
return cp;
191205
}
192206

193207
/**

test/unit/ui/index-spec.js

+42-16
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ const sinon = require('sinon');
77
const proxyquire = require('proxyquire').noCallThru();
88
const logSymbols = require('log-symbols');
99
const streamTestUtils = require('../../utils/stream');
10-
const modulePath = '../../../lib/ui'
11-
const UI = require(modulePath);
10+
const EventEmitter = require('events');
11+
12+
const modulePath = '../../../lib/ui';
1213

1314
describe('Unit: UI', function () {
1415
it('can be created successfully', function () {
16+
const UI = require(modulePath);
1517
const ui = new UI();
1618

1719
expect(ui).to.be.ok;
@@ -21,6 +23,7 @@ describe('Unit: UI', function () {
2123
let ui;
2224

2325
before(function () {
26+
const UI = require(modulePath);
2427
ui = new UI();
2528
});
2629

@@ -69,6 +72,7 @@ describe('Unit: UI', function () {
6972
});
7073

7174
it('#table creates a pretty looking table', function (done) {
75+
const UI = require(modulePath);
7276
const ui = new UI();
7377
const ctx = {log: sinon.stub()};
7478
const expectTable = [
@@ -98,6 +102,7 @@ describe('Unit: UI', function () {
98102
let ui;
99103

100104
before(function () {
105+
const UI = require(modulePath);
101106
ui = new UI();
102107
});
103108

@@ -143,6 +148,7 @@ describe('Unit: UI', function () {
143148
});
144149

145150
it('#confirm calls prompt', function (done) {
151+
const UI = require(modulePath);
146152
const ui = new UI();
147153
const ctx = {prompt: sinon.stub()};
148154
const testA = {
@@ -204,6 +210,7 @@ describe('Unit: UI', function () {
204210

205211
// @todo: Is this an acceptable way to test
206212
it('passes context through', function () {
213+
const UI = require(modulePath);
207214
const ui = new UI();
208215
const context = {
209216
write: 'tests',
@@ -222,6 +229,7 @@ describe('Unit: UI', function () {
222229
});
223230

224231
it('ignores context if requested', function () {
232+
const UI = require(modulePath);
225233
const ui = new UI();
226234
const tasks = [{
227235
title: 'test',
@@ -235,29 +243,37 @@ describe('Unit: UI', function () {
235243
});
236244

237245
it('#sudo runs a sudo command', function (done) {
238-
const execa = require('execa');
239-
const ctx = {
240-
log: sinon.stub(),
241-
noSpin: sinon.stub().callsFake((run) => run())
242-
}
243-
const UI = proxyquire(modulePath, {execa: execa});
246+
const shellStub = sinon.stub();
247+
const UI = proxyquire(modulePath, {execa: {shell: shellStub}});
244248
const ui = new UI();
245-
const eCall = new RegExp(`sudo ${process.argv.slice(0, 2).join(' ')} restart`);
246-
sinon.stub(execa,'shell');
247249

248-
ui.sudo.bind(ctx)('ghost restart');
250+
const logStub = sinon.stub(ui, 'log');
251+
const promptStub = sinon.stub(ui, 'prompt').resolves({password: 'password'});
252+
const stderr = new EventEmitter();
249253

250-
expect(ctx.log.calledOnce).to.be.true;
251-
expect(execa.shell.calledOnce).to.be.true;
252-
expect(execa.shell.firstCall.args[0]).to.match(eCall);
253-
expect(execa.shell.firstCall.args[1].stdio).to.equal('inherit');
254-
done();
254+
const eCall = new RegExp(`sudo -S -p '#node-sudo-passwd#' -E -u ghost ${process.argv.slice(0, 2).join(' ')} -v`);
255+
256+
const stdin = streamTestUtils.getWritableStream((output) => {
257+
expect(output).to.equal('password\n');
258+
expect(logStub.calledOnce).to.be.true;
259+
expect(logStub.calledWithExactly('Running sudo command: ghost -v', 'gray')).to.be.true;
260+
expect(shellStub.calledOnce).to.be.true;
261+
expect(shellStub.args[0][0]).to.match(eCall);
262+
expect(shellStub.args[0][1]).to.deep.equal({cwd: '/var/foo'});
263+
expect(promptStub.calledOnce).to.be.true;
264+
done();
265+
});
266+
shellStub.returns({stdin: stdin, stderr: stderr});
267+
268+
ui.sudo('ghost -v', {cwd: '/var/foo', sudoArgs: ['-E -u ghost']});
269+
stderr.emit('data', '#node-sudo-passwd#');
255270
});
256271

257272
describe('#noSpin', function () {
258273
let ui;
259274

260275
before(function () {
276+
const UI = require(modulePath);
261277
ui = new UI();
262278
});
263279

@@ -300,6 +316,7 @@ describe('Unit: UI', function () {
300316
});
301317
stdout.on('error', done);
302318

319+
const UI = require(modulePath);
303320
const ui = new UI({stdout: stdout});
304321
ui.log('test');
305322
});
@@ -314,6 +331,7 @@ describe('Unit: UI', function () {
314331
});
315332
stdout.on('error', done);
316333

334+
const UI = require(modulePath);
317335
const ui = new UI({stdout: stdout});
318336
ui.log('test', 'green');
319337
});
@@ -323,6 +341,7 @@ describe('Unit: UI', function () {
323341
stdout: {write: sinon.stub()},
324342
stderr: {write: sinon.stub()}
325343
};
344+
const UI = require(modulePath);
326345
const ui = new UI();
327346

328347
ui.log.bind(ctx)('Error', null, true);
@@ -337,6 +356,7 @@ describe('Unit: UI', function () {
337356
});
338357

339358
it('resets spinner', function (done) {
359+
const UI = require(modulePath);
340360
const ui = new UI();
341361
const write = sinon.stub()
342362
const ctx = {
@@ -363,6 +383,7 @@ describe('Unit: UI', function () {
363383

364384
describe('#logVerbose', function () {
365385
it('passes through options to log method when verbose is set', function () {
386+
const UI = require(modulePath);
366387
const ui = new UI({verbose: true});
367388
const logStub = sinon.stub(ui, 'log');
368389

@@ -372,6 +393,7 @@ describe('Unit: UI', function () {
372393
});
373394

374395
it('does not call log when verbose is false', function () {
396+
const UI = require(modulePath);
375397
const ui = new UI({verbose: false});
376398
const logStub = sinon.stub(ui, 'log');
377399

@@ -389,6 +411,7 @@ describe('Unit: UI', function () {
389411
});
390412
stdout.on('error', done);
391413

414+
const UI = require(modulePath);
392415
const ui = new UI({stdout: stdout});
393416
ui.success('test');
394417
});
@@ -402,6 +425,7 @@ describe('Unit: UI', function () {
402425
});
403426
stdout.on('error', done);
404427

428+
const UI = require(modulePath);
405429
const ui = new UI({stdout: stdout});
406430
ui.fail('test');
407431
});
@@ -411,6 +435,7 @@ describe('Unit: UI', function () {
411435
let ui;
412436

413437
before(function () {
438+
const UI = require(modulePath);
414439
ui = new UI();
415440
});
416441

@@ -582,6 +607,7 @@ describe('Unit: UI', function () {
582607
environment: 'Earth'
583608
};
584609
const SPACES = ' ';
610+
const UI = require(modulePath);
585611
const ui = new UI();
586612
const expected = ['Debug Information:',
587613
`${SPACES}Node Version: ${process.version}`,

0 commit comments

Comments
 (0)