diff --git a/__tests__/exec.test.ts b/__tests__/exec.test.ts index 1f83ce4..130f382 100644 --- a/__tests__/exec.test.ts +++ b/__tests__/exec.test.ts @@ -11,6 +11,11 @@ import * as io from '@actions/io' /* eslint-disable @typescript-eslint/unbound-method */ const IS_WINDOWS = process.platform === 'win32' +const SPAWN_WAIT_SCRIPT = path.join( + __dirname, + 'scripts', + 'spawn-wait-for-file.js' +) let outstream: stream.Writable let errstream: stream.Writable @@ -365,35 +370,20 @@ describe('@actions/exec', () => { fs.writeFileSync(semaphorePath, '') const nodePath = await io.which('node', true) - const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js') const debugList: string[] = [] const _testExecOptions = getExecOptions() _testExecOptions.delay = 500 - _testExecOptions.windowsVerbatimArguments = true _testExecOptions.listeners = { debug: (data: string) => { debugList.push(data) } } - let exitCode: number - if (IS_WINDOWS) { - const toolName: string = await io.which('cmd.exe', true) - const args = [ - '/D', // Disable execution of AutoRun commands from registry. - '/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. - '/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. - '/S', // Will cause first and last quote after /C to be stripped. - '/C', - `"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}""` - ] - exitCode = await exec.exec(`"${toolName}"`, args, _testExecOptions) - } else { - const toolName: string = await io.which('bash', true) - const args = ['-c', `node '${scriptPath}' 'file=${semaphorePath}' &`] - - exitCode = await exec.exec(`"${toolName}"`, args, _testExecOptions) - } + const exitCode = await exec.exec( + `"${nodePath}"`, + [SPAWN_WAIT_SCRIPT, `file=${semaphorePath}`], + _testExecOptions + ) expect(exitCode).toBe(0) expect( @@ -411,36 +401,21 @@ describe('@actions/exec', () => { fs.writeFileSync(semaphorePath, '') const nodePath = await io.which('node', true) - const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js') const debugList: string[] = [] const _testExecOptions = getExecOptions() _testExecOptions.delay = 500 - _testExecOptions.windowsVerbatimArguments = true _testExecOptions.listeners = { debug: (data: string) => { debugList.push(data) } } - let toolName: string - let args: string[] - if (IS_WINDOWS) { - toolName = await io.which('cmd.exe', true) - args = [ - '/D', // Disable execution of AutoRun commands from registry. - '/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. - '/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. - '/S', // Will cause first and last quote after /C to be stripped. - '/C', - `"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}"" & exit /b 123` - ] - } else { - toolName = await io.which('bash', true) - args = ['-c', `node '${scriptPath}' 'file=${semaphorePath}' & exit 123`] - } - await exec - .exec(`"${toolName}"`, args, _testExecOptions) + .exec( + `"${nodePath}"`, + [SPAWN_WAIT_SCRIPT, `file=${semaphorePath}`, 'exitCode=123'], + _testExecOptions + ) .then(() => { throw new Error('Should not have succeeded') }) @@ -465,40 +440,22 @@ describe('@actions/exec', () => { fs.writeFileSync(semaphorePath, '') const nodePath = await io.which('node', true) - const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js') const debugList: string[] = [] const _testExecOptions = getExecOptions() _testExecOptions.delay = 500 _testExecOptions.failOnStdErr = true - _testExecOptions.windowsVerbatimArguments = true _testExecOptions.listeners = { debug: (data: string) => { debugList.push(data) } } - let toolName: string - let args: string[] - if (IS_WINDOWS) { - toolName = await io.which('cmd.exe', true) - args = [ - '/D', // Disable execution of AutoRun commands from registry. - '/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. - '/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. - '/S', // Will cause first and last quote after /C to be stripped. - '/C', - `"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}"" & echo hi 1>&2` - ] - } else { - toolName = await io.which('bash', true) - args = [ - '-c', - `node '${scriptPath}' 'file=${semaphorePath}' & echo hi 1>&2` - ] - } - await exec - .exec(`"${toolName}"`, args, _testExecOptions) + .exec( + `"${nodePath}"`, + [SPAWN_WAIT_SCRIPT, `file=${semaphorePath}`, 'stderr=true'], + _testExecOptions + ) .then(() => { throw new Error('Should not have succeeded') }) @@ -515,7 +472,7 @@ describe('@actions/exec', () => { ).toBe(1) fs.unlinkSync(semaphorePath) - }) + }, 10000) it('Exec roots relative tool path using unrooted options.cwd', async () => { let exitCode: number diff --git a/__tests__/scripts/spawn-wait-for-file.js b/__tests__/scripts/spawn-wait-for-file.js new file mode 100644 index 0000000..720b784 --- /dev/null +++ b/__tests__/scripts/spawn-wait-for-file.js @@ -0,0 +1,44 @@ +const childProcess = require('child_process') +const path = require('path') + +// Parse args +const args = {} +for (const arg of process.argv.slice(2)) { + const idx = arg.indexOf('=') + if (idx !== -1) { + args[arg.slice(0, idx)] = arg.slice(idx + 1) + } +} + +const filePath = args.file +if (!filePath) { + throw new Error('file is not specified') +} + +// Spawn wait-for-file.js with inherited stdio +// This creates a grandchild process that holds the stdio handles open +// after this process (the child) exits +const waitScript = path.join(__dirname, 'wait-for-file.js') +const isWindows = process.platform === 'win32' + +// On Windows, use detached:true to properly keep streams open +// On Unix, detached:true also works and creates a new process group +const child = childProcess.spawn(process.execPath, [waitScript, `file=${filePath}`], { + stdio: ['ignore', 'inherit', 'inherit'], + detached: true, + windowsHide: true +}) + +// Don't wait for child to exit +child.unref() + +// Handle optional stderr output (must happen BEFORE we exit) +if (args.stderr === 'true') { + process.stderr.write('hi') +} + +// Small delay to ensure child has started and inherited handles +setTimeout(() => { + const exitCode = args.exitCode ? parseInt(args.exitCode, 10) : 0 + process.exit(exitCode) +}, 50)