1/*
2 * Jake JavaScript build tool
3 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *         http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17*/
18
19
20let util = require('util'); // Native Node util module
21let spawn = require('child_process').spawn;
22let EventEmitter = require('events').EventEmitter;
23let logger = require('./logger');
24let file = require('./file');
25let Exec;
26
27const _UUID_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
28
29let parseArgs = function (argumentsObj) {
30  let args;
31  let arg;
32  let cmds;
33  let callback;
34  let opts = {
35    interactive: false,
36    printStdout: false,
37    printStderr: false,
38    breakOnError: true
39  };
40
41  args = Array.prototype.slice.call(argumentsObj);
42
43  cmds = args.shift();
44  // Arrayize if passed a single string command
45  if (typeof cmds == 'string') {
46    cmds = [cmds];
47  }
48  // Make a copy if it's an actual list
49  else {
50    cmds = cmds.slice();
51  }
52
53  // Get optional callback or opts
54  while((arg = args.shift())) {
55    if (typeof arg == 'function') {
56      callback = arg;
57    }
58    else if (typeof arg == 'object') {
59      opts = Object.assign(opts, arg);
60    }
61  }
62
63  // Backward-compat shim
64  if (typeof opts.stdout != 'undefined') {
65    opts.printStdout = opts.stdout;
66    delete opts.stdout;
67  }
68  if (typeof opts.stderr != 'undefined') {
69    opts.printStderr = opts.stderr;
70    delete opts.stderr;
71  }
72
73  return {
74    cmds: cmds,
75    opts: opts,
76    callback: callback
77  };
78};
79
80/**
81  @name jake
82  @namespace jake
83*/
84let utils = new (function () {
85  /**
86    @name jake.exec
87    @static
88    @function
89    @description Executes shell-commands asynchronously with an optional
90    final callback.
91    `
92    @param {String[]} cmds The list of shell-commands to execute
93    @param {Object} [opts]
94      @param {Boolean} [opts.printStdout=false] Print stdout from each command
95      @param {Boolean} [opts.printStderr=false] Print stderr from each command
96      @param {Boolean} [opts.breakOnError=true] Stop further execution on
97      the first error.
98      @param {Boolean} [opts.windowsVerbatimArguments=false] Don't translate
99      arguments on Windows.
100    @param {Function} [callback] Callback to run after executing  the
101    commands
102
103    @example
104    let cmds = [
105          'echo "showing directories"'
106        , 'ls -al | grep ^d'
107        , 'echo "moving up a directory"'
108        , 'cd ../'
109        ]
110      , callback = function () {
111          console.log('Finished running commands.');
112        }
113    jake.exec(cmds, {stdout: true}, callback);
114   */
115  this.exec = function (a, b, c) {
116    let parsed = parseArgs(arguments);
117    let cmds = parsed.cmds;
118    let opts = parsed.opts;
119    let callback = parsed.callback;
120
121    let ex = new Exec(cmds, opts, callback);
122
123    ex.addListener('error', function (msg, code) {
124      if (opts.breakOnError) {
125        fail(msg, code);
126      }
127    });
128    ex.run();
129
130    return ex;
131  };
132
133  this.createExec = function (a, b, c) {
134    return new Exec(a, b, c);
135  };
136
137  // From Math.uuid.js, https://github.com/broofa/node-uuid
138  // Robert Kieffer (robert@broofa.com), MIT license
139  this.uuid = function (length, radix) {
140    var chars = _UUID_CHARS
141      , uuid = []
142      , r
143      , i;
144
145    radix = radix || chars.length;
146
147    if (length) {
148      // Compact form
149      i = -1;
150      while (++i < length) {
151        uuid[i] = chars[0 | Math.random()*radix];
152      }
153    } else {
154      // rfc4122, version 4 form
155
156      // rfc4122 requires these characters
157      uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
158      uuid[14] = '4';
159
160      // Fill in random data.  At i==19 set the high bits of clock sequence as
161      // per rfc4122, sec. 4.1.5
162      i = -1;
163      while (++i < 36) {
164        if (!uuid[i]) {
165          r = 0 | Math.random()*16;
166          uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
167        }
168      }
169    }
170
171    return uuid.join('');
172  };
173
174})();
175
176Exec = function () {
177  let parsed = parseArgs(arguments);
178  let cmds = parsed.cmds;
179  let opts = parsed.opts;
180  let callback = parsed.callback;
181
182  this._cmds = cmds;
183  this._callback = callback;
184  this._config = opts;
185};
186
187util.inherits(Exec, EventEmitter);
188
189Object.assign(Exec.prototype, new (function () {
190
191  let _run = function () {
192    let self = this;
193    let sh;
194    let cmd;
195    let args;
196    let next = this._cmds.shift();
197    let config = this._config;
198    let errData = '';
199    let shStdio;
200    let handleStdoutData = function (data) {
201      self.emit('stdout', data);
202    };
203    let handleStderrData = function (data) {
204      let d = data.toString();
205      self.emit('stderr', data);
206      // Accumulate the error-data so we can use it as the
207      // stack if the process exits with an error
208      errData += d;
209    };
210
211    // Keep running as long as there are commands in the array
212    if (next) {
213      let spawnOpts = {};
214      this.emit('cmdStart', next);
215
216      // Ganking part of Node's child_process.exec to get cmdline args parsed
217      if (process.platform == 'win32') {
218        cmd = 'cmd';
219        args = ['/c', next];
220        if (config.windowsVerbatimArguments) {
221          spawnOpts.windowsVerbatimArguments = true;
222        }
223      }
224      else {
225        cmd = '/bin/sh';
226        args = ['-c', next];
227      }
228
229      if (config.interactive) {
230        spawnOpts.stdio = 'inherit';
231        sh = spawn(cmd, args, spawnOpts);
232      }
233      else {
234        shStdio = [
235          process.stdin
236        ];
237        if (config.printStdout) {
238          shStdio.push(process.stdout);
239        }
240        else {
241          shStdio.push('pipe');
242        }
243        if (config.printStderr) {
244          shStdio.push(process.stderr);
245        }
246        else {
247          shStdio.push('pipe');
248        }
249        spawnOpts.stdio = shStdio;
250        sh = spawn(cmd, args, spawnOpts);
251        if (!config.printStdout) {
252          sh.stdout.addListener('data', handleStdoutData);
253        }
254        if (!config.printStderr) {
255          sh.stderr.addListener('data', handleStderrData);
256        }
257      }
258
259      // Exit, handle err or run next
260      sh.on('exit', function (code) {
261        let msg;
262        if (code !== 0) {
263          msg = errData || 'Process exited with error.';
264          msg = msg.trim();
265          self.emit('error', msg, code);
266        }
267        if (code === 0 || !config.breakOnError) {
268          self.emit('cmdEnd', next);
269          setTimeout(function () { _run.call(self); }, 0);
270        }
271      });
272
273    }
274    else {
275      self.emit('end');
276      if (typeof self._callback == 'function') {
277        self._callback();
278      }
279    }
280  };
281
282  this.append = function (cmd) {
283    this._cmds.push(cmd);
284  };
285
286  this.run = function () {
287    _run.call(this);
288  };
289
290})());
291
292utils.Exec = Exec;
293utils.file = file;
294utils.logger = logger;
295
296module.exports = utils;
297
298