From bff4def1dd5797efdbde1efb48dc2231ad496206 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Tue, 29 Aug 2023 02:39:02 -0700 Subject: [PATCH 1/2] refactor: replace `Q` with native promises (#905) * refactor: replace `Q` with native promises * Changed how promises were deferred to fix failing tests * Bumped version to 5.0.0 * Switched to using promise constructors instead of custom Deferred class * Continue support of Q until the release of v5. Marked the methods that make use of Q as deprecated for now * Added JSDoc deprecation annotation * Updated version and changelog * Updated changelog message * Apply review suggestion Co-authored-by: Jamie Magee * Tests for execAsync --------- Co-authored-by: Aleksandr Levochkin Co-authored-by: Aleksandr Levochkin <107044793+aleksandrlevochkin@users.noreply.github.com> Co-authored-by: Aleksandr Levockin (Akvelon INC) --- node/CHANGELOG.md | 4 + node/mock-task.ts | 13 +- node/mock-toolrunner.ts | 110 +- node/package-lock.json | 10 +- node/package.json | 4 +- node/task.ts | 28 + node/test/toolrunnerTestsWithExecAsync.ts | 1632 +++++++++++++++++++++ node/test/toolrunnertests.ts | 110 +- node/test/tsconfig.json | 1 + node/toolrunner.ts | 315 +++- node/vault.ts | 1 - 11 files changed, 2159 insertions(+), 69 deletions(-) create mode 100644 node/test/toolrunnerTestsWithExecAsync.ts diff --git a/node/CHANGELOG.md b/node/CHANGELOG.md index 6dfd4d457..2e79dd0c6 100644 --- a/node/CHANGELOG.md +++ b/node/CHANGELOG.md @@ -48,3 +48,7 @@ Backported from ver.`3.4.0`: ## 4.4.0 - Add `getBoolFeatureFlag` [#936](https://github.com/microsoft/azure-pipelines-task-lib/pull/936) + +## 4.5.0 + +- Added `execAsync` methods that return native promises. Marked `exec` methods that return promises from the Q library as deprecated [#905](https://github.com/microsoft/azure-pipelines-task-lib/pull/905) diff --git a/node/mock-task.ts b/node/mock-task.ts index 8b2b0d37f..c6a77ee3f 100644 --- a/node/mock-task.ts +++ b/node/mock-task.ts @@ -1,4 +1,3 @@ - import Q = require('q'); import path = require('path'); import fs = require('fs'); @@ -335,6 +334,18 @@ export function exec(tool: string, args: any, options?: trm.IExecOptions): Q.Pro return tr.exec(options); } +//----------------------------------------------------- +// Exec convenience wrapper +//----------------------------------------------------- +export function execAsync(tool: string, args: any, options?: trm.IExecOptions): Promise { + var toolPath = which(tool, true); + var tr: trm.ToolRunner = this.tool(toolPath); + if (args) { + tr.arg(args); + } + return tr.execAsync(options); +} + export function execSync(tool: string, args: any, options?: trm.IExecSyncOptions): trm.IExecSyncResult { var toolPath = which(tool, true); var tr: trm.ToolRunner = this.tool(toolPath); diff --git a/node/mock-toolrunner.ts b/node/mock-toolrunner.ts index 4c6c9d237..8f5a3380a 100644 --- a/node/mock-toolrunner.ts +++ b/node/mock-toolrunner.ts @@ -1,4 +1,3 @@ - import Q = require('q'); import os = require('os'); import events = require('events'); @@ -162,6 +161,113 @@ export class ToolRunner extends events.EventEmitter { // Exec - use for long running tools where you need to stream live output as it runs // returns a promise with return code. // + public execAsync(options?: IExecOptions): Promise { + this._debug('exec tool: ' + this.toolPath); + this._debug('Arguments:'); + this.args.forEach((arg) => { + this._debug(' ' + arg); + }); + + var success = true; + options = options || {}; + + var ops: IExecOptions = { + cwd: options.cwd || process.cwd(), + env: options.env || process.env, + silent: options.silent || false, + outStream: options.outStream || process.stdout, + errStream: options.errStream || process.stderr, + failOnStdErr: options.failOnStdErr || false, + ignoreReturnCode: options.ignoreReturnCode || false, + windowsVerbatimArguments: options.windowsVerbatimArguments + }; + + var argString = this.args.join(' ') || ''; + var cmdString = this.toolPath; + if (argString) { + cmdString += (' ' + argString); + } + + // Using split/join to replace the temp path + cmdString = this.ignoreTempPath(cmdString); + + if (!ops.silent) { + if(this.pipeOutputToTool) { + var pipeToolArgString = this.pipeOutputToTool.args.join(' ') || ''; + var pipeToolCmdString = this.ignoreTempPath(this.pipeOutputToTool.toolPath); + if(pipeToolArgString) { + pipeToolCmdString += (' ' + pipeToolArgString); + } + + cmdString += ' | ' + pipeToolCmdString; + } + + ops.outStream.write('[command]' + cmdString + os.EOL); + } + + // TODO: filter process.env + var res = mock.getResponse('exec', cmdString, debug); + if (res.stdout) { + this.emit('stdout', res.stdout); + if (!ops.silent) { + ops.outStream.write(res.stdout + os.EOL); + } + const stdLineArray = res.stdout.split(os.EOL); + for (const line of stdLineArray.slice(0, -1)) { + this.emit('stdline', line); + } + if(stdLineArray.length > 0 && stdLineArray[stdLineArray.length - 1].length > 0) { + this.emit('stdline', stdLineArray[stdLineArray.length - 1]); + } + } + + if (res.stderr) { + this.emit('stderr', res.stderr); + + success = !ops.failOnStdErr; + if (!ops.silent) { + var s = ops.failOnStdErr ? ops.errStream : ops.outStream; + s.write(res.stderr + os.EOL); + } + const stdErrArray = res.stderr.split(os.EOL); + for (const line of stdErrArray.slice(0, -1)) { + this.emit('errline', line); + } + if (stdErrArray.length > 0 && stdErrArray[stdErrArray.length - 1].length > 0) { + this.emit('errline', stdErrArray[stdErrArray.length - 1]); + } + } + + + var code = res.code; + + if (!ops.silent) { + ops.outStream.write('rc:' + res.code + os.EOL); + } + + if (code != 0 && !ops.ignoreReturnCode) { + success = false; + } + + if (!ops.silent) { + ops.outStream.write('success:' + success + os.EOL); + } + + return new Promise((resolve, reject) => { + if (success) { + resolve(code); + } + else { + reject(new Error(this.toolPath + ' failed with return code: ' + code)); + } + }); + } + + /** + * Exec - use for long running tools where you need to stream live output as it runs + * @deprecated use `execAsync` instead + * @returns a promise with return code. + */ public exec(options?: IExecOptions): Q.Promise { var defer = Q.defer(); @@ -270,8 +376,6 @@ export class ToolRunner extends events.EventEmitter { // but also has limits. For example, no live output and limited to max buffer // public execSync(options?: IExecSyncOptions): IExecSyncResult { - var defer = Q.defer(); - this._debug('exec tool: ' + this.toolPath); this._debug('Arguments:'); this.args.forEach((arg) => { diff --git a/node/package-lock.json b/node/package-lock.json index dbe46d6c4..21cad67f2 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -1,6 +1,6 @@ { "name": "azure-pipelines-task-lib", - "version": "4.4.0", + "version": "4.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -49,9 +49,9 @@ "dev": true }, "@types/node": { - "version": "16.11.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.39.tgz", - "integrity": "sha512-K0MsdV42vPwm9L6UwhIxMAOmcvH/1OoVkZyCgEtVu4Wx7sElGloy/W7kMBNe/oJ7V/jW9BVt1F6RahH6e7tPXw==" + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" }, "@types/q": { "version": "1.5.4", @@ -764,7 +764,7 @@ "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" }, "qs": { "version": "6.11.1", diff --git a/node/package.json b/node/package.json index 31484d447..cf32c9e09 100644 --- a/node/package.json +++ b/node/package.json @@ -1,6 +1,6 @@ { "name": "azure-pipelines-task-lib", - "version": "4.4.0", + "version": "4.5.0", "description": "Azure Pipelines Task SDK", "main": "./task.js", "typings": "./task.d.ts", @@ -39,7 +39,7 @@ "@types/minimatch": "3.0.3", "@types/mocha": "^9.1.1", "@types/mockery": "^1.4.29", - "@types/node": "^16.11.39", + "@types/node": "^10.17.0", "@types/q": "^1.5.4", "@types/semver": "^7.3.4", "@types/shelljs": "^0.8.8", diff --git a/node/task.ts b/node/task.ts index 42ac50ccd..a07af639b 100644 --- a/node/task.ts +++ b/node/task.ts @@ -1447,6 +1447,34 @@ export function rmRF(inputPath: string): void { * @param options optional exec options. See IExecOptions * @returns number */ +export function execAsync(tool: string, args: any, options?: trm.IExecOptions): Promise { + let tr: trm.ToolRunner = this.tool(tool); + tr.on('debug', (data: string) => { + debug(data); + }); + + if (args) { + if (args instanceof Array) { + tr.arg(args); + } + else if (typeof (args) === 'string') { + tr.line(args) + } + } + return tr.execAsync(options); +} + +/** + * Exec a tool. Convenience wrapper over ToolRunner to exec with args in one call. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @deprecated Use the {@link execAsync} method that returns a native Javascript Promise instead + * @param tool path to tool to exec + * @param args an arg string or array of args + * @param options optional exec options. See IExecOptions + * @returns number + */ export function exec(tool: string, args: any, options?: trm.IExecOptions): Q.Promise { let tr: trm.ToolRunner = this.tool(tool); tr.on('debug', (data: string) => { diff --git a/node/test/toolrunnerTestsWithExecAsync.ts b/node/test/toolrunnerTestsWithExecAsync.ts new file mode 100644 index 000000000..1b532e092 --- /dev/null +++ b/node/test/toolrunnerTestsWithExecAsync.ts @@ -0,0 +1,1632 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import assert = require('assert'); +import child_process = require('child_process'); +import fs = require('fs'); +import path = require('path'); +import os = require('os'); +import stream = require('stream'); +import * as tl from '../_build/task'; +import * as trm from '../_build/toolrunner'; + +import testutil = require('./testutil'); + +describe('Toolrunner Tests With ExecAsync', function () { + + before(function (done) { + try { + testutil.initialize(); + } + catch (err) { + assert.fail('Failed to load task lib: ' + err.message); + } + done(); + }); + + after(function () { + + }); + + it('Exec convenience with stdout', function (done) { + this.timeout(10000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + if (os.platform() === 'win32') { + tl.execAsync('cmd', '/c echo \'azure-pipelines-task-lib\'', _testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of cmd should be 0'); + done(); + }) + .catch(function (err) { + done(err); + }); + } + else { + tl.execAsync('ls', '-l -a', _testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of ls should be 0'); + done(); + }) + .catch(function (err) { + done(err); + }); + } + }) + it('ToolRunner writes debug', function (done) { + this.timeout(10000); + + var stdStream = testutil.createStringStream(); + tl.setStdStream(stdStream); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + if (os.platform() === 'win32') { + var cmdPath = tl.which('cmd', true); + var cmd = tl.tool(cmdPath); + cmd.arg('/c echo \'azure-pipelines-task-lib\''); + + cmd.execAsync(_testExecOptions) + .then(function (code) { + var contents = stdStream.getContents(); + assert(contents.indexOf('exec tool: ' + cmdPath) >= 0, 'should exec cmd'); + assert.equal(code, 0, 'return code of cmd should be 0'); + done(); + }) + .catch(function (err) { + done(err); + }); + } + else { + var ls = tl.tool(tl.which('ls', true)); + ls.arg('-l'); + ls.arg('-a'); + + ls.execAsync(_testExecOptions) + .then(function (code) { + var contents = stdStream.getContents(); + const usr = os.platform() === 'linux' ? '/usr' : ''; + assert(contents.indexOf(`exec tool: ${usr}/bin/ls`) >= 0, 'should exec ls'); + assert.equal(code, 0, 'return code of ls should be 0'); + done(); + }) + .catch(function (err) { + done(err); + }); + } + }) + it('Execs with stdout', function (done) { + this.timeout(10000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + var output = ''; + if (os.platform() === 'win32') { + var cmd = tl.tool(tl.which('cmd', true)) + .arg('/c') + .arg('echo \'azure-pipelines-task-lib\''); + + cmd.on('stdout', (data) => { + output = data.toString(); + }); + + cmd.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of cmd should be 0'); + assert(output && output.length > 0, 'should have emitted stdout'); + done(); + }) + .catch(function (err) { + done(err); + }); + } + else { + var ls = tl.tool(tl.which('ls', true)); + ls.arg('-l'); + ls.arg('-a'); + + ls.on('stdout', (data) => { + output = data.toString(); + }); + + ls.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of ls should be 0'); + assert(output && output.length > 0, 'should have emitted stdout'); + done(); + }) + .catch(function (err) { + done(err); + }); + } + }) + it('Fails on return code 1 with stderr', function (done) { + this.timeout(10000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + if (os.platform() === 'win32') { + var cmd = tl.tool(tl.which('cmd', true)); + cmd.arg('/c notExist'); + + var output = ''; + cmd.on('stderr', (data) => { + output = data.toString(); + }); + + var succeeded = false; + cmd.execAsync(_testExecOptions) + .then(function (code) { + succeeded = true; + assert.fail('should not have succeeded'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(err.message.indexOf('failed with exit code 1') >= 0, `expected error message to indicate "failed with exit code 1". actual error message: "${err}"`); + assert(output && output.length > 0, 'should have emitted stderr'); + done(); + } + }) + .catch(function (err) { + done(err); + }); + } + else { + var bash = tl.tool(tl.which('bash', true)); + bash.arg('--noprofile'); + bash.arg('--norc'); + bash.arg('-c'); + bash.arg('echo hello from STDERR 1>&2 ; exit 123'); + var output = ''; + bash.on('stderr', (data) => { + output = data.toString(); + }); + + var succeeded = false; + bash.execAsync(_testExecOptions) + .then(function () { + succeeded = true; + assert.fail('should not have succeeded'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(err.message.indexOf('failed with exit code 123') >= 0, `expected error message to indicate "failed with exit code 123". actual error message: "${err}"`); + assert(output && output.length > 0, 'should have emitted stderr'); + done(); + } + }) + .catch(function (err) { + done(err); + }); + } + }) + it('Succeeds on stderr by default', function (done) { + this.timeout(10000); + + var scriptPath = path.join(__dirname, 'scripts', 'stderroutput.js'); + var ls = tl.tool(tl.which('node', true)); + ls.arg(scriptPath); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + ls.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'should have succeeded on stderr'); + done(); + }) + .catch(function (err) { + done(new Error('did not succeed on stderr')) + }) + }) + it('Fails on stderr if specified', function (done) { + this.timeout(10000); + + var scriptPath = path.join(__dirname, 'scripts', 'stderroutput.js'); + var node = tl.tool(tl.which('node', true)) + .arg(scriptPath); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: true, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + } + + var output = ''; + node.on('stderr', (data) => { + output = data.toString(); + }); + + var succeeded = false; + node.execAsync(_testExecOptions) + .then(function () { + succeeded = true; + assert.fail('should not have succeeded'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(err.message.indexOf('one or more lines were written to the STDERR stream') >= 0, `expected error message to indicate "one or more lines were written to the STDERR stream". actual error message: "${err}"`); + assert(output && output.length > 0, 'should have emitted stderr'); + done(); + } + }) + .catch(function (err) { + done(err); + }); + }) + it('Fails when process fails to launch', function (done) { + this.timeout(10000); + + var tool = tl.tool(tl.which('node', true)); + var _testExecOptions = { + cwd: path.join(testutil.getTestTemp(), 'nosuchdir'), + env: {}, + silent: false, + failOnStdErr: true, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + } + + var output = ''; + tool.on('stderr', (data) => { + output = data.toString(); + }); + + var succeeded = false; + tool.execAsync(_testExecOptions) + .then(function () { + succeeded = true; + assert.fail('should not have succeeded'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(err.message.indexOf('This may indicate the process failed to start') >= 0, `expected error message to indicate "This may indicate the process failed to start". actual error message: "${err}"`); + done(); + } + }) + .catch(function (err) { + done(err); + }); + }) + it('Handles child process holding streams open', function (done) { + this.timeout(10000); + + let semaphorePath = path.join(testutil.getTestTemp(), 'child-process-semaphore.txt'); + fs.writeFileSync(semaphorePath, ''); + + let nodePath = tl.which('node', true); + let scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js'); + let shell: trm.ToolRunner; + if (os.platform() == 'win32') { + shell = tl.tool(tl.which('cmd.exe', true)) + .arg('/D') // Disable execution of AutoRun commands from registry. + .arg('/E:ON') // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. + .arg('/V:OFF') // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. + .arg('/S') // Will cause first and last quote after /C to be stripped. + .arg('/C') + .arg(`"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}""`); + } + else { + shell = tl.tool(tl.which('bash', true)) + .arg('-c') + .arg(`node '${scriptPath}' 'file=${semaphorePath}' &`); + } + + let toolRunnerDebug = []; + shell.on('debug', function (data) { + toolRunnerDebug.push(data); + }); + + process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY'] = "500"; // 0.5 seconds + + let options = { + cwd: __dirname, + env: process.env, + silent: false, + failOnStdErr: true, + ignoreReturnCode: false, + outStream: process.stdout, + errStream: process.stdout, + windowsVerbatimArguments: true + }; + + shell.execAsync(options) + .then(function () { + assert(toolRunnerDebug.filter((x) => x.indexOf('STDIO streams did not close') >= 0).length == 1, 'Did not find expected debug message'); + done(); + }) + .catch(function (err) { + done(err); + }) + .finally(function () { + fs.unlinkSync(semaphorePath); + delete process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY']; + }); + }) + it('Handles child process holding streams open and non-zero exit code', function (done) { + this.timeout(10000); + + let semaphorePath = path.join(testutil.getTestTemp(), 'child-process-semaphore.txt'); + fs.writeFileSync(semaphorePath, ''); + + let nodePath = tl.which('node', true); + let scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js'); + let shell: trm.ToolRunner; + if (os.platform() == 'win32') { + shell = tl.tool(tl.which('cmd.exe', true)) + .arg('/D') // Disable execution of AutoRun commands from registry. + .arg('/E:ON') // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. + .arg('/V:OFF') // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. + .arg('/S') // Will cause first and last quote after /C to be stripped. + .arg('/C') + .arg(`"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}"" & exit /b 123`); + } + else { + shell = tl.tool(tl.which('bash', true)) + .arg('-c') + .arg(`node '${scriptPath}' 'file=${semaphorePath}' & exit 123`); + } + + let toolRunnerDebug = []; + shell.on('debug', function (data) { + toolRunnerDebug.push(data); + }); + + process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY'] = "500"; // 0.5 seconds + + let options = { + cwd: __dirname, + env: process.env, + silent: false, + failOnStdErr: true, + ignoreReturnCode: false, + outStream: process.stdout, + errStream: process.stdout, + windowsVerbatimArguments: true + }; + + shell.execAsync(options) + .then(function () { + done(new Error('should not have been successful')); + done(); + }) + .catch(function (err) { + assert(toolRunnerDebug.filter((x) => x.indexOf('STDIO streams did not close') >= 0).length == 1, 'Did not find expected debug message'); + assert(err.message.indexOf('failed with exit code 123') >= 0); + done(); + }) + .catch(function (err) { + done(err); + }) + .finally(function () { + fs.unlinkSync(semaphorePath); + delete process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY']; + }); + }) + it('Handles child process holding streams open and stderr', function (done) { + this.timeout(10000); + + let semaphorePath = path.join(testutil.getTestTemp(), 'child-process-semaphore.txt'); + fs.writeFileSync(semaphorePath, ''); + + let nodePath = tl.which('node', true); + let scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js'); + let shell: trm.ToolRunner; + if (os.platform() == 'win32') { + shell = tl.tool(tl.which('cmd.exe', true)) + .arg('/D') // Disable execution of AutoRun commands from registry. + .arg('/E:ON') // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. + .arg('/V:OFF') // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. + .arg('/S') // Will cause first and last quote after /C to be stripped. + .arg('/C') + .arg(`"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}"" & echo hi 1>&2`); + } + else { + shell = tl.tool(tl.which('bash', true)) + .arg('-c') + .arg(`node '${scriptPath}' 'file=${semaphorePath}' & echo hi 1>&2`); + } + + let toolRunnerDebug = []; + shell.on('debug', function (data) { + toolRunnerDebug.push(data); + }); + + process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY'] = "500"; // 0.5 seconds + + let options = { + cwd: __dirname, + env: process.env, + silent: false, + failOnStdErr: true, + ignoreReturnCode: false, + outStream: process.stdout, + errStream: process.stdout, + windowsVerbatimArguments: true + }; + + shell.execAsync(options) + .then(function () { + done(new Error('should not have been successful')); + done(); + }) + .catch(function (err) { + assert(toolRunnerDebug.filter((x) => x.indexOf('STDIO streams did not close') >= 0).length == 1, 'Did not find expected debug message'); + assert(err.message.indexOf('failed because one or more lines were written to the STDERR stream') >= 0); + done(); + }) + .catch(function (err) { + done(err); + }) + .finally(function () { + fs.unlinkSync(semaphorePath); + delete process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY']; + }); + }) + it('Exec pipe output to another tool, succeeds if both tools succeed', function (done) { + this.timeout(30000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + if (os.platform() === 'win32') { + var matchExe = tl.tool(compileMatchExe()) + .arg('0') // exit code + .arg('line 2'); // match value + var outputExe = tl.tool(compileOutputExe()) + .arg('0') // exit code + .arg('line 1') + .arg('line 2') + .arg('line 3'); + outputExe.pipeExecOutputToTool(matchExe); + + var output = ''; + outputExe.on('stdout', (data) => { + output += data.toString(); + }); + + outputExe.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of exec should be 0'); + assert(output && output.length > 0 && output.indexOf('line 2') >= 0, 'should have emitted stdout ' + output); + done(); + }) + .catch(function (err) { + done(err); + }); + } + else { + var grep = tl.tool(tl.which('grep', true)); + grep.arg('node'); + + var ps = tl.tool(tl.which('ps', true)); + ps.arg('ax'); + ps.pipeExecOutputToTool(grep); + + var output = ''; + ps.on('stdout', (data) => { + output += data.toString(); + }); + + ps.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of exec should be 0'); + assert(output && output.length > 0 && output.indexOf('node') >= 0, 'should have emitted stdout ' + output); + done(); + }) + .catch(function (err) { + done(err); + }); + } + }) + it('Exec pipe output to another tool, fails if first tool fails', function (done) { + this.timeout(20000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + if (os.platform() === 'win32') { + var matchExe = tl.tool(compileMatchExe()) + .arg('0') // exit code + .arg('line 2'); // match value + var outputExe = tl.tool(compileOutputExe()) + .arg('1') // exit code + .arg('line 1') + .arg('line 2') + .arg('line 3'); + outputExe.pipeExecOutputToTool(matchExe); + + var output = ''; + outputExe.on('stdout', (data) => { + output += data.toString(); + }); + + var succeeded = false; + outputExe.execAsync(_testExecOptions) + .then(function () { + succeeded = true; + assert.fail('print-output.exe | findstr "line 2" was a bad command and it did not fail'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(err && err.message && err.message.indexOf('print-output.exe') >= 0, 'error from print-output.exe is not reported'); + done(); + } + }) + .catch(function (err) { + done(err); + }); + } + else { + var grep = tl.tool(tl.which('grep', true)); + grep.arg('ssh'); + + var ps = tl.tool(tl.which('ps', true)); + ps.arg('bad'); + ps.pipeExecOutputToTool(grep); + + var output = ''; + ps.on('stdout', (data) => { + output += data.toString(); + }); + + var succeeded = false; + ps.execAsync(_testExecOptions) + .then(function () { + succeeded = true; + assert.fail('ps bad | grep ssh was a bad command and it did not fail'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + //assert(output && output.length > 0 && output.indexOf('ps: illegal option') >= 0, `error output "ps: illegal option" is expected. actual "${output}"`); + assert(err && err.message && err.message.indexOf('/bin/ps') >= 0, 'error from ps is not reported'); + done(); + } + }) + .catch(function (err) { + done(err); + }) + } + }) + it('Exec pipe output to another tool, fails if second tool fails', function (done) { + this.timeout(20000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + if (os.platform() === 'win32') { + var matchExe = tl.tool(compileMatchExe()) + .arg('1') // exit code + .arg('line 2') // match value + .arg('some error message'); // error + var outputExe = tl.tool(compileOutputExe()) + .arg('0') // exit code + .arg('line 1') + .arg('line 2') + .arg('line 3'); + outputExe.pipeExecOutputToTool(matchExe); + + var output = ''; + outputExe.on('stdout', (data) => { + output += data.toString(); + }); + + var errOut = ''; + outputExe.on('stderr', (data) => { + errOut += data.toString(); + }); + + var succeeded = false; + outputExe.execAsync(_testExecOptions) + .then(function (code) { + succeeded = true; + assert.fail('print-output.exe 0 "line 1" "line 2" "line 3" | match-input.exe 1 "line 2" "some error message" was a bad command and it did not fail'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(errOut && errOut.length > 0 && errOut.indexOf('some error message') >= 0, 'error output from match-input.exe is expected'); + assert(err && err.message && err.message.indexOf('match-input.exe') >= 0, 'error from find does not match expeced. actual: ' + err.message); + done(); + } + }) + .catch(function (err) { + done(err); + }); + } + else { + var grep = tl.tool(tl.which('grep', true)); + grep.arg('--?'); + + var node = tl.tool(tl.which('node', true)) + .arg('-e') + .arg('console.log("line1"); setTimeout(function () { console.log("line2"); }, 200);'); // allow long enough to hook up stdout to stdin + node.pipeExecOutputToTool(grep); + + var output = ''; + node.on('stdout', (data) => { + output += data.toString(); + }); + + var errOut = ''; + node.on('stderr', (data) => { + errOut += data.toString(); + }) + + var succeeded = false; + node.execAsync(_testExecOptions) + .then(function (code) { + succeeded = true; + assert.fail('node [...] | grep --? was a bad command and it did not fail'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(errOut && errOut.length > 0 && errOut.indexOf('grep: unrecognized option') >= 0, 'error output from ps command is expected'); + // grep is /bin/grep on Linux and /usr/bin/grep on OSX + assert(err && err.message && err.message.match(/\/[usr\/]?bin\/grep/), 'error from grep is not reported. actual: ' + err.message); + done(); + } + }) + .catch(function (err) { + done(err); + }); + } + }) + it('Exec pipe output to file and another tool, succeeds if both tools succeed', function (done) { + this.timeout(20000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + const testFile = path.join(testutil.getTestTemp(), 'BothToolsSucceed.log'); + + if (os.platform() === 'win32') { + var matchExe = tl.tool(compileMatchExe()) + .arg('0') // exit code + .arg('line 2'); // match value + var outputExe = tl.tool(compileOutputExe()) + .arg('0') // exit code + .arg('line 1') + .arg('line 2') + .arg('line 3'); + outputExe.pipeExecOutputToTool(matchExe, testFile); + + var output = ''; + outputExe.on('stdout', (data) => { + output += data.toString(); + }); + + outputExe.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of exec should be 0'); + assert(output && output.length > 0 && output.indexOf('line 2') >= 0, 'should have emitted stdout ' + output); + assert(fs.existsSync(testFile), 'Log of first tool output is created when both tools succeed'); + const fileContents = fs.readFileSync(testFile); + assert(fileContents.indexOf('line 2') >= 0, 'Log file of first tool should have stdout from first tool: ' + fileContents); + done(); + }) + .catch(function (err) { + done(err); + }); + } + else { + var grep = tl.tool(tl.which('grep', true)); + grep.arg('node'); + + var ps = tl.tool(tl.which('ps', true)); + ps.arg('ax'); + ps.pipeExecOutputToTool(grep, testFile); + + var output = ''; + ps.on('stdout', (data) => { + output += data.toString(); + }); + + ps.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of exec should be 0'); + assert(output && output.length > 0 && output.indexOf('node') >= 0, 'should have emitted stdout ' + output); + assert(fs.existsSync(testFile), 'Log of first tool output is created when both tools succeed'); + const fileContents = fs.readFileSync(testFile); + assert(fileContents.indexOf('PID') >= 0, 'Log of first tool should have stdout from first tool: ' + fileContents); + done(); + }) + .catch(function (err) { + done(err); + }); + } + }) + it('Exec pipe output to file and another tool, fails if first tool fails', function (done) { + this.timeout(20000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + const testFile = path.join(testutil.getTestTemp(), 'FirstToolFails.log'); + + if (os.platform() === 'win32') { + var matchExe = tl.tool(compileMatchExe()) + .arg('0') // exit code + .arg('line 2'); // match value + var outputExe = tl.tool(compileOutputExe()) + .arg('1') // exit code + .arg('line 1') + .arg('line 2') + .arg('line 3'); + outputExe.pipeExecOutputToTool(matchExe, testFile); + + var output = ''; + outputExe.on('stdout', (data) => { + output += data.toString(); + }); + + var succeeded = false; + outputExe.execAsync(_testExecOptions) + .then(function () { + succeeded = true; + assert.fail('print-output.exe | findstr "line 2" was a bad command and it did not fail'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(err && err.message && err.message.indexOf('print-output.exe') >= 0, 'error from print-output.exe is not reported'); + assert(fs.existsSync(testFile), 'Log of first tool output is created when first tool fails'); + const fileContents = fs.readFileSync(testFile); + assert(fileContents.indexOf('line 3') >= 0, 'Error from first tool should be written to log file: ' + fileContents); + done(); + } + }) + .catch(function (err) { + done(err); + }); + } + else { + var grep = tl.tool(tl.which('grep', true)); + grep.arg('ssh'); + + var ps = tl.tool(tl.which('ps', true)); + ps.arg('bad'); + ps.pipeExecOutputToTool(grep, testFile); + + var output = ''; + ps.on('stdout', (data) => { + output += data.toString(); + }); + + var succeeded = false; + ps.execAsync(_testExecOptions) + .then(function () { + succeeded = true; + assert.fail('ps bad | grep ssh was a bad command and it did not fail'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(err && err.message && err.message.indexOf('/bin/ps') >= 0, 'error from ps is not reported'); + assert(fs.existsSync(testFile), 'Log of first tool output is created when first tool fails'); + const fileContents = fs.readFileSync(testFile); + assert(fileContents.indexOf('illegal option') >= 0 || fileContents.indexOf('unsupported option') >= 0, + 'error from first tool should be written to log file: ' + fileContents); + done(); + } + }) + .catch(function (err) { + done(err); + }) + } + }) + it('Exec pipe output to file and another tool, fails if second tool fails', function (done) { + this.timeout(20000); + + var _testExecOptions = { + cwd: __dirname, + env: {}, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + const testFile = path.join(testutil.getTestTemp(), 'SecondToolFails.log'); + + if (os.platform() === 'win32') { + var matchExe = tl.tool(compileMatchExe()) + .arg('1') // exit code + .arg('line 2') // match value + .arg('some error message'); // error + var outputExe = tl.tool(compileOutputExe()) + .arg('0') // exit code + .arg('line 1') + .arg('line 2') + .arg('line 3'); + outputExe.pipeExecOutputToTool(matchExe, testFile); + + var output = ''; + outputExe.on('stdout', (data) => { + output += data.toString(); + }); + + var errOut = ''; + outputExe.on('stderr', (data) => { + errOut += data.toString(); + }); + + var succeeded = false; + outputExe.execAsync(_testExecOptions) + .then(function (code) { + succeeded = true; + assert.fail('print-output.exe 0 "line 1" "line 2" "line 3" | match-input.exe 1 "line 2" "some error message" was a bad command and it did not fail'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(errOut && errOut.length > 0 && errOut.indexOf('some error message') >= 0, 'error output from match-input.exe is expected'); + assert(err && err.message && err.message.indexOf('match-input.exe') >= 0, 'error from find does not match expeced. actual: ' + err.message); + assert(fs.existsSync(testFile), 'Log of first tool output is created when second tool fails'); + const fileContents = fs.readFileSync(testFile); + assert(fileContents.indexOf('some error message') < 0, 'error from second tool should not be in the log for first tool: ' + fileContents); + done(); + } + }) + .catch(function (err) { + done(err); + }); + } + else { + var grep = tl.tool(tl.which('grep', true)); + grep.arg('--?'); + + var ps = tl.tool(tl.which('ps', true)); + ps.arg('ax'); + ps.pipeExecOutputToTool(grep, testFile); + + var output = ''; + ps.on('stdout', (data) => { + output += data.toString(); + }); + + var errOut = ''; + ps.on('stderr', (data) => { + errOut += data.toString(); + }) + + var succeeded = false; + ps.execAsync(_testExecOptions) + .then(function (code) { + succeeded = true; + assert.fail('ps ax | grep --? was a bad command and it did not fail'); + }) + .catch(function (err) { + if (succeeded) { + done(err); + } + else { + assert(errOut && errOut.length > 0 && errOut.indexOf('grep: unrecognized option') >= 0, 'error output from ps command is expected'); + // grep is /bin/grep on Linux and /usr/bin/grep on OSX + assert(err && err.message && err.message.match(/\/[usr\/]?bin\/grep/), 'error from grep is not reported. actual: ' + err.message); + assert(fs.existsSync(testFile), 'Log of first tool output is created when second tool fails'); + const fileContents = fs.readFileSync(testFile); + assert(fileContents.indexOf('unrecognized option') < 0, 'error from second tool should not be in the first tool log file: ' + fileContents); + done(); + } + }) + .catch(function (err) { + done(err); + }); + } + }) + + if (process.platform != 'win32') { + it('exec prints [command] (OSX/Linux)', function (done) { + this.timeout(10000); + let bash = tl.tool(tl.which('bash')) + .arg('--norc') + .arg('--noprofile') + .arg('-c') + .arg('echo hello world'); + let outStream = testutil.createStringStream(); + let options = { outStream: outStream, windowsVerbatimArguments: true }; + let output = ''; + bash.on('stdout', (data) => { + output += data.toString(); + }); + bash.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + `[command]${tl.which('bash')} --norc --noprofile -c echo hello world`); + // validate stdout + assert.equal( + output.trim(), + 'hello world'); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + } + else { // process.platform == 'win32' + + // -------------------------- + // exec arg tests (Windows) + // -------------------------- + + it('exec .exe AND verbatim args (Windows)', function (done) { + this.timeout(10000); + + // the echo built-in is a good tool for this test + let exePath = process.env.ComSpec; + let exeRunner = tl.tool(exePath) + .arg('/c') + .arg('echo') + .arg('helloworld') + .arg('hello:"world again"'); + let outStream = testutil.createStringStream(); + let options = { outStream: outStream, windowsVerbatimArguments: true }; + let output = ''; + exeRunner.on('stdout', (data) => { + output += data.toString(); + }); + exeRunner.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + `[command]"${exePath}" /c echo helloworld hello:"world again"`); + // validate stdout + assert.equal( + output.trim(), + 'helloworld hello:"world again"'); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it('exec .exe AND arg quoting (Windows)', function (done) { + this.timeout(10000); + + // the echo built-in is a good tool for this test + let exePath = process.env.ComSpec; + let exeRunner = tl.tool(exePath) + .arg('/c') + .arg('echo') + .arg('helloworld') + .arg('hello world') + .arg('hello:"world again"') + .arg('hello,world'); // "," should not be quoted for .exe (should be for .cmd) + let outStream = testutil.createStringStream(); + let options = { outStream: outStream }; + let output = ''; + exeRunner.on('stdout', (data) => { + output += data.toString(); + }); + exeRunner.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + '[command]' + exePath + ' /c echo' + + ' helloworld' + + ' "hello world"' + + ' "hello:\\"world again\\""' + + ' hello,world'); + // validate stdout + assert.equal( + output.trim(), + 'helloworld' + + ' "hello world"' + + ' "hello:\\"world again\\""' + + ' hello,world'); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it('exec .exe with space AND verbatim args (Windows)', function (done) { + this.timeout(20000); + + // this test validates the quoting that tool runner adds around the tool path + // when using the windowsVerbatimArguments option. otherwise the target process + // interprets the args as starting after the first space in the tool path. + let exePath = compileArgsExe('print args exe with spaces.exe'); + let exeRunner = tl.tool(exePath) + .arg('myarg1 myarg2'); + let outStream = testutil.createStringStream(); + let options = { outStream: outStream, windowsVerbatimArguments: true }; + let output = ''; + exeRunner.on('stdout', (data) => { + output += data.toString(); + }); + exeRunner.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + `[command]"${exePath}" myarg1 myarg2`); + // validate stdout + assert.equal( + output.trim(), + "args[0]: 'args'\r\n" + + "args[1]: 'exe'\r\n" + + "args[2]: 'with'\r\n" + + "args[3]: 'spaces.exe'\r\n" + + "args[4]: 'myarg1'\r\n" + + "args[5]: 'myarg2'"); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it('exec .cmd with space AND verbatim args (Windows)', function (done) { + this.timeout(10000); + + // this test validates the quoting that tool runner adds around the script path. + // otherwise cmd.exe will not be able to resolve the path to the script. + let cmdPath = path.join(__dirname, 'scripts', 'print args cmd with spaces.cmd'); + let cmdRunner = tl.tool(cmdPath) + .arg('arg1 arg2') + .arg('arg3'); + let outStream = testutil.createStringStream(); + let options = { outStream: outStream, windowsVerbatimArguments: true }; + let output = ''; + cmdRunner.on('stdout', (data) => { + output += data.toString(); + }); + cmdRunner.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + `[command]${process.env.ComSpec} /D /S /C ""${cmdPath}" arg1 arg2 arg3"`); + // validate stdout + assert.equal( + output.trim(), + 'args[0]: "arg1"\r\n' + + 'args[1]: "arg2"\r\n' + + 'args[2]: "arg3"'); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it('exec .cmd with space AND arg with space (Windows)', function (done) { + this.timeout(10000); + + // this test validates the command is wrapped in quotes (i.e. cmd.exe /S /C ""). + // otherwise the leading quote (around the script with space path) would be stripped + // and cmd.exe would not be able to resolve the script path. + let cmdPath = path.join(__dirname, 'scripts', 'print args cmd with spaces.cmd'); + let cmdRunner = tl.tool(cmdPath) + .arg('my arg 1') + .arg('my arg 2'); + let outStream = testutil.createStringStream(); + let options = { outStream: outStream }; + let output = ''; + cmdRunner.on('stdout', (data) => { + output += data.toString(); + }); + cmdRunner.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + `[command]${process.env.ComSpec} /D /S /C ""${cmdPath}" "my arg 1" "my arg 2""`); + // validate stdout + assert.equal( + output.trim(), + 'args[0]: "my arg 1"\r\n' + + 'args[1]: "my arg 2"'); + done(); + }) + .catch(function (err) { + done(err); + }) + }); + + it('exec .cmd AND arg quoting (Windows)', function (done) { + this.timeout(10000); + + // this test validates .cmd quoting rules are applied, not the default libuv rules + let cmdPath = path.join(__dirname, 'scripts', 'print args cmd with spaces.cmd'); + let cmdRunner = tl.tool(cmdPath) + .arg('helloworld') + .arg('hello world') + .arg('hello\tworld') + .arg('hello&world') + .arg('hello(world') + .arg('hello)world') + .arg('hello[world') + .arg('hello]world') + .arg('hello{world') + .arg('hello}world') + .arg('hello^world') + .arg('hello=world') + .arg('hello;world') + .arg('hello!world') + .arg('hello\'world') + .arg('hello+world') + .arg('hello,world') + .arg('hello`world') + .arg('hello~world') + .arg('hello|world') + .arg('helloworld') + .arg('hello:"world again"') + .arg('hello world\\'); + let outStream = testutil.createStringStream(); + let options = { outStream: outStream }; + let output = ''; + cmdRunner.on('stdout', (data) => { + output += data.toString(); + }); + cmdRunner.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + '[command]' + process.env.ComSpec + ' /D /S /C ""' + cmdPath + '"' + + ' helloworld' + + ' "hello world"' + + ' "hello\tworld"' + + ' "hello&world"' + + ' "hello(world"' + + ' "hello)world"' + + ' "hello[world"' + + ' "hello]world"' + + ' "hello{world"' + + ' "hello}world"' + + ' "hello^world"' + + ' "hello=world"' + + ' "hello;world"' + + ' "hello!world"' + + ' "hello\'world"' + + ' "hello+world"' + + ' "hello,world"' + + ' "hello`world"' + + ' "hello~world"' + + ' "hello|world"' + + ' "helloworld"' + + ' "hello:""world again"""' + + ' "hello world\\\\"' + + '"'); + // validate stdout + assert.equal( + output.trim(), + 'args[0]: "helloworld"\r\n' + + 'args[1]: "hello world"\r\n' + + 'args[2]: "hello\tworld"\r\n' + + 'args[3]: "hello&world"\r\n' + + 'args[4]: "hello(world"\r\n' + + 'args[5]: "hello)world"\r\n' + + 'args[6]: "hello[world"\r\n' + + 'args[7]: "hello]world"\r\n' + + 'args[8]: "hello{world"\r\n' + + 'args[9]: "hello}world"\r\n' + + 'args[10]: "hello^world"\r\n' + + 'args[11]: "hello=world"\r\n' + + 'args[12]: "hello;world"\r\n' + + 'args[13]: "hello!world"\r\n' + + 'args[14]: "hello\'world"\r\n' + + 'args[15]: "hello+world"\r\n' + + 'args[16]: "hello,world"\r\n' + + 'args[17]: "hello`world"\r\n' + + 'args[18]: "hello~world"\r\n' + + 'args[19]: "hello|world"\r\n' + + 'args[20]: "hello"\r\n' + + 'args[21]: "hello>world"\r\n' + + 'args[22]: "hello:world again"\r\n' + + 'args[23]: "hello world\\\\"'); + done(); + }) + .catch(function (err) { + done(err); + }) + }); + + // ------------------------------- + // exec pipe arg tests (Windows) + // ------------------------------- + + it('exec pipe .cmd to .exe AND arg quoting (Windows)', function (done) { + this.timeout(10000); + + let cmdPath = path.join(__dirname, 'scripts', 'print args cmd with spaces.cmd'); + let cmdRunner = tl.tool(cmdPath) + .arg('"hello world"'); + + let exePath = path.join(process.env.windir, 'System32', 'find.exe'); + let exeRunner = tl.tool(exePath) + .arg('hello world'); + + let outStream = testutil.createStringStream(); + let options = { outStream: outStream }; + let output = ''; + cmdRunner.on('stdout', (data) => { + output += data.toString(); + }); + cmdRunner.pipeExecOutputToTool(exeRunner); + cmdRunner.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + '[command]' + process.env.ComSpec + ' /D /S /C ""' + cmdPath + '" """hello world""""' + + ' | ' + exePath + ' "hello world"'); + // validate stdout + assert.equal( + output.trim(), + 'args[0]: "hello world"'); + done(); + }) + .catch(function (err) { + done(err); + }) + }); + + it('exec pipe .cmd to .exe AND verbatim args (Windows)', function (done) { + this.timeout(10000); + + let cmdPath = path.join(__dirname, 'scripts', 'print args cmd with spaces.cmd'); + let cmdRunner = tl.tool(cmdPath) + .arg('hello world'); + + let exePath = path.join(process.env.windir, 'System32', 'find.exe'); + let exeRunner = tl.tool(exePath) + .arg('"world"'); + + let outStream = testutil.createStringStream(); + let options = { outStream: outStream, windowsVerbatimArguments: true }; + let output = ''; + cmdRunner.on('stdout', (data) => { + output += data.toString(); + }); + cmdRunner.pipeExecOutputToTool(exeRunner); + cmdRunner.execAsync(options) + .then(function (code) { + assert.equal(code, 0, 'return code should be 0'); + // validate the [command] header + assert.equal( + outStream.getContents().split(os.EOL)[0], + '[command]' + process.env.ComSpec + ' /D /S /C ""' + cmdPath + '" hello world"' + + ' | "' + exePath + '" "world"'); + // validate stdout + assert.equal( + output.trim(), + 'args[1]: "world"'); + done(); + }) + .catch(function (err) { + done(err); + }) + }); + } + + // function to compile a .NET program on Windows. + let compileExe = (sourceFileName: string, targetFileName: string): string => { + let directory = path.join(testutil.getTestTemp(), sourceFileName); + tl.mkdirP(directory); + let exePath = path.join(directory, targetFileName); + + // short-circuit if already compiled + try { + fs.statSync(exePath); + return exePath; + } + catch (err) { + if (err.code != 'ENOENT') { + throw err; + } + } + + let sourceFile = path.join(__dirname, 'scripts', sourceFileName); + let cscPath = 'C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe'; + fs.statSync(cscPath); + child_process.execFileSync( + cscPath, + [ + '/target:exe', + `/out:${exePath}`, + sourceFile + ]); + return exePath; + } + + describe('Executing inside shell', function () { + + let tempPath: string = testutil.getTestTemp(); + let _testExecOptions: trm.IExecOptions; + + before (function () { + _testExecOptions = { + cwd: __dirname, + env: { + WIN_TEST: 'test value', + TESTPATH: tempPath, + TEST_NODE: 'node', + TEST: 'test value' + }, + silent: false, + failOnStdErr: false, + ignoreReturnCode: false, + shell: true, + outStream: testutil.getNullStream(), + errStream: testutil.getNullStream() + }; + + }) + + it('Exec inside shell', function (done) { + this.timeout(10000); + + let output: string = ''; + if (os.platform() === 'win32') { + let exePath = compileArgsExe('print args with spaces.exe'); + let exeRunner = tl.tool(exePath); + exeRunner.line('%WIN_TEST%'); + exeRunner.on('stdout', (data) => { + output = data.toString(); + }); + exeRunner.execAsync(_testExecOptions).then(function (code) { + assert.equal(code, 0, 'return code of cmd should be 0'); + assert.equal(output.trim(), 'args[0]: \'test value\'', 'Command should return \"args[0]: \'test value\'\"'); + done(); + }) + .catch(function (err) { + done(err); + }); + } + else { + let statRunner = tl.tool('stat'); + statRunner.line('$TESTPATH'); + statRunner.on('stdout', (data) => { + output = data.toString(); + }); + statRunner.execAsync(_testExecOptions).then(function (code) { + assert.equal(code, 0, 'return code of stat should be 0'); + assert(output.includes(tempPath), `Result should include \'${tempPath}\'`); + done(); + }) + .catch(function (err) { + done(err); + }); + } + }); + it('Exec pipe output to another tool inside shell, succeeds if both tools succeed', function (done) { + this.timeout(30000); + + if (os.platform() === 'win32') { + const matchExe = tl.tool(compileMatchExe()) + .arg('0') // exit code + .arg('test value'); // match value + const outputExe = tl.tool(compileOutputExe()) + .arg('0') // exit code + .arg('line 1') + .arg('"%WIN_TEST%"') + .arg('line 3'); + outputExe.pipeExecOutputToTool(matchExe); + + let output = ''; + outputExe.on('stdout', (data) => { + output += data.toString(); + }); + + outputExe.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of exec should be 0'); + assert(output && output.length > 0 && output.indexOf('test value') >= 0, 'should have emitted stdout ' + output); + done(); + }) + .catch(function (err) { + done(err); + }); + } + else { + const grep = tl.tool(tl.which('grep', true)); + grep.arg('$TEST_NODE'); + + const ps = tl.tool(tl.which('ps', true)); + ps.arg('ax'); + ps.pipeExecOutputToTool(grep); + + let output = ''; + ps.on('stdout', (data) => { + output += data.toString(); + }); + + ps.execAsync(_testExecOptions) + .then(function (code) { + assert.equal(code, 0, 'return code of exec should be 0'); + assert(output && output.length > 0 && output.indexOf('node') >= 0, 'should have emitted stdout ' + output); + done(); + }) + .catch(function (err) { + done(err); + }); + } + }); + it('Should handle arguments with quotes properly', function (done) { + this.timeout(10000); + + let output: string = ''; + if (os.platform() === 'win32') { + let exePath = compileArgsExe('print args with spaces.exe'); + let exeRunner = tl.tool(exePath); + exeRunner.line('-TEST1="space test" "-TEST2=%WIN_TEST%" \'-TEST3=value\''); + exeRunner.on('stdout', (data) => { + output += data.toString(); + }); + exeRunner.execAsync(_testExecOptions).then(function (code) { + assert.equal(code, 0, 'return code of cmd should be 0'); + assert.equal(output.trim(), 'args[0]: \'-TEST1=space test\'\r\n' + + 'args[1]: \'-TEST2=test value\'\r\n' + + 'args[2]: \'\'-TEST3=value\'\''); + done(); + }) + .catch(function (err) { + done(err); + }); + } + else { + let statRunner = tl.tool('echo'); + statRunner.line('-TEST1="$TEST;test" "-TEST2=/one/two/three" \'-TEST3=out:$TEST\''); + statRunner.on('stdout', (data) => { + output = data.toString(); + }); + statRunner.execAsync(_testExecOptions).then(function (code) { + assert.equal(code, 0, 'return code of stat should be 0'); + assert.equal(output, '-TEST1=test value;test -TEST2=/one/two/three -TEST3=out:$TEST\n'); + done(); + }) + .catch(function (err) { + done(err); + }); + } + }); + }) + + // function to compile a .NET program that prints the command line args. + // the helper program is used to validate that command line args are passed correctly. + let compileArgsExe = (targetFileName: string): string => { + return compileExe('print-args-exe.cs', targetFileName); + } + + // function to compile a .NET program that matches input lines. + // the helper program is used on Windows to validate piping output between tools. + let compileMatchExe = (): string => { + return compileExe('match-input-exe.cs', 'match-input.exe'); + } + + // function to compile a .NET program that prints lines. + // the helper program is used on Windows to validate piping output between tools. + let compileOutputExe = (): string => { + return compileExe('print-output-exe.cs', 'print-output.exe'); + } +}); diff --git a/node/test/toolrunnertests.ts b/node/test/toolrunnertests.ts index f534a2787..ac0e0eeb1 100644 --- a/node/test/toolrunnertests.ts +++ b/node/test/toolrunnertests.ts @@ -135,7 +135,7 @@ describe('Toolrunner Tests', function () { assert.equal(code, 0, 'return code of cmd should be 0'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -145,7 +145,7 @@ describe('Toolrunner Tests', function () { assert.equal(code, 0, 'return code of ls should be 0'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -178,7 +178,7 @@ describe('Toolrunner Tests', function () { assert.equal(code, 0, 'return code of cmd should be 0'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -195,7 +195,7 @@ describe('Toolrunner Tests', function () { assert.equal(code, 0, 'return code of ls should be 0'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -229,7 +229,7 @@ describe('Toolrunner Tests', function () { assert(output && output.length > 0, 'should have emitted stdout'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -248,7 +248,7 @@ describe('Toolrunner Tests', function () { assert(output && output.length > 0, 'should have emitted stdout'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -281,7 +281,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('should not have succeeded'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -291,7 +291,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -312,7 +312,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('should not have succeeded'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -322,7 +322,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -348,7 +348,7 @@ describe('Toolrunner Tests', function () { assert.equal(code, 0, 'should have succeeded on stderr'); done(); }) - .fail(function (err) { + .catch(function (err) { done(new Error('did not succeed on stderr')) }) }) @@ -380,7 +380,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('should not have succeeded'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -390,7 +390,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); }) @@ -419,7 +419,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('should not have succeeded'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -428,7 +428,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); }) @@ -479,7 +479,7 @@ describe('Toolrunner Tests', function () { assert(toolRunnerDebug.filter((x) => x.indexOf('STDIO streams did not close') >= 0).length == 1, 'Did not find expected debug message'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }) .finally(function () { @@ -534,12 +534,12 @@ describe('Toolrunner Tests', function () { done(new Error('should not have been successful')); done(); }) - .fail(function (err) { + .catch(function (err) { assert(toolRunnerDebug.filter((x) => x.indexOf('STDIO streams did not close') >= 0).length == 1, 'Did not find expected debug message'); assert(err.message.indexOf('failed with exit code 123') >= 0); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }) .finally(function () { @@ -594,12 +594,12 @@ describe('Toolrunner Tests', function () { done(new Error('should not have been successful')); done(); }) - .fail(function (err) { + .catch(function (err) { assert(toolRunnerDebug.filter((x) => x.indexOf('STDIO streams did not close') >= 0).length == 1, 'Did not find expected debug message'); assert(err.message.indexOf('failed because one or more lines were written to the STDERR stream') >= 0); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }) .finally(function () { @@ -642,7 +642,7 @@ describe('Toolrunner Tests', function () { assert(output && output.length > 0 && output.indexOf('line 2') >= 0, 'should have emitted stdout ' + output); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -665,7 +665,7 @@ describe('Toolrunner Tests', function () { assert(output && output.length > 0 && output.indexOf('node') >= 0, 'should have emitted stdout ' + output); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -705,7 +705,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('print-output.exe | findstr "line 2" was a bad command and it did not fail'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -714,7 +714,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -737,7 +737,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('ps bad | grep ssh was a bad command and it did not fail'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -747,7 +747,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }) } @@ -793,7 +793,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('print-output.exe 0 "line 1" "line 2" "line 3" | match-input.exe 1 "line 2" "some error message" was a bad command and it did not fail'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -803,7 +803,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -832,7 +832,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('node [...] | grep --? was a bad command and it did not fail'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -843,7 +843,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -888,7 +888,7 @@ describe('Toolrunner Tests', function () { assert(fileContents.indexOf('line 2') >= 0, 'Log file of first tool should have stdout from first tool: ' + fileContents); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -914,7 +914,7 @@ describe('Toolrunner Tests', function () { assert(fileContents.indexOf('PID') >= 0, 'Log of first tool should have stdout from first tool: ' + fileContents); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -956,7 +956,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('print-output.exe | findstr "line 2" was a bad command and it did not fail'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -968,7 +968,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -991,7 +991,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('ps bad | grep ssh was a bad command and it did not fail'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -1004,7 +1004,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }) } @@ -1052,7 +1052,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('print-output.exe 0 "line 1" "line 2" "line 3" | match-input.exe 1 "line 2" "some error message" was a bad command and it did not fail'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -1065,7 +1065,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -1093,7 +1093,7 @@ describe('Toolrunner Tests', function () { succeeded = true; assert.fail('ps ax | grep --? was a bad command and it did not fail'); }) - .fail(function (err) { + .catch(function (err) { if (succeeded) { done(err); } @@ -1107,7 +1107,7 @@ describe('Toolrunner Tests', function () { done(); } }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -1266,7 +1266,7 @@ describe('Toolrunner Tests', function () { 'hello world'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); }); @@ -1306,7 +1306,7 @@ describe('Toolrunner Tests', function () { 'helloworld hello:"world again"'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); }); @@ -1349,7 +1349,7 @@ describe('Toolrunner Tests', function () { + ' hello,world'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); }); @@ -1387,7 +1387,7 @@ describe('Toolrunner Tests', function () { + "args[5]: 'myarg2'"); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); }); @@ -1422,7 +1422,7 @@ describe('Toolrunner Tests', function () { + 'args[2]: "arg3"'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); }); @@ -1457,7 +1457,7 @@ describe('Toolrunner Tests', function () { + 'args[1]: "my arg 2"'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }) }); @@ -1559,7 +1559,7 @@ describe('Toolrunner Tests', function () { + 'args[23]: "hello world\\\\"'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }) }); @@ -1833,7 +1833,7 @@ describe('Toolrunner Tests', function () { 'args[0]: "hello world"'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }) }); @@ -1870,7 +1870,7 @@ describe('Toolrunner Tests', function () { 'args[1]: "world"'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }) }); @@ -2136,7 +2136,7 @@ describe('Toolrunner Tests', function () { assert.equal(output.trim(), 'args[0]: \'test value\'', 'Command should return \"args[0]: \'test value\'\"'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -2151,7 +2151,7 @@ describe('Toolrunner Tests', function () { assert(output.includes(tempPath), `Result should include \'${tempPath}\'`); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -2181,7 +2181,7 @@ describe('Toolrunner Tests', function () { assert(output && output.length > 0 && output.indexOf('test value') >= 0, 'should have emitted stdout ' + output); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -2204,7 +2204,7 @@ describe('Toolrunner Tests', function () { assert(output && output.length > 0 && output.indexOf('node') >= 0, 'should have emitted stdout ' + output); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -2227,7 +2227,7 @@ describe('Toolrunner Tests', function () { + 'args[2]: \'\'-TEST3=value\'\''); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } @@ -2242,7 +2242,7 @@ describe('Toolrunner Tests', function () { assert.equal(output, '-TEST1=test value;test -TEST2=/one/two/three -TEST3=out:$TEST\n'); done(); }) - .fail(function (err) { + .catch(function (err) { done(err); }); } diff --git a/node/test/tsconfig.json b/node/test/tsconfig.json index 98230ecfc..1a820766b 100644 --- a/node/test/tsconfig.json +++ b/node/test/tsconfig.json @@ -16,6 +16,7 @@ "legacyfindfilestests.ts", "vaulttests.ts", "toolrunnertests.ts", + "toolrunnerTestsWithExecAsync.ts", "cctests.ts", "loctests.ts", "matchtests.ts", diff --git a/node/toolrunner.ts b/node/toolrunner.ts index 7fbfb0635..9705e0f45 100644 --- a/node/toolrunner.ts +++ b/node/toolrunner.ts @@ -1,5 +1,3 @@ - - import Q = require('q'); import os = require('os'); import events = require('events'); @@ -602,6 +600,204 @@ export class ToolRunner extends events.EventEmitter { return result; } + private execWithPipingAsync(pipeOutputToTool: ToolRunner, options?: IExecOptions): Promise { + this._debug('exec tool: ' + this.toolPath); + this._debug('arguments:'); + this.args.forEach((arg) => { + this._debug(' ' + arg); + }); + + let success = true; + const optionsNonNull = this._cloneExecOptions(options); + + if (!optionsNonNull.silent) { + optionsNonNull.outStream!.write(this._getCommandString(optionsNonNull) + os.EOL); + } + + let cp: child.ChildProcess; + let toolPath: string = pipeOutputToTool.toolPath; + let toolPathFirst: string; + let successFirst = true; + let returnCodeFirst: number; + let fileStream: fs.WriteStream | null; + let waitingEvents: number = 0; // number of process or stream events we are waiting on to complete + let returnCode: number = 0; + let error: any; + + toolPathFirst = this.toolPath; + + // Following node documentation example from this link on how to pipe output of one process to another + // https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options + + //start the child process for both tools + waitingEvents++; + var cpFirst = child.spawn( + this._getSpawnFileName(optionsNonNull), + this._getSpawnArgs(optionsNonNull), + this._getSpawnOptions(optionsNonNull)); + + waitingEvents ++; + cp = child.spawn( + pipeOutputToTool._getSpawnFileName(optionsNonNull), + pipeOutputToTool._getSpawnArgs(optionsNonNull), + pipeOutputToTool._getSpawnOptions(optionsNonNull)); + + fileStream = this.pipeOutputToFile ? fs.createWriteStream(this.pipeOutputToFile) : null; + + return new Promise((resolve, reject) => { + if (fileStream) { + waitingEvents++; + fileStream.on('finish', () => { + waitingEvents--; //file write is complete + fileStream = null; + if(waitingEvents == 0) { + if (error) { + reject(error); + } else { + resolve(returnCode); + } + } + }); + fileStream.on('error', (err: Error) => { + waitingEvents--; //there were errors writing to the file, write is done + this._debug(`Failed to pipe output of ${toolPathFirst} to file ${this.pipeOutputToFile}. Error = ${err}`); + fileStream = null; + if(waitingEvents == 0) { + if (error) { + reject(error); + } else { + resolve(returnCode); + } + } + }); + } + + //pipe stdout of first tool to stdin of second tool + cpFirst.stdout?.on('data', (data: Buffer) => { + try { + if (fileStream) { + fileStream.write(data); + } + cp.stdin?.write(data); + } catch (err) { + this._debug('Failed to pipe output of ' + toolPathFirst + ' to ' + toolPath); + this._debug(toolPath + ' might have exited due to errors prematurely. Verify the arguments passed are valid.'); + } + }); + cpFirst.stderr?.on('data', (data: Buffer) => { + if (fileStream) { + fileStream.write(data); + } + successFirst = !optionsNonNull.failOnStdErr; + if (!optionsNonNull.silent) { + var s = optionsNonNull.failOnStdErr ? optionsNonNull.errStream! : optionsNonNull.outStream!; + s.write(data); + } + }); + cpFirst.on('error', (err: Error) => { + waitingEvents--; //first process is complete with errors + if (fileStream) { + fileStream.end(); + } + cp.stdin?.end(); + error = new Error(toolPathFirst + ' failed. ' + err.message); + if(waitingEvents == 0) { + reject(error); + } + }); + cpFirst.on('close', (code: number, signal: any) => { + waitingEvents--; //first process is complete + if (code != 0 && !optionsNonNull.ignoreReturnCode) { + successFirst = false; + returnCodeFirst = code; + returnCode = returnCodeFirst; + } + this._debug('success of first tool:' + successFirst); + if (fileStream) { + fileStream.end(); + } + cp.stdin?.end(); + if(waitingEvents == 0) { + if (error) { + reject(error); + } else { + resolve(returnCode); + } + } + }); + + var stdbuffer: string = ''; + cp.stdout?.on('data', (data: Buffer) => { + this.emit('stdout', data); + + if (!optionsNonNull.silent) { + optionsNonNull.outStream!.write(data); + } + + this._processLineBuffer(data, stdbuffer, (line: string) => { + this.emit('stdline', line); + }); + }); + + var errbuffer: string = ''; + cp.stderr?.on('data', (data: Buffer) => { + this.emit('stderr', data); + + success = !optionsNonNull.failOnStdErr; + if (!optionsNonNull.silent) { + var s = optionsNonNull.failOnStdErr ? optionsNonNull.errStream! : optionsNonNull.outStream!; + s.write(data); + } + + this._processLineBuffer(data, errbuffer, (line: string) => { + this.emit('errline', line); + }); + }); + + cp.on('error', (err: Error) => { + waitingEvents--; //process is done with errors + error = new Error(toolPath + ' failed. ' + err.message); + if(waitingEvents == 0) { + reject(error); + } + }); + + cp.on('close', (code: number, signal: any) => { + waitingEvents--; //process is complete + this._debug('rc:' + code); + returnCode = code; + + if (stdbuffer.length > 0) { + this.emit('stdline', stdbuffer); + } + + if (errbuffer.length > 0) { + this.emit('errline', errbuffer); + } + + if (code != 0 && !optionsNonNull.ignoreReturnCode) { + success = false; + } + + this._debug('success:' + success); + + if (!successFirst) { //in the case output is piped to another tool, check exit code of both tools + error = new Error(toolPathFirst + ' failed with return code: ' + returnCodeFirst); + } else if (!success) { + error = new Error(toolPath + ' failed with return code: ' + code); + } + + if(waitingEvents == 0) { + if (error) { + reject(error); + } else { + resolve(returnCode); + } + } + }); + }); + } + private execWithPiping(pipeOutputToTool: ToolRunner, options?: IExecOptions): Q.Promise { var defer = Q.defer(); @@ -881,6 +1077,121 @@ export class ToolRunner extends events.EventEmitter { * @param options optional exec options. See IExecOptions * @returns number */ + public execAsync(options?: IExecOptions): Promise { + if (this.pipeOutputToTool) { + return this.execWithPipingAsync(this.pipeOutputToTool, options); + } + + this._debug('exec tool: ' + this.toolPath); + this._debug('arguments:'); + this.args.forEach((arg) => { + this._debug(' ' + arg); + }); + + const optionsNonNull = this._cloneExecOptions(options); + if (!optionsNonNull.silent) { + optionsNonNull.outStream!.write(this._getCommandString(optionsNonNull) + os.EOL); + } + + let state = new ExecState(optionsNonNull, this.toolPath); + state.on('debug', (message: string) => { + this._debug(message); + }); + + let cp = child.spawn(this._getSpawnFileName(options), this._getSpawnArgs(optionsNonNull), this._getSpawnOptions(options)); + this.childProcess = cp; + // it is possible for the child process to end its last line without a new line. + // because stdout is buffered, this causes the last line to not get sent to the parent + // stream. Adding this event forces a flush before the child streams are closed. + cp.stdout?.on('finish', () => { + if (!optionsNonNull.silent) { + optionsNonNull.outStream!.write(os.EOL); + } + }); + + var stdbuffer: string = ''; + cp.stdout?.on('data', (data: Buffer) => { + this.emit('stdout', data); + + if (!optionsNonNull.silent) { + optionsNonNull.outStream!.write(data); + } + + this._processLineBuffer(data, stdbuffer, (line: string) => { + this.emit('stdline', line); + }); + }); + + + var errbuffer: string = ''; + cp.stderr?.on('data', (data: Buffer) => { + state.processStderr = true; + this.emit('stderr', data); + + if (!optionsNonNull.silent) { + var s = optionsNonNull.failOnStdErr ? optionsNonNull.errStream! : optionsNonNull.outStream!; + s.write(data); + } + + this._processLineBuffer(data, errbuffer, (line: string) => { + this.emit('errline', line); + }); + }); + + cp.on('error', (err: Error) => { + state.processError = err.message; + state.processExited = true; + state.processClosed = true; + state.CheckComplete(); + }); + + cp.on('exit', (code: number, signal: any) => { + state.processExitCode = code; + state.processExited = true; + this._debug(`Exit code ${code} received from tool '${this.toolPath}'`); + state.CheckComplete() + }); + + cp.on('close', (code: number, signal: any) => { + state.processExitCode = code; + state.processExited = true; + state.processClosed = true; + this._debug(`STDIO streams have closed for tool '${this.toolPath}'`) + state.CheckComplete(); + }); + + return new Promise((resolve, reject) => { + state.on('done', (error: Error, exitCode: number) => { + if (stdbuffer.length > 0) { + this.emit('stdline', stdbuffer); + } + + if (errbuffer.length > 0) { + this.emit('errline', errbuffer); + } + + cp.removeAllListeners(); + + if (error) { + reject(error); + } + else { + resolve(exitCode); + } + }); + }); + } + + /** + * Exec a tool. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @deprecated Use the `execAsync` method that returns a native Javascript promise instead + * @param tool path to tool to exec + * @param options optional exec options. See IExecOptions + * @returns number + */ public exec(options?: IExecOptions): Q.Promise { if (this.pipeOutputToTool) { return this.execWithPiping(this.pipeOutputToTool, options); diff --git a/node/vault.ts b/node/vault.ts index f5c8bdf80..c7fad048e 100644 --- a/node/vault.ts +++ b/node/vault.ts @@ -1,5 +1,4 @@ -import Q = require('q'); import fs = require('fs'); import path = require('path'); import crypto = require('crypto'); From bb9d85db46d75b9aab7ab818d2002c71669e94d4 Mon Sep 17 00:00:00 2001 From: Aleksandr Levochkin <107044793+aleksandrlevochkin@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:37:17 +0200 Subject: [PATCH 2/2] Increased version and updated release notes (#966) --- powershell/Docs/ReleaseNotes.md | 3 +++ powershell/package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/powershell/Docs/ReleaseNotes.md b/powershell/Docs/ReleaseNotes.md index b4180428a..bd2d3774c 100644 --- a/powershell/Docs/ReleaseNotes.md +++ b/powershell/Docs/ReleaseNotes.md @@ -1,5 +1,8 @@ # Release Notes +## 0.15.0 +* Removed the `Q` library + ## 0.14.0 * Improved error handling in function `Find-Files` diff --git a/powershell/package.json b/powershell/package.json index 95b8d4a56..cc6e8b226 100644 --- a/powershell/package.json +++ b/powershell/package.json @@ -1,6 +1,6 @@ { "name": "vsts-task-sdk", - "version": "0.14.0", + "version": "0.15.0", "description": "VSTS Task SDK", "scripts": { "build": "node make.js build",