Rewrite debugger to remove monkeypatches and use of call/cc

This commit is contained in:
Jack Nagel 2014-09-18 14:16:07 -05:00
parent 67a9164278
commit 3bbc9998a5
7 changed files with 144 additions and 213 deletions

View File

@ -8,7 +8,7 @@ require "build_options"
require "cxxstdlib"
require "keg"
require "extend/ENV"
require "debrew" if ARGV.debug?
require "debrew"
require "fcntl"
class Build
@ -109,6 +109,11 @@ class Build
end
end
if ARGV.debug?
formula.extend(Debrew::Formula)
formula.resources.each { |r| r.extend(Debrew::Resource) }
end
formula.brew do
if ARGV.flag? '--git'
system "git", "init"
@ -129,17 +134,7 @@ class Build
else
formula.prefix.mkpath
formula.resources.each { |r| r.extend(ResourceDebugger) } if ARGV.debug?
begin
formula.install
rescue Exception => e
if ARGV.debug?
debrew(e, formula)
else
raise
end
end
stdlibs = detect_stdlibs
Tab.create(formula, ENV.compiler, stdlibs.first, formula.build).write
@ -190,7 +185,6 @@ begin
build = Build.new(formula, options)
build.install
rescue Exception => e
e.continuation = nil if ARGV.debug?
Marshal.dump(e, error_pipe)
error_pipe.close
exit! 1

View File

