Mailspring/src/buffered-process.coffee
Ben Gotow 8133cc88d6 feat(logging): Developer bar, verbose logging to logstash, Electron 0.26.0
Summary:
- We now make verbose log files continuously as you use the app
- We ship the logs to LogStash via S3 when an exception occurs
- We log the DatabaseStore, ActionBridge and Analytics packages

- We are now on the latest version of Electron 0.26.0
- We are now on Chrome 42 and io.js 1.4.3
- We should be setup to use ASAR soon.

Update atom.sh to reflect that we're now electron

oniguruma was unnecessary

correctly find log files that haven't been shipped yet

Fix a small issue with nodeIsVisible after upgrade to Chrome 42

Delete old logs, better logging from database store, don't ship empty logs

Test Plan: Run existing tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D1531
2015-05-19 17:02:46 -07:00

245 lines
7.4 KiB
CoffeeScript

_ = require 'underscore'
ChildProcess = require 'child_process'
{Emitter} = require 'event-kit'
path = require 'path'
# 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
###
Section: Construction
###
# 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` {Object} (optional) The options {Object} to pass to Node's
# `ChildProcess.spawn` method.
# * `stdout` {Function} (optional) The callback 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.
# * `data` {String}
# * `stderr` {Function} (optional) The callback 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.
# * `data` {String}
# * `exit` {Function} (optional) The callback which receives a single
# argument containing the exit status.
# * `code` {Number}
constructor: ({command, args, options, stdout, stderr, exit}={}) ->
@emitter = new Emitter
options ?= {}
@command = command
# 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 @isExplorerCommand(command) 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
@spawn(@getCmdPath(), cmdArgs, cmdOptions)
else
@spawn(command, args, options)
@killed = false
@handleEvents(stdout, stderr, exit)
###
Section: Event Subscription
###
# Public: Will call your callback when an error will be raised by the process.
# Usually this is due to the command not being available or not on the PATH.
# You can call `handle()` on the object passed to your callback to indicate
# that you have handled this error.
#
# * `callback` {Function} callback
# * `errorObject` {Object}
# * `error` {Object} the error object
# * `handle` {Function} call this to indicate you have handled the error.
# The error will not be thrown if this function is called.
#
# Returns a {Disposable}
onWillThrowError: (callback) ->
@emitter.on 'will-throw-error', callback
###
Section: Helper Methods
###
# 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: ->
return unless @process?
parentPid = @process.pid
cmd = 'wmic'
args = [
'process'
'where'
"(ParentProcessId=#{parentPid})"
'get'
'processid'
]
try
wmicProcess = ChildProcess.spawn(cmd, args)
catch spawnError
@killProcess()
return
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
isExplorerCommand: (command) ->
if command is 'explorer.exe' or command is 'explorer'
true
else if process.env.SystemRoot
command is path.join(process.env.SystemRoot, 'explorer.exe') or command is path.join(process.env.SystemRoot, 'explorer')
else
false
getCmdPath: ->
if process.env.comspec
process.env.comspec
else if process.env.SystemRoot
path.join(process.env.SystemRoot, 'System32', 'cmd.exe')
else
'cmd.exe'
# Public: Terminate the process.
kill: ->
return if @killed
@killed = true
if process.platform is 'win32'
@killOnWindows()
else
@killProcess()
undefined
spawn: (command, args, options) ->
try
@process = ChildProcess.spawn(command, args, options)
catch spawnError
process.nextTick => @handleError(spawnError)
handleEvents: (stdout, stderr, exit) ->
return unless @process?
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()
@process.on 'error', (error) => @handleError(error)
return
handleError: (error) ->
handled = false
handle = -> handled = true
@emitter.emit 'will-throw-error', {error, handle}
if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
error = new Error("Failed to spawn command `#{@command}`. Make sure `#{@command}` is installed and on your PATH", error.path)
error.name = 'BufferedProcessError'
throw error unless handled