1 | /**
|
2 | Licensed to the Apache Software Foundation (ASF) under one
|
3 | or more contributor license agreements. See the NOTICE file
|
4 | distributed with this work for additional information
|
5 | regarding copyright ownership. The ASF licenses this file
|
6 | to you under the Apache License, Version 2.0 (the
|
7 | "License"); you may not use this file except in compliance
|
8 | with the License. You may obtain a copy of the License at
|
9 |
|
10 | http://www.apache.org/licenses/LICENSE-2.0
|
11 |
|
12 | Unless required by applicable law or agreed to in writing,
|
13 | software distributed under the License is distributed on an
|
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
15 | KIND, either express or implied. See the License for the
|
16 | specific language governing permissions and limitations
|
17 | under the License.
|
18 | */
|
19 |
|
20 | var child_process = require('child_process');
|
21 | var fs = require('fs');
|
22 | var path = require('path');
|
23 | var _ = require('underscore');
|
24 | var Q = require('q');
|
25 | var shell = require('shelljs');
|
26 | var events = require('./events');
|
27 | var iswin32 = process.platform === 'win32';
|
28 |
|
29 | // On Windows, spawn() for batch files requires absolute path & having the extension.
|
30 | function resolveWindowsExe (cmd) {
|
31 | var winExtensions = ['.exe', '.bat', '.cmd', '.js', '.vbs'];
|
32 | function isValidExe (c) {
|
33 | return winExtensions.indexOf(path.extname(c)) !== -1 && fs.existsSync(c);
|
34 | }
|
35 | if (isValidExe(cmd)) {
|
36 | return cmd;
|
37 | }
|
38 | cmd = shell.which(cmd) || cmd;
|
39 | if (!isValidExe(cmd)) {
|
40 | winExtensions.some(function (ext) {
|
41 | if (fs.existsSync(cmd + ext)) {
|
42 | cmd = cmd + ext;
|
43 | return true;
|
44 | }
|
45 | });
|
46 | }
|
47 | return cmd;
|
48 | }
|
49 |
|
50 | function maybeQuote (a) {
|
51 | if (/^[^"].*[ &].*[^"]/.test(a)) return '"' + a + '"';
|
52 | return a;
|
53 | }
|
54 |
|
55 | /**
|
56 | * A special implementation for child_process.spawn that handles
|
57 | * Windows-specific issues with batch files and spaces in paths. Returns a
|
58 | * promise that succeeds only for return code 0. It is also possible to
|
59 | * subscribe on spawned process' stdout and stderr streams using progress
|
60 | * handler for resultant promise.
|
61 | *
|
62 | * @example spawn('mycommand', [], {stdio: 'pipe'}) .progress(function (stdio){
|
63 | * if (stdio.stderr) { console.error(stdio.stderr); } })
|
64 | * .then(function(result){ // do other stuff })
|
65 | *
|
66 | * @param {String} cmd A command to spawn
|
67 | * @param {String[]} [args=[]] An array of arguments, passed to spawned
|
68 | * process
|
69 | * @param {Object} [opts={}] A configuration object
|
70 | * @param {String|String[]|Object} opts.stdio Property that configures how
|
71 | * spawned process' stdio will behave. Has the same meaning and possible
|
72 | * values as 'stdio' options for child_process.spawn method
|
73 | * (https://nodejs.org/api/child_process.html#child_process_options_stdio).
|
74 | * @param {Object} [env={}] A map of extra environment variables
|
75 | * @param {String} [cwd=process.cwd()] Working directory for the command
|
76 | * @param {Boolean} [chmod=false] If truthy, will attempt to set the execute
|
77 | * bit before executing on non-Windows platforms
|
78 | *
|
79 | * @return {Promise} A promise that is either fulfilled if the spawned
|
80 | * process is exited with zero error code or rejected otherwise. If the
|
81 | * 'stdio' option set to 'default' or 'pipe', the promise also emits progress
|
82 | * messages with the following contents:
|
83 | * {
|
84 | * 'stdout': ...,
|
85 | * 'stderr': ...
|
86 | * }
|
87 | */
|
88 | exports.spawn = function (cmd, args, opts) {
|
89 | args = args || [];
|
90 | opts = opts || {};
|
91 | var spawnOpts = {};
|
92 | var d = Q.defer();
|
93 |
|
94 | if (iswin32) {
|
95 | cmd = resolveWindowsExe(cmd);
|
96 | // If we couldn't find the file, likely we'll end up failing,
|
97 | // but for things like "del", cmd will do the trick.
|
98 | if (path.extname(cmd) !== '.exe') {
|
99 | var cmdArgs = '"' + [cmd].concat(args).map(maybeQuote).join(' ') + '"';
|
100 | // We need to use /s to ensure that spaces are parsed properly with cmd spawned content
|
101 | args = [['/s', '/c', cmdArgs].join(' ')];
|
102 | cmd = 'cmd';
|
103 | spawnOpts.windowsVerbatimArguments = true;
|
104 | } else if (!fs.existsSync(cmd)) {
|
105 | // We need to use /s to ensure that spaces are parsed properly with cmd spawned content
|
106 | args = ['/s', '/c', cmd].concat(args).map(maybeQuote);
|
107 | }
|
108 | }
|
109 |
|
110 | if (opts.stdio !== 'default') {
|
111 | // Ignore 'default' value for stdio because it corresponds to child_process's default 'pipe' option
|
112 | spawnOpts.stdio = opts.stdio;
|
113 | }
|
114 |
|
115 | if (opts.cwd) {
|
116 | spawnOpts.cwd = opts.cwd;
|
117 | }
|
118 |
|
119 | if (opts.env) {
|
120 | spawnOpts.env = _.extend(_.extend({}, process.env), opts.env);
|
121 | }
|
122 |
|
123 | if (opts.chmod && !iswin32) {
|
124 | try {
|
125 | // This fails when module is installed in a system directory (e.g. via sudo npm install)
|
126 | fs.chmodSync(cmd, '755');
|
127 | } catch (e) {
|
128 | // If the perms weren't set right, then this will come as an error upon execution.
|
129 | }
|
130 | }
|
131 |
|
132 | events.emit(opts.printCommand ? 'log' : 'verbose', 'Running command: ' + maybeQuote(cmd) + ' ' + args.map(maybeQuote).join(' '));
|
133 |
|
134 | var child = child_process.spawn(cmd, args, spawnOpts);
|
135 | var capturedOut = '';
|
136 | var capturedErr = '';
|
137 |
|
138 | if (child.stdout) {
|
139 | child.stdout.setEncoding('utf8');
|
140 | child.stdout.on('data', function (data) {
|
141 | capturedOut += data;
|
142 | d.notify({'stdout': data});
|
143 | });
|
144 | }
|
145 |
|
146 | if (child.stderr) {
|
147 | child.stderr.setEncoding('utf8');
|
148 | child.stderr.on('data', function (data) {
|
149 | capturedErr += data;
|
150 | d.notify({'stderr': data});
|
151 | });
|
152 | }
|
153 |
|
154 | child.on('close', whenDone);
|
155 | child.on('error', whenDone);
|
156 | function whenDone (arg) {
|
157 | child.removeListener('close', whenDone);
|
158 | child.removeListener('error', whenDone);
|
159 | var code = typeof arg === 'number' ? arg : arg && arg.code;
|
160 |
|
161 | events.emit('verbose', 'Command finished with error code ' + code + ': ' + cmd + ' ' + args);
|
162 | if (code === 0) {
|
163 | d.resolve(capturedOut.trim());
|
164 | } else {
|
165 | var errMsg = cmd + ': Command failed with exit code ' + code;
|
166 | if (capturedErr) {
|
167 | errMsg += ' Error output:\n' + capturedErr.trim();
|
168 | }
|
169 | var err = new Error(errMsg);
|
170 | if (capturedErr) {
|
171 | err.stderr = capturedErr;
|
172 | }
|
173 | if (capturedOut) {
|
174 | err.stdout = capturedOut;
|
175 | }
|
176 | err.code = code;
|
177 | d.reject(err);
|
178 | }
|
179 | }
|
180 |
|
181 | return d.promise;
|
182 | };
|
183 |
|
184 | exports.maybeSpawn = function (cmd, args, opts) {
|
185 | if (fs.existsSync(cmd)) {
|
186 | return exports.spawn(cmd, args, opts);
|
187 | }
|
188 | return Q(null);
|
189 | };
|