@ -1,75 +1,148 @@
require 'debrew/menu'
require 'debrew/raise_plus'
require 'set'
require "mutex_m"
require "debrew/irb" unless ENV["HOMEBREW_NO_READLINE"]
unless ENV['HOMEBREW_NO_READLINE']
begin
require 'rubygems'
require 'ruby-debug'
rescue LoadError
module Debrew
extend Mutex_m
Ignorable = Module.new
module Raise
def raise(*)
super
rescue Exception => e
e.extend(Ignorable)
super(e) unless Debrew.debug(e) == :ignore
end
require 'debrew/irb'
end
alias_method :fail, :raise
end
class Object
include RaisePlus
end
module Formula
def install
Debrew.debrew { super }
end
end
module ResourceDebugger
def stage(target=nil, &block)
module Resource
def unpack(target=nil)
return super if target
super do
begin
block.call(self)
yield self
rescue Exception => e
if ARGV.debug?
debrew e
Debrew.debug(e)
end
end
end
end
class Menu
Entry = Struct.new(:name, :action)
attr_accessor :prompt, :entries
def initialize
@entries = []
end
def choice(name, &action)
entries << Entry.new(name.to_s, action)
end
def self.choose
menu = new
yield menu
choice = nil
while choice.nil?
menu.entries.each_with_index { |e, i| puts "#{i+1}. #{e.name}" }
print menu.prompt unless menu.prompt.nil?
input = $stdin.gets.chomp
i = input.to_i
if i > 0
choice = menu.entries[i-1]
else
raise
end
end
end
end
end
possible = menu.entries.find_all { |e| e.name.start_with?(input) }
$debugged_exceptions = Set.new
case possible.size
when 0 then puts "No such option"
when 1 then choice = possible.first
else puts "Multiple options match: #{possible.map(&:name).join(" ")}"
end
end
end
def debrew(exception, formula=nil)
raise exception unless $debugged_exceptions.add?(exception)
choice[:action].call
end
end
puts "#{exception.backtrace.first}"
puts "#{Tty.red}#{exception.class.name}#{Tty.reset}: #{exception}"
class << self
alias_method :original_raise, :raise
end
@active = false
@debugged_exceptions = Set.new
def self.active?
@active
end
def self.debugged_exceptions
@debugged_exceptions
end
def self.debrew
@active = true
Object.send(:include, Raise)
begin
again = false
choose do |menu|
yield
rescue Exception => e
debug(e)
ensure
@active = false
end
end
def self.debug(e)
original_raise(e) unless active? &&
debugged_exceptions.add?(e) &&
try_lock
begin
puts "#{e.backtrace.first}"
puts "#{Tty.red}#{e.class.name}#{Tty.reset}: #{e}"
loop do
Menu.choose do |menu|
menu.prompt = "Choose an action: "
menu.choice(:raise) { original_raise exception }
menu.choice(:ignore) { exception.restart } if exception.continuation
menu.choice(:backtrace) { puts exception.backtrace; again = true }
menu.choice(:debug) do
puts "When you exit the debugger, execution will continue."
exception.restart { debugger }
end if Object.const_defined?(:Debugger)
menu.choice(:raise) { original_raise(e) }
menu.choice(:ignore) { return :ignore } if Ignorable === e
menu.choice(:backtrace) { puts e.backtrace }
menu.choice(:irb) do
puts "When you exit this IRB session, execution will continue."
exception.restart do
# we need to capture the binding after returning from raise
set_trace_func proc { |event, file, line, id, binding, classname|
if event == 'return'
set_trace_func nil
IRB.start_within(binding)
set_trace_func proc { |event, _, _, id, binding, klass|
if klass == Raise && id == :raise && event == "return"
set_trace_func(nil)
synchronize { IRB.start_within(binding) }
end
}
end
end if Object.const_defined?(:IRB) && exception.continuation
return :ignore
end if Object.const_defined?(:IRB) && Ignorable === e
menu.choice(:shell) do
puts "When you exit this shell, you will return to the menu."
interactive_shell formula
again=true
interactive_shell
end
end
end
ensure
unlock
end
end
end while again
end

View File

@ -1,7 +0,0 @@
class Exception
attr_accessor :continuation
def restart(&block)
continuation.call block
end
end

View File

@ -1,39 +0,0 @@
class Menu
attr_accessor :prompt
attr_accessor :entries
def initialize
@entries = []
end
def choice(name, &action)
entries << { :name => name, :action => action }
end
end
def choose
menu = Menu.new
yield menu
choice = nil
while choice.nil?
menu.entries.each_with_index do |entry, i|
puts "#{i+1}. #{entry[:name]}"
end
print menu.prompt unless menu.prompt.nil?
reply = $stdin.gets.chomp
i = reply.to_i
if i > 0
choice = menu.entries[i-1]
else
possible = menu.entries.find_all {|e| e[:name].to_s.start_with? reply }
case possible.size
when 0 then puts "No such option"
when 1 then choice = possible.first
else puts "Multiple options match: #{possible.map{|e| e[:name]}.join(' ')}"
end
end
end
choice[:action].call
end

View File

@ -1,46 +0,0 @@
require 'continuation' if RUBY_VERSION.to_f >= 1.9
class Exception
attr_accessor :continuation
def restart(&block)
continuation.call block
end
end
module RaisePlus
alias :original_raise :raise
private
def raise(*args)
exception = case
when args.size == 0
$!.nil? ? RuntimeError.exception : $!
when args.size == 1 && args[0].is_a?(String)
RuntimeError.exception(args[0])
when args.size == 2 && args[0].is_a?(Exception)
args[0].exception(args[1])
when args[0].is_a?(Class) && args[0].ancestors.include?(Exception)
args[0].exception(args[1])
else
args[0]
end
# passing something other than a String or Exception is illegal, but if someone does it anyway,
# that object won't have backtrace or continuation methods. in that case, let's pass it on to
# the original raise, which will reject it
return super exception unless exception.is_a?(Exception)
# keep original backtrace if reraising
exception.set_backtrace(args.size >= 3 ? args[2] : caller) if exception.backtrace.nil?
blk = callcc do |cc|
exception.continuation = cc
super exception
end
blk.call unless blk.nil?
end
alias :fail :raise
end

View File

@ -12,6 +12,7 @@ require 'formula_cellar_checks'
require 'install_renamed'
require 'cmd/tap'
require 'hooks/bottles'
require 'debrew'
class FormulaInstaller
include FormulaCellarChecks

View File

@ -1,45 +0,0 @@
require 'testing_env'
require 'debrew/raise_plus'
class RaisePlusTests < Homebrew::TestCase
include RaisePlus
def test_raises_runtime_error_when_no_args
assert_raises(RuntimeError) { raise }
end
def test_raises_runtime_error_with_string_arg
raise "foo"
rescue Exception => e
assert_kind_of RuntimeError, e
assert_equal "foo", e.to_s
end
def test_raises_given_exception_with_new_to_s
a = Exception.new("foo")
raise a, "bar"
rescue Exception => e
assert_equal "bar", e.to_s
end
def test_raises_same_instance
a = Exception.new("foo")
raise a
rescue Exception => e
assert_same e, a
end
def test_raises_exception_class
assert_raises(StandardError) { raise StandardError }
end
def test_raises_type_error_for_bad_args
assert_raises(TypeError) { raise 1 }
end
def test_raise_is_private
assert_raises(NoMethodError) do
Object.new.extend(RaisePlus).raise(RuntimeError)
end
end
end