@kui-shell/core
Version:
An Electron-based shell for cloud-native development
656 lines • 28.7 kB
JavaScript
/*
* 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 = (this && this.__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)
*
*/
import minimist from 'yargs-parser';
import Debug from 'debug';
const debug = Debug('core/repl');
const debugCommandErrors = Debug('core/repl/errors');
import { v4 as uuid } from 'uuid';
import isError from './error';
import encodeComponent from './encode';
import { splitIntoPipeStages } from './pipe-stages';
import { split, patterns, semiSplit } from './split';
import { isRawResponse } from '../models/entity';
import { getHistoryForTab } from '../models/history';
import { DirectReplEval } from './types';
import { isMultiModalResponse } from '../models/mmr/is';
import { isNavResponse } from '../models/NavResponse';
import expandHomeDir from '../util/home';
import { ExecType } from '../models/command';
import { DefaultExecOptions, DefaultExecOptionsForTab } from '../models/execOptions';
import eventChannelUnsafe, { eventBus } from '../core/events';
import { isHeadless, hasLocalAccess } from '../core/capabilities';
import { promiseEach } from '../util/async';
import SymbolTable from '../core/symbol-table';
import { getModel } from '../commands/tree';
import { isSuccessfulCommandResolution } from '../commands/resolution';
import { getCurrentTab, getTabId, splitFor } from '../webapp/tab';
import enforceUsage from './enforce-usage';
import { isXtermResponse } from '../models/XtermResponse';
let currentEvaluatorImpl = new DirectReplEval();
export const setEvaluatorImpl = (impl) => {
debug('setting evaluator impl', impl.name);
currentEvaluatorImpl = impl;
};
/** trim the optional suffix e.g. --last [actionName] */
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 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 getModel().read(argvNoOptions, execOptions, tryCatchalls).catch(okIf404);
if (!isSuccessfulCommandResolution(evaluator)) {
// then try treating options as unary
const tryCatchalls2 = false;
const argvNoOptions2 = argv.filter(_ => _.charAt(0) !== '-');
const evaluator2 = yield getModel().read(argvNoOptions2, execOptions, tryCatchalls2).catch(okIf404);
if (isSuccessfulCommandResolution(evaluator2)) {
return evaluator2;
}
else {
const tryCatchalls3 = true;
const evaluator3 = yield getModel().read(argvNoOptions, execOptions, tryCatchalls3);
if (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 (!isHeadless()) {
const curDic = SymbolTable.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 !== ExecType.Nested) {
const historyModel = 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) {
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 = isMultiModalResponse(response)
? 'MultiModalResponse'
: isNavResponse(response)
? 'NavResponse'
: 'ScalarResponse';
const fullEvent = Object.assign(endEvent, { response, responseType, historyIdx });
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 = minimist(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 || getCurrentTab();
const execType = (execOptions && execOptions.type) || 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(patterns.suffixComments, '$1')
.replace(patterns.prefixComments, '# $1');
const argv = split(command);
// pipeline splits, e.g. if command='a b|c', the pipeStages=[['a','b'],'c']
const pipeStages = splitIntoPipeStages(execOptions && execOptions.masquerade ? execOptions.masquerade : command);
// debug('command', commandUntrimmed)
const evaluator = yield lookupCommandEvaluator(argv, execOptions);
if (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 && !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 (isHeadless() &&
!parsedOptions.cli &&
!(parsedOptions.h || parsedOptions.help) &&
((process.env.DEFAULT_TO_UI && !parsedOptions.cli) || (evaluator.options && evaluator.options.needsUI))) {
import('../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 || uuid();
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 = isHeadless() ? -1 : this.pushHistory(originalCommand, execOptions, tab);
try {
if (!/(&&|\|\|)/.test(commandUntrimmed)) {
enforceUsage(argv, evaluator, execOptions);
}
}
catch (err) {
debug('usage enforcement failure', err, execType === 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 === 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(getTabId(tab), execUUID, 'stderr')),
createOutputStream: execOptions.createOutputStream || (() => this.makeStream(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 ? [] : 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) || ExecType.TopLevel,
isDrilldown: execOptions.isDrilldown,
command,
parsedOptions
});
return response;
});
}
catch (err) {
debugCommandErrors(err);
evaluator.error(command, tab, execType, err);
if (execType === ExecType.Nested || execOptions.rethrowErrors) {
throw err;
}
response = err;
}
if (evaluator.options.viewTransformer && execType !== 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 && !isError(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 !== 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 (isHeadless()) {
const { streamTo: headlessStreamTo } = yield import('../main/headless-support');
return headlessStreamTo(which);
}
else {
const stream = (response) => new Promise(resolve => {
eventChannelUnsafe.once(`/command/stdout/done/${tabUUID}/${execUUID}`, () => {
resolve();
});
eventChannelUnsafe.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();
export const exec = (commandUntrimmed, execOptions = emptyExecOptions()) => {
return currentExecutorImpl.exec(commandUntrimmed, execOptions);
};
/**
* User hit enter in the REPL
*
* @param execUUID for command re-execution
*
*/
export const doEval = (_tab, block, command, execUUID, type) => {
const tab = splitFor(_tab);
const defaultExecOptions = new DefaultExecOptionsForTab(tab, block, execUUID);
const execOptions = execUUID === undefined && type === undefined
? defaultExecOptions
: Object.assign(defaultExecOptions, { execUUID, type: type === undefined ? ExecType.Rerun : type });
return exec(command, execOptions);
};
/**
* If, while evaluating a command, it needs to evaluate a sub-command...
*
*/
export const qexec = (command, block, contextChangeOK, execOptions, nextBlock) => {
return exec(command, Object.assign({
block,
nextBlock,
noHistory: true,
contextChangeOK
}, execOptions, {
type: ExecType.Nested
}));
};
export 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
*
*/
export 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 (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
*
*/
export const reexec = (command, execOptions) => {
return exec(command, Object.assign({ echo: true, type: ExecType.Rerun }, execOptions));
};
/**
* Programmatic exec, as opposed to human typing and hitting enter
*
*/
export const pexec = (command, execOptions) => {
return exec(command, Object.assign({ echo: true, type: 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
export function click(command, evt) {
return __awaiter(this, void 0, void 0, function* () { });
}
/**
* Update the executor impl
*
*/
export const setExecutorImpl = (impl) => {
currentExecutorImpl = impl;
};
/**
* If the command is semicolon-separated, invoke each element of the
* split separately
*
*/
function semicolonInvoke(commands, execOptions) {
return __awaiter(this, void 0, void 0, function* () {
debug('semicolonInvoke', commands);
const nonEmptyCommands = commands.filter(_ => _);
const result = yield 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
*
*/
export function getImpl(tab) {
const impl = { qexec, rexec, pexec, reexec, click, encodeComponent, 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' || isXtermResponse(response)) {
try {
const data = isXtermResponse(response) ? response.rows.map(i => i.map(j => j.innerText)).join(' ') : response;
const writeOptions = redirector === '>>' ? '--append' : '';
yield rexec(`vfs fwrite ${encodeComponent(expandHomeDir(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');
}
});
}
//# sourceMappingURL=exec.js.map