/
pry-rescue.rb
203 lines (179 loc) · 6.42 KB
/
pry-rescue.rb
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
require 'rubygems'
require 'interception'
require 'pry'
require File.expand_path('../pry-rescue/core_ext', __FILE__)
require File.expand_path('../pry-rescue/commands', __FILE__)
require File.expand_path('../pry-rescue/rack', __FILE__)
require File.expand_path('../pry-rescue/peek.rb', __FILE__)
if ENV['PRY_RESCUE_RAILS']
require File.expand_path('../pry-rescue/rails', __FILE__)
end
case ENV['PRY_PEEK']
when nil
PryRescue.peek_on_signal('QUIT') unless Pry::Helpers::BaseHelpers.windows?
when ''
# explicitly disable QUIT.
else
PryRescue.peek_on_signal(ENV['PRY_PEEK'])
end
begin
require 'pry-stack_explorer'
rescue LoadError
end
# Ensure that any exceptions raised within pry are available
Pry.config.hooks.add_hook :before_session, :enable_rescuing do
Pry.enable_rescuing!
end
# PryRescue provides the ability to open a Pry shell whenever an unhandled exception is
# raised in your code.
#
# The main API is exposed via the Pry object, but here are a load of helpers that I didn't
# want to pollute the Pry namespace with.
#
# @see {Pry::rescue}
class PryRescue
class << self
# Start a Pry session in the context of the exception.
# @param [Array<Exception, Array<Binding>>] raised The exceptions raised
def enter_exception_context(exception)
@exception_context_depth ||= 0
@exception_context_depth += 1
exception = exception.instance_variable_get(:@rescue_cause) if phantom_load_raise?(exception)
bindings = exception.instance_variable_get(:@rescue_bindings)
bindings = without_bindings_below_raise(bindings)
bindings = without_duplicates(bindings)
with_program_name "#$PROGRAM_NAME [in pry-rescue @ #{Dir.pwd}]" do
if defined?(PryStackExplorer)
pry :call_stack => bindings,
:hooks => pry_hooks(exception),
:initial_frame => initial_frame(bindings)
else
Pry.start bindings.first, :hooks => pry_hooks(exception)
end
end
ensure
@exception_context_depth -= 1
end
# Load a script wrapped in Pry::rescue{ }
# @param [String] script The name of the script
def load(script)
Pry::rescue{ Kernel.load script }
end
# Is the user currently inside pry rescue?
# @return [Boolean]
def in_exception_context?
@exception_context_depth && @exception_context_depth > 0
end
private
# Did this raise happen within pry-rescue?
#
# This is designed to remove the extra raise that is caused by PryRescue.load.
# TODO: we should figure out why it happens...
#
# @param [Exception] e The raised exception
# @param [Array<Binding>] bindings The call stack
def phantom_load_raise?(e)
bindings = e.instance_variable_get(:@rescue_bindings)
bindings.any? && bindings.first.eval("__FILE__") == __FILE__
end
# When using pry-stack-explorer we want to start the rescue session outside of gems
# and the standard library, as that is most helpful for users.
#
# @param [Array<Bindings>] bindings All bindings
# @return [Fixnum] The offset of the first binding of user code
def initial_frame(bindings)
bindings.each_with_index do |binding, i|
return i if user_path?(binding.eval("__FILE__"))
end
0
end
# Is this path likely to be code the user is working with right now?
#
# @param [String] file the absolute path
# @return [Boolean]
def user_path?(file)
return true if current_path?(file)
return false if stdlib_path?(file) || gem_path?(file)
true
end
# Is this file definitely part of the codebase the user is working on?
#
# This function exists because sometimes Dir.pwd can be a gem_path?,
# and the user expects to be able to debug a gem when they're cd'd
# into it.
#
# @param [String] file the absolute path
# @return [Boolean]
def current_path?(file)
file.start_with?(Dir.pwd) && !file.match(%r(/vendor/))
end
# Is this path included in a gem?
#
# @param [String] file the absolute path
# @return [Boolean]
def gem_path?(file)
# rubygems 1.8
if Gem::Specification.respond_to?(:any?)
Gem::Specification.any?{ |gem| file.start_with?(gem.full_gem_path) }
# rubygems 1.6
else
Gem.all_load_paths.any?{ |path| file.start_with?(path) }
end
end
# Is this path in the ruby standard library?
#
# @param [String] file the absolute path
# @return [Boolean]
def stdlib_path?(file)
file.start_with?(RbConfig::CONFIG['libdir']) || %w( (eval) <internal:prelude> ).include?(file)
end
# Remove bindings that are part of Interception/Pry.rescue's internal
# event handling that happens as part of the exception hooking process.
#
# @param [Array<Binding>] bindings The call stack.
def without_bindings_below_raise(bindings)
return bindings if bindings.size <= 1
bindings.drop_while do |b|
b.eval("__FILE__") == File.expand_path("../pry-rescue/core_ext.rb", __FILE__)
end.drop_while do |b|
Interception == b.eval("self")
end
end
# Remove multiple bindings for the same function.
#
# @param [Array<Bindings>] bindings The call stack
# @return [Array<Bindings>]
def without_duplicates(bindings)
bindings = Array(bindings)
bindings.zip([nil] + bindings).reject do |b, c|
# The eval('__method__') is there as a shortcut as loading a method
# from a binding is very slow.
c && (b.eval("::Kernel.__method__") == c.eval("::Kernel.__method__")) &&
Pry::Method.from_binding(b) == Pry::Method.from_binding(c)
end.map(&:first)
end
# Define the :before_session hook for the Pry instance.
# This ensures that the `_ex_` and `_raised_` sticky locals are
# properly set.
#
# @param [Exception] ex The exception we're currently looking at
# @param [Array<Exception, Array<Binding>>] raised The exceptions raised
def pry_hooks(ex)
hooks = Pry.config.hooks.dup
hooks.add_hook(:before_session, :save_captured_exception) do |_, _, _pry_|
_pry_.last_exception = ex
_pry_.backtrace = ex.backtrace
_pry_.sticky_locals.merge!(:_rescued_ => ex)
_pry_.exception_handler.call(_pry_.output, ex, _pry_)
end
hooks
end
def with_program_name name
before = $PROGRAM_NAME
$PROGRAM_NAME = name
yield
ensure
$PROGRAM_NAME = before
end
end
end