Merge pull request #4819 from woodruffw/error-pipe
utils: Use JSON to marshal child errors
This commit is contained in:
commit
eacdca872e
@ -190,7 +190,20 @@ begin
|
|||||||
build = Build.new(formula, options)
|
build = Build.new(formula, options)
|
||||||
build.install
|
build.install
|
||||||
rescue Exception => e # rubocop:disable Lint/RescueException
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
||||||
Marshal.dump(e, error_pipe)
|
error_hash = JSON.parse e.to_json
|
||||||
|
|
||||||
|
# Special case: need to recreate BuildErrors in full
|
||||||
|
# for proper analytics reporting and error messages.
|
||||||
|
# BuildErrors are specific to build processses and not other
|
||||||
|
# children, which is why we create the necessary state here
|
||||||
|
# and not in Utils.safe_fork.
|
||||||
|
if error_hash["json_class"] == "BuildError"
|
||||||
|
error_hash["cmd"] = e.cmd
|
||||||
|
error_hash["args"] = e.args
|
||||||
|
error_hash["env"] = e.env
|
||||||
|
end
|
||||||
|
|
||||||
|
error_pipe.puts error_hash.to_json
|
||||||
error_pipe.close
|
error_pipe.close
|
||||||
exit! 1
|
exit! 1
|
||||||
end
|
end
|
||||||
|
|||||||
@ -93,9 +93,6 @@ module Homebrew
|
|||||||
exec(*args)
|
exec(*args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue ::Test::Unit::AssertionFailedError => e
|
|
||||||
ofail "#{f.full_name}: failed"
|
|
||||||
puts e.message
|
|
||||||
rescue Exception => e # rubocop:disable Lint/RescueException
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
||||||
ofail "#{f.full_name}: failed"
|
ofail "#{f.full_name}: failed"
|
||||||
puts e, e.backtrace
|
puts e, e.backtrace
|
||||||
|
|||||||
@ -352,14 +352,16 @@ class FormulaAmbiguousPythonError < RuntimeError
|
|||||||
end
|
end
|
||||||
|
|
||||||
class BuildError < RuntimeError
|
class BuildError < RuntimeError
|
||||||
attr_reader :formula, :env
|
attr_reader :cmd, :args, :env
|
||||||
attr_accessor :options
|
attr_accessor :formula, :options
|
||||||
|
|
||||||
def initialize(formula, cmd, args, env)
|
def initialize(formula, cmd, args, env)
|
||||||
@formula = formula
|
@formula = formula
|
||||||
|
@cmd = cmd
|
||||||
|
@args = args
|
||||||
@env = env
|
@env = env
|
||||||
args = args.map { |arg| arg.to_s.gsub " ", "\\ " }.join(" ")
|
pretty_args = args.map { |arg| arg.to_s.gsub " ", "\\ " }.join(" ")
|
||||||
super "Failed executing: #{cmd} #{args}"
|
super "Failed executing: #{cmd} #{pretty_args}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def issues
|
def issues
|
||||||
@ -526,12 +528,22 @@ end
|
|||||||
|
|
||||||
# raised by safe_system in utils.rb
|
# raised by safe_system in utils.rb
|
||||||
class ErrorDuringExecution < RuntimeError
|
class ErrorDuringExecution < RuntimeError
|
||||||
|
attr_reader :cmd
|
||||||
attr_reader :status
|
attr_reader :status
|
||||||
|
attr_reader :output
|
||||||
|
|
||||||
def initialize(cmd, status:, output: nil)
|
def initialize(cmd, status:, output: nil)
|
||||||
|
@cmd = cmd
|
||||||
@status = status
|
@status = status
|
||||||
|
@output = output
|
||||||
|
|
||||||
s = "Failure while executing; `#{cmd.shelljoin.gsub(/\\=/, "=")}` exited with #{status.exitstatus}."
|
exitstatus = if status.respond_to?(:exitstatus)
|
||||||
|
status.exitstatus
|
||||||
|
else
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
s = "Failure while executing; `#{cmd.shelljoin.gsub(/\\=/, "=")}` exited with #{exitstatus}."
|
||||||
|
|
||||||
unless [*output].empty?
|
unless [*output].empty?
|
||||||
format_output_line = lambda do |type, line|
|
format_output_line = lambda do |type, line|
|
||||||
@ -596,3 +608,22 @@ class BottleFormulaUnavailableError < RuntimeError
|
|||||||
EOS
|
EOS
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Raised when a child process sends us an exception over its error pipe.
|
||||||
|
class ChildProcessError < RuntimeError
|
||||||
|
attr_reader :inner
|
||||||
|
attr_reader :inner_class
|
||||||
|
|
||||||
|
def initialize(inner)
|
||||||
|
@inner = inner
|
||||||
|
@inner_class = Object.const_get inner["json_class"]
|
||||||
|
|
||||||
|
super <<~EOS
|
||||||
|
An exception occured within a build process:
|
||||||
|
#{inner_class}: #{inner["m"]}
|
||||||
|
EOS
|
||||||
|
|
||||||
|
# Clobber our real (but irrelevant) backtrace with that of the inner exception.
|
||||||
|
set_backtrace inner["b"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@ -744,14 +744,18 @@ class FormulaInstaller
|
|||||||
raise "Empty installation"
|
raise "Empty installation"
|
||||||
end
|
end
|
||||||
rescue Exception => e # rubocop:disable Lint/RescueException
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
||||||
e.options = display_options(formula) if e.is_a?(BuildError)
|
if e.is_a? BuildError
|
||||||
|
e.formula = formula
|
||||||
|
e.options = options
|
||||||
|
end
|
||||||
|
|
||||||
ignore_interrupts do
|
ignore_interrupts do
|
||||||
# any exceptions must leave us with nothing installed
|
# any exceptions must leave us with nothing installed
|
||||||
formula.update_head_version
|
formula.update_head_version
|
||||||
formula.prefix.rmtree if formula.prefix.directory?
|
formula.prefix.rmtree if formula.prefix.directory?
|
||||||
formula.rack.rmdir_if_possible
|
formula.rack.rmdir_if_possible
|
||||||
end
|
end
|
||||||
raise
|
raise e
|
||||||
end
|
end
|
||||||
|
|
||||||
def link(keg)
|
def link(keg)
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
require "pathname"
|
require "pathname"
|
||||||
require "English"
|
require "English"
|
||||||
|
require "json"
|
||||||
|
require "json/add/exception"
|
||||||
|
|
||||||
HOMEBREW_LIBRARY_PATH = Pathname.new(__FILE__).realpath.parent
|
HOMEBREW_LIBRARY_PATH = Pathname.new(__FILE__).realpath.parent
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ begin
|
|||||||
formula.extend(Debrew::Formula) if ARGV.debug?
|
formula.extend(Debrew::Formula) if ARGV.debug?
|
||||||
formula.run_post_install
|
formula.run_post_install
|
||||||
rescue Exception => e # rubocop:disable Lint/RescueException
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
||||||
Marshal.dump(e, error_pipe)
|
error_pipe.puts e.to_json
|
||||||
error_pipe.close
|
error_pipe.close
|
||||||
exit! 1
|
exit! 1
|
||||||
end
|
end
|
||||||
|
|||||||
@ -28,7 +28,7 @@ begin
|
|||||||
raise "test returned false" if formula.run_test == false
|
raise "test returned false" if formula.run_test == false
|
||||||
end
|
end
|
||||||
rescue Exception => e # rubocop:disable Lint/RescueException
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
||||||
Marshal.dump(e, error_pipe)
|
error_pipe.puts e.to_json
|
||||||
error_pipe.close
|
error_pipe.close
|
||||||
exit! 1
|
exit! 1
|
||||||
end
|
end
|
||||||
|
|||||||
@ -4,6 +4,7 @@ require "keg"
|
|||||||
require "tab"
|
require "tab"
|
||||||
require "test/support/fixtures/testball"
|
require "test/support/fixtures/testball"
|
||||||
require "test/support/fixtures/testball_bottle"
|
require "test/support/fixtures/testball_bottle"
|
||||||
|
require "test/support/fixtures/failball"
|
||||||
|
|
||||||
describe FormulaInstaller do
|
describe FormulaInstaller do
|
||||||
define_negated_matcher :need_bottle, :be_bottle_unneeded
|
define_negated_matcher :need_bottle, :be_bottle_unneeded
|
||||||
@ -28,7 +29,7 @@ describe FormulaInstaller do
|
|||||||
Tab.clear_cache
|
Tab.clear_cache
|
||||||
expect(Tab.for_keg(keg)).not_to be_poured_from_bottle
|
expect(Tab.for_keg(keg)).not_to be_poured_from_bottle
|
||||||
|
|
||||||
yield formula
|
yield formula if block_given?
|
||||||
ensure
|
ensure
|
||||||
Tab.clear_cache
|
Tab.clear_cache
|
||||||
keg.unlink
|
keg.unlink
|
||||||
@ -132,4 +133,21 @@ describe FormulaInstaller do
|
|||||||
fi.check_install_sanity
|
fi.check_install_sanity
|
||||||
}.to raise_error(CannotInstallFormulaError)
|
}.to raise_error(CannotInstallFormulaError)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
specify "install fails with BuildError when a system() call fails" do
|
||||||
|
ENV["HOMEBREW_TEST_NO_EXIT_CLEANUP"] = "1"
|
||||||
|
ENV["FAILBALL_BUILD_ERROR"] = "1"
|
||||||
|
|
||||||
|
expect {
|
||||||
|
temporary_install(Failball.new)
|
||||||
|
}.to raise_error(BuildError)
|
||||||
|
end
|
||||||
|
|
||||||
|
specify "install fails with a RuntimeError when #install raises" do
|
||||||
|
ENV["HOMEBREW_TEST_NO_EXIT_CLEANUP"] = "1"
|
||||||
|
|
||||||
|
expect {
|
||||||
|
temporary_install(Failball.new)
|
||||||
|
}.to raise_error(RuntimeError)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
20
Library/Homebrew/test/support/fixtures/failball.rb
Normal file
20
Library/Homebrew/test/support/fixtures/failball.rb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
class Failball < Formula
|
||||||
|
def initialize(name = "failball", path = Pathname.new(__FILE__).expand_path, spec = :stable, alias_path: nil)
|
||||||
|
self.class.instance_eval do
|
||||||
|
stable.url "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz"
|
||||||
|
stable.sha256 TESTBALL_SHA256
|
||||||
|
end
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def install
|
||||||
|
prefix.install "bin"
|
||||||
|
prefix.install "libexec"
|
||||||
|
|
||||||
|
# This should get marshalled into a BuildError.
|
||||||
|
system "/usr/bin/false" if ENV["FAILBALL_BUILD_ERROR"]
|
||||||
|
|
||||||
|
# This should get marshalled into a RuntimeError.
|
||||||
|
raise "something that isn't a build error happened!"
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -8,7 +8,11 @@ HOMEBREW_BREW_FILE = Pathname.new(ENV["HOMEBREW_BREW_FILE"])
|
|||||||
|
|
||||||
TEST_TMPDIR = ENV.fetch("HOMEBREW_TEST_TMPDIR") do |k|
|
TEST_TMPDIR = ENV.fetch("HOMEBREW_TEST_TMPDIR") do |k|
|
||||||
dir = Dir.mktmpdir("homebrew-tests-", ENV["HOMEBREW_TEMP"] || "/tmp")
|
dir = Dir.mktmpdir("homebrew-tests-", ENV["HOMEBREW_TEMP"] || "/tmp")
|
||||||
at_exit { FileUtils.remove_entry(dir) }
|
at_exit do
|
||||||
|
# Child processes inherit this at_exit handler, but we don't want them
|
||||||
|
# to clean TEST_TMPDIR up prematurely (i.e., when they exit early for a test).
|
||||||
|
FileUtils.remove_entry(dir) unless ENV["HOMEBREW_TEST_NO_EXIT_CLEANUP"]
|
||||||
|
end
|
||||||
ENV[k] = dir
|
ENV[k] = dir
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
21
Library/Homebrew/test/utils/fork_spec.rb
Normal file
21
Library/Homebrew/test/utils/fork_spec.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
require "utils/fork"
|
||||||
|
|
||||||
|
describe Utils do
|
||||||
|
describe "#safe_fork" do
|
||||||
|
it "raises a RuntimeError on an error that isn't ErrorDuringExecution" do
|
||||||
|
expect {
|
||||||
|
described_class.safe_fork do
|
||||||
|
raise "this is an exception in the child"
|
||||||
|
end
|
||||||
|
}.to raise_error(RuntimeError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an ErrorDuringExecution on one in the child" do
|
||||||
|
expect {
|
||||||
|
described_class.safe_fork do
|
||||||
|
safe_system "/usr/bin/false"
|
||||||
|
end
|
||||||
|
}.to raise_error(ErrorDuringExecution)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -2,6 +2,26 @@ require "fcntl"
|
|||||||
require "socket"
|
require "socket"
|
||||||
|
|
||||||
module Utils
|
module Utils
|
||||||
|
def self.rewrite_child_error(child_error)
|
||||||
|
error = if child_error.inner_class == ErrorDuringExecution
|
||||||
|
ErrorDuringExecution.new(child_error.inner["cmd"],
|
||||||
|
status: child_error.inner["status"],
|
||||||
|
output: child_error.inner["output"])
|
||||||
|
elsif child_error.inner_class == BuildError
|
||||||
|
# We fill `BuildError#formula` and `BuildError#options` in later,
|
||||||
|
# when we rescue this in `FormulaInstaller#build`.
|
||||||
|
BuildError.new(nil, child_error.inner["cmd"],
|
||||||
|
child_error.inner["args"], child_error.inner["env"])
|
||||||
|
else
|
||||||
|
# Everything other error in the child just becomes a RuntimeError.
|
||||||
|
RuntimeError.new(child_error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
error.set_backtrace child_error.backtrace
|
||||||
|
|
||||||
|
error
|
||||||
|
end
|
||||||
|
|
||||||
def self.safe_fork(&_block)
|
def self.safe_fork(&_block)
|
||||||
Dir.mktmpdir("homebrew", HOMEBREW_TEMP) do |tmpdir|
|
Dir.mktmpdir("homebrew", HOMEBREW_TEMP) do |tmpdir|
|
||||||
UNIXServer.open("#{tmpdir}/socket") do |server|
|
UNIXServer.open("#{tmpdir}/socket") do |server|
|
||||||
@ -15,7 +35,18 @@ module Utils
|
|||||||
write.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
write.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
||||||
yield
|
yield
|
||||||
rescue Exception => e # rubocop:disable Lint/RescueException
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
||||||
Marshal.dump(e, write)
|
error_hash = JSON.parse e.to_json
|
||||||
|
|
||||||
|
# Special case: We need to recreate ErrorDuringExecutions
|
||||||
|
# for proper error messages and because other code expects
|
||||||
|
# to rescue them further down.
|
||||||
|
if e.is_a?(ErrorDuringExecution)
|
||||||
|
error_hash["cmd"] = e.cmd
|
||||||
|
error_hash["status"] = e.status.exitstatus
|
||||||
|
error_hash["output"] = e.output
|
||||||
|
end
|
||||||
|
|
||||||
|
write.puts error_hash.to_json
|
||||||
write.close
|
write.close
|
||||||
exit!
|
exit!
|
||||||
else
|
else
|
||||||
@ -33,10 +64,20 @@ module Utils
|
|||||||
socket.close
|
socket.close
|
||||||
end
|
end
|
||||||
write.close
|
write.close
|
||||||
data = read.read
|
# Each line on the error pipe contains a JSON-serialized exception.
|
||||||
|
# We read the first, since only one is necessary for a failure.
|
||||||
|
data = read.gets
|
||||||
read.close
|
read.close
|
||||||
Process.wait(pid) unless socket.nil?
|
Process.wait(pid) unless socket.nil?
|
||||||
raise Marshal.load(data) unless data.nil? || data.empty? # rubocop:disable Security/MarshalLoad
|
|
||||||
|
if data && !data.empty?
|
||||||
|
error_hash = JSON.parse(data) unless data.nil? || data.empty?
|
||||||
|
|
||||||
|
e = ChildProcessError.new(error_hash)
|
||||||
|
|
||||||
|
raise rewrite_child_error(e)
|
||||||
|
end
|
||||||
|
|
||||||
raise Interrupt if $CHILD_STATUS.exitstatus == 130
|
raise Interrupt if $CHILD_STATUS.exitstatus == 130
|
||||||
raise "Forked child process failed: #{$CHILD_STATUS}" unless $CHILD_STATUS.success?
|
raise "Forked child process failed: #{$CHILD_STATUS}" unless $CHILD_STATUS.success?
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user