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