| /** |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| |
| var child_process = require('child_process'); |
| var fs = require('fs-extra'); |
| var path = require('path'); |
| var _ = require('underscore'); |
| var Q = require('q'); |
| var which = require('which'); |
| var events = require('./events'); |
| var iswin32 = process.platform === 'win32'; |
| |
| // On Windows, spawn() for batch files requires absolute path & having the extension. |
| function resolveWindowsExe (cmd) { |
| var winExtensions = ['.exe', '.bat', '.cmd', '.js', '.vbs']; |
| function isValidExe (c) { |
| return winExtensions.includes(path.extname(c)) && fs.existsSync(c); |
| } |
| if (isValidExe(cmd)) { |
| return cmd; |
| } |
| cmd = which.sync(cmd, { nothrow: true }) || cmd; |
| if (!isValidExe(cmd)) { |
| winExtensions.some(function (ext) { |
| if (fs.existsSync(cmd + ext)) { |
| cmd = cmd + ext; |
| return true; |
| } |
| }); |
| } |
| return cmd; |
| } |
| |
| function maybeQuote (a) { |
| if (/^[^"].*[ &].*[^"]/.test(a)) return '"' + a + '"'; |
| return a; |
| } |
| |
| /** |
| * A special implementation for child_process.spawn that handles |
| * Windows-specific issues with batch files and spaces in paths. Returns a |
| * promise that succeeds only for return code 0. It is also possible to |
| * subscribe on spawned process' stdout and stderr streams using progress |
| * handler for resultant promise. |
| * |
| * @example spawn('mycommand', [], {stdio: 'pipe'}) .progress(function (stdio){ |
| * if (stdio.stderr) { console.error(stdio.stderr); } }) |
| * .then(function(result){ // do other stuff }) |
| * |
| * @param {String} cmd A command to spawn |
| * @param {String[]} [args=[]] An array of arguments, passed to spawned |
| * process |
| * @param {Object} [opts={}] A configuration object |
| * @param {String|String[]|Object} opts.stdio Property that configures how |
| * spawned process' stdio will behave. Has the same meaning and possible |
| * values as 'stdio' options for child_process.spawn method |
| * (https://nodejs.org/api/child_process.html#child_process_options_stdio). |
| * @param {Object} [env={}] A map of extra environment variables |
| * @param {String} [cwd=process.cwd()] Working directory for the command |
| * @param {Boolean} [chmod=false] If truthy, will attempt to set the execute |
| * bit before executing on non-Windows platforms |
| * |
| * @return {Promise} A promise that is either fulfilled if the spawned |
| * process is exited with zero error code or rejected otherwise. If the |
| * 'stdio' option set to 'default' or 'pipe', the promise also emits progress |
| * messages with the following contents: |
| * { |
| * 'stdout': ..., |
| * 'stderr': ... |
| * } |
| */ |
| exports.spawn = function (cmd, args, opts) { |
| args = args || []; |
| opts = opts || {}; |
| var spawnOpts = {}; |
| var d = Q.defer(); |
| |
| if (iswin32) { |
| cmd = resolveWindowsExe(cmd); |
| // If we couldn't find the file, likely we'll end up failing, |
| // but for things like "del", cmd will do the trick. |
| if (path.extname(cmd) !== '.exe') { |
| var cmdArgs = '"' + [cmd].concat(args).map(maybeQuote).join(' ') + '"'; |
| // We need to use /s to ensure that spaces are parsed properly with cmd spawned content |
| args = [['/s', '/c', cmdArgs].join(' ')]; |
| cmd = 'cmd'; |
| spawnOpts.windowsVerbatimArguments = true; |
| } else if (!fs.existsSync(cmd)) { |
| // We need to use /s to ensure that spaces are parsed properly with cmd spawned content |
| args = ['/s', '/c', cmd].concat(args).map(maybeQuote); |
| } |
| } |
| |
| if (opts.stdio !== 'default') { |
| // Ignore 'default' value for stdio because it corresponds to child_process's default 'pipe' option |
| spawnOpts.stdio = opts.stdio; |
| } |
| |
| if (opts.cwd) { |
| spawnOpts.cwd = opts.cwd; |
| } |
| |
| if (opts.env) { |
| spawnOpts.env = _.extend(_.extend({}, process.env), opts.env); |
| } |
| |
| if (opts.chmod && !iswin32) { |
| try { |
| // This fails when module is installed in a system directory (e.g. via sudo npm install) |
| fs.chmodSync(cmd, '755'); |
| } catch (e) { |
| // If the perms weren't set right, then this will come as an error upon execution. |
| } |
| } |
| |
| events.emit(opts.printCommand ? 'log' : 'verbose', 'Running command: ' + maybeQuote(cmd) + ' ' + args.map(maybeQuote).join(' ')); |
| |
| var child = child_process.spawn(cmd, args, spawnOpts); |
| var capturedOut = ''; |
| var capturedErr = ''; |
| |
| if (child.stdout) { |
| child.stdout.setEncoding('utf8'); |
| child.stdout.on('data', function (data) { |
| capturedOut += data; |
| d.notify({'stdout': data}); |
| }); |
| } |
| |
| if (child.stderr) { |
| child.stderr.setEncoding('utf8'); |
| child.stderr.on('data', function (data) { |
| capturedErr += data; |
| d.notify({'stderr': data}); |
| }); |
| } |
| |
| child.on('close', whenDone); |
| child.on('error', whenDone); |
| function whenDone (arg) { |
| child.removeListener('close', whenDone); |
| child.removeListener('error', whenDone); |
| var code = typeof arg === 'number' ? arg : arg && arg.code; |
| |
| events.emit('verbose', 'Command finished with error code ' + code + ': ' + cmd + ' ' + args); |
| if (code === 0) { |
| d.resolve(capturedOut.trim()); |
| } else { |
| var errMsg = cmd + ': Command failed with exit code ' + code; |
| if (capturedErr) { |
| errMsg += ' Error output:\n' + capturedErr.trim(); |
| } |
| var err = new Error(errMsg); |
| if (capturedErr) { |
| err.stderr = capturedErr; |
| } |
| if (capturedOut) { |
| err.stdout = capturedOut; |
| } |
| err.code = code; |
| d.reject(err); |
| } |
| } |
| |
| return d.promise; |
| }; |
| |
| exports.maybeSpawn = function (cmd, args, opts) { |
| if (fs.existsSync(cmd)) { |
| return exports.spawn(cmd, args, opts); |
| } |
| return Q(null); |
| }; |