vaguerent/lib/vagrant/util/subprocess.rb
2011-12-22 11:29:58 -08:00

170 lines
5.2 KiB
Ruby

require 'childprocess'
require 'log4r'
module Vagrant
module Util
# Execute a command in a subprocess, gathering the results and
# exit status.
#
# This class also allows you to read the data as it is outputted
# from the subprocess in real time, by simply passing a block to
# the execute method.
class Subprocess
# Convenience method for executing a method.
def self.execute(*command, &block)
new(*command).execute(&block)
end
def initialize(*command)
@command = command
@logger = Log4r::Logger.new("vagrant::util::subprocess")
end
def execute
# Build the ChildProcess
@logger.info("Starting process: #{@command.inspect}")
process = ChildProcess.build(*@command)
# Create the pipes so we can read the output in real time as
# we execute the command.
stdout, stdout_writer = IO.pipe
stderr, stderr_writer = IO.pipe
process.io.stdout = stdout_writer
process.io.stderr = stderr_writer
process.duplex = true
# Start the process
begin
process.start
rescue Exception => e
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
if e.is_a?(NativeException)
# This usually means that the process failed to start, so we
# raise that error.
raise ProcessFailedToStart
end
end
raise
end
# Make sure the stdin does not buffer
process.io.stdin.sync = true
# Close the writer pipes, since we're just reading
stdout_writer.close
stderr_writer.close
# Create a dictionary to store all the output we see.
io_data = { stdout => "", stderr => "" }
@logger.debug("Selecting on IO")
while true
results = IO.select([stdout, stderr], [process.io.stdin], nil, 5)
readers, writers = results
# Check the readers to see if they're ready
if !readers.empty?
readers.each do |r|
# Read from the IO object
data = read_io(r)
# We don't need to do anything if the data is empty
next if data.empty?
io_name = r == stdout ? :stdout : :stderr
@logger.debug("#{io_name}: #{data}")
if io_name == :stderr && io_data[r] == "" && data =~ /Errno::ENOENT/
# This is how we detect that a process failed to start on
# Linux. Hacky, but it works fairly well.
raise ProcessFailedToStart
end
io_data[r] += data
yield io_name, data if block_given?
end
end
# Break out if the process exited. We have to do this before
# attempting to write to stdin otherwise we'll get a broken pipe
# error.
break if process.exited?
# Check the writers to see if they're ready, and notify any listeners
if !writers.empty?
yield :stdin, process.io.stdin if block_given?
end
end
# Wait for the process to end.
process.poll_for_exit(32000)
@logger.debug("Exit status: #{process.exit_code}")
# Read the final output data, since it is possible we missed a small
# amount of text between the time we last read data and when the
# process exited.
[stdout, stderr].each do |io|
# Read the extra data, ignoring if there isn't any
extra_data = read_io(io)
next if extra_data == ""
# Log it out and accumulate
@logger.debug(extra_data)
io_data[io] += extra_data
# Yield to any listeners any remaining data
io_name = io == stdout ? :stdout : :stderr
yield io_name, extra_data if block_given?
end
# Return an exit status container
return Result.new(process.exit_code, io_data[stdout], io_data[stderr])
end
protected
# Reads data from an IO object while it can, returning the data it reads.
# When it encounters a case when it can't read anymore, it returns the
# data.
#
# @return [String]
def read_io(io)
data = ""
while true
begin
data << io.read_nonblock(1024)
rescue IO::WaitReadable, EOFError
# An IO::WaitReadable means there may be more IO but this
# IO object is not ready to be read from yet. No problem,
# we read as much as we can, so we break.
# An `EOFError`, on the other hand, means this IO object
# is done! We still just break out.
break
end
end
data
end
# An error which occurs when a process fails to start.
class ProcessFailedToStart < StandardError; end
# Container class to store the results of executing a subprocess.
class Result
attr_reader :exit_code
attr_reader :stdout
attr_reader :stderr
def initialize(exit_code, stdout, stderr)
@exit_code = exit_code
@stdout = stdout
@stderr = stderr
end
end
end
end
end