UNPKG

grunt-run

Version:

Invite external commands into your grunt process with three tasks `run`, `wait` and `stop`.

336 lines (284 loc) 9 kB
'use strict'; /* * grunt-run * https://github.com/spenceralger/grunt-run * * Copyright (c) 2013 Spencer Alger * Licensed under the MIT license. */ module.exports = makeTask; function makeTask(grunt) { var stripAnsi = require('strip-ansi'); var childProcess = require('child_process'); var shouldEscapeRE = / |"|'|\$|&|\\/; var dangerArgsRE = /"|\$|\\/g; var runningProcs = []; process.on('exit', function () { return kill(runningProcs); }); function getPid(name) { return grunt.config.get('stop.' + grunt.config.escape(name) + '._pid'); } function savePid(name, pid) { grunt.config.set('stop.' + grunt.config.escape(name) + '._pid', pid); grunt.config.set('wait.' + grunt.config.escape(name) + '._pid', pid); } function clearPid(name) { grunt.config.set('stop.' + grunt.config.escape(name) + '._pid', null); grunt.config.set('wait.' + grunt.config.escape(name) + '._pid', null); } function kill(procs) { var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = procs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var proc = _step.value; proc.kill(); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } function getProcs(pid) { return runningProcs.filter(function (proc) { return proc.pid === pid; }); } function remove(array, item) { do { var i = array.indexOf(item); if (i === -1) { return; } array.splice(i, 1); } while (true); } function includes(array, target) { var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = array[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var item = _step2.value; if (item === target) { return true; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } return false; } grunt.task.registerMultiTask('run', 'used to start external processes (like servers)', function (keepalive) { var name = this.target; var cmd = this.data.cmd || 'node'; var args = this.data.args || []; var additionalArgs = []; var opts = this.options({ wait: true, failOnError: false, quiet: false, ready: 1000, cwd: process.cwd(), passArgs: [], itterable: false, readyBufferLength: 1024 }); if (keepalive === 'keepalive') { // override the wait setting opts.wait = true; } var spawnOpts = { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] }; if (opts.env) { spawnOpts.env = opts.env; } var pid = getPid(name); if (pid && getProcs(pid).length) { grunt.log.warn(name + ' is already running'); return; } if (!opts.itterable && includes(process.argv, 'run')) { grunt.log.warn('Skipping run:' + this.target + ' since it not itterable. Call it directly or from another task.'); return; } opts.passArgs.map(function (arg) { var val = grunt.option(arg); if (val !== void 0) { if (shouldEscapeRE.test(arg)) { val = '"' + arg.replace(dangerArgsRE, function (match) { return '\\' + match; }) + '"'; } additionalArgs.push('--' + arg + '=' + val); } }); if (this.data.exec) { // logic is from node's cp.exec method, adapted to benefit from // streaming io if (process.platform === 'win32') { cmd = 'cmd.exe'; args = ['/s', '/c', '"' + this.data.exec + '"']; spawnOpts.windowsVerbatimArguments = true; } else { cmd = '/bin/sh'; args = ['-c', this.data.exec]; } if (additionalArgs.length) { if (process.platform === 'win32') { args[2] += ' ' + additionalArgs.join(' '); } else { args[1] += ' ' + additionalArgs.join(' '); } } } else { args = args.concat(additionalArgs); } grunt.verbose.writeln('running', cmd, 'with args', args); var proc = childProcess.spawn(cmd, args, spawnOpts); savePid(name, proc.pid); var done = this.async(); // handle stdout, stderr if (!opts.quiet) { proc.stdout.pipe(process.stdout); proc.stderr.pipe(process.stderr); proc.on('close', function () { proc.stdout.unpipe(process.stdout); proc.stderr.unpipe(process.stderr); }); } // handle errors that prevent the proc from starting proc.on('error', function (err) { grunt.log.error(err); }); if (opts.wait) { waitForProc(); } else { trackBackgroundProc(); if (opts.ready instanceof RegExp) { waitForReadyOutput(); } else if (opts.ready) { waitForTimeout(); } else { doNotWait(); } } // ensure that the streams are draining if we aren't already draining them (like quiet=true) try { proc.stdout.resume(); proc.stderr.resume(); } catch (e) { //node versions > 0.8 start streams in flow mode so resume will throw an error } return; // we are waiting for the proc to close before moving on function waitForProc() { proc.on('close', function (exitCode) { done(exitCode && new Error('non-zero exit code ' + exitCode)); }); } // we aren't waiting for this proc to close, so setup some tracking stuff function trackBackgroundProc() { runningProcs.push(proc); proc.on('close', function () { remove(runningProcs, proc); clearPid(name); grunt.log.debug('Process ' + name + ' closed.'); }); } // we are scanning the output for a specific regular expression function waitForReadyOutput() { function onCloseBeforeReady(exitCode) { done(exitCode && new Error('non-zero exit code ' + exitCode)); } var outputBuffer = ''; function checkChunkForReady(chunk) { outputBuffer += chunk.toString('utf8'); // ensure the buffer doesn't grow out of control if (outputBuffer.length >= opts.readyBufferLength) { outputBuffer = outputBuffer.slice(outputBuffer.length - opts.readyBufferLength); } // don't strip ansi until we check, incase an ansi marker is split across chuncks. if (!opts.ready.test(stripAnsi(outputBuffer))) return; outputBuffer = ''; proc.removeListener('close', onCloseBeforeReady); proc.stdout.removeListener('data', checkChunkForReady); proc.stderr.removeListener('data', checkChunkForReady); done(); } proc.on('close', onCloseBeforeReady); proc.stdout.on('data', checkChunkForReady); proc.stderr.on('data', checkChunkForReady); } function waitForTimeout() { setTimeout(function () { grunt.log.ok(name + ' started'); done(); }, opts.ready); } function doNotWait() { grunt.log.ok(name + ' started'); done(); } }); grunt.task.registerMultiTask('stop', 'stop a process started with "run" ' + '(only works for tasks that use wait:false)', function () { var pid = this.data._pid; var name = this.target; var procs = getProcs(pid); clearPid(name); if (procs.length) { var closeHandler = function closeHandler() { if (--counter === 0) { grunt.log.ok(name + ' stopped'); done(); } }; var done = this.async(); var counter = procs.length; procs.forEach(function (proc) { proc.once('close', closeHandler); }); if (process.platform === 'win32') { childProcess.execSync('taskkill /f /t /pid ' + pid); } else { kill(procs); } } else { grunt.log.ok(name + ' already stopped'); } }); grunt.task.registerMultiTask('wait', 'wait for a process started with "run" to close ' + '(only works for tasks that use wait:false)', function () { var pid = this.data._pid; var proc = getProcs(pid)[0]; if (proc) { proc.once('close', this.async()); } else { grunt.log.writeln(this.target + ' (' + pid + ') is already stopped.'); } }); }