aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/debrew.rb
blob: bc1495a99eea4f8222fad94462e6cb94e5ebdb91 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
require "mutex_m"
require "debrew/irb" unless ENV["HOMEBREW_NO_READLINE"]

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

    alias_method :fail, :raise
  end

  module Formula
    def install
      Debrew.debrew { super }
    end

    def test
      Debrew.debrew { super }
    end
  end

  module Resource
    def unpack(target=nil)
      return super if target
      super do
        begin
          yield self
        rescue Exception => 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 or exit
        input.chomp!

        i = input.to_i
        if i > 0
          choice = menu.entries[i-1]
        else
          possible = menu.entries.find_all { |e| e.name.start_with?(input) }

          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

      choice[:action].call
    end
  end

  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
      yield
    rescue SystemExit
      original_raise
    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(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."
            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
            }

            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
          end
        end
      end
    ensure
      unlock
    end
  end
end