Add a REPL mode to the Kibana server (#17638)

Add a repl option to the Kibana server. This will run Kibana in dev mode with a repl that has access to the server object.
This commit is contained in:
Chris Davies 2018-05-10 16:33:04 -04:00 committed by GitHub
parent 45002180a7
commit fc5e8d81fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 354 additions and 0 deletions

View file

@ -20,6 +20,7 @@ export default class ClusterManager {
constructor(opts, config) {
this.log = new Log(opts.quiet, opts.silent);
this.addedCount = 0;
this.inReplMode = !!opts.repl;
const serverArgv = [];
const optimizerArgv = [
@ -144,6 +145,12 @@ export default class ClusterManager {
}
setupManualRestart() {
// If we're in REPL mode, the user can use the REPL to manually restart.
// The setupManualRestart method interferes with stdin/stdout, in a way
// that negatively affects the REPL.
if (this.inReplMode) {
return;
}
const readline = require('readline');
const rl = readline.createInterface(process.stdin, process.stdout);

View file

@ -0,0 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`repl it colorizes raw values 1`] = `"{ meaning: 42 }"`;
exports[`repl it handles deep and recursive objects 1`] = `
"{ '0':
{ '1':
{ '2':
{ '3':
{ '4':
{ '5':
{ '6':
{ '7':
{ '8':
{ '9':
{ '10': { '11': { '12': { '13': { '14': { '15': [Object] } } } } } } } } } } } } } } },
whoops: [Circular] }"
`;
exports[`repl it handles undefined 1`] = `"undefined"`;
exports[`repl it prints promise rejects 1`] = `
Array [
Array [
"Waiting for promise...",
],
Array [
"Promise Rejected:
",
"'Dang, diggity!'",
],
]
`;
exports[`repl it prints promise resolves 1`] = `
Array [
Array [
"Waiting for promise...",
],
Array [
"Promise Resolved:
",
"[ 1, 2, 3 ]",
],
]
`;
exports[`repl promises rejects only write to a specific depth 1`] = `
Array [
Array [
"Waiting for promise...",
],
Array [
"Promise Rejected:
",
"{ '0':
{ '1':
{ '2':
{ '3':
{ '4':
{ '5':
{ '6':
{ '7':
{ '8':
{ '9':
{ '10': { '11': { '12': { '13': { '14': { '15': [Object] } } } } } } } } } } } } } } },
whoops: [Circular] }",
],
]
`;
exports[`repl promises resolves only write to a specific depth 1`] = `
Array [
Array [
"Waiting for promise...",
],
Array [
"Promise Resolved:
",
"{ '0':
{ '1':
{ '2':
{ '3':
{ '4':
{ '5':
{ '6':
{ '7':
{ '8':
{ '9':
{ '10': { '11': { '12': { '13': { '14': { '15': [Object] } } } } } } } } } } } } } } },
whoops: [Circular] }",
],
]
`;
exports[`repl repl exposes a print object that lets you tailor depth 1`] = `
Array [
Array [
"{ hello: { world: [Object] } }",
],
]
`;
exports[`repl repl exposes a print object that prints promises 1`] = `
Array [
Array [
"",
],
Array [
"Waiting for promise...",
],
Array [
"Promise Resolved:
",
"{ hello: { world: [Object] } }",
],
]
`;

56
src/cli/repl/index.js Normal file
View file

@ -0,0 +1,56 @@
import repl from 'repl';
import util from 'util';
/**
* Starts an interactive REPL with a global `server` object.
*
* @param {KibanaServer} kbnServer
*/
export function startRepl(kbnServer) {
const replServer = repl.start({
prompt: 'Kibana> ',
useColors: true,
writer: promiseFriendlyWriter(() => replServer.displayPrompt()),
});
replServer.context.kbnServer = kbnServer;
replServer.context.server = kbnServer.server;
replServer.context.repl = {
print(obj, depth = null) {
console.log(promisePrint(obj, () => replServer.displayPrompt(), depth));
return '';
},
};
return replServer;
}
function colorize(o, depth) {
return util.inspect(o, { colors: true, depth });
}
function prettyPrint(text, o, depth) {
console.log(text, colorize(o, depth));
}
// This lets us handle promises more gracefully than the default REPL,
// which doesn't show the results.
function promiseFriendlyWriter(displayPrompt) {
const PRINT_DEPTH = 15;
return (result) => promisePrint(result, displayPrompt, PRINT_DEPTH);
}
function promisePrint(result, displayPrompt, depth) {
if (result && typeof result.then === 'function') {
// Bit of a hack to encourage the user to wait for the result of a promise
// by printing text out beside the current prompt.
Promise.resolve()
.then(() => console.log('Waiting for promise...'))
.then(() => result)
.then((o) => prettyPrint('Promise Resolved: \n', o, depth))
.catch((err) => prettyPrint('Promise Rejected: \n', err, depth))
.then(displayPrompt);
return '';
}
return colorize(result, depth);
}

151
src/cli/repl/repl.test.js Normal file
View file

@ -0,0 +1,151 @@
jest.mock('repl', () => ({ start: (opts) => ({ opts, context: {} }) }), { virtual: true });
describe('repl', () => {
const originalConsoleLog = console.log;
beforeEach(() => {
global.console.log = jest.fn();
require('repl').start = (opts) => {
return {
opts,
context: { },
};
};
});
afterEach(() => {
global.console.log = originalConsoleLog;
});
test('it exposes the server object', () => {
const { startRepl } = require('.');
const testServer = {
server: { },
};
const replServer = startRepl(testServer);
expect(replServer.context.server).toBe(testServer.server);
expect(replServer.context.kbnServer).toBe(testServer);
});
test('it prompts with Kibana>', () => {
const { startRepl } = require('.');
expect(startRepl({}).opts.prompt).toBe('Kibana> ');
});
test('it colorizes raw values', () => {
const { startRepl } = require('.');
const replServer = startRepl({});
expect(replServer.opts.writer({ meaning: 42 }))
.toMatchSnapshot();
});
test('it handles undefined', () => {
const { startRepl } = require('.');
const replServer = startRepl({});
expect(replServer.opts.writer())
.toMatchSnapshot();
});
test('it handles deep and recursive objects', () => {
const { startRepl } = require('.');
const replServer = startRepl({});
const splosion = {};
let child = splosion;
for (let i = 0; i < 2000; ++i) {
child[i] = {};
child = child[i];
}
splosion.whoops = splosion;
expect(replServer.opts.writer(splosion))
.toMatchSnapshot();
});
test('it prints promise resolves', async () => {
const { startRepl } = require('.');
const replServer = startRepl({});
const calls = await waitForPrompt(
replServer,
() => replServer.opts.writer(Promise.resolve([1, 2, 3])),
);
expect(calls)
.toMatchSnapshot();
});
test('it prints promise rejects', async () => {
const { startRepl } = require('.');
const replServer = startRepl({});
const calls = await waitForPrompt(
replServer,
() => replServer.opts.writer(Promise.reject('Dang, diggity!')),
);
expect(calls)
.toMatchSnapshot();
});
test('promises resolves only write to a specific depth', async () => {
const { startRepl } = require('.');
const replServer = startRepl({});
const splosion = {};
let child = splosion;
for (let i = 0; i < 2000; ++i) {
child[i] = {};
child = child[i];
}
splosion.whoops = splosion;
const calls = await waitForPrompt(
replServer,
() => replServer.opts.writer(Promise.resolve(splosion)),
);
expect(calls)
.toMatchSnapshot();
});
test('promises rejects only write to a specific depth', async () => {
const { startRepl } = require('.');
const replServer = startRepl({});
const splosion = {};
let child = splosion;
for (let i = 0; i < 2000; ++i) {
child[i] = {};
child = child[i];
}
splosion.whoops = splosion;
const calls = await waitForPrompt(
replServer,
() => replServer.opts.writer(Promise.reject(splosion)),
);
expect(calls)
.toMatchSnapshot();
});
test('repl exposes a print object that lets you tailor depth', () => {
const { startRepl } = require('.');
const replServer = startRepl({});
replServer.context.repl.print({ hello: { world: { nstuff: 'yo' } } }, 1);
expect(global.console.log.mock.calls)
.toMatchSnapshot();
});
test('repl exposes a print object that prints promises', async () => {
const { startRepl } = require('.');
const replServer = startRepl({});
const promise = Promise.resolve({ hello: { world: { nstuff: 'yo' } } });
const calls = await waitForPrompt(
replServer,
() => replServer.context.repl.print(promise, 1),
);
expect(calls)
.toMatchSnapshot();
});
async function waitForPrompt(replServer, fn) {
let resolveDone;
const done = new Promise((resolve) => resolveDone = resolve);
replServer.displayPrompt = () => {
resolveDone();
};
fn();
await done;
return global.console.log.mock.calls;
}
});

View file

@ -10,6 +10,8 @@ import { readKeystore } from './read_keystore';
import { DEV_SSL_CERT_PATH, DEV_SSL_KEY_PATH } from '../dev_ssl';
const { startRepl } = canRequire('../repl') ? require('../repl') : { };
function canRequire(path) {
try {
require.resolve(path);
@ -152,6 +154,10 @@ export default function (program) {
)
.option('--plugins <path>', 'an alias for --plugin-dir', pluginDirCollector);
if (!!startRepl) {
command.option('--repl', 'Run the server with a REPL prompt and access to the server object');
}
if (XPACK_OPTIONAL) {
command
.option('--oss', 'Start Kibana without X-Pack');
@ -223,10 +229,25 @@ export default function (program) {
kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.');
});
if (shouldStartRepl(opts)) {
startRepl(kbnServer);
}
return kbnServer;
});
}
function shouldStartRepl(opts) {
if (opts.repl && !startRepl) {
throw new Error('Kibana REPL mode can only be run in development mode.');
}
// The kbnWorkerType check is necessary to prevent the repl
// from being started multiple times in different processes.
// We only want one REPL.
return opts.repl && process.env.kbnWorkerType === 'server';
}
function logFatal(message, server) {
if (server) {
server.log(['fatal'], message);

View file

@ -17,6 +17,7 @@ export const CopySourceTask = {
'!src/core_plugins/testbed/**',
'!src/core_plugins/console/public/tests/**',
'!src/cli/cluster/**',
'!src/cli/repl/**',
'!src/es_archiver/**',
'!src/functional_test_runner/**',
'!src/dev/**',