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