github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/action/node_modules/@actions/exec/lib/toolrunner.js (about) 1 "use strict"; 2 var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 if (k2 === undefined) k2 = k; 4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 }) : (function(o, m, k, k2) { 6 if (k2 === undefined) k2 = k; 7 o[k2] = m[k]; 8 })); 9 var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 }) : function(o, v) { 12 o["default"] = v; 13 }); 14 var __importStar = (this && this.__importStar) || function (mod) { 15 if (mod && mod.__esModule) return mod; 16 var result = {}; 17 if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 __setModuleDefault(result, mod); 19 return result; 20 }; 21 var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 22 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 23 return new (P || (P = Promise))(function (resolve, reject) { 24 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 25 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 26 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 27 step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 }); 29 }; 30 Object.defineProperty(exports, "__esModule", { value: true }); 31 exports.argStringToArray = exports.ToolRunner = void 0; 32 const os = __importStar(require("os")); 33 const events = __importStar(require("events")); 34 const child = __importStar(require("child_process")); 35 const path = __importStar(require("path")); 36 const io = __importStar(require("@actions/io")); 37 const ioUtil = __importStar(require("@actions/io/lib/io-util")); 38 const timers_1 = require("timers"); 39 /* eslint-disable @typescript-eslint/unbound-method */ 40 const IS_WINDOWS = process.platform === 'win32'; 41 /* 42 * Class for running command line tools. Handles quoting and arg parsing in a platform agnostic way. 43 */ 44 class ToolRunner extends events.EventEmitter { 45 constructor(toolPath, args, options) { 46 super(); 47 if (!toolPath) { 48 throw new Error("Parameter 'toolPath' cannot be null or empty."); 49 } 50 this.toolPath = toolPath; 51 this.args = args || []; 52 this.options = options || {}; 53 } 54 _debug(message) { 55 if (this.options.listeners && this.options.listeners.debug) { 56 this.options.listeners.debug(message); 57 } 58 } 59 _getCommandString(options, noPrefix) { 60 const toolPath = this._getSpawnFileName(); 61 const args = this._getSpawnArgs(options); 62 let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool 63 if (IS_WINDOWS) { 64 // Windows + cmd file 65 if (this._isCmdFile()) { 66 cmd += toolPath; 67 for (const a of args) { 68 cmd += ` ${a}`; 69 } 70 } 71 // Windows + verbatim 72 else if (options.windowsVerbatimArguments) { 73 cmd += `"${toolPath}"`; 74 for (const a of args) { 75 cmd += ` ${a}`; 76 } 77 } 78 // Windows (regular) 79 else { 80 cmd += this._windowsQuoteCmdArg(toolPath); 81 for (const a of args) { 82 cmd += ` ${this._windowsQuoteCmdArg(a)}`; 83 } 84 } 85 } 86 else { 87 // OSX/Linux - this can likely be improved with some form of quoting. 88 // creating processes on Unix is fundamentally different than Windows. 89 // on Unix, execvp() takes an arg array. 90 cmd += toolPath; 91 for (const a of args) { 92 cmd += ` ${a}`; 93 } 94 } 95 return cmd; 96 } 97 _processLineBuffer(data, strBuffer, onLine) { 98 try { 99 let s = strBuffer + data.toString(); 100 let n = s.indexOf(os.EOL); 101 while (n > -1) { 102 const line = s.substring(0, n); 103 onLine(line); 104 // the rest of the string ... 105 s = s.substring(n + os.EOL.length); 106 n = s.indexOf(os.EOL); 107 } 108 return s; 109 } 110 catch (err) { 111 // streaming lines to console is best effort. Don't fail a build. 112 this._debug(`error processing line. Failed with error ${err}`); 113 return ''; 114 } 115 } 116 _getSpawnFileName() { 117 if (IS_WINDOWS) { 118 if (this._isCmdFile()) { 119 return process.env['COMSPEC'] || 'cmd.exe'; 120 } 121 } 122 return this.toolPath; 123 } 124 _getSpawnArgs(options) { 125 if (IS_WINDOWS) { 126 if (this._isCmdFile()) { 127 let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`; 128 for (const a of this.args) { 129 argline += ' '; 130 argline += options.windowsVerbatimArguments 131 ? a 132 : this._windowsQuoteCmdArg(a); 133 } 134 argline += '"'; 135 return [argline]; 136 } 137 } 138 return this.args; 139 } 140 _endsWith(str, end) { 141 return str.endsWith(end); 142 } 143 _isCmdFile() { 144 const upperToolPath = this.toolPath.toUpperCase(); 145 return (this._endsWith(upperToolPath, '.CMD') || 146 this._endsWith(upperToolPath, '.BAT')); 147 } 148 _windowsQuoteCmdArg(arg) { 149 // for .exe, apply the normal quoting rules that libuv applies 150 if (!this._isCmdFile()) { 151 return this._uvQuoteCmdArg(arg); 152 } 153 // otherwise apply quoting rules specific to the cmd.exe command line parser. 154 // the libuv rules are generic and are not designed specifically for cmd.exe 155 // command line parser. 156 // 157 // for a detailed description of the cmd.exe command line parser, refer to 158 // http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912 159 // need quotes for empty arg 160 if (!arg) { 161 return '""'; 162 } 163 // determine whether the arg needs to be quoted 164 const cmdSpecialChars = [ 165 ' ', 166 '\t', 167 '&', 168 '(', 169 ')', 170 '[', 171 ']', 172 '{', 173 '}', 174 '^', 175 '=', 176 ';', 177 '!', 178 "'", 179 '+', 180 ',', 181 '`', 182 '~', 183 '|', 184 '<', 185 '>', 186 '"' 187 ]; 188 let needsQuotes = false; 189 for (const char of arg) { 190 if (cmdSpecialChars.some(x => x === char)) { 191 needsQuotes = true; 192 break; 193 } 194 } 195 // short-circuit if quotes not needed 196 if (!needsQuotes) { 197 return arg; 198 } 199 // the following quoting rules are very similar to the rules that by libuv applies. 200 // 201 // 1) wrap the string in quotes 202 // 203 // 2) double-up quotes - i.e. " => "" 204 // 205 // this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately 206 // doesn't work well with a cmd.exe command line. 207 // 208 // note, replacing " with "" also works well if the arg is passed to a downstream .NET console app. 209 // for example, the command line: 210 // foo.exe "myarg:""my val""" 211 // is parsed by a .NET console app into an arg array: 212 // [ "myarg:\"my val\"" ] 213 // which is the same end result when applying libuv quoting rules. although the actual 214 // command line from libuv quoting rules would look like: 215 // foo.exe "myarg:\"my val\"" 216 // 217 // 3) double-up slashes that precede a quote, 218 // e.g. hello \world => "hello \world" 219 // hello\"world => "hello\\""world" 220 // hello\\"world => "hello\\\\""world" 221 // hello world\ => "hello world\\" 222 // 223 // technically this is not required for a cmd.exe command line, or the batch argument parser. 224 // the reasons for including this as a .cmd quoting rule are: 225 // 226 // a) this is optimized for the scenario where the argument is passed from the .cmd file to an 227 // external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule. 228 // 229 // b) it's what we've been doing previously (by deferring to node default behavior) and we 230 // haven't heard any complaints about that aspect. 231 // 232 // note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be 233 // escaped when used on the command line directly - even though within a .cmd file % can be escaped 234 // by using %%. 235 // 236 // the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts 237 // the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing. 238 // 239 // one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would 240 // often work, since it is unlikely that var^ would exist, and the ^ character is removed when the 241 // variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args 242 // to an external program. 243 // 244 // an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file. 245 // % can be escaped within a .cmd file. 246 let reverse = '"'; 247 let quoteHit = true; 248 for (let i = arg.length; i > 0; i--) { 249 // walk the string in reverse 250 reverse += arg[i - 1]; 251 if (quoteHit && arg[i - 1] === '\\') { 252 reverse += '\\'; // double the slash 253 } 254 else if (arg[i - 1] === '"') { 255 quoteHit = true; 256 reverse += '"'; // double the quote 257 } 258 else { 259 quoteHit = false; 260 } 261 } 262 reverse += '"'; 263 return reverse 264 .split('') 265 .reverse() 266 .join(''); 267 } 268 _uvQuoteCmdArg(arg) { 269 // Tool runner wraps child_process.spawn() and needs to apply the same quoting as 270 // Node in certain cases where the undocumented spawn option windowsVerbatimArguments 271 // is used. 272 // 273 // Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV, 274 // see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details), 275 // pasting copyright notice from Node within this function: 276 // 277 // Copyright Joyent, Inc. and other Node contributors. All rights reserved. 278 // 279 // Permission is hereby granted, free of charge, to any person obtaining a copy 280 // of this software and associated documentation files (the "Software"), to 281 // deal in the Software without restriction, including without limitation the 282 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 283 // sell copies of the Software, and to permit persons to whom the Software is 284 // furnished to do so, subject to the following conditions: 285 // 286 // The above copyright notice and this permission notice shall be included in 287 // all copies or substantial portions of the Software. 288 // 289 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 290 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 291 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 292 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 293 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 294 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 295 // IN THE SOFTWARE. 296 if (!arg) { 297 // Need double quotation for empty argument 298 return '""'; 299 } 300 if (!arg.includes(' ') && !arg.includes('\t') && !arg.includes('"')) { 301 // No quotation needed 302 return arg; 303 } 304 if (!arg.includes('"') && !arg.includes('\\')) { 305 // No embedded double quotes or backslashes, so I can just wrap 306 // quote marks around the whole thing. 307 return `"${arg}"`; 308 } 309 // Expected input/output: 310 // input : hello"world 311 // output: "hello\"world" 312 // input : hello""world 313 // output: "hello\"\"world" 314 // input : hello\world 315 // output: hello\world 316 // input : hello\\world 317 // output: hello\\world 318 // input : hello\"world 319 // output: "hello\\\"world" 320 // input : hello\\"world 321 // output: "hello\\\\\"world" 322 // input : hello world\ 323 // output: "hello world\\" - note the comment in libuv actually reads "hello world\" 324 // but it appears the comment is wrong, it should be "hello world\\" 325 let reverse = '"'; 326 let quoteHit = true; 327 for (let i = arg.length; i > 0; i--) { 328 // walk the string in reverse 329 reverse += arg[i - 1]; 330 if (quoteHit && arg[i - 1] === '\\') { 331 reverse += '\\'; 332 } 333 else if (arg[i - 1] === '"') { 334 quoteHit = true; 335 reverse += '\\'; 336 } 337 else { 338 quoteHit = false; 339 } 340 } 341 reverse += '"'; 342 return reverse 343 .split('') 344 .reverse() 345 .join(''); 346 } 347 _cloneExecOptions(options) { 348 options = options || {}; 349 const result = { 350 cwd: options.cwd || process.cwd(), 351 env: options.env || process.env, 352 silent: options.silent || false, 353 windowsVerbatimArguments: options.windowsVerbatimArguments || false, 354 failOnStdErr: options.failOnStdErr || false, 355 ignoreReturnCode: options.ignoreReturnCode || false, 356 delay: options.delay || 10000 357 }; 358 result.outStream = options.outStream || process.stdout; 359 result.errStream = options.errStream || process.stderr; 360 return result; 361 } 362 _getSpawnOptions(options, toolPath) { 363 options = options || {}; 364 const result = {}; 365 result.cwd = options.cwd; 366 result.env = options.env; 367 result['windowsVerbatimArguments'] = 368 options.windowsVerbatimArguments || this._isCmdFile(); 369 if (options.windowsVerbatimArguments) { 370 result.argv0 = `"${toolPath}"`; 371 } 372 return result; 373 } 374 /** 375 * Exec a tool. 376 * Output will be streamed to the live console. 377 * Returns promise with return code 378 * 379 * @param tool path to tool to exec 380 * @param options optional exec options. See ExecOptions 381 * @returns number 382 */ 383 exec() { 384 return __awaiter(this, void 0, void 0, function* () { 385 // root the tool path if it is unrooted and contains relative pathing 386 if (!ioUtil.isRooted(this.toolPath) && 387 (this.toolPath.includes('/') || 388 (IS_WINDOWS && this.toolPath.includes('\\')))) { 389 // prefer options.cwd if it is specified, however options.cwd may also need to be rooted 390 this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath); 391 } 392 // if the tool is only a file name, then resolve it from the PATH 393 // otherwise verify it exists (add extension on Windows if necessary) 394 this.toolPath = yield io.which(this.toolPath, true); 395 return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { 396 this._debug(`exec tool: ${this.toolPath}`); 397 this._debug('arguments:'); 398 for (const arg of this.args) { 399 this._debug(` ${arg}`); 400 } 401 const optionsNonNull = this._cloneExecOptions(this.options); 402 if (!optionsNonNull.silent && optionsNonNull.outStream) { 403 optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os.EOL); 404 } 405 const state = new ExecState(optionsNonNull, this.toolPath); 406 state.on('debug', (message) => { 407 this._debug(message); 408 }); 409 if (this.options.cwd && !(yield ioUtil.exists(this.options.cwd))) { 410 return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`)); 411 } 412 const fileName = this._getSpawnFileName(); 413 const cp = child.spawn(fileName, this._getSpawnArgs(optionsNonNull), this._getSpawnOptions(this.options, fileName)); 414 let stdbuffer = ''; 415 if (cp.stdout) { 416 cp.stdout.on('data', (data) => { 417 if (this.options.listeners && this.options.listeners.stdout) { 418 this.options.listeners.stdout(data); 419 } 420 if (!optionsNonNull.silent && optionsNonNull.outStream) { 421 optionsNonNull.outStream.write(data); 422 } 423 stdbuffer = this._processLineBuffer(data, stdbuffer, (line) => { 424 if (this.options.listeners && this.options.listeners.stdline) { 425 this.options.listeners.stdline(line); 426 } 427 }); 428 }); 429 } 430 let errbuffer = ''; 431 if (cp.stderr) { 432 cp.stderr.on('data', (data) => { 433 state.processStderr = true; 434 if (this.options.listeners && this.options.listeners.stderr) { 435 this.options.listeners.stderr(data); 436 } 437 if (!optionsNonNull.silent && 438 optionsNonNull.errStream && 439 optionsNonNull.outStream) { 440 const s = optionsNonNull.failOnStdErr 441 ? optionsNonNull.errStream 442 : optionsNonNull.outStream; 443 s.write(data); 444 } 445 errbuffer = this._processLineBuffer(data, errbuffer, (line) => { 446 if (this.options.listeners && this.options.listeners.errline) { 447 this.options.listeners.errline(line); 448 } 449 }); 450 }); 451 } 452 cp.on('error', (err) => { 453 state.processError = err.message; 454 state.processExited = true; 455 state.processClosed = true; 456 state.CheckComplete(); 457 }); 458 cp.on('exit', (code) => { 459 state.processExitCode = code; 460 state.processExited = true; 461 this._debug(`Exit code ${code} received from tool '${this.toolPath}'`); 462 state.CheckComplete(); 463 }); 464 cp.on('close', (code) => { 465 state.processExitCode = code; 466 state.processExited = true; 467 state.processClosed = true; 468 this._debug(`STDIO streams have closed for tool '${this.toolPath}'`); 469 state.CheckComplete(); 470 }); 471 state.on('done', (error, exitCode) => { 472 if (stdbuffer.length > 0) { 473 this.emit('stdline', stdbuffer); 474 } 475 if (errbuffer.length > 0) { 476 this.emit('errline', errbuffer); 477 } 478 cp.removeAllListeners(); 479 if (error) { 480 reject(error); 481 } 482 else { 483 resolve(exitCode); 484 } 485 }); 486 if (this.options.input) { 487 if (!cp.stdin) { 488 throw new Error('child process missing stdin'); 489 } 490 cp.stdin.end(this.options.input); 491 } 492 })); 493 }); 494 } 495 } 496 exports.ToolRunner = ToolRunner; 497 /** 498 * Convert an arg string to an array of args. Handles escaping 499 * 500 * @param argString string of arguments 501 * @returns string[] array of arguments 502 */ 503 function argStringToArray(argString) { 504 const args = []; 505 let inQuotes = false; 506 let escaped = false; 507 let arg = ''; 508 function append(c) { 509 // we only escape double quotes. 510 if (escaped && c !== '"') { 511 arg += '\\'; 512 } 513 arg += c; 514 escaped = false; 515 } 516 for (let i = 0; i < argString.length; i++) { 517 const c = argString.charAt(i); 518 if (c === '"') { 519 if (!escaped) { 520 inQuotes = !inQuotes; 521 } 522 else { 523 append(c); 524 } 525 continue; 526 } 527 if (c === '\\' && escaped) { 528 append(c); 529 continue; 530 } 531 if (c === '\\' && inQuotes) { 532 escaped = true; 533 continue; 534 } 535 if (c === ' ' && !inQuotes) { 536 if (arg.length > 0) { 537 args.push(arg); 538 arg = ''; 539 } 540 continue; 541 } 542 append(c); 543 } 544 if (arg.length > 0) { 545 args.push(arg.trim()); 546 } 547 return args; 548 } 549 exports.argStringToArray = argStringToArray; 550 class ExecState extends events.EventEmitter { 551 constructor(options, toolPath) { 552 super(); 553 this.processClosed = false; // tracks whether the process has exited and stdio is closed 554 this.processError = ''; 555 this.processExitCode = 0; 556 this.processExited = false; // tracks whether the process has exited 557 this.processStderr = false; // tracks whether stderr was written to 558 this.delay = 10000; // 10 seconds 559 this.done = false; 560 this.timeout = null; 561 if (!toolPath) { 562 throw new Error('toolPath must not be empty'); 563 } 564 this.options = options; 565 this.toolPath = toolPath; 566 if (options.delay) { 567 this.delay = options.delay; 568 } 569 } 570 CheckComplete() { 571 if (this.done) { 572 return; 573 } 574 if (this.processClosed) { 575 this._setResult(); 576 } 577 else if (this.processExited) { 578 this.timeout = timers_1.setTimeout(ExecState.HandleTimeout, this.delay, this); 579 } 580 } 581 _debug(message) { 582 this.emit('debug', message); 583 } 584 _setResult() { 585 // determine whether there is an error 586 let error; 587 if (this.processExited) { 588 if (this.processError) { 589 error = new Error(`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`); 590 } 591 else if (this.processExitCode !== 0 && !this.options.ignoreReturnCode) { 592 error = new Error(`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`); 593 } 594 else if (this.processStderr && this.options.failOnStdErr) { 595 error = new Error(`The process '${this.toolPath}' failed because one or more lines were written to the STDERR stream`); 596 } 597 } 598 // clear the timeout 599 if (this.timeout) { 600 clearTimeout(this.timeout); 601 this.timeout = null; 602 } 603 this.done = true; 604 this.emit('done', error, this.processExitCode); 605 } 606 static HandleTimeout(state) { 607 if (state.done) { 608 return; 609 } 610 if (!state.processClosed && state.processExited) { 611 const message = `The STDIO streams did not close within ${state.delay / 612 1000} seconds of the exit event from process '${state.toolPath}'. This may indicate a child process inherited the STDIO streams and has not yet exited.`; 613 state._debug(message); 614 } 615 state._setResult(); 616 } 617 } 618 //# sourceMappingURL=toolrunner.js.map