mirror of https://github.com/actions/toolkit
Add getExecOutput function (#814)
* Add getExecOutput function * Add tests for exec output * Modify tests to not rely on buffer size, but only test larger output * Handle split multi-byte characters + PR feedback * Fix tests * Lint * Update how split byte are sent for testspull/817/head
parent
566ea66979
commit
ddd04b6997
|
@ -636,6 +636,165 @@ describe('@actions/exec', () => {
|
|||
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
|
||||
})
|
||||
|
||||
it('correctly outputs for getExecOutput', async () => {
|
||||
const stdErrPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stderroutput.js'
|
||||
)
|
||||
const stdOutPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdoutoutput.js'
|
||||
)
|
||||
const nodePath: string = await io.which('node', true)
|
||||
|
||||
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdOutPath],
|
||||
getExecOptions()
|
||||
)
|
||||
|
||||
expect(exitCodeOut).toBe(0)
|
||||
expect(stdout).toBe('this is output to stdout')
|
||||
|
||||
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdErrPath],
|
||||
getExecOptions()
|
||||
)
|
||||
expect(exitCodeErr).toBe(0)
|
||||
expect(stderr).toBe('this is output to stderr')
|
||||
})
|
||||
|
||||
it('correctly outputs for getExecOutput with additional listeners', async () => {
|
||||
const stdErrPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stderroutput.js'
|
||||
)
|
||||
const stdOutPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdoutoutput.js'
|
||||
)
|
||||
|
||||
const nodePath: string = await io.which('node', true)
|
||||
let listenerOut = ''
|
||||
|
||||
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdOutPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stdout: data => {
|
||||
listenerOut = data.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(exitCodeOut).toBe(0)
|
||||
expect(stdout).toBe('this is output to stdout')
|
||||
expect(listenerOut).toBe('this is output to stdout')
|
||||
|
||||
let listenerErr = ''
|
||||
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdErrPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stderr: data => {
|
||||
listenerErr = data.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(exitCodeErr).toBe(0)
|
||||
expect(stderr).toBe('this is output to stderr')
|
||||
expect(listenerErr).toBe('this is output to stderr')
|
||||
})
|
||||
|
||||
it('correctly outputs for getExecOutput when total size exceeds buffer size', async () => {
|
||||
const stdErrPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stderroutput.js'
|
||||
)
|
||||
const stdOutPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdoutoutputlarge.js'
|
||||
)
|
||||
|
||||
const nodePath: string = await io.which('node', true)
|
||||
let listenerOut = ''
|
||||
|
||||
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdOutPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stdout: data => {
|
||||
listenerOut += data.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(exitCodeOut).toBe(0)
|
||||
expect(Buffer.byteLength(stdout || '', 'utf8')).toBe(2 ** 25)
|
||||
expect(Buffer.byteLength(listenerOut, 'utf8')).toBe(2 ** 25)
|
||||
|
||||
let listenerErr = ''
|
||||
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdErrPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stderr: data => {
|
||||
listenerErr = data.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(exitCodeErr).toBe(0)
|
||||
expect(stderr).toBe('this is output to stderr')
|
||||
expect(listenerErr).toBe('this is output to stderr')
|
||||
})
|
||||
|
||||
it('correctly outputs for getExecOutput with multi-byte characters', async () => {
|
||||
const stdOutPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdoutputspecial.js'
|
||||
)
|
||||
|
||||
const nodePath: string = await io.which('node', true)
|
||||
let numStdOutBufferCalls = 0
|
||||
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdOutPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stdout: () => {
|
||||
numStdOutBufferCalls += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(exitCodeOut).toBe(0)
|
||||
//one call for each half of the © character, ensuring it was actually split and not sent together
|
||||
expect(numStdOutBufferCalls).toBe(2)
|
||||
expect(stdout).toBe('©')
|
||||
})
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
|
||||
let exitCode: number
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
//Default highWaterMark for readable stream buffers us 64K (2^16)
|
||||
//so we go over that to get more than a buffer's worth
|
||||
process.stdout.write('a\n'.repeat(2**24));
|
|
@ -0,0 +1,5 @@
|
|||
//first half of © character
|
||||
process.stdout.write(Buffer.from([0xC2]), (err) => {
|
||||
//write in the callback so that the second byte is sent separately
|
||||
process.stdout.write(Buffer.from([0xA9])) //second half of © character
|
||||
})
|
|
@ -1,7 +1,8 @@
|
|||
import {ExecOptions} from './interfaces'
|
||||
import {StringDecoder} from 'string_decoder'
|
||||
import {ExecOptions, ExecOutput, ExecListeners} from './interfaces'
|
||||
import * as tr from './toolrunner'
|
||||
|
||||
export {ExecOptions}
|
||||
export {ExecOptions, ExecOutput, ExecListeners}
|
||||
|
||||
/**
|
||||
* Exec a command.
|
||||
|
@ -28,3 +29,63 @@ export async function exec(
|
|||
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
|
||||
return runner.exec()
|
||||
}
|
||||
|
||||
/**
|
||||
* Exec a command and get the output.
|
||||
* Output will be streamed to the live console.
|
||||
* Returns promise with the exit code and collected stdout and stderr
|
||||
*
|
||||
* @param commandLine command to execute (can include additional args). Must be correctly escaped.
|
||||
* @param args optional arguments for tool. Escaping is handled by the lib.
|
||||
* @param options optional exec options. See ExecOptions
|
||||
* @returns Promise<ExecOutput> exit code, stdout, and stderr
|
||||
*/
|
||||
|
||||
export async function getExecOutput(
|
||||
commandLine: string,
|
||||
args?: string[],
|
||||
options?: ExecOptions
|
||||
): Promise<ExecOutput> {
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
||||
//Using string decoder covers the case where a mult-byte character is split
|
||||
const stdoutDecoder = new StringDecoder('utf8')
|
||||
const stderrDecoder = new StringDecoder('utf8')
|
||||
|
||||
const originalStdoutListener = options?.listeners?.stdout
|
||||
const originalStdErrListener = options?.listeners?.stderr
|
||||
|
||||
const stdErrListener = (data: Buffer): void => {
|
||||
stderr += stderrDecoder.write(data)
|
||||
if (originalStdErrListener) {
|
||||
originalStdErrListener(data)
|
||||
}
|
||||
}
|
||||
|
||||
const stdOutListener = (data: Buffer): void => {
|
||||
stdout += stdoutDecoder.write(data)
|
||||
if (originalStdoutListener) {
|
||||
originalStdoutListener(data)
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: ExecListeners = {
|
||||
...options?.listeners,
|
||||
stdout: stdOutListener,
|
||||
stderr: stdErrListener
|
||||
}
|
||||
|
||||
const exitCode = await exec(commandLine, args, {...options, listeners})
|
||||
|
||||
//flush any remaining characters
|
||||
stdout += stdoutDecoder.end()
|
||||
stderr += stderrDecoder.end()
|
||||
|
||||
//return undefined for stdout/stderr if they are empty
|
||||
return {
|
||||
exitCode,
|
||||
stdout,
|
||||
stderr
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,15 +34,39 @@ export interface ExecOptions {
|
|||
input?: Buffer
|
||||
|
||||
/** optional. Listeners for output. Callback functions that will be called on these events */
|
||||
listeners?: {
|
||||
stdout?: (data: Buffer) => void
|
||||
|
||||
stderr?: (data: Buffer) => void
|
||||
|
||||
stdline?: (data: string) => void
|
||||
|
||||
errline?: (data: string) => void
|
||||
|
||||
debug?: (data: string) => void
|
||||
}
|
||||
listeners?: ExecListeners
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the output of getExecOutput()
|
||||
*/
|
||||
export interface ExecOutput {
|
||||
/**The exit code of the process */
|
||||
exitCode: number
|
||||
|
||||
/**The entire stdout of the process as a string */
|
||||
stdout: string
|
||||
|
||||
/**The entire stderr of the process as a string */
|
||||
stderr: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The user defined listeners for an exec call
|
||||
*/
|
||||
export interface ExecListeners {
|
||||
/** A call back for each buffer of stdout */
|
||||
stdout?: (data: Buffer) => void
|
||||
|
||||
/** A call back for each buffer of stderr */
|
||||
stderr?: (data: Buffer) => void
|
||||
|
||||
/** A call back for each line of stdout */
|
||||
stdline?: (data: string) => void
|
||||
|
||||
/** A call back for each line of stderr */
|
||||
errline?: (data: string) => void
|
||||
|
||||
/** A call back for each debug log */
|
||||
debug?: (data: string) => void
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue