-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdebugger.rb
189 lines (159 loc) · 4.8 KB
/
debugger.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
# frozen_string_literal: true
require "reline"
require "rbconfig"
module Debugger
class Session
def initialize
@breakpoints = []
end
def suspend!(binding, bp: nil)
if bp
puts "Suspended by: #{bp.name}"
# The initial breakpoint is a one-time breakpoint, so we need to delete it after we hit it
@breakpoints.delete(bp) if bp.once
end
display_code(binding)
while input = Reline.readline("(debug) ")
cmd, arg = input.split(" ", 2)
case cmd
when "break"
case arg
when /\A(\d+)\z/
add_breakpoint(binding.source_location[0], $1.to_i)
when /\A(.+)[:\s+](\d+)\z/
add_breakpoint($1, $2.to_i)
when nil
if @breakpoints.empty?
puts "No breakpoints"
else
@breakpoints.each_with_index do |bp, index|
puts "##{index} - #{bp.location}"
end
end
else
puts "Unknown break format: #{arg}"
end
when "delete"
index = arg.to_i
if bp = @breakpoints.delete_at(index)
bp.disable
puts "Breakpoint ##{index} (#{bp.location}) has been deleted"
else
puts "Breakpoint ##{index} not found"
end
when "step"
step_in
break
when "next"
step_over
break
when "continue"
break
when "exit"
exit
else
puts "=> " + eval_input(binding, input).inspect
end
end
end
# We add it to the public API because we'll need it later
def add_breakpoint(file, line, **options)
bp = LineBreakpoint.new(file, line, **options)
@breakpoints << bp
puts "Breakpoint added: #{bp.location}" unless bp.once
bp.enable
end
private
def step_in
TracePoint.trace(:line) do |tp|
# There are some internal files we don't want to step into
next if internal_path?(File.expand_path(tp.path))
# Disable the TracePoint after we hit the next execution
tp.disable
suspend!(tp.binding)
end
end
def step_over
# ignore call frames from the debugger itself
current_depth = caller.length - 2
TracePoint.trace(:line) do |tp|
# There are some internal files we don't want to step into
next if internal_path?(File.expand_path(tp.path))
depth = caller.length
next if current_depth < depth
tp.disable
suspend!(tp.binding)
end
end
RELINE_PATH = Gem.loaded_specs["reline"].full_require_paths.first
# 1. Check if the path is inside the debugger itself
# 2. Check if the path is inside Ruby's standard library
# 3. Check if the path is inside Ruby's internal files
# 4. Check if the path is inside Reline
def internal_path?(path)
path.start_with?(__dir__) || path.start_with?(RbConfig::CONFIG["rubylibdir"]) ||
path.match?(/<internal:/) || path.start_with?(RELINE_PATH)
end
def eval_input(binding, input)
binding.eval(input)
rescue Exception => e
puts "Evaluation error: #{e.inspect}"
end
def display_code(binding)
file, current_line = binding.source_location
if File.exist?(file)
lines = File.readlines(file)
end_line = [current_line + 5, lines.count].min - 1
start_line = [end_line - 9, 0].max
puts "[#{start_line + 1}, #{end_line + 1}] in #{file}"
max_lineno_width = (end_line + 1).to_s.size
lines[start_line..end_line].each_with_index do |line, index|
lineno = start_line + index + 1
lineno_str = lineno.to_s.rjust(max_lineno_width)
if lineno == current_line
puts " => #{lineno_str}| #{line}"
else
puts " #{lineno_str}| #{line}"
end
end
end
end
end
SESSION = Session.new
class LineBreakpoint
attr_reader :once
def initialize(file, line, once: false)
@file = file
@line = line
@once = once
@tp =
TracePoint.new(:line) do |tp|
# we need to expand paths to make sure they'll match
if File.expand_path(tp.path) == File.expand_path(@file) && tp.lineno == @line
SESSION.suspend!(tp.binding, bp: self)
end
end
end
def location
"#{@file}:#{@line}"
end
def name
"Breakpoint at #{location}"
end
def enable
@tp.enable
end
def disable
@tp.disable
end
end
end
class Binding
def debug
Debugger::SESSION.suspend!(self)
end
end
# If the program is run with `exe/debug`, we'll add a breakpoint at the first line
if ENV["RUBYOPT"] && ENV["RUBYOPT"].split.include?("-rdebugger")
Debugger::SESSION.add_breakpoint($0, 1, once: true)
end