UNPKG

@kui-shell/core

Version:

An Electron-based shell for cloud-native development

656 lines • 28.7 kB
/* * 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