UNPKG

@kui-shell/core

Version:

An Electron-based shell for cloud-native development

705 lines (703 loc) • 27.4 kB
"use strict"; 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'); } }); }