1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | module.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 |
|
56 | exitCodes = _.isArray(exitCodes) ? exitCodes : [exitCodes];
|
57 |
|
58 |
|
59 |
|
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 |
|
74 | execOptions.cwd = execOptions.cwd || process.cwd();
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | var maxBuffer = data.maxBuffer || execOptions.maxBuffer || (200*1024);
|
80 |
|
81 |
|
82 | execOptions.timeout = execOptions.timeout || data.timeout || 0;
|
83 |
|
84 | execOptions.killSignal = execOptions.killSignal || data.killSignal || 'SIGTERM';
|
85 |
|
86 |
|
87 | var shell = (typeof data.shell === 'undefined') ? execOptions.shell : data.shell;
|
88 | execOptions.shell = (typeof shell === 'string') ? shell : (shell === false ? false : true);
|
89 |
|
90 |
|
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 |
|
120 | var splitArgs = function(command) {
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
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 |
|
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 |
|
176 | var bufferedOutput = callback !== defaultCallback;
|
177 |
|
178 |
|
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';
|
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 |
|
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 |
|
256 | done(false);
|
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 |
|
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 |
|
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 |
|
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 |
|
312 | if (sync) { pipeOut(childProcess.stderr); }
|
313 | else { childProcess.stderr.on('data', function (d) { pipeErr(d); }); }
|
314 | }
|
315 |
|
316 |
|
317 |
|
318 |
|
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 |
|
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 |
|
392 | if (sync) {
|
393 | exitFunc(childProcess.status);
|
394 | }
|
395 | else {
|
396 | childProcess.on('exit', exitFunc);
|
397 | }
|
398 |
|
399 | });
|
400 | };
|