UNPKG

14.4 kBJavaScriptView Raw
1// grunt-exec
2// ==========
3// * GitHub: https://github.com/jharding/grunt-exec
4// * Original Copyright (c) 2012 Jake Harding
5// * Copyright (c) 2017 grunt-exec
6// * Licensed under the MIT license.
7
8// grunt-exe 2.0.0+ simulates the convenience of child_process.exec with the capabilities of child_process.spawn
9// this was done primarily to preserve colored output from applications such as npm
10// a lot of work was done to simulate the original behavior of both child_process.exec and grunt-exec
11// as such there may be unintended consequences so the major revision was bumped
12// a breaking change was made to the 'maxBuffer kill process' scenario so it is treated as an error and provides more detail (--verbose)
13// stdout and stderr buffering & maxBuffer constraints are removed entirely where possible
14// new features: detached (boolean), argv0 (override the executable name passed to the application), shell (boolean or string)
15// fd #s greater than 2 not yet supported (ipc piping) which is spawn-specific and very rarely required
16// TODO: support stdout and stderr Buffer objects passed in
17// TODO: stdin/stdout/stderr string as file name => open the file and read/write from it
18
19module.exports = function(grunt) {
20 var cp = require('child_process')
21 , f = require('util').format
22 , _ = grunt.util._
23 , log = grunt.log
24 , verbose = grunt.verbose;
25
26 grunt.registerMultiTask('exec', 'Execute shell commands.', function() {
27
28 var callbackErrors = false;
29
30 var defaultOut = log.write;
31 var defaultError = log.error;
32
33 var defaultCallback = function(err, stdout, stderr) {
34 if (err) {
35 callbackErrors = true;
36 defaultError('Error executing child process: ' + err.toString());
37 }
38 };
39
40 var data = this.data
41 , execOptions = data.options !== undefined ? data.options : {}
42 , stdout = data.stdout !== undefined ? data.stdout : true
43 , stderr = data.stderr !== undefined ? data.stderr : true
44 , stdin = data.stdin !== undefined ? data.stdin : false
45 , stdio = data.stdio
46 , callback = _.isFunction(data.callback) ? data.callback : defaultCallback
47 , callbackArgs = data.callbackArgs !== undefined ? data.callbackArgs : []
48 , sync = data.sync !== undefined ? data.sync : false
49 , exitCodes = data.exitCode || data.exitCodes || 0
50 , command
51 , childProcess
52 , args = [].slice.call(arguments, 0)
53 , done = this.async();
54
55 // https://github.com/jharding/grunt-exec/pull/30
56 exitCodes = _.isArray(exitCodes) ? exitCodes : [exitCodes];
57
58 // allow for command to be specified in either
59 // 'command' or 'cmd' property, or as a string.
60 command = data.command || data.cmd || (_.isString(data) && data);
61
62 if (!command) {
63 defaultError('Missing command property.');
64 return done(false);
65 }
66
67 if (data.cwd && _.isFunction(data.cwd)) {
68 execOptions.cwd = data.cwd.apply(grunt, args);
69 } else if (data.cwd) {
70 execOptions.cwd = data.cwd;
71 }
72
73 // default to current process cwd
74 execOptions.cwd = execOptions.cwd || process.cwd();
75
76 // manually supported (spawn vs exec)
77 // 200*1024 is default maxBuffer of child_process.exec
78 // NOTE: must be < require('buffer').kMaxLength or a RangeError will be triggered
79 var maxBuffer = data.maxBuffer || execOptions.maxBuffer || (200*1024);
80
81 // timeout manually supportted (spawn vs exec)
82 execOptions.timeout = execOptions.timeout || data.timeout || 0;
83 // kill signal manually supportted (spawn vs exec)
84 execOptions.killSignal = execOptions.killSignal || data.killSignal || 'SIGTERM';
85
86 // support shell scripts like 'npm.cmd' by default (spawn vs exec)
87 var shell = (typeof data.shell === 'undefined') ? execOptions.shell : data.shell;
88 execOptions.shell = (typeof shell === 'string') ? shell : (shell === false ? false : true);
89
90 // kept in data.encoding in case it is set to 'buffer' for final callback
91 data.encoding = data.encoding || execOptions.encoding || 'utf8';
92
93 stdio = stdio || execOptions.stdio || undefined;
94 if (stdio === 'inherit') {
95 stdout = 'inherit';
96 stderr = 'inherit';
97 stdin = 'inherit';
98 } else if (stdio === 'pipe') {
99 stdout = 'pipe';
100 stderr = 'pipe';
101 stdin = 'pipe';
102 } else if (stdio === 'ignore') {
103 stdout = 'ignore';
104 stderr = 'ignore';
105 stdin = 'ignore';
106 }
107
108 if (_.isFunction(command)) {
109 command = command.apply(grunt, args);
110 }
111
112 if (!_.isString(command)) {
113 defaultError('Command property must be a string.');
114 return done(false);
115 }
116
117 verbose.subhead(command);
118
119 // manually parse args into array (spawn vs exec)
120 var splitArgs = function(command) {
121 // Regex Explanation Regex
122 // ---------------------------------------------------------------------
123 // 0-* spaces \s*
124 // followed by either:
125 // [NOT: a space, half quote, or double quote] 1-* times [^\s'"]+
126 // followed by either:
127 // [half quote or double quote] in the future (?=['"])
128 // or 1-* spaces \s+
129 // or end of string $
130 // or half quote [']
131 // followed by 0-*:
132 // [NOT: a backslash, or half quote] [^\\']
133 // or a backslash followed by any character \\.
134 // followed by a half quote [']
135 // or double quote ["]
136 // followed by 0-*:
137 // [NOT: a backslash, or double quote] [^\\"]
138 // or a backslash followed by any character \\.
139 // followed by a double quote ["]
140 // or end of string $
141 var pieces = command.match(/\s*([^\s'"]+(?:(?=['"])|\s+|$)|(?:(?:['](?:([^\\']|\\.)*)['])|(?:["](?:([^\\"]|\\.)*)["]))|$)/g);
142 var args = [];
143 var next = false;
144
145 for (var i = 0; i < pieces.length; i++) {
146 var piece = pieces[i];
147 if (piece.length > 0) {
148 if (next || args.length === 0 || piece.charAt(0) === ' ') {
149 args.push(piece.trim());
150 } else {
151 var last = args.length - 1;
152 args[last] = args[last] + piece.trim();
153 }
154 next = piece.endsWith(' ');
155 }
156 }
157
158 // NodeJS on Windows does not have this issue
159 if (process.platform !== 'win32') {
160 args = [args.join(' ')];
161 }
162
163 return args;
164 };
165
166 var args = splitArgs(command);
167 command = args[0];
168
169 if (args.length > 1) {
170 args = args.slice(1);
171 } else {
172 args = [];
173 }
174
175 // only save stdout and stderr if a custom callback is used
176 var bufferedOutput = callback !== defaultCallback;
177
178 // different stdio behavior (spawn vs exec)
179 var stdioOption = function(value, integerValue, inheritValue) {
180 return value === integerValue ? integerValue
181 : value === 'inherit' ? inheritValue
182 : bufferedOutput ? 'pipe' : value === 'pipe' || value === true || value === null || value === undefined ? 'pipe'
183 : 'ignore'; /* value === false || value === 'ignore' */
184 }
185
186 execOptions.stdio = [
187 stdioOption(stdin, 0, process.stdin),
188 stdioOption(stdout, 1, process.stdout),
189 stdioOption(stderr, 2, process.stderr)
190 ];
191
192 var encoding = data.encoding;
193 var bufferedStdOut = bufferedOutput && execOptions.stdio[1] === 'pipe';
194 var bufferedStdErr = bufferedOutput && execOptions.stdio[2] === 'pipe';
195 var stdOutLength = 0;
196 var stdErrLength = 0;
197 var stdOutBuffers = [];
198 var stdErrBuffers = [];
199
200 if (bufferedOutput && !Buffer.isEncoding(encoding)) {
201 if (encoding === 'buffer') {
202 encoding = 'binary';
203 } else {
204 grunt.fail.fail('Encoding "' + encoding + '" is not a supported character encoding!');
205 done(false);
206 }
207 }
208
209 if (verbose) {
210 stdioDescriptions = execOptions.stdio.slice();
211 for (var i = 0; i < stdioDescriptions.length; i++) {
212 stdioDescription = stdioDescriptions[i];
213 if (stdioDescription === process.stdin) {
214 stdioDescriptions[i] = 'process.stdin';
215 } else if (stdioDescription === process.stdout) {
216 stdioDescriptions[i] = 'process.stdout';
217 } else if (stdioDescription === process.stderr) {
218 stdioDescriptions[i] = 'process.stderr';
219 }
220 }
221
222 verbose.writeln('buffer : ' + (bufferedOutput ?
223 (bufferedStdOut ? 'stdout=enabled' : 'stdout=disabled')
224 + ';' +
225 (bufferedStdErr ? 'stderr=enabled' : 'stderr=disabled')
226 + ';' +
227 'max size=' + maxBuffer
228 : 'disabled'));
229 verbose.writeln('timeout : ' + (execOptions.timeout === 0 ? 'infinite' : '' + execOptions.timeout + 'ms'));
230 verbose.writeln('killSig : ' + execOptions.killSignal);
231 verbose.writeln('shell : ' + execOptions.shell);
232 verbose.writeln('command : ' + command);
233 verbose.writeln('args : [' + args.join(',') + ']');
234 verbose.writeln('stdio : [' + stdioDescriptions.join(',') + ']');
235 verbose.writeln('cwd : ' + execOptions.cwd);
236 //verbose.writeln('env path : ' + process.env.PATH);
237 verbose.writeln('exitcodes:', exitCodes.join(','));
238 }
239
240 if (sync)
241 {
242 childProcess = cp.spawnSync(command, args, execOptions);
243 }
244 else {
245 childProcess = cp.spawn(command, args, execOptions);
246 }
247
248 if (verbose) {
249 verbose.writeln('pid : ' + childProcess.pid);
250 }
251
252 var killChild = function (reason) {
253 defaultError(reason);
254 process.kill(childProcess.pid, execOptions.killSignal);
255 //childProcess.kill(execOptions.killSignal);
256 done(false); // unlike exec, this will indicate an error - after all, it did kill the process
257 };
258
259 if (execOptions.timeout !== 0) {
260 var timeoutProcess = function() {
261 killChild('Timeout child process');
262 };
263 setInterval(timeoutProcess, execOptions.timeout);
264 }
265
266 var writeStdOutBuffer = function(d) {
267 var b = !Buffer.isBuffer(d) ? new Buffer(d.toString(encoding)) : d;
268 if (stdOutLength + b.length > maxBuffer) {
269 if (verbose) {
270 verbose.writeln("EXCEEDING MAX BUFFER: stdOut " + stdOutLength + " buffer " + b.length + " maxBuffer " + maxBuffer);
271 }
272 killChild("stdout maxBuffer exceeded");
273 } else {
274 stdOutLength += b.length;
275 stdOutBuffers.push(b);
276 }
277
278 // default piping behavior
279 if (stdout !== false && data.encoding !== 'buffer') {
280 defaultOut(d);
281 }
282 };
283
284 var writeStdErrBuffer = function(d) {
285 var b = !Buffer.isBuffer(d) ? new Buffer(d.toString(encoding)) : d;
286 if (stdErrLength + b.length > maxBuffer) {
287 if (verbose) {
288 verbose.writeln("EXCEEDING MAX BUFFER: stdErr " + stdErrLength + " buffer " + b.length + " maxBuffer " + maxBuffer);
289 }
290 killChild("stderr maxBuffer exceeded");
291 } else {
292 stdErrLength += b.length;
293 stdErrBuffers.push(b);
294 }
295
296 // default piping behavior
297 if (stderr !== false && data.encoding !== 'buffer') {
298 defaultError(d);
299 }
300 };
301
302 if (execOptions.stdio[1] === 'pipe') {
303 var pipeOut = bufferedStdOut ? writeStdOutBuffer : defaultOut;
304 // Asynchronous + Synchronous Support
305 if (sync) { pipeOut(childProcess.stdout); }
306 else { childProcess.stdout.on('data', function (d) { pipeOut(d); }); }
307 }
308
309 if (execOptions.stdio[2] === 'pipe') {
310 var pipeErr = bufferedStdErr ? writeStdErrBuffer : defaultError;
311 // Asynchronous + Synchronous Support
312 if (sync) { pipeOut(childProcess.stderr); }
313 else { childProcess.stderr.on('data', function (d) { pipeErr(d); }); }
314 }
315
316 // Catches failing to execute the command at all (eg spawn ENOENT),
317 // since in that case an 'exit' event will not be emitted.
318 // Asynchronous + Synchronous Support
319 if (sync) {
320 if (childProcess.error != null)
321 {
322 defaultError(f('Failed with: %s', error.message));
323 done(false);
324 }
325 }
326 else {
327 childProcess.on('error', function (err) {
328 defaultError(f('Failed with: %s', err));
329 done(false);
330 });
331 }
332
333 // Exit Function (used for process exit callback / exit function)
334 var exitFunc = function (code) {
335 if (callbackErrors) {
336 defaultError('Node returned an error for this child process');
337 return done(false);
338 }
339
340 var stdOutBuffer = undefined;
341 var stdErrBuffer = undefined;
342
343 if (bufferedStdOut) {
344 stdOutBuffer = new Buffer(stdOutLength);
345 var offset = 0;
346 for (var i = 0; i < stdOutBuffers.length; i++) {
347 var buf = stdOutBuffers[i];
348 buf.copy(stdOutBuffer, offset);
349 offset += buf.length;
350 }
351
352 if (data.encoding !== 'buffer') {
353 stdOutBuffer = stdOutBuffer.toString(encoding);
354 }
355 }
356
357 if (bufferedStdErr) {
358 stdErrBuffer = new Buffer(stdErrLength);
359 var offset = 0;
360 for (var i = 0; i < stdErrBuffers.length; i++) {
361 var buf = stdErrBuffers[i];
362 buf.copy(stdErrBuffer, offset);
363 offset += buf.length;
364 }
365
366 if (data.encoding !== 'buffer') {
367 stdErrBuffer = stdErrBuffer.toString(encoding);
368 }
369 }
370
371 if (exitCodes.indexOf(code) < 0) {
372 defaultError(f('Exited with code: %d.', code));
373 if (callback) {
374 var err = new Error(f('Process exited with code %d.', code));
375 err.code = code;
376
377 callback(err, stdOutBuffer, stdErrBuffer, callbackArgs);
378 }
379 return done(false);
380 }
381
382 verbose.ok(f('Exited with code: %d.', code));
383
384 if (callback) {
385 callback(null, stdOutBuffer, stdErrBuffer, callbackArgs);
386 }
387
388 done();
389 }
390
391 // Asynchronous + Synchronous Support
392 if (sync) {
393 exitFunc(childProcess.status);
394 }
395 else {
396 childProcess.on('exit', exitFunc);
397 }
398
399 });
400};