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"`)
|
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) {
|
if (IS_WINDOWS) {
|
||||||
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
|
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
|
||||||
let exitCode: number
|
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'
|
import * as tr from './toolrunner'
|
||||||
|
|
||||||
export {ExecOptions}
|
export {ExecOptions, ExecOutput, ExecListeners}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exec a command.
|
* Exec a command.
|
||||||
|
@ -28,3 +29,63 @@ export async function exec(
|
||||||
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
|
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
|
||||||
return runner.exec()
|
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
|
input?: Buffer
|
||||||
|
|
||||||
/** optional. Listeners for output. Callback functions that will be called on these events */
|
/** optional. Listeners for output. Callback functions that will be called on these events */
|
||||||
listeners?: {
|
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
|
stdout?: (data: Buffer) => void
|
||||||
|
|
||||||
|
/** A call back for each buffer of stderr */
|
||||||
stderr?: (data: Buffer) => void
|
stderr?: (data: Buffer) => void
|
||||||
|
|
||||||
|
/** A call back for each line of stdout */
|
||||||
stdline?: (data: string) => void
|
stdline?: (data: string) => void
|
||||||
|
|
||||||
|
/** A call back for each line of stderr */
|
||||||
errline?: (data: string) => void
|
errline?: (data: string) => void
|
||||||
|
|
||||||
|
/** A call back for each debug log */
|
||||||
debug?: (data: string) => void
|
debug?: (data: string) => void
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue