_ = require 'underscore-plus' ChildProcess = require 'child_process' # Extended: A wrapper which provides standard error/output line buffering for # Node's ChildProcess. # # ## Examples # # ```coffee # {BufferedProcess} = require 'atom' # # command = 'ps' # args = ['-ef'] # stdout = (output) -> console.log(output) # exit = (code) -> console.log("ps -ef exited with #{code}") # process = new BufferedProcess({command, args, stdout, exit}) # ``` module.exports = class BufferedProcess # Public: Runs the given command by spawning a new child process. # # * `options` An {Object} with the following keys: # * `command` The {String} command to execute. # * `args` The {Array} of arguments to pass to the command (optional). # * `options` The options {Object} to pass to Node's `ChildProcess.spawn` # method (optional). # * `stdout` The callback {Function} that receives a single argument which # contains the standard output from the command. The callback is # called as data is received but it's buffered to ensure only # complete lines are passed until the source stream closes. After # the source stream has closed all remaining data is sent in a # final call (optional). # * `stderr` The callback {Function} that receives a single argument which # contains the standard error output from the command. The # callback is called as data is received but it's buffered to # ensure only complete lines are passed until the source stream # closes. After the source stream has closed all remaining data # is sent in a final call (optional). # * `exit` The callback {Function} which receives a single argument # containing the exit status (optional). constructor: ({command, args, options, stdout, stderr, exit}={}) -> options ?= {} # Related to joyent/node#2318 if process.platform is "win32" # Quote all arguments and escapes inner quotes if args? cmdArgs = args.filter (arg) -> arg? cmdArgs = cmdArgs.map (arg) -> if command in ['explorer.exe', 'explorer'] and /^\/[a-zA-Z]+,.*$/.test(arg) # Don't wrap /root,C:\folder style arguments to explorer calls in # quotes since they will not be interpreted correctly if they are arg else "\"#{arg.toString().replace(/"/g, '\\"')}\"" else cmdArgs = [] if /\s/.test(command) cmdArgs.unshift("\"#{command}\"") else cmdArgs.unshift(command) cmdArgs = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""] cmdOptions = _.clone(options) cmdOptions.windowsVerbatimArguments = true @process = ChildProcess.spawn(process.env.comspec or 'cmd.exe', cmdArgs, cmdOptions) else @process = ChildProcess.spawn(command, args, options) @killed = false stdoutClosed = true stderrClosed = true processExited = true exitCode = 0 triggerExitCallback = -> return if @killed if stdoutClosed and stderrClosed and processExited exit?(exitCode) if stdout stdoutClosed = false @bufferStream @process.stdout, stdout, -> stdoutClosed = true triggerExitCallback() if stderr stderrClosed = false @bufferStream @process.stderr, stderr, -> stderrClosed = true triggerExitCallback() if exit processExited = false @process.on 'exit', (code) -> exitCode = code processExited = true triggerExitCallback() # Helper method to pass data line by line. # # * `stream` The Stream to read from. # * `onLines` The callback to call with each line of data. # * `onDone` The callback to call when the stream has closed. bufferStream: (stream, onLines, onDone) -> stream.setEncoding('utf8') buffered = '' stream.on 'data', (data) => return if @killed buffered += data lastNewlineIndex = buffered.lastIndexOf('\n') if lastNewlineIndex isnt -1 onLines(buffered.substring(0, lastNewlineIndex + 1)) buffered = buffered.substring(lastNewlineIndex + 1) stream.on 'close', => return if @killed onLines(buffered) if buffered.length > 0 onDone() # Kill all child processes of the spawned cmd.exe process on Windows. # # This is required since killing the cmd.exe does not terminate child # processes. killOnWindows: -> parentPid = @process.pid cmd = 'wmic' args = [ 'process' 'where' "(ParentProcessId=#{parentPid})" 'get' 'processid' ] wmicProcess = ChildProcess.spawn(cmd, args) wmicProcess.on 'error', -> # ignore errors output = '' wmicProcess.stdout.on 'data', (data) -> output += data wmicProcess.stdout.on 'close', => pidsToKill = output.split(/\s+/) .filter (pid) -> /^\d+$/.test(pid) .map (pid) -> parseInt(pid) .filter (pid) -> pid isnt parentPid and 0 < pid < Infinity for pid in pidsToKill try process.kill(pid) @killProcess() killProcess: -> @process?.kill() @process = null # Public: Terminate the process. kill: -> return if @killed @killed = true if process.platform is 'win32' @killOnWindows() else @killProcess() undefined