Skip to content

Commit

Permalink
fix: redirect to a file also echos the output to the kui terminal
Browse files Browse the repository at this point in the history
part of #8089
  • Loading branch information
starpit committed Sep 30, 2021
1 parent 629a2af commit 58af7b9
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 16 deletions.
3 changes: 3 additions & 0 deletions packages/core/src/repl/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export interface CommandStartEvent {
echo: boolean
evaluatorOptions: CommandOptions
pipeStages: CommandLine['pipeStages']

/** The output will be redirected to a file; do not display any live output */
redirectDesired: boolean
}

export type ResponseType = 'MultiModalResponse' | 'NavResponse' | 'ScalarResponse' | 'Incomplete' | 'Error'
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/repl/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,15 @@ class InProcessExecutor implements Executor {
execOptions.execUUID = execUUID
const evaluatorOptions = evaluator.options

// are we asked to redirect the output to a file?
const redirectDesired = !noCoreRedirect && !!pipeStages.redirect

this.emitStartEvent({
tab,
route: evaluator.route,
startTime,
command: commandUntrimmed,
redirectDesired,
pipeStages,
evaluatorOptions,
execType,
Expand Down Expand Up @@ -451,12 +455,16 @@ class InProcessExecutor implements Executor {
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: T | Promise<T> | MixedResponse

const commands = evaluatorOptions.semiExpand === false ? [] : semiSplit(command)
if (commands.length > 1) {
// e.g. "a ; b ; c"
response = await semicolonInvoke(commands, execOptions)
} else {
// e.g. just "a" or "a > /tmp/foo"
try {
response = await Promise.resolve(
currentEvaluatorImpl.apply<T, O>(commandUntrimmed, execOptions, evaluator, args)
Expand Down Expand Up @@ -502,9 +510,14 @@ class InProcessExecutor implements Executor {
}
}

if (!noCoreRedirect && pipeStages.redirect) {
// Here is where we handle redirecting the response to a file
if (redirectDesired) {
try {
await redirectResponse(response, pipeStages.redirect, pipeStages.redirector)

// mimic "no response", now that we've successfully
// redirected the response to a file
response = true as T
} catch (err) {
response = err
}
Expand Down
22 changes: 15 additions & 7 deletions plugins/plugin-bash-like/src/test/bash-like/bash-like.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
*/

import { v4 as uuid } from 'uuid'
import { fileSync as tmpFile } from 'tmp'
import { writeFileSync } from 'fs'
import { strictEqual } from 'assert'
import { fileSync as tmpFile } from 'tmp'

import { Common, CLI, ReplExpect } from '@kui-shell/test'
import { Common, CLI, ReplExpect, Selectors } from '@kui-shell/test'

/** expect the given folder within the help tree */
export const header = (folder: string) => folder
Expand Down Expand Up @@ -141,11 +142,18 @@ describe(`bash-like commands ${process.env.MOCHA_RUN_TARGET || ''}`, function(th
.catch(Common.oops(this, true))
)

Common.pit('should echo ho to a file', () =>
CLI.command(`echo ho > "${dirname}"/testTmp`, this.app)
.then(ReplExpect.ok)
.catch(Common.oops(this, true))
)
Common.pit('should echo ho to a file', async () => {
try {
const res = await CLI.command(`echo ho > "${dirname}"/testTmp`, this.app).then(ReplExpect.ok)

// verify that there is indeed no output in the block
// see https://github.com/kubernetes-sigs/kui/issues/8089
const txt = await this.app.client.$(Selectors.OUTPUT_N(res.count, res.splitIndex)).then(_ => _.getText())
strictEqual(txt.length, 0, 'Expect no output in the block, due to redirect')
} catch (err) {
await Common.oops(this, true)(err)
}
})
Common.pit('should cat that file', () =>
CLI.command(`cat "${dirname}"/testTmp`, this.app)
.then(ReplExpect.okWithPtyOutput('ho'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,11 @@ export function hasStartEvent(block: BlockModel): block is BlockModel & WithComm
return !isAnnouncement(block) && (isProcessing(block) || isOk(block) || isOops(block))
}

/** @return whether the output of this command execution be redirected to a file */
export function isOutputRedirected(block: BlockModel): boolean {
return hasStartEvent(block) && block.startEvent.redirectDesired
}

/** @return whether the block is section break */
export function isSectionBreak(block: BlockModel): block is CompleteBlock & WithSectionBreak {
return isOk(block) && block.isSectionBreak
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
isCancelled,
isEmpty,
isOutputOnly,
isOutputRedirected,
isOops,
isReplay,
isWithCompleteEvent
Expand Down Expand Up @@ -120,6 +121,8 @@ export default class Output extends React.PureComponent<Props, State> {
if (hasUUID(this.props.model)) {
const tabUUID = this.props.uuid
const execUUID = this.props.model.execUUID

// done with this part... not done with all parts
const done = () => {
this.props.onRender()
eventChannelUnsafe.emit(`/command/stdout/done/${tabUUID}/${execUUID}`)
Expand All @@ -134,21 +137,33 @@ export default class Output extends React.PureComponent<Props, State> {
nStreamingOutputs: 0
}
} else {
this.streamingOutput.push(part)
setTimeout(() => {
this.setState({
nStreamingOutputs: this.streamingOutput.length
})
setTimeout(done, 10)
}, 10)
if (isOutputRedirected(this.props.model)) {
// if we were asked to redirect to a file, then we can
// immediately indicate that we are done with this part
done()
} else {
this.streamingOutput.push(part)

// use setTimeout to introduce hysteresis, so we aren't
// forcing a react re-render for a bunch of tiny streaming
// updates
setTimeout(() => {
this.setState({
nStreamingOutputs: this.streamingOutput.length
})
setTimeout(done, 10)
}, 10)
}
}
}
}

public static getDerivedStateFromProps(props: Props, state: State) {
if (isProcessing(props.model) && !state.alreadyListen) {
// listen for streaming output (unless the output has been redirected to a file)
const tabUUID = props.uuid
eventChannelUnsafe.on(`/command/stdout/${tabUUID}/${props.model.execUUID}`, state.streamingConsumer)

return {
alreadyListen: true,
isResultRendered: false,
Expand Down

0 comments on commit 58af7b9

Please sign in to comment.