UNPKG

grunt-shell-spawn

Version:
191 lines (154 loc) 6.73 kB
/* * (c) Sindre Sorhus * sindresorhus.com * MIT License */ module.exports = function( grunt ) { 'use strict'; var _ = grunt.util._; var log = grunt.log; // Keep track of killable and killed processes. var killable = {}; var killed = {}; function killPid(pid) { // Kill the process group. Note that the proc.pid represents the PID of the parent shell // process (/bin/sh or cmd.exe). If we simply kill the parent process, the child // processes will remain running (they will become orphaned). Methods for killing the // entire process group vary by platform. if (process.platform === 'win32') { var cp = require('child_process'); // On windows, we can run taskkill.exe with the /T parameter to do a tree kill. This // needs to be run synchronously in case the :kill task is the last task in the // list, as otherwise grunt will exit first and the process will keep running. cp.execSync('taskkill /f /t /pid ' + pid); } else { // On Unix, we can kill the entire process group by passing in a negative PID. Note // this requires passing in a signal, and it also required us to launch the process // with the option { detached: true }. process.kill(-pid, 'SIGKILL'); } } grunt.registerMultiTask( 'shell', 'Run shell commands', function() { var cp = require('child_process'); var proc; var options = this.options({stdout: true, stderr: true, failOnError: true, canKill: true, stopIfStarted: false}); var data = this.data; var done = options.async ? function() {} : this.async(); var target = this.target; var file, args, opts; var cmd = data.command; if (cmd === undefined) { throw new Error('`command` required'); } cmd = grunt.template.process(typeof cmd === 'function' ? cmd.apply(grunt, arguments) : cmd); grunt.verbose.writeflags(options, 'Options'); // On Unix, set detached option to make it possible to kill the entire process group later opts = _.defaults({}, options.execOptions, { detached: (process.platform !== 'win32') }); // Tests to see if user is trying to kill a running process var shouldKill = options.canKill && this.args.length === 1 && this.args[0] === 'kill'; if (shouldKill) { proc = killable[target]; if (!proc) { grunt.log.writeln('No running process for target:' + target); return; } grunt.verbose.writeln('Killing process for target: ' + target + ' (pid = ' + proc.pid + ')'); // Mark that we're killing this process. This is required because a killed process will // have a null or a non-zero exit code, which would result in an error if the // failOnError option is set. It is assumed that if the user wants to kill a task, that // shouldn't cause an error (they meant to do it) and we should continue processing tasks. killed[target] = proc; killPid(proc.pid); delete killable[target]; done(); return; } if (process.platform === 'win32') { file = 'cmd.exe'; args = ['/s', '/c', cmd ]; opts.windowsVerbatimArguments = true; } else { file = '/bin/sh'; args = ['-c', cmd]; } grunt.verbose.writeln('Command: ' + file); grunt.verbose.writeflags(args, 'Args'); var BUFF_LENGTH = 200*1024, stdOutPos = 0, stdErrPos = 0, stdOutBuf, stdErrBuf; var stdBuffering = _.isFunction( options.callback ); if (stdBuffering) { stdOutBuf = Buffer.alloc(BUFF_LENGTH); stdErrBuf = Buffer.alloc(BUFF_LENGTH); } proc = cp.spawn(file, args, opts); // Store proc to be killed! if (options.canKill) { if (killable[target]) { if (options.stopIfStarted) { var oldProc = killable[target]; oldProc.removeAllListeners('close'); oldProc.on('close', function() { log.ok('restarted :' + target); }); killPid(oldProc.pid); } else { grunt.fatal('Process :' + target + ' already started.'); } } killable[target] = proc; } // proc.stdout and proc.stderr don't exist when stdio: 'inherit' is // passed to spawn. if (proc.stdout) { proc.stdout.setEncoding('utf8'); proc.stdout.on('data', function(data) { if (stdBuffering) { stdOutPos += stdOutBuf.write(data, stdOutPos); } if( _.isFunction( options.stdout ) ) { options.stdout(data); } else if(options.stdout === true || grunt.option('verbose')) { log.write(data); } }); } if (proc.stderr) { proc.stderr.setEncoding('utf8'); proc.stderr.on('data', function(data) { if (stdBuffering) { stdErrPos += stdErrBuf.write(data, stdErrPos); } if( _.isFunction( options.stderr ) ) { options.stderr(data); } else if(options.stderr === true || grunt.option('verbose')) { log.error(data); } }); } proc.on('close', function (code) { delete killable[target]; if ( _.isFunction( options.callback ) ) { var stdOutString = stdOutBuf.toString('utf8', 0, stdOutPos), stdErrString = stdErrBuf.toString('utf8', 0, stdErrPos); options.callback.call(this, code, stdOutString, stdErrString, done); } else if (code !== 0 && options.failOnError && !killed[target]){ grunt.warn('Done, with errors: command "' + data.command + '" (target "' + target + '") exited with code ' + code + '.'); done(); } else { done(); } }); }); process.on('exit', function () { _.forEach(killable, function (proc, key, collection) { killPid(proc.pid); delete collection[key]; }); }); process.on('SIGINT', function () { process.exit(); }); };