Switch posix-spawn to childprocess for better cross-OS support
This commit is contained in:
parent
2174d02439
commit
203056a0db
|
@ -2,7 +2,7 @@ require 'digest/sha1'
|
||||||
require 'pathname'
|
require 'pathname'
|
||||||
require 'yaml'
|
require 'yaml'
|
||||||
|
|
||||||
require 'posix-spawn'
|
require 'childprocess'
|
||||||
|
|
||||||
require 'vagrant/util/file_checksum'
|
require 'vagrant/util/file_checksum'
|
||||||
|
|
||||||
|
@ -39,9 +39,11 @@ namespace :acceptance do
|
||||||
|
|
||||||
# TODO: This isn't Windows friendly yet. Move to a OS-independent
|
# TODO: This isn't Windows friendly yet. Move to a OS-independent
|
||||||
# download.
|
# download.
|
||||||
pid = POSIX::Spawn.spawn("wget", box["url"], "-O", box_file.to_s)
|
process = ChildProcess.build("wget", box["url"], "-O", box_file.to_s)
|
||||||
pid, status = Process.waitpid2(pid)
|
process.io.inherit!
|
||||||
if status.exitstatus != 0
|
process.start
|
||||||
|
process.poll_for_exit(64000)
|
||||||
|
if process.exit_code != 0
|
||||||
puts "Download failed!"
|
puts "Download failed!"
|
||||||
abort
|
abort
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
require "fileutils"
|
require "fileutils"
|
||||||
require "pathname"
|
require "pathname"
|
||||||
|
require "tempfile"
|
||||||
|
|
||||||
require "log4r"
|
require "log4r"
|
||||||
require "posix-spawn"
|
require "childprocess"
|
||||||
|
|
||||||
require File.expand_path("../tempdir", __FILE__)
|
require File.expand_path("../tempdir", __FILE__)
|
||||||
require File.expand_path("../virtualbox", __FILE__)
|
require File.expand_path("../virtualbox", __FILE__)
|
||||||
|
@ -12,8 +13,6 @@ module Acceptance
|
||||||
# run in. It creates a temporary directory to act as the
|
# run in. It creates a temporary directory to act as the
|
||||||
# working directory as well as sets a custom home directory.
|
# working directory as well as sets a custom home directory.
|
||||||
class IsolatedEnvironment
|
class IsolatedEnvironment
|
||||||
include POSIX::Spawn
|
|
||||||
|
|
||||||
attr_reader :homedir
|
attr_reader :homedir
|
||||||
attr_reader :workdir
|
attr_reader :workdir
|
||||||
|
|
||||||
|
@ -54,31 +53,46 @@ module Acceptance
|
||||||
def execute(command, *argN)
|
def execute(command, *argN)
|
||||||
command = replace_command(command)
|
command = replace_command(command)
|
||||||
|
|
||||||
# Setup the options that will be passed to the ``popen4``
|
# Get the hash options passed to this method
|
||||||
# method.
|
options = argN.last.is_a?(Hash) ? argN.pop : {}
|
||||||
argN << {} if !argN.last.is_a?(Hash)
|
|
||||||
options = argN.last
|
|
||||||
options[:chdir] ||= @workdir.to_s
|
|
||||||
|
|
||||||
# Determine the timeout for the process
|
|
||||||
timeout = options.delete(:timeout)
|
timeout = options.delete(:timeout)
|
||||||
|
|
||||||
# Execute in a separate process, wait for it to complete, and
|
# Build a child process to run this command. For the stdout/stderr
|
||||||
# return the IO streams.
|
# we use pipes so that we can select() on it and block and stream
|
||||||
|
# data in as it comes.
|
||||||
@logger.info("Executing: #{command} #{argN.inspect}. Output will stream in...")
|
@logger.info("Executing: #{command} #{argN.inspect}. Output will stream in...")
|
||||||
pid, stdin, stdout, stderr = popen4(@env, command, *argN)
|
process = ChildProcess.build(command, *argN)
|
||||||
status = nil
|
stdout, stdout_writer = IO.pipe
|
||||||
|
process.io.stdout = stdout_writer
|
||||||
|
|
||||||
io_data = {
|
stderr, stderr_writer = IO.pipe
|
||||||
stdout => "",
|
process.io.stderr = stderr_writer
|
||||||
stderr => ""
|
process.duplex = true
|
||||||
}
|
|
||||||
|
Dir.chdir(@workdir.to_s) do
|
||||||
|
with_env_changes do
|
||||||
|
process.start
|
||||||
|
process.io.stdin.sync = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Close our side of the pipes, since we're just reading
|
||||||
|
stdout_writer.close
|
||||||
|
stderr_writer.close
|
||||||
|
|
||||||
|
# Create a hash to store all the data we see.
|
||||||
|
io_data = { stdout => "", stderr => "" }
|
||||||
|
|
||||||
# Record the start time for timeout purposes
|
# Record the start time for timeout purposes
|
||||||
start_time = Time.now.to_i
|
start_time = Time.now.to_i
|
||||||
|
|
||||||
while results = IO.select([stdout, stderr], [stdin], nil, timeout || 5)
|
@logger.debug("Selecting on IO...")
|
||||||
raise TimeoutExceeded, pid if timeout && (Time.now.to_i - start_time) > timeout
|
while true
|
||||||
|
results = IO.select([stdout, stderr],
|
||||||
|
[process.io.stdin], nil, timeout || 5)
|
||||||
|
|
||||||
|
# Check if we have exceeded our timeout from waiting on a select()
|
||||||
|
raise TimeoutExceeded, process.pid if timeout && (Time.now.to_i - start_time) > timeout
|
||||||
|
|
||||||
# Check the readers first to see if they're ready
|
# Check the readers first to see if they're ready
|
||||||
readers = results[0]
|
readers = results[0]
|
||||||
|
@ -87,7 +101,6 @@ module Acceptance
|
||||||
readers.each do |r|
|
readers.each do |r|
|
||||||
data = r.readline
|
data = r.readline
|
||||||
io_data[r] += data
|
io_data[r] += data
|
||||||
|
|
||||||
io_name = r == stdout ? "stdout" : "stderr"
|
io_name = r == stdout ? "stdout" : "stderr"
|
||||||
@logger.debug("[#{io_name}] #{data.chomp}")
|
@logger.debug("[#{io_name}] #{data.chomp}")
|
||||||
yield io_name.to_sym, data if block_given?
|
yield io_name.to_sym, data if block_given?
|
||||||
|
@ -98,38 +111,31 @@ module Acceptance
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check here if the process has exited, and if so, exit the
|
# Check if the process exited in order to break the loop before
|
||||||
# loop.
|
# we try to see if any stdin is ready.
|
||||||
exit_pid, status = Process.waitpid2(pid, Process::WNOHANG)
|
break if process.exited?
|
||||||
break if exit_pid
|
|
||||||
|
|
||||||
# Check the writers to see if they're ready, and notify any
|
# Check the writers to see if they're ready, and notify any listeners
|
||||||
# listeners...
|
|
||||||
if !results[1].empty?
|
if !results[1].empty?
|
||||||
yield :stdin, stdin if block_given?
|
yield :stdin, process.io.stdin if block_given?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Continually try to wait for the process to end, but do so asynchronously
|
# Continually try to wait for the process to end, but do so asynchronously
|
||||||
# so that we can also check to see if we have exceeded a timeout.
|
# so that we can also check to see if we have exceeded a timeout.
|
||||||
while true
|
begin
|
||||||
# Break if status because it was already obtained above
|
# If a timeout is not set, we set a very large timeout to
|
||||||
break if status
|
# simulate "forever"
|
||||||
|
@logger.debug("Waiting for process to exit...")
|
||||||
# Try to wait for the PID to exit, and exit this loop if it does
|
remaining = (timeout || 32000) - (Time.now.to_i - start_time)
|
||||||
exitpid, status = Process.waitpid2(pid, Process::WNOHANG)
|
remaining = 0 if remaining < 0
|
||||||
break if exitpid
|
process.poll_for_exit(remaining)
|
||||||
|
rescue ChildProcess::TimeoutError
|
||||||
# Check to see if we exceeded our process timeout while waiting for
|
raise TimeoutExceeded, process.pid
|
||||||
# it to end.
|
|
||||||
raise TimeoutExceeded, pid if timeout && (Time.now.to_i - start_time) > timeout
|
|
||||||
|
|
||||||
# Sleep between checks so that we're not constantly hitting the syscall
|
|
||||||
sleep 0.5
|
|
||||||
end
|
end
|
||||||
@logger.debug("Exit status: #{status.exitstatus}")
|
|
||||||
|
|
||||||
return ExecuteProcess.new(status.exitstatus, io_data[stdout], io_data[stderr])
|
@logger.debug("Exit status: #{process.exit_code}")
|
||||||
|
return ExecuteProcess.new(process.exit_code, io_data[stdout], io_data[stderr])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Closes the environment, cleans up the temporary directories, etc.
|
# Closes the environment, cleans up the temporary directories, etc.
|
||||||
|
@ -182,6 +188,28 @@ module Acceptance
|
||||||
return @apps[command] if @apps.has_key?(command)
|
return @apps[command] if @apps.has_key?(command)
|
||||||
return command
|
return command
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This method changes the environmental variables of the process to
|
||||||
|
# that of this environment, yields, and then resets them. This allows
|
||||||
|
# us to change the environment temporarily.
|
||||||
|
#
|
||||||
|
# NOTE: NOT threadsafe.
|
||||||
|
def with_env_changes
|
||||||
|
stashed = {}
|
||||||
|
|
||||||
|
begin
|
||||||
|
@env.each do |key, value|
|
||||||
|
stashed[key] = ENV[key]
|
||||||
|
ENV[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
stashed.each do |key, value|
|
||||||
|
ENV[key] = stashed[key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# This class represents a process which has run via the IsolatedEnvironment.
|
# This class represents a process which has run via the IsolatedEnvironment.
|
||||||
|
|
|
@ -28,7 +28,7 @@ Gem::Specification.new do |s|
|
||||||
s.add_development_dependency "log4r", "~> 1.1.9"
|
s.add_development_dependency "log4r", "~> 1.1.9"
|
||||||
s.add_development_dependency "minitest", "~> 2.5.1"
|
s.add_development_dependency "minitest", "~> 2.5.1"
|
||||||
s.add_development_dependency "mocha"
|
s.add_development_dependency "mocha"
|
||||||
s.add_development_dependency "posix-spawn", "~> 0.3.6"
|
s.add_development_dependency "childprocess", "~> 0.2.2"
|
||||||
s.add_development_dependency "sys-proctable", "~> 0.9.1"
|
s.add_development_dependency "sys-proctable", "~> 0.9.1"
|
||||||
s.add_development_dependency "rspec-core", "~> 2.7.1"
|
s.add_development_dependency "rspec-core", "~> 2.7.1"
|
||||||
s.add_development_dependency "rspec-expectations", "~> 2.7.0"
|
s.add_development_dependency "rspec-expectations", "~> 2.7.0"
|
||||||
|
|
Loading…
Reference in New Issue