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:
parent
45002180a7
commit
fc5e8d81fe
|
@ -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);
|
||||
|
||||
|
|
118
src/cli/repl/__snapshots__/repl.test.js.snap
Normal file
118
src/cli/repl/__snapshots__/repl.test.js.snap
Normal file
|
@ -0,0 +1,118 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`repl it colorizes raw values 1`] = `"{ meaning: [33m42[39m }"`;
|
||||
|
||||
exports[`repl it handles deep and recursive objects 1`] = `
|
||||
"{ [32m'0'[39m:
|
||||
{ [32m'1'[39m:
|
||||
{ [32m'2'[39m:
|
||||
{ [32m'3'[39m:
|
||||
{ [32m'4'[39m:
|
||||
{ [32m'5'[39m:
|
||||
{ [32m'6'[39m:
|
||||
{ [32m'7'[39m:
|
||||
{ [32m'8'[39m:
|
||||
{ [32m'9'[39m:
|
||||
{ [32m'10'[39m: { [32m'11'[39m: { [32m'12'[39m: { [32m'13'[39m: { [32m'14'[39m: { [32m'15'[39m: [36m[Object][39m } } } } } } } } } } } } } } },
|
||||
whoops: [36m[Circular][39m }"
|
||||
`;
|
||||
|
||||
exports[`repl it handles undefined 1`] = `"[90mundefined[39m"`;
|
||||
|
||||
exports[`repl it prints promise rejects 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"Waiting for promise...",
|
||||
],
|
||||
Array [
|
||||
"Promise Rejected:
|
||||
",
|
||||
"[32m'Dang, diggity!'[39m",
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`repl it prints promise resolves 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"Waiting for promise...",
|
||||
],
|
||||
Array [
|
||||
"Promise Resolved:
|
||||
",
|
||||
"[ [33m1[39m, [33m2[39m, [33m3[39m ]",
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`repl promises rejects only write to a specific depth 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"Waiting for promise...",
|
||||
],
|
||||
Array [
|
||||
"Promise Rejected:
|
||||
",
|
||||
"{ [32m'0'[39m:
|
||||
{ [32m'1'[39m:
|
||||
{ [32m'2'[39m:
|
||||
{ [32m'3'[39m:
|
||||
{ [32m'4'[39m:
|
||||
{ [32m'5'[39m:
|
||||
{ [32m'6'[39m:
|
||||
{ [32m'7'[39m:
|
||||
{ [32m'8'[39m:
|
||||
{ [32m'9'[39m:
|
||||
{ [32m'10'[39m: { [32m'11'[39m: { [32m'12'[39m: { [32m'13'[39m: { [32m'14'[39m: { [32m'15'[39m: [36m[Object][39m } } } } } } } } } } } } } } },
|
||||
whoops: [36m[Circular][39m }",
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`repl promises resolves only write to a specific depth 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"Waiting for promise...",
|
||||
],
|
||||
Array [
|
||||
"Promise Resolved:
|
||||
",
|
||||
"{ [32m'0'[39m:
|
||||
{ [32m'1'[39m:
|
||||
{ [32m'2'[39m:
|
||||
{ [32m'3'[39m:
|
||||
{ [32m'4'[39m:
|
||||
{ [32m'5'[39m:
|
||||
{ [32m'6'[39m:
|
||||
{ [32m'7'[39m:
|
||||
{ [32m'8'[39m:
|
||||
{ [32m'9'[39m:
|
||||
{ [32m'10'[39m: { [32m'11'[39m: { [32m'12'[39m: { [32m'13'[39m: { [32m'14'[39m: { [32m'15'[39m: [36m[Object][39m } } } } } } } } } } } } } } },
|
||||
whoops: [36m[Circular][39m }",
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`repl repl exposes a print object that lets you tailor depth 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"{ hello: { world: [36m[Object][39m } }",
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`repl repl exposes a print object that prints promises 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"",
|
||||
],
|
||||
Array [
|
||||
"Waiting for promise...",
|
||||
],
|
||||
Array [
|
||||
"Promise Resolved:
|
||||
",
|
||||
"{ hello: { world: [36m[Object][39m } }",
|
||||
],
|
||||
]
|
||||
`;
|
56
src/cli/repl/index.js
Normal file
56
src/cli/repl/index.js
Normal 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
151
src/cli/repl/repl.test.js
Normal 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;
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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/**',
|
||||
|
|
Loading…
Reference in a new issue