debug 1.0.0.beta6 → 1.0.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +108 -106
- data/Gemfile +1 -0
- data/README.md +452 -226
- data/Rakefile +2 -1
- data/TODO.md +0 -6
- data/debug.gemspec +1 -0
- data/exe/rdbg +4 -7
- data/ext/debug/debug.c +11 -1
- data/lib/debug/breakpoint.rb +96 -63
- data/lib/debug/client.rb +8 -12
- data/lib/debug/color.rb +25 -6
- data/lib/debug/config.rb +376 -154
- data/lib/debug/console.rb +75 -66
- data/lib/debug/frame_info.rb +40 -7
- data/lib/debug/local.rb +91 -0
- data/lib/debug/open.rb +1 -1
- data/lib/debug/open_nonstop.rb +15 -0
- data/lib/debug/server.rb +74 -30
- data/lib/debug/server_dap.rb +32 -7
- data/lib/debug/session.rb +729 -319
- data/lib/debug/{run.rb → start.rb} +1 -1
- data/lib/debug/thread_client.rb +651 -156
- data/lib/debug/tracer.rb +242 -0
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +338 -217
- metadata +20 -3
data/lib/debug/thread_client.rb
CHANGED
|
@@ -7,6 +7,17 @@ require_relative 'frame_info'
|
|
|
7
7
|
require_relative 'color'
|
|
8
8
|
|
|
9
9
|
module DEBUGGER__
|
|
10
|
+
module SkipPathHelper
|
|
11
|
+
def skip_path?(path)
|
|
12
|
+
(skip_paths = CONFIG[:skip_path]) && skip_paths.any?{|skip_path| path.match?(skip_path)}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def skip_location?(loc)
|
|
16
|
+
loc_path = loc.absolute_path || "!eval:#{loc.path}"
|
|
17
|
+
skip_path?(loc_path)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
10
21
|
class ThreadClient
|
|
11
22
|
def self.current
|
|
12
23
|
Thread.current[:DEBUGGER__ThreadClient] || begin
|
|
@@ -16,8 +27,9 @@ module DEBUGGER__
|
|
|
16
27
|
end
|
|
17
28
|
|
|
18
29
|
include Color
|
|
30
|
+
include SkipPathHelper
|
|
19
31
|
|
|
20
|
-
attr_reader :location, :thread, :
|
|
32
|
+
attr_reader :location, :thread, :id, :recorder
|
|
21
33
|
|
|
22
34
|
def assemble_arguments(args)
|
|
23
35
|
args.map do |arg|
|
|
@@ -54,14 +66,14 @@ module DEBUGGER__
|
|
|
54
66
|
result = "#{call_identifier_str} at #{location_str}"
|
|
55
67
|
|
|
56
68
|
if return_str = frame.return_str
|
|
57
|
-
|
|
58
|
-
result += " #=> #{return_str}"
|
|
69
|
+
result += " #=> #{colorize_magenta(frame.return_str)}"
|
|
59
70
|
end
|
|
60
71
|
|
|
61
72
|
result
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
def initialize id, q_evt, q_cmd, thr = Thread.current
|
|
76
|
+
@is_management = false
|
|
65
77
|
@id = id
|
|
66
78
|
@thread = thr
|
|
67
79
|
@target_frames = nil
|
|
@@ -69,11 +81,52 @@ module DEBUGGER__
|
|
|
69
81
|
@q_cmd = q_cmd
|
|
70
82
|
@step_tp = nil
|
|
71
83
|
@output = []
|
|
72
|
-
@src_lines_on_stop = (::DEBUGGER__::CONFIG[:show_src_lines] || 10).to_i
|
|
73
|
-
@show_frames_on_stop = (::DEBUGGER__::CONFIG[:show_frames] || 2).to_i
|
|
74
84
|
@frame_formatter = method(:default_frame_formatter)
|
|
75
85
|
@var_map = {} # { thread_local_var_id => obj } for DAP
|
|
76
|
-
|
|
86
|
+
@recorder = nil
|
|
87
|
+
@mode = :waiting
|
|
88
|
+
set_mode :running
|
|
89
|
+
thr.instance_variable_set(:@__thread_client_id, id)
|
|
90
|
+
|
|
91
|
+
::DEBUGGER__.info("Thread \##{@id} is created.")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def deactivate
|
|
95
|
+
@step_tp.disable if @step_tp
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def management?
|
|
99
|
+
@is_management
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def is_management
|
|
103
|
+
@is_management = true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def set_mode mode
|
|
107
|
+
# STDERR.puts "#{@mode} => #{mode} @ #{caller.inspect}"
|
|
108
|
+
#pp caller
|
|
109
|
+
|
|
110
|
+
# mode transition check
|
|
111
|
+
case mode
|
|
112
|
+
when :running
|
|
113
|
+
raise "#{mode} is given, but #{mode}" unless self.waiting?
|
|
114
|
+
when :waiting
|
|
115
|
+
# TODO: there is waiting -> waiting
|
|
116
|
+
# raise "#{mode} is given, but #{mode}" unless self.running?
|
|
117
|
+
else
|
|
118
|
+
raise
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@mode = mode
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def running?
|
|
125
|
+
@mode == :running
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def waiting?
|
|
129
|
+
@mode == :waiting
|
|
77
130
|
end
|
|
78
131
|
|
|
79
132
|
def name
|
|
@@ -85,17 +138,33 @@ module DEBUGGER__
|
|
|
85
138
|
end
|
|
86
139
|
|
|
87
140
|
def inspect
|
|
88
|
-
"#<DBG:TC #{self.id}:#{
|
|
141
|
+
"#<DBG:TC #{self.id}:#{@mode}@#{@thread.backtrace[-1]}>"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def to_s
|
|
145
|
+
loc = current_frame&.location
|
|
146
|
+
|
|
147
|
+
if loc
|
|
148
|
+
str = "(#{@thread.name || @thread.status})@#{loc}"
|
|
149
|
+
else
|
|
150
|
+
str = "(#{@thread.name || @thread.status})@#{@thread.to_s}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
str += " (not under control)" unless self.waiting?
|
|
154
|
+
str
|
|
89
155
|
end
|
|
90
156
|
|
|
91
157
|
def puts str = ''
|
|
158
|
+
if @recorder&.replaying?
|
|
159
|
+
prefix = colorize_dim("[replay] ")
|
|
160
|
+
end
|
|
92
161
|
case str
|
|
93
162
|
when nil
|
|
94
163
|
@output << "\n"
|
|
95
164
|
when Array
|
|
96
165
|
str.each{|s| puts s}
|
|
97
166
|
else
|
|
98
|
-
@output << str.chomp
|
|
167
|
+
@output << "#{prefix}#{str.chomp}\n"
|
|
99
168
|
end
|
|
100
169
|
end
|
|
101
170
|
|
|
@@ -116,40 +185,61 @@ module DEBUGGER__
|
|
|
116
185
|
|
|
117
186
|
## events
|
|
118
187
|
|
|
119
|
-
def
|
|
120
|
-
if
|
|
121
|
-
# raise Interrupt
|
|
122
|
-
else
|
|
123
|
-
on_suspend :trap, sig: sig
|
|
124
|
-
end
|
|
125
|
-
end
|
|
188
|
+
def wait_reply event_arg
|
|
189
|
+
return if management?
|
|
126
190
|
|
|
127
|
-
|
|
128
|
-
|
|
191
|
+
set_mode :waiting
|
|
192
|
+
|
|
193
|
+
event!(*event_arg)
|
|
194
|
+
wait_next_action
|
|
129
195
|
end
|
|
130
196
|
|
|
131
197
|
def on_thread_begin th
|
|
132
|
-
|
|
133
|
-
wait_next_action
|
|
198
|
+
wait_reply [:thread_begin, th]
|
|
134
199
|
end
|
|
135
200
|
|
|
136
201
|
def on_load iseq, eval_src
|
|
137
|
-
|
|
138
|
-
wait_next_action
|
|
202
|
+
wait_reply [:load, iseq, eval_src]
|
|
139
203
|
end
|
|
140
204
|
|
|
141
205
|
def on_init name
|
|
142
|
-
|
|
143
|
-
|
|
206
|
+
wait_reply [:init, name]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def on_trace trace_id, msg
|
|
210
|
+
wait_reply [:trace, trace_id, msg]
|
|
144
211
|
end
|
|
145
212
|
|
|
146
213
|
def on_breakpoint tp, bp
|
|
147
|
-
|
|
214
|
+
suspend tp.event, tp, bp: bp
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def on_trap sig
|
|
218
|
+
if waiting?
|
|
219
|
+
# raise Interrupt
|
|
220
|
+
else
|
|
221
|
+
suspend :trap, sig: sig
|
|
222
|
+
end
|
|
148
223
|
end
|
|
149
224
|
|
|
150
|
-
def
|
|
225
|
+
def on_pause
|
|
226
|
+
suspend :pause
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def suspend event, tp = nil, bp: nil, sig: nil, postmortem_frames: nil, replay_frames: nil
|
|
230
|
+
return if management?
|
|
231
|
+
|
|
151
232
|
@current_frame_index = 0
|
|
152
|
-
|
|
233
|
+
|
|
234
|
+
case
|
|
235
|
+
when postmortem_frames
|
|
236
|
+
@target_frames = postmortem_frames
|
|
237
|
+
@postmortem = true
|
|
238
|
+
when replay_frames
|
|
239
|
+
@target_frames = replay_frames
|
|
240
|
+
else
|
|
241
|
+
@target_frames = DEBUGGER__.capture_frames(__dir__)
|
|
242
|
+
end
|
|
153
243
|
|
|
154
244
|
cf = @target_frames.first
|
|
155
245
|
if cf
|
|
@@ -167,8 +257,10 @@ module DEBUGGER__
|
|
|
167
257
|
end
|
|
168
258
|
|
|
169
259
|
if event != :pause
|
|
170
|
-
show_src max_lines:
|
|
171
|
-
show_frames
|
|
260
|
+
show_src max_lines: (CONFIG[:show_src_lines] || 10)
|
|
261
|
+
show_frames CONFIG[:show_frames] || 2
|
|
262
|
+
|
|
263
|
+
set_mode :waiting
|
|
172
264
|
|
|
173
265
|
if bp
|
|
174
266
|
event! :suspend, :breakpoint, bp.key
|
|
@@ -177,11 +269,18 @@ module DEBUGGER__
|
|
|
177
269
|
else
|
|
178
270
|
event! :suspend, event
|
|
179
271
|
end
|
|
272
|
+
else
|
|
273
|
+
set_mode :waiting
|
|
180
274
|
end
|
|
181
275
|
|
|
182
276
|
wait_next_action
|
|
183
277
|
end
|
|
184
278
|
|
|
279
|
+
def replay_suspend
|
|
280
|
+
# @recorder.current_position
|
|
281
|
+
suspend :replay, replay_frames: @recorder.current_frame
|
|
282
|
+
end
|
|
283
|
+
|
|
185
284
|
## control all
|
|
186
285
|
|
|
187
286
|
begin
|
|
@@ -191,41 +290,77 @@ module DEBUGGER__
|
|
|
191
290
|
SUPPORT_TARGET_THREAD = false
|
|
192
291
|
end
|
|
193
292
|
|
|
194
|
-
def step_tp
|
|
293
|
+
def step_tp iter
|
|
195
294
|
@step_tp.disable if @step_tp
|
|
196
295
|
|
|
197
296
|
thread = Thread.current
|
|
198
297
|
|
|
199
298
|
if SUPPORT_TARGET_THREAD
|
|
200
299
|
@step_tp = TracePoint.new(:line, :b_return, :return){|tp|
|
|
201
|
-
next if SESSION.
|
|
300
|
+
next if SESSION.break_at? tp.path, tp.lineno
|
|
202
301
|
next if !yield
|
|
203
302
|
next if tp.path.start_with?(__dir__)
|
|
204
|
-
next
|
|
303
|
+
next if tp.path.start_with?('<internal:trace_point>')
|
|
304
|
+
next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
|
|
305
|
+
loc = caller_locations(1, 1).first
|
|
306
|
+
next if skip_location?(loc)
|
|
307
|
+
next if iter && (iter -= 1) > 0
|
|
205
308
|
|
|
206
309
|
tp.disable
|
|
207
|
-
|
|
310
|
+
suspend tp.event, tp
|
|
208
311
|
}
|
|
209
312
|
@step_tp.enable(target_thread: thread)
|
|
210
313
|
else
|
|
211
314
|
@step_tp = TracePoint.new(:line, :b_return, :return){|tp|
|
|
212
315
|
next if thread != Thread.current
|
|
213
|
-
next if SESSION.
|
|
316
|
+
next if SESSION.break_at? tp.path, tp.lineno
|
|
214
317
|
next if !yield
|
|
215
|
-
next
|
|
318
|
+
next if tp.path.start_with?(__dir__)
|
|
319
|
+
next if tp.path.start_with?('<internal:trace_point>')
|
|
320
|
+
next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
|
|
321
|
+
loc = caller_locations(1, 1).first
|
|
322
|
+
next if skip_location?(loc)
|
|
323
|
+
next if iter && (iter -= 1) > 0
|
|
216
324
|
|
|
217
325
|
tp.disable
|
|
218
|
-
|
|
326
|
+
suspend tp.event, tp
|
|
219
327
|
}
|
|
220
328
|
@step_tp.enable
|
|
221
329
|
end
|
|
222
330
|
end
|
|
223
331
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
332
|
+
## cmd helpers
|
|
333
|
+
|
|
334
|
+
# this method is extracted to hide frame_eval's local variables from C method eval's binding
|
|
335
|
+
def instance_eval_for_cmethod frame_self, src
|
|
336
|
+
frame_self.instance_eval(src)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def frame_eval src, re_raise: false
|
|
340
|
+
begin
|
|
341
|
+
@success_last_eval = false
|
|
342
|
+
|
|
343
|
+
b = current_frame.eval_binding
|
|
344
|
+
result = if b
|
|
345
|
+
f, _l = b.source_location
|
|
346
|
+
b.eval(src, "(rdbg)/#{f}")
|
|
347
|
+
else
|
|
348
|
+
frame_self = current_frame.self
|
|
349
|
+
instance_eval_for_cmethod(frame_self, src)
|
|
350
|
+
end
|
|
351
|
+
@success_last_eval = true
|
|
352
|
+
result
|
|
353
|
+
|
|
354
|
+
rescue Exception => e
|
|
355
|
+
return yield(e) if block_given?
|
|
356
|
+
|
|
357
|
+
puts "eval error: #{e}"
|
|
358
|
+
|
|
359
|
+
e.backtrace_locations.each do |loc|
|
|
360
|
+
break if loc.path == __FILE__
|
|
361
|
+
puts " #{loc}"
|
|
362
|
+
end
|
|
363
|
+
raise if re_raise
|
|
229
364
|
end
|
|
230
365
|
end
|
|
231
366
|
|
|
@@ -240,11 +375,9 @@ module DEBUGGER__
|
|
|
240
375
|
frame_line = frame.location.lineno - 1
|
|
241
376
|
|
|
242
377
|
lines = file_lines.map.with_index do |e, i|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
" #{'%4d' % (i+1)}| #{e}"
|
|
247
|
-
end
|
|
378
|
+
cur = i == frame_line ? '=>' : ' '
|
|
379
|
+
line = colorize_dim('%4d|' % (i+1))
|
|
380
|
+
"#{cur}#{line} #{e}"
|
|
248
381
|
end
|
|
249
382
|
|
|
250
383
|
unless start_line
|
|
@@ -282,98 +415,160 @@ module DEBUGGER__
|
|
|
282
415
|
exit!
|
|
283
416
|
end
|
|
284
417
|
|
|
285
|
-
def
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
path = frame.path
|
|
289
|
-
else
|
|
290
|
-
return # can't get path
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
if File.exist?(path)
|
|
295
|
-
if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR'])
|
|
296
|
-
puts "command: #{editor}"
|
|
297
|
-
puts " path: #{path}"
|
|
298
|
-
system(editor, path)
|
|
299
|
-
else
|
|
300
|
-
puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']"
|
|
301
|
-
end
|
|
418
|
+
def current_frame
|
|
419
|
+
if @target_frames
|
|
420
|
+
@target_frames[@current_frame_index]
|
|
302
421
|
else
|
|
303
|
-
|
|
422
|
+
nil
|
|
304
423
|
end
|
|
305
424
|
end
|
|
306
425
|
|
|
307
|
-
|
|
426
|
+
## cmd: show
|
|
427
|
+
|
|
428
|
+
def show_locals pat
|
|
308
429
|
if s = current_frame&.self
|
|
309
|
-
|
|
430
|
+
puts_variable_info '%self', s, pat
|
|
310
431
|
end
|
|
311
432
|
if current_frame&.has_return_value
|
|
312
|
-
|
|
433
|
+
puts_variable_info '%return', current_frame.return_value, pat
|
|
313
434
|
end
|
|
314
435
|
if current_frame&.has_raised_exception
|
|
315
|
-
|
|
436
|
+
puts_variable_info "%raised", current_frame.raised_exception, pat
|
|
316
437
|
end
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
438
|
+
|
|
439
|
+
if vars = current_frame&.local_variables
|
|
440
|
+
vars.each{|var, val|
|
|
441
|
+
puts_variable_info var, val, pat
|
|
321
442
|
}
|
|
322
443
|
end
|
|
323
444
|
end
|
|
324
445
|
|
|
325
|
-
def show_ivars
|
|
446
|
+
def show_ivars pat
|
|
326
447
|
if s = current_frame&.self
|
|
327
|
-
s.instance_variables.each{|iv|
|
|
448
|
+
s.instance_variables.sort.each{|iv|
|
|
328
449
|
value = s.instance_variable_get(iv)
|
|
329
|
-
|
|
450
|
+
puts_variable_info iv, value, pat
|
|
330
451
|
}
|
|
331
452
|
end
|
|
332
453
|
end
|
|
333
454
|
|
|
334
|
-
def
|
|
335
|
-
|
|
336
|
-
|
|
455
|
+
def show_consts pat, only_self: false
|
|
456
|
+
if s = current_frame&.self
|
|
457
|
+
cs = {}
|
|
458
|
+
if s.kind_of? Module
|
|
459
|
+
cs[s] = :self
|
|
460
|
+
else
|
|
461
|
+
s = s.class
|
|
462
|
+
cs[s] = :self unless only_self
|
|
463
|
+
end
|
|
337
464
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
@success_last_eval = true
|
|
347
|
-
result
|
|
465
|
+
unless only_self
|
|
466
|
+
s.ancestors.each{|c| break if c == Object; cs[c] = :ancestors}
|
|
467
|
+
if b = current_frame&.binding
|
|
468
|
+
b.eval('Module.nesting').each{|c| cs[c] = :nesting unless cs.has_key? c}
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
names = {}
|
|
348
473
|
|
|
474
|
+
cs.each{|c, _|
|
|
475
|
+
c.constants(false).sort.each{|name|
|
|
476
|
+
next if names.has_key? name
|
|
477
|
+
names[name] = nil
|
|
478
|
+
value = c.const_get(name)
|
|
479
|
+
puts_variable_info name, value, pat
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
SKIP_GLOBAL_LIST = %i[$= $KCODE $-K $SAFE].freeze
|
|
486
|
+
def show_globals pat
|
|
487
|
+
global_variables.sort.each{|name|
|
|
488
|
+
next if SKIP_GLOBAL_LIST.include? name
|
|
489
|
+
|
|
490
|
+
value = eval(name.to_s)
|
|
491
|
+
puts_variable_info name, value, pat
|
|
492
|
+
}
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def puts_variable_info label, obj, pat
|
|
496
|
+
return if pat && pat !~ label
|
|
497
|
+
|
|
498
|
+
begin
|
|
499
|
+
inspected = obj.inspect
|
|
349
500
|
rescue Exception => e
|
|
350
|
-
|
|
501
|
+
inspected = e.inspect
|
|
502
|
+
end
|
|
503
|
+
mono_info = "#{label} = #{inspected}"
|
|
351
504
|
|
|
352
|
-
|
|
505
|
+
w = SESSION::width
|
|
353
506
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
507
|
+
if mono_info.length >= w
|
|
508
|
+
info = truncate(mono_info, width: w)
|
|
509
|
+
else
|
|
510
|
+
valstr = colored_inspect(obj, width: 2 ** 30)
|
|
511
|
+
valstr = inspected if valstr.lines.size > 1
|
|
512
|
+
info = "#{colorize_cyan(label)} = #{valstr}"
|
|
359
513
|
end
|
|
514
|
+
|
|
515
|
+
puts info
|
|
360
516
|
end
|
|
361
517
|
|
|
362
|
-
def
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
frame_string = @frame_formatter.call(frame)
|
|
367
|
-
"#{prefix}\t#{frame_string}"
|
|
518
|
+
def truncate(string, width:)
|
|
519
|
+
str = string[0 .. (width-4)] + '...'
|
|
520
|
+
str += ">" if str.start_with?("#<")
|
|
521
|
+
str
|
|
368
522
|
end
|
|
369
523
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
524
|
+
### cmd: show edit
|
|
525
|
+
|
|
526
|
+
def show_by_editor path = nil
|
|
527
|
+
unless path
|
|
528
|
+
if @target_frames && frame = @target_frames[@current_frame_index]
|
|
529
|
+
path = frame.path
|
|
530
|
+
else
|
|
531
|
+
return # can't get path
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
if File.exist?(path)
|
|
536
|
+
if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR'])
|
|
537
|
+
puts "command: #{editor}"
|
|
538
|
+
puts " path: #{path}"
|
|
539
|
+
system(editor, path)
|
|
540
|
+
else
|
|
541
|
+
puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']"
|
|
542
|
+
end
|
|
543
|
+
else
|
|
544
|
+
puts "Can not find file: #{path}"
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
### cmd: show frames
|
|
549
|
+
|
|
550
|
+
def show_frames max = nil, pattern = nil
|
|
551
|
+
if @target_frames && (max ||= @target_frames.size) > 0
|
|
552
|
+
frames = []
|
|
553
|
+
@target_frames.each_with_index{|f, i|
|
|
554
|
+
next if pattern && !(f.name.match?(pattern) || f.location_str.match?(pattern))
|
|
555
|
+
next if CONFIG[:skip_path] && CONFIG[:skip_path].any?{|pat|
|
|
556
|
+
case pat
|
|
557
|
+
when String
|
|
558
|
+
f.location_str.start_with?(pat)
|
|
559
|
+
when Regexp
|
|
560
|
+
f.location_str.match?(pat)
|
|
561
|
+
end
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
frames << [i, f]
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
size = frames.size
|
|
374
568
|
max.times{|i|
|
|
375
|
-
break
|
|
376
|
-
|
|
569
|
+
break unless frames[i]
|
|
570
|
+
index, frame = frames[i]
|
|
571
|
+
puts frame_str(index, frame: frame)
|
|
377
572
|
}
|
|
378
573
|
puts " # and #{size - max} frames (use `bt' command for all frames)" if max < size
|
|
379
574
|
end
|
|
@@ -383,29 +578,60 @@ module DEBUGGER__
|
|
|
383
578
|
puts frame_str(i)
|
|
384
579
|
end
|
|
385
580
|
|
|
386
|
-
def
|
|
581
|
+
def frame_str(i, frame: @target_frames[i])
|
|
582
|
+
cur_str = (@current_frame_index == i ? '=>' : ' ')
|
|
583
|
+
prefix = "#{cur_str}##{i}"
|
|
584
|
+
frame_string = @frame_formatter.call(frame)
|
|
585
|
+
"#{prefix}\t#{frame_string}"
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
### cmd: show outline
|
|
589
|
+
|
|
590
|
+
def show_outline expr
|
|
387
591
|
begin
|
|
388
|
-
|
|
592
|
+
obj = frame_eval(expr, re_raise: true)
|
|
389
593
|
rescue Exception
|
|
390
594
|
# ignore
|
|
391
595
|
else
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
596
|
+
o = Output.new(@output)
|
|
597
|
+
|
|
598
|
+
locals = current_frame&.local_variables
|
|
599
|
+
klass = (obj.class == Class || obj.class == Module ? obj : obj.class)
|
|
600
|
+
|
|
601
|
+
o.dump("constants", obj.constants) if obj.respond_to?(:constants)
|
|
602
|
+
outline_method(o, klass, obj)
|
|
603
|
+
o.dump("instance variables", obj.instance_variables)
|
|
604
|
+
o.dump("class variables", klass.class_variables)
|
|
605
|
+
o.dump("locals", locals.keys) if locals
|
|
401
606
|
end
|
|
402
607
|
end
|
|
403
608
|
|
|
609
|
+
def outline_method(o, klass, obj)
|
|
610
|
+
singleton_class = begin obj.singleton_class; rescue TypeError; nil end
|
|
611
|
+
maps = class_method_map((singleton_class || klass).ancestors)
|
|
612
|
+
maps.each do |mod, methods|
|
|
613
|
+
name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods"
|
|
614
|
+
o.dump(name, methods)
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def class_method_map(classes)
|
|
619
|
+
dumped = Array.new
|
|
620
|
+
classes.reject { |mod| mod >= Object }.map do |mod|
|
|
621
|
+
methods = mod.public_instance_methods(false).select do |m|
|
|
622
|
+
dumped.push(m) unless dumped.include?(m)
|
|
623
|
+
end
|
|
624
|
+
[mod, methods]
|
|
625
|
+
end.reverse
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
## cmd: breakpoint
|
|
629
|
+
|
|
404
630
|
def make_breakpoint args
|
|
405
631
|
case args.first
|
|
406
632
|
when :method
|
|
407
|
-
klass_name, op, method_name, cond = args[1..]
|
|
408
|
-
bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond)
|
|
633
|
+
klass_name, op, method_name, cond, cmd = args[1..]
|
|
634
|
+
bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond: cond, command: cmd)
|
|
409
635
|
begin
|
|
410
636
|
bp.enable
|
|
411
637
|
rescue Exception => e
|
|
@@ -422,28 +648,58 @@ module DEBUGGER__
|
|
|
422
648
|
end
|
|
423
649
|
end
|
|
424
650
|
|
|
425
|
-
|
|
426
|
-
@mode = mode
|
|
651
|
+
class SuspendReplay < Exception
|
|
427
652
|
end
|
|
428
653
|
|
|
429
654
|
def wait_next_action
|
|
430
|
-
|
|
655
|
+
wait_next_action_
|
|
656
|
+
rescue SuspendReplay
|
|
657
|
+
replay_suspend
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def wait_next_action_
|
|
661
|
+
# assertions
|
|
662
|
+
raise "@mode is #{@mode}" unless @mode == :waiting
|
|
431
663
|
|
|
432
|
-
SESSION.
|
|
664
|
+
unless SESSION.active?
|
|
665
|
+
pp caller
|
|
666
|
+
set_mode :running
|
|
667
|
+
return
|
|
668
|
+
end
|
|
669
|
+
# SESSION.check_forked
|
|
433
670
|
|
|
434
|
-
while
|
|
435
|
-
|
|
671
|
+
while true
|
|
672
|
+
begin
|
|
673
|
+
set_mode :waiting if @mode != :waiting
|
|
674
|
+
cmds = @q_cmd.pop
|
|
675
|
+
# pp [self, cmds: cmds]
|
|
676
|
+
break unless cmds
|
|
677
|
+
ensure
|
|
678
|
+
set_mode :running
|
|
679
|
+
end
|
|
436
680
|
|
|
437
681
|
cmd, *args = *cmds
|
|
438
682
|
|
|
439
683
|
case cmd
|
|
440
684
|
when :continue
|
|
441
685
|
break
|
|
686
|
+
|
|
442
687
|
when :step
|
|
443
688
|
step_type = args[0]
|
|
689
|
+
iter = args[1]
|
|
690
|
+
|
|
444
691
|
case step_type
|
|
445
692
|
when :in
|
|
446
|
-
|
|
693
|
+
if @recorder&.replaying?
|
|
694
|
+
@recorder.step_forward
|
|
695
|
+
raise SuspendReplay
|
|
696
|
+
else
|
|
697
|
+
step_tp iter do
|
|
698
|
+
true
|
|
699
|
+
end
|
|
700
|
+
break
|
|
701
|
+
end
|
|
702
|
+
|
|
447
703
|
when :next
|
|
448
704
|
frame = @target_frames.first
|
|
449
705
|
path = frame.location.absolute_path || "!eval:#{frame.path}"
|
|
@@ -459,7 +715,7 @@ module DEBUGGER__
|
|
|
459
715
|
|
|
460
716
|
depth = @target_frames.first.frame_depth
|
|
461
717
|
|
|
462
|
-
step_tp
|
|
718
|
+
step_tp iter do
|
|
463
719
|
loc = caller_locations(2, 1).first
|
|
464
720
|
loc_path = loc.absolute_path || "!eval:#{loc.path}"
|
|
465
721
|
|
|
@@ -470,17 +726,39 @@ module DEBUGGER__
|
|
|
470
726
|
(next_line && loc_path == path &&
|
|
471
727
|
(loc_lineno = loc.lineno) > line &&
|
|
472
728
|
loc_lineno <= next_line)
|
|
473
|
-
|
|
729
|
+
end
|
|
730
|
+
break
|
|
731
|
+
|
|
474
732
|
when :finish
|
|
475
733
|
depth = @target_frames.first.frame_depth
|
|
476
|
-
step_tp
|
|
734
|
+
step_tp iter do
|
|
477
735
|
# 3 is debugger's frame count
|
|
478
736
|
DEBUGGER__.frame_depth - 3 < depth
|
|
479
|
-
|
|
737
|
+
end
|
|
738
|
+
break
|
|
739
|
+
|
|
740
|
+
when :back
|
|
741
|
+
if @recorder&.can_step_back?
|
|
742
|
+
unless @recorder.backup_frames
|
|
743
|
+
@recorder.backup_frames = @target_frames
|
|
744
|
+
end
|
|
745
|
+
@recorder.step_back
|
|
746
|
+
raise SuspendReplay
|
|
747
|
+
else
|
|
748
|
+
puts "Can not step back more."
|
|
749
|
+
event! :result, nil
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
when :reset
|
|
753
|
+
if @recorder&.replaying?
|
|
754
|
+
@recorder.step_reset
|
|
755
|
+
raise SuspendReplay
|
|
756
|
+
end
|
|
757
|
+
|
|
480
758
|
else
|
|
481
|
-
raise
|
|
759
|
+
raise "unknown: #{type}"
|
|
482
760
|
end
|
|
483
|
-
|
|
761
|
+
|
|
484
762
|
when :eval
|
|
485
763
|
eval_type, eval_src = *args
|
|
486
764
|
|
|
@@ -489,12 +767,16 @@ module DEBUGGER__
|
|
|
489
767
|
case eval_type
|
|
490
768
|
when :p
|
|
491
769
|
result = frame_eval(eval_src)
|
|
492
|
-
puts "=> " + result
|
|
770
|
+
puts "=> " + color_pp(result, 2 ** 30)
|
|
771
|
+
if alloc_path = ObjectSpace.allocation_sourcefile(result)
|
|
772
|
+
puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}"
|
|
773
|
+
end
|
|
493
774
|
when :pp
|
|
494
775
|
result = frame_eval(eval_src)
|
|
495
|
-
puts
|
|
496
|
-
|
|
497
|
-
|
|
776
|
+
puts color_pp(result, SESSION.width)
|
|
777
|
+
if alloc_path = ObjectSpace.allocation_sourcefile(result)
|
|
778
|
+
puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}"
|
|
779
|
+
end
|
|
498
780
|
when :call
|
|
499
781
|
result = frame_eval(eval_src)
|
|
500
782
|
when :display, :try_display
|
|
@@ -544,12 +826,14 @@ module DEBUGGER__
|
|
|
544
826
|
raise "unsupported frame operation: #{arg.inspect}"
|
|
545
827
|
end
|
|
546
828
|
event! :result, nil
|
|
829
|
+
|
|
547
830
|
when :show
|
|
548
831
|
type = args.shift
|
|
549
832
|
|
|
550
833
|
case type
|
|
551
834
|
when :backtrace
|
|
552
|
-
|
|
835
|
+
max_lines, pattern = *args
|
|
836
|
+
show_frames max_lines, pattern
|
|
553
837
|
|
|
554
838
|
when :list
|
|
555
839
|
show_src(update_line: true, **(args.first || {}))
|
|
@@ -557,20 +841,37 @@ module DEBUGGER__
|
|
|
557
841
|
when :edit
|
|
558
842
|
show_by_editor(args.first)
|
|
559
843
|
|
|
560
|
-
when :
|
|
561
|
-
|
|
562
|
-
show_locals
|
|
563
|
-
show_ivars
|
|
844
|
+
when :default
|
|
845
|
+
pat = args.shift
|
|
846
|
+
show_locals pat
|
|
847
|
+
show_ivars pat
|
|
848
|
+
show_consts pat, only_self: true
|
|
849
|
+
|
|
850
|
+
when :locals
|
|
851
|
+
pat = args.shift
|
|
852
|
+
show_locals pat
|
|
564
853
|
|
|
565
|
-
when :
|
|
566
|
-
|
|
567
|
-
|
|
854
|
+
when :ivars
|
|
855
|
+
pat = args.shift
|
|
856
|
+
show_ivars pat
|
|
857
|
+
|
|
858
|
+
when :consts
|
|
859
|
+
pat = args.shift
|
|
860
|
+
show_consts pat
|
|
861
|
+
|
|
862
|
+
when :globals
|
|
863
|
+
pat = args.shift
|
|
864
|
+
show_globals pat
|
|
865
|
+
|
|
866
|
+
when :outline
|
|
867
|
+
show_outline args.first || 'self'
|
|
568
868
|
|
|
569
869
|
else
|
|
570
870
|
raise "unknown show param: " + [type, *args].inspect
|
|
571
871
|
end
|
|
572
872
|
|
|
573
873
|
event! :result, nil
|
|
874
|
+
|
|
574
875
|
when :breakpoint
|
|
575
876
|
case args[0]
|
|
576
877
|
when :method
|
|
@@ -593,6 +894,55 @@ module DEBUGGER__
|
|
|
593
894
|
event! :result, nil
|
|
594
895
|
end
|
|
595
896
|
end
|
|
897
|
+
|
|
898
|
+
when :trace
|
|
899
|
+
case args.shift
|
|
900
|
+
when :object
|
|
901
|
+
begin
|
|
902
|
+
obj = frame_eval args.shift, re_raise: true
|
|
903
|
+
opt = args.shift
|
|
904
|
+
obj_inspect = obj.inspect
|
|
905
|
+
|
|
906
|
+
width = 50
|
|
907
|
+
|
|
908
|
+
if obj_inspect.length >= width
|
|
909
|
+
obj_inspect = truncate(obj_inspect, width: width)
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
event! :result, :trace_pass, obj.object_id, obj_inspect, opt
|
|
913
|
+
rescue => e
|
|
914
|
+
puts e.message
|
|
915
|
+
event! :result, nil
|
|
916
|
+
end
|
|
917
|
+
else
|
|
918
|
+
raise "unreachable"
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
when :record
|
|
922
|
+
case args[0]
|
|
923
|
+
when nil
|
|
924
|
+
# ok
|
|
925
|
+
when :on
|
|
926
|
+
# enable recording
|
|
927
|
+
if !@recorder
|
|
928
|
+
@recorder = Recorder.new
|
|
929
|
+
@recorder.enable
|
|
930
|
+
end
|
|
931
|
+
when :off
|
|
932
|
+
if @recorder&.enabled?
|
|
933
|
+
@recorder.disable
|
|
934
|
+
end
|
|
935
|
+
else
|
|
936
|
+
raise "unknown: #{args.inspect}"
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
if @recorder&.enabled?
|
|
940
|
+
puts "Recorder for #{Thread.current}: on (#{@recorder.log.size} records)"
|
|
941
|
+
else
|
|
942
|
+
puts "Recorder for #{Thread.current}: off"
|
|
943
|
+
end
|
|
944
|
+
event! :result, nil
|
|
945
|
+
|
|
596
946
|
when :dap
|
|
597
947
|
process_dap args
|
|
598
948
|
else
|
|
@@ -600,26 +950,171 @@ module DEBUGGER__
|
|
|
600
950
|
end
|
|
601
951
|
end
|
|
602
952
|
|
|
603
|
-
rescue SystemExit
|
|
953
|
+
rescue SuspendReplay, SystemExit
|
|
604
954
|
raise
|
|
605
955
|
rescue Exception => e
|
|
606
|
-
pp [__FILE__
|
|
956
|
+
pp ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
|
|
607
957
|
raise
|
|
608
|
-
ensure
|
|
609
|
-
set_mode nil
|
|
610
958
|
end
|
|
611
959
|
|
|
612
|
-
|
|
613
|
-
|
|
960
|
+
class Recorder
|
|
961
|
+
attr_reader :log, :index
|
|
962
|
+
attr_accessor :backup_frames
|
|
963
|
+
|
|
964
|
+
include SkipPathHelper
|
|
965
|
+
|
|
966
|
+
def initialize
|
|
967
|
+
@log = []
|
|
968
|
+
@index = 0
|
|
969
|
+
@backup_frames = nil
|
|
970
|
+
thread = Thread.current
|
|
971
|
+
|
|
972
|
+
@tp_recorder ||= TracePoint.new(:line){|tp|
|
|
973
|
+
next unless Thread.current == thread
|
|
974
|
+
next if tp.path.start_with? __dir__
|
|
975
|
+
next if tp.path.start_with? '<internal:'
|
|
976
|
+
loc = caller_locations(1, 1).first
|
|
977
|
+
next if skip_location?(loc)
|
|
978
|
+
|
|
979
|
+
frames = DEBUGGER__.capture_frames(__dir__)
|
|
980
|
+
frames.each{|frame|
|
|
981
|
+
if b = frame.binding
|
|
982
|
+
frame.binding = nil
|
|
983
|
+
frame._local_variables = b.local_variables.map{|name|
|
|
984
|
+
[name, b.local_variable_get(name)]
|
|
985
|
+
}.to_h
|
|
986
|
+
frame._callee = b.eval('__callee__')
|
|
987
|
+
end
|
|
988
|
+
}
|
|
989
|
+
@log << frames
|
|
990
|
+
}
|
|
991
|
+
end
|
|
614
992
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
993
|
+
def enable
|
|
994
|
+
unless @tp_recorder.enabled?
|
|
995
|
+
@log.clear
|
|
996
|
+
@tp_recorder.enable
|
|
997
|
+
end
|
|
619
998
|
end
|
|
620
999
|
|
|
621
|
-
|
|
622
|
-
|
|
1000
|
+
def disable
|
|
1001
|
+
if @tp_recorder.enabled?
|
|
1002
|
+
@log.clear
|
|
1003
|
+
@tp_recorder.disable
|
|
1004
|
+
end
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
def enabled?
|
|
1008
|
+
@tp_recorder.enabled?
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def step_back
|
|
1012
|
+
@index += 1
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def step_forward
|
|
1016
|
+
@index -= 1
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def step_reset
|
|
1020
|
+
@index = 0
|
|
1021
|
+
@backup_frames = nil
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
def replaying?
|
|
1025
|
+
@index > 0
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
def can_step_back?
|
|
1029
|
+
log.size > @index
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def log_index
|
|
1033
|
+
@log.size - @index
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
def current_frame
|
|
1037
|
+
if @index == 0
|
|
1038
|
+
f = @backup_frames
|
|
1039
|
+
@backup_frames = nil
|
|
1040
|
+
f
|
|
1041
|
+
else
|
|
1042
|
+
frames = @log[log_index]
|
|
1043
|
+
frames
|
|
1044
|
+
end
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
# for debugging
|
|
1048
|
+
def current_position
|
|
1049
|
+
puts "INDEX: #{@index}"
|
|
1050
|
+
li = log_index
|
|
1051
|
+
@log.each_with_index{|frame, i|
|
|
1052
|
+
loc = frame.first&.location
|
|
1053
|
+
prefix = i == li ? "=> " : ' '
|
|
1054
|
+
puts "#{prefix} #{loc}"
|
|
1055
|
+
}
|
|
1056
|
+
end
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
# copyed from irb
|
|
1060
|
+
class Output
|
|
1061
|
+
include Color
|
|
1062
|
+
|
|
1063
|
+
MARGIN = " "
|
|
1064
|
+
|
|
1065
|
+
def initialize(output)
|
|
1066
|
+
@output = output
|
|
1067
|
+
@line_width = screen_width - MARGIN.length # right padding
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
def dump(name, strs)
|
|
1071
|
+
strs = strs.sort
|
|
1072
|
+
return if strs.empty?
|
|
1073
|
+
|
|
1074
|
+
line = "#{colorize_blue(name)}: "
|
|
1075
|
+
|
|
1076
|
+
# Attempt a single line
|
|
1077
|
+
if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length)
|
|
1078
|
+
line += strs.join(MARGIN)
|
|
1079
|
+
@output << line
|
|
1080
|
+
return
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
# Multi-line
|
|
1084
|
+
@output << line
|
|
1085
|
+
|
|
1086
|
+
# Dump with the largest # of columns that fits on a line
|
|
1087
|
+
cols = strs.size
|
|
1088
|
+
until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1
|
|
1089
|
+
cols -= 1
|
|
1090
|
+
end
|
|
1091
|
+
widths = col_widths(strs, cols: cols)
|
|
1092
|
+
strs.each_slice(cols) do |ss|
|
|
1093
|
+
@output << ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
private
|
|
1098
|
+
|
|
1099
|
+
def fits_on_line?(strs, cols:, offset: 0)
|
|
1100
|
+
width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1)
|
|
1101
|
+
width <= @line_width - offset
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
def col_widths(strs, cols:)
|
|
1105
|
+
cols.times.map do |col|
|
|
1106
|
+
(col...strs.size).step(cols).map do |i|
|
|
1107
|
+
strs[i].length
|
|
1108
|
+
end.max
|
|
1109
|
+
end
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
def screen_width
|
|
1113
|
+
SESSION.width
|
|
1114
|
+
rescue Errno::EINVAL # in `winsize': Invalid argument - <STDIN>
|
|
1115
|
+
80
|
|
1116
|
+
end
|
|
623
1117
|
end
|
|
1118
|
+
private_constant :Output
|
|
624
1119
|
end
|
|
625
1120
|
end
|