@kui-shell/core
Version:
An Electron-based shell for cloud-native development
705 lines (703 loc) • 27.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.click = click;
exports.exec = exports.doEval = void 0;
exports.getImpl = getImpl;
exports.setExecutorImpl = exports.setEvaluatorImpl = exports.rexec = exports.reexec = exports.qfexec = exports.qexec = exports.pexec = void 0;
var _yargsParser = _interopRequireDefault(require("yargs-parser"));
var _debug = _interopRequireDefault(require("debug"));
var _uuid = require("uuid");
var _error = _interopRequireDefault(require("./error"));
var _encode = _interopRequireDefault(require("./encode"));
var _pipeStages = require("./pipe-stages");
var _split = require("./split");
var _entity = require("../models/entity");
var _history = require("../models/history");
var _types = require("./types");
var _is = require("../models/mmr/is");
var _NavResponse = require("../models/NavResponse");
var _home = _interopRequireDefault(require("../util/home"));
var _command = require("../models/command");
var _execOptions = require("../models/execOptions");
var _events = _interopRequireWildcard(require("../core/events"));
var _capabilities = require("../core/capabilities");
var _async = require("../util/async");
var _symbolTable = _interopRequireDefault(require("../core/symbol-table"));
var _tree = require("../commands/tree");
var _resolution = require("../commands/resolution");
var _tab2 = require("../webapp/tab");
var _enforceUsage = _interopRequireDefault(require("./enforce-usage"));
var _XtermResponse = require("../models/XtermResponse");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*
* Copyright 2017 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/* eslint-disable @typescript-eslint/no-use-before-define */
/**
* The Read-Eval-Print Loop (REPL)
*
*/
const debug = (0, _debug.default)('core/repl');
const debugCommandErrors = (0, _debug.default)('core/repl/errors');
let currentEvaluatorImpl = new _types.DirectReplEval();
const setEvaluatorImpl = impl => {
debug('setting evaluator impl', impl.name);
currentEvaluatorImpl = impl;
};
/** trim the optional suffix e.g. --last [actionName] */
exports.setEvaluatorImpl = setEvaluatorImpl;
const stripTrailer = str => str && str.replace(/\s+.*$/, '');
/** turn --foo into foo and -f into f */
const unflag = opt => opt && stripTrailer(opt.replace(/^[-]+/, ''));
const emptyExecOptions = () => new _execOptions.DefaultExecOptions();
function okIf404(err) {
if (err.code === 404) {
return false;
} else {
throw err;
}
}
/**
* Find a matching command evaluator
*
*/
function lookupCommandEvaluator(argv, execOptions) {
return __awaiter(this, void 0, void 0, function* () {
// first try treating options as binary
const tryCatchalls = false;
const argvNoOptions = argv.filter((_, idx, A) => _.charAt(0) !== '-' && (idx === 0 || A[idx - 1].charAt(0) !== '-'));
const evaluator = yield (0, _tree.getModel)().read(argvNoOptions, execOptions, tryCatchalls).catch(okIf404);
if (!(0, _resolution.isSuccessfulCommandResolution)(evaluator)) {
// then try treating options as unary
const tryCatchalls2 = false;
const argvNoOptions2 = argv.filter(_ => _.charAt(0) !== '-');
const evaluator2 = yield (0, _tree.getModel)().read(argvNoOptions2, execOptions, tryCatchalls2).catch(okIf404);
if ((0, _resolution.isSuccessfulCommandResolution)(evaluator2)) {
return evaluator2;
} else {
const tryCatchalls3 = true;
const evaluator3 = yield (0, _tree.getModel)().read(argvNoOptions, execOptions, tryCatchalls3);
if ((0, _resolution.isSuccessfulCommandResolution)(evaluator3)) {
return evaluator3;
}
}
}
return evaluator;
});
}
/**
* Execute the given command-line directly in this process
*
*/
class InProcessExecutor {
constructor() {
this.name = 'InProcessExecutor';
}
loadSymbolTable(tab, execOptions) {
if (!(0, _capabilities.isHeadless)()) {
const curDic = _symbolTable.default.read(tab);
if (typeof curDic !== 'undefined') {
if (!execOptions.env) {
execOptions.env = {};
}
execOptions.env = Object.assign({}, execOptions.env, curDic);
}
}
}
/** Add a history entry */
pushHistory(command, execOptions, tab) {
if (!execOptions || !execOptions.noHistory) {
if (!execOptions || !execOptions.quiet) {
if (!execOptions || execOptions.type !== _command.ExecType.Nested) {
const historyModel = (0, _history.getHistoryForTab)(tab.uuid);
return execOptions.history = historyModel.add({
raw: command
});
}
}
}
}
/** Update a history entry with the response */
/* private updateHistory(cursor: number, endEvent: CommandCompleteEvent) {
getHistoryForTab(endEvent.tab.uuid).update(cursor, async line => {
const resp = await endEvent.response
if (!isHTML(resp) && !isReactResponse(resp)) {
try {
JSON.stringify(resp)
line.response = resp
line.execUUID = endEvent.execUUID
line.historyIdx = endEvent.historyIdx
line.responseType = endEvent.responseType
} catch (err) {
debug('non-serializable response', resp)
}
}
})
} */
/** Notify the world that a command execution has begun */
emitStartEvent(startEvent) {
_events.eventBus.emitCommandStart(startEvent);
}
/** Notify the world that a command execution has finished */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
emitCompletionEvent(presponse, endEvent, historyIdx) {
return Promise.resolve(presponse).then(response => {
const responseType = (0, _is.isMultiModalResponse)(response) ? 'MultiModalResponse' : (0, _NavResponse.isNavResponse)(response) ? 'NavResponse' : 'ScalarResponse';
const fullEvent = Object.assign(endEvent, {
response,
responseType,
historyIdx
});
_events.eventBus.emitCommandComplete(fullEvent);
/* if (historyIdx) {
this.updateHistory(historyIdx, fullEvent)
} */
});
}
/**
* Split an `argv` into a pair of `argvNoOptions` and `ParsedOptions`.
*
*/
parseOptions(argv, evaluator) {
/* interface ArgCount {
[key: string]: number
} */
//
// fetch the usage model for the command
//
const _usage = evaluator.options && evaluator.options.usage;
const usage = _usage && _usage.fn ? _usage.fn(_usage.command) : _usage;
// debug('usage', usage)
/* if (execOptions && execOptions.failWithUsage && !usage) {
debug('caller needs usage model, but none exists for this command', evaluator)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (false as any) as T
} */
const builtInOptions = [{
name: '--quiet',
alias: '-q',
hidden: true,
boolean: true
}];
if (!usage || !usage.noHelp) {
// usage might tell us not to add help, or not to add the -h help alias
const help = {
name: '--help',
hidden: true,
boolean: true
};
if (!usage || !usage.noHelpAlias) {
help.alias = '-h';
}
builtInOptions.push(help);
}
// here, we encode some common aliases, and then overlay any flags from the command
// narg: any flags that take more than one argument e.g. -p key value would have { narg: { p: 2 } }
const commandFlags = evaluator.options && evaluator.options.flags || evaluator.options && evaluator.options.synonymFor && evaluator.options.synonymFor.options && evaluator.options.synonymFor.options.flags || {};
const optional = builtInOptions.concat(evaluator.options && evaluator.options.usage && evaluator.options.usage.optional || []);
const optionalBooleans = optional && optional.filter(({
boolean
}) => boolean).map(_ => unflag(_.name));
const optionalAliases = optional && optional.filter(({
alias
}) => alias).reduce((M, {
name,
alias
}) => {
M[unflag(alias)] = unflag(name);
return M;
}, {});
const allFlags = {
configuration: Object.assign({
'camel-case-expansion': false
}, evaluator.options && evaluator.options.flags && evaluator.options.flags.configuration || usage && usage.configuration || {}),
string: commandFlags.string || [],
array: commandFlags.array || [],
boolean: (commandFlags.boolean || []).concat(optionalBooleans || []),
alias: Object.assign({}, commandFlags.alias || {}, optionalAliases || {}),
narg: Object.assign({}, commandFlags.narg || {},
// narg from registrar.listen(route, handler, { flags: { narg: ... }})
optional && optional.reduce((N, {
name,
alias,
narg
}) => {
// narg from listen(route, handler, { usage: { optional: [...] }})
if (narg) {
N[unflag(name)] = narg;
N[unflag(alias)] = narg;
}
return N;
}, {}) || {})
};
const parsedOptions = (0, _yargsParser.default)(argv, allFlags);
const argvNoOptions = parsedOptions._;
return {
argvNoOptions,
parsedOptions
};
}
execUnsafe(commandUntrimmed, execOptions = emptyExecOptions()) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
const tab = execOptions.tab || (0, _tab2.getCurrentTab)();
const execType = execOptions && execOptions.type || _command.ExecType.TopLevel;
const REPL = tab.REPL || getImpl(tab);
// trim suffix comments, e.g. "kubectl get pods # comments start here"
// insert whitespace for whitespace-free prefix comments, e.g. "#comments" -> "# comments"
let command = (commandUntrimmed || '').trim().replace(_split.patterns.suffixComments, '$1').replace(_split.patterns.prefixComments, '# $1');
const argv = (0, _split.split)(command);
// pipeline splits, e.g. if command='a b|c', the pipeStages=[['a','b'],'c']
const pipeStages = (0, _pipeStages.splitIntoPipeStages)(execOptions && execOptions.masquerade ? execOptions.masquerade : command);
// debug('command', commandUntrimmed)
const evaluator = yield lookupCommandEvaluator(argv, execOptions);
if ((0, _resolution.isSuccessfulCommandResolution)(evaluator)) {
// If core handles redirect, argv and command shouldn't contain the redirect part;
// otherwise, controllers may use argv and command incorrectly e.g. kuiecho hi > file will print "hi > file" instead of "hi"
const noCoreRedirect = execOptions.noCoreRedirect || evaluator.options && evaluator.options.noCoreRedirect;
// are we asked to redirect the output to a file?
let redirectDesired = !noCoreRedirect && !!pipeStages.redirect && !/\/dev/.test(pipeStages.redirect);
const originalCommand = command;
if (redirectDesired && pipeStages.redirect) {
argv.splice(argv.indexOf(pipeStages.redirector), 2);
command = command.replace(new RegExp(`\\s*${pipeStages.redirector}\\s*${pipeStages.redirect}\\s*$`), '');
}
const {
argvNoOptions,
parsedOptions
} = this.parseOptions(argv, evaluator);
if (evaluator.options && evaluator.options.requiresLocal && !(0, _capabilities.hasLocalAccess)()) {
debug('command does not work in a browser', originalCommand);
const err = new Error('Command requires local access');
err.code = 406; // http not acceptable
err.kind = 'commandresolution';
return err;
}
// if we don't have a head (yet), but this command requires one,
// then ask for a head and try again. note that we ignore this
// needsUI constraint if the user is asking for help
if ((0, _capabilities.isHeadless)() && !parsedOptions.cli && !(parsedOptions.h || parsedOptions.help) && (process.env.DEFAULT_TO_UI && !parsedOptions.cli || evaluator.options && evaluator.options.needsUI)) {
Promise.resolve().then(() => require('../main/headless')).then(({
createWindow
}) => {
const desiredWindowTitle = evaluator.options ? typeof evaluator.options.title === 'string' ? evaluator.options.title : typeof evaluator.options.title === 'function' ? evaluator.options.title(argv) : undefined : undefined;
const options = Object.assign({}, evaluator.options, {
title: desiredWindowTitle
});
return createWindow(argv, evaluator.options.fullscreen, options, true);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return true;
}
const execUUID = execOptions.execUUID || (0, _uuid.v4)();
execOptions.execUUID = execUUID;
const evaluatorOptions = evaluator.options;
this.emitStartEvent({
tab,
route: evaluator.route,
startTime,
command: commandUntrimmed,
redirectDesired,
pipeStages,
evaluatorOptions,
execType,
execUUID,
execOptions,
echo: execOptions.echo
});
if (command.length === 0) {
// blank line (after stripping off comments)
this.emitCompletionEvent(true, {
tab,
execType,
completeTime: Date.now(),
command: commandUntrimmed,
argvNoOptions,
parsedOptions,
pipeStages,
execOptions,
execUUID,
cancelled: true,
echo: execOptions.echo,
evaluatorOptions
});
return;
}
const historyIdx = (0, _capabilities.isHeadless)() ? -1 : this.pushHistory(originalCommand, execOptions, tab);
try {
if (!/(&&|\|\|)/.test(commandUntrimmed)) {
(0, _enforceUsage.default)(argv, evaluator, execOptions);
}
} catch (err) {
debug('usage enforcement failure', err, execType === _command.ExecType.Nested);
this.emitCompletionEvent(err, {
tab,
execType,
completeTime: Date.now(),
command: commandUntrimmed,
argvNoOptions,
parsedOptions,
pipeStages,
execOptions,
cancelled: false,
echo: execOptions.echo,
execUUID,
evaluatorOptions
}, historyIdx || -1);
if (execOptions.type === _command.ExecType.Nested) {
throw err;
} else {
return;
}
}
this.loadSymbolTable(tab, execOptions);
const args = {
tab,
REPL,
block: execOptions.block,
nextBlock: undefined,
argv,
command,
execOptions,
argvNoOptions,
pipeStages,
parsedOptions: parsedOptions,
createErrorStream: execOptions.createErrorStream || (() => this.makeStream((0, _tab2.getTabId)(tab), execUUID, 'stderr')),
createOutputStream: execOptions.createOutputStream || (() => this.makeStream((0, _tab2.getTabId)(tab), execUUID, 'stdout'))
};
// COMMAND EXECUTION ABOUT TO COMMENCE. This is where we
// actually execute the command line and populate this
// `response` variable:
let response;
const commands = evaluatorOptions.semiExpand === false ? [] : (0, _split.semiSplit)(command);
if (commands.length > 1) {
// e.g. "a ; b ; c"
response = yield semicolonInvoke(commands, execOptions);
redirectDesired = false; // any redirects will be handled in the respective semi invoke
} else {
// e.g. just "a" or "a > /tmp/foo"
try {
response = yield Promise.resolve(currentEvaluatorImpl.apply(commandUntrimmed, execOptions, evaluator, args)).then(response => {
// indicate that the command was successfuly completed
evaluator.success({
tab,
type: execOptions && execOptions.type || _command.ExecType.TopLevel,
isDrilldown: execOptions.isDrilldown,
command,
parsedOptions
});
return response;
});
} catch (err) {
debugCommandErrors(err);
evaluator.error(command, tab, execType, err);
if (execType === _command.ExecType.Nested || execOptions.rethrowErrors) {
throw err;
}
response = err;
}
if (evaluator.options.viewTransformer && execType !== _command.ExecType.Nested) {
response = yield Promise.resolve(response).then(_ => __awaiter(this, void 0, void 0, function* () {
const maybeAView = yield evaluator.options.viewTransformer(args, _);
return maybeAView || _;
})).catch(err => {
// view transformer failed; treat this as the response to the user
return err;
});
}
// the || true part is a safeguard for cases where typescript
// didn't catch a command handler returning nothing; it
// shouldn't happen, but probably isn't a sign of a dire
// problem. issue a debug warning, in any case
if (!response) {
debug('warning: command handler returned nothing', commandUntrimmed);
}
}
// Here is where we handle redirecting the response to a file
if (redirectDesired && !(0, _error.default)(yield response)) {
try {
yield redirectResponse(response, pipeStages.redirect, pipeStages.redirector);
// mimic "no response", now that we've successfully
// redirected the response to a file
response = true;
} catch (err) {
response = err;
}
}
this.emitCompletionEvent(response || true, {
tab,
execType,
completeTime: Date.now(),
command: commandUntrimmed,
argvNoOptions,
parsedOptions,
pipeStages,
execUUID,
cancelled: false,
echo: execOptions.echo,
evaluatorOptions,
execOptions
}, historyIdx || -1);
return response;
} else {
const err = new Error('Command not found');
err.code = 404; // http not acceptable
err.kind = 'commandresolution';
return err;
}
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
exec(commandUntrimmed, execOptions = emptyExecOptions()) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield this.execUnsafe(commandUntrimmed, execOptions);
} catch (err) {
if (execOptions.type !== _command.ExecType.Nested && !execOptions.rethrowErrors) {
console.error('Internal Error: uncaught exception in exec', err);
return err;
} else {
throw err;
}
}
});
}
makeStream(tabUUID, execUUID, which) {
return __awaiter(this, void 0, void 0, function* () {
if ((0, _capabilities.isHeadless)()) {
const {
streamTo: headlessStreamTo
} = yield Promise.resolve().then(() => require('../main/headless-support'));
return headlessStreamTo(which);
} else {
const stream = response => new Promise(resolve => {
_events.default.once(`/command/stdout/done/${tabUUID}/${execUUID}`, () => {
resolve();
});
_events.default.emit(`/command/stdout/${tabUUID}/${execUUID}`, response);
});
return Promise.resolve(stream);
}
});
}
} /* InProcessExecutor */
/**
* Execute the given command-line. This function operates by
* delegation to the IExecutor impl.
*
*/
let currentExecutorImpl = new InProcessExecutor();
const exec = (commandUntrimmed, execOptions = emptyExecOptions()) => {
return currentExecutorImpl.exec(commandUntrimmed, execOptions);
};
/**
* User hit enter in the REPL
*
* @param execUUID for command re-execution
*
*/
exports.exec = exec;
const doEval = (_tab, block, command, execUUID, type) => {
const tab = (0, _tab2.splitFor)(_tab);
const defaultExecOptions = new _execOptions.DefaultExecOptionsForTab(tab, block, execUUID);
const execOptions = execUUID === undefined && type === undefined ? defaultExecOptions : Object.assign(defaultExecOptions, {
execUUID,
type: type === undefined ? _command.ExecType.Rerun : type
});
return exec(command, execOptions);
};
/**
* If, while evaluating a command, it needs to evaluate a sub-command...
*
*/
exports.doEval = doEval;
const qexec = (command, block, contextChangeOK, execOptions, nextBlock) => {
return exec(command, Object.assign({
block,
nextBlock,
noHistory: true,
contextChangeOK
}, execOptions, {
type: _command.ExecType.Nested
}));
};
exports.qexec = qexec;
const qfexec = (command, block, nextBlock, execOptions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => {
// context change ok, final exec in a chain of nested execs
return qexec(command, block, true, execOptions, nextBlock);
};
/**
* "raw" exec, where we want the data model back directly
*
*/
exports.qfexec = qfexec;
const rexec = (command, execOptions = emptyExecOptions()) => __awaiter(void 0, void 0, void 0, function* () {
const content = yield qexec(command, undefined, undefined, Object.assign({
raw: true
}, execOptions));
if ((0, _entity.isRawResponse)(content)) {
return content;
} else {
// bad actors may return a string; adapt this to RawResponse
return {
mode: 'raw',
content
};
}
});
/**
* Evaluate a command and place the result in the current active view for the given tab
*
*/
exports.rexec = rexec;
const reexec = (command, execOptions) => {
return exec(command, Object.assign({
echo: true,
type: _command.ExecType.Rerun
}, execOptions));
};
/**
* Programmatic exec, as opposed to human typing and hitting enter
*
*/
exports.reexec = reexec;
const pexec = (command, execOptions) => {
return exec(command, Object.assign({
echo: true,
type: _command.ExecType.ClickHandler
}, execOptions));
};
/**
* Execute a command in response to an in-view click
*
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
exports.pexec = pexec;
function click(command, evt) {
return __awaiter(this, void 0, void 0, function* () {});
}
/**
* Update the executor impl
*
*/
const setExecutorImpl = impl => {
currentExecutorImpl = impl;
};
/**
* If the command is semicolon-separated, invoke each element of the
* split separately
*
*/
exports.setExecutorImpl = setExecutorImpl;
function semicolonInvoke(commands, execOptions) {
return __awaiter(this, void 0, void 0, function* () {
debug('semicolonInvoke', commands);
const nonEmptyCommands = commands.filter(_ => _);
const result = yield (0, _async.promiseEach)(nonEmptyCommands, command => __awaiter(this, void 0, void 0, function* () {
try {
const entity = yield qexec(command, undefined, undefined, Object.assign({}, execOptions, {
quiet: false,
/* block, */execUUID: execOptions.execUUID
}));
if (entity === true) {
// pty output
return '';
} else {
return entity;
}
} catch (err) {
return err;
}
}));
return result;
});
}
/**
* @return an instance that obeys the REPL interface
*
*/
function getImpl(tab) {
const impl = {
qexec,
rexec,
pexec,
reexec,
click,
encodeComponent: _encode.default,
split: _split.split
};
tab.REPL = impl;
return impl;
}
/**
* redirect a string response
*
*/
function redirectResponse(_response, filepath, redirector) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield _response;
if (response === true) {
// probably a mis-parsing of the redirect
} else if (Buffer.isBuffer(response) || typeof response === 'string' || (0, _XtermResponse.isXtermResponse)(response)) {
try {
const data = (0, _XtermResponse.isXtermResponse)(response) ? response.rows.map(i => i.map(j => j.innerText)).join(' ') : response;
const writeOptions = redirector === '>>' ? '--append' : '';
yield rexec(`vfs fwrite ${(0, _encode.default)((0, _home.default)(filepath))} ${writeOptions}`, {
data
});
debug(`redirected response to ${filepath}`, response);
} catch (err) {
console.error(err);
throw new Error(`Error in redirect: ${err.message}`);
}
} else {
debug('Unsupported redirect response', response);
throw new Error('Error: unsupported redirect response');
}
});
}