debug 1.0.0.beta8 → 1.0.0.rc1
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/README.md +90 -30
- data/debug.gemspec +1 -0
- data/exe/rdbg +3 -6
- data/ext/debug/debug.c +11 -1
- data/lib/debug/breakpoint.rb +55 -22
- data/lib/debug/client.rb +7 -12
- data/lib/debug/color.rb +19 -4
- data/lib/debug/config.rb +354 -177
- data/lib/debug/console.rb +76 -68
- data/lib/debug/frame_info.rb +40 -7
- data/lib/debug/local.rb +91 -0
- data/lib/debug/server.rb +74 -26
- data/lib/debug/server_dap.rb +32 -7
- data/lib/debug/session.rb +568 -274
- data/lib/debug/thread_client.rb +620 -162
- data/lib/debug/tracer.rb +242 -0
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +26 -25
- metadata +18 -2
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
|
@@ -71,11 +83,52 @@ module DEBUGGER__
|
|
71
83
|
@output = []
|
72
84
|
@frame_formatter = method(:default_frame_formatter)
|
73
85
|
@var_map = {} # { thread_local_var_id => obj } for DAP
|
74
|
-
|
86
|
+
@recorder = nil
|
87
|
+
@mode = :waiting
|
88
|
+
set_mode :running
|
89
|
+
thr.instance_variable_set(:@__thread_client_id, id)
|
75
90
|
|
76
91
|
::DEBUGGER__.info("Thread \##{@id} is created.")
|
77
92
|
end
|
78
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
|
130
|
+
end
|
131
|
+
|
79
132
|
def name
|
80
133
|
"##{@id} #{@thread.name || @thread.backtrace.last}"
|
81
134
|
end
|
@@ -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
|
148
215
|
end
|
149
216
|
|
150
|
-
def
|
217
|
+
def on_trap sig
|
218
|
+
if waiting?
|
219
|
+
# raise Interrupt
|
220
|
+
else
|
221
|
+
suspend :trap, sig: sig
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
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,113 +415,138 @@ 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
|
-
|
308
|
-
info = "#{colorize_cyan(label)} => #{colored_inspect(obj)}".lines
|
309
|
-
w = SESSION.width
|
310
|
-
max_inspect_lines = CONFIG[:show_inspect_lines] || 10
|
426
|
+
## cmd: show
|
311
427
|
|
312
|
-
|
313
|
-
info = "#{colorize_cyan(label)} => #{colored_inspect(obj, no_color: true)}".lines
|
314
|
-
if max_inspect_lines > 0 && info.size > max_inspect_lines
|
315
|
-
info = info.first(max_inspect_lines - 2) +
|
316
|
-
["...(#{info.size - (max_inspect_lines - 1)} lines)\n" + info.last]
|
317
|
-
end
|
318
|
-
info.map!{|l|
|
319
|
-
l.length > w ? l[0..(w-4)] + '...' : l
|
320
|
-
}
|
321
|
-
end
|
322
|
-
|
323
|
-
puts info
|
324
|
-
end
|
325
|
-
|
326
|
-
def show_locals
|
428
|
+
def show_locals pat
|
327
429
|
if s = current_frame&.self
|
328
|
-
puts_variable_info '%self', s
|
430
|
+
puts_variable_info '%self', s, pat
|
329
431
|
end
|
330
432
|
if current_frame&.has_return_value
|
331
|
-
puts_variable_info '%return', current_frame.return_value
|
433
|
+
puts_variable_info '%return', current_frame.return_value, pat
|
332
434
|
end
|
333
435
|
if current_frame&.has_raised_exception
|
334
|
-
puts_variable_info "%raised", current_frame.raised_exception
|
436
|
+
puts_variable_info "%raised", current_frame.raised_exception, pat
|
335
437
|
end
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
puts_variable_info
|
438
|
+
|
439
|
+
if vars = current_frame&.local_variables
|
440
|
+
vars.each{|var, val|
|
441
|
+
puts_variable_info var, val, pat
|
340
442
|
}
|
341
443
|
end
|
342
444
|
end
|
343
445
|
|
344
|
-
def show_ivars
|
446
|
+
def show_ivars pat
|
345
447
|
if s = current_frame&.self
|
346
|
-
s.instance_variables.each{|iv|
|
448
|
+
s.instance_variables.sort.each{|iv|
|
347
449
|
value = s.instance_variable_get(iv)
|
348
|
-
puts_variable_info iv, value
|
450
|
+
puts_variable_info iv, value, pat
|
349
451
|
}
|
350
452
|
end
|
351
453
|
end
|
352
454
|
|
353
|
-
def
|
354
|
-
|
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
|
464
|
+
|
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 = {}
|
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
|
355
483
|
end
|
356
484
|
|
357
|
-
|
358
|
-
|
359
|
-
|
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
|
360
489
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
end
|
369
|
-
@success_last_eval = true
|
370
|
-
result
|
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
|
371
497
|
|
498
|
+
begin
|
499
|
+
inspected = obj.inspect
|
372
500
|
rescue Exception => e
|
373
|
-
|
501
|
+
inspected = e.inspect
|
502
|
+
end
|
503
|
+
mono_info = "#{label} = #{inspected}"
|
374
504
|
|
375
|
-
|
505
|
+
w = SESSION::width
|
376
506
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
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}"
|
382
513
|
end
|
514
|
+
|
515
|
+
puts info
|
383
516
|
end
|
384
517
|
|
385
|
-
def
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
518
|
+
def truncate(string, width:)
|
519
|
+
str = string[0 .. (width-4)] + '...'
|
520
|
+
str += ">" if str.start_with?("#<")
|
521
|
+
str
|
522
|
+
end
|
523
|
+
|
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
|
390
546
|
end
|
391
547
|
|
548
|
+
### cmd: show frames
|
549
|
+
|
392
550
|
def show_frames max = nil, pattern = nil
|
393
551
|
if @target_frames && (max ||= @target_frames.size) > 0
|
394
552
|
frames = []
|
@@ -420,29 +578,60 @@ module DEBUGGER__
|
|
420
578
|
puts frame_str(i)
|
421
579
|
end
|
422
580
|
|
423
|
-
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
|
424
591
|
begin
|
425
|
-
|
592
|
+
obj = frame_eval(expr, re_raise: true)
|
426
593
|
rescue Exception
|
427
594
|
# ignore
|
428
595
|
else
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
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
|
438
606
|
end
|
439
607
|
end
|
440
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
|
+
|
441
630
|
def make_breakpoint args
|
442
631
|
case args.first
|
443
632
|
when :method
|
444
633
|
klass_name, op, method_name, cond, cmd = args[1..]
|
445
|
-
bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond, command: cmd)
|
634
|
+
bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond: cond, command: cmd)
|
446
635
|
begin
|
447
636
|
bp.enable
|
448
637
|
rescue Exception => e
|
@@ -459,28 +648,58 @@ module DEBUGGER__
|
|
459
648
|
end
|
460
649
|
end
|
461
650
|
|
462
|
-
|
463
|
-
@mode = mode
|
651
|
+
class SuspendReplay < Exception
|
464
652
|
end
|
465
653
|
|
466
654
|
def wait_next_action
|
467
|
-
|
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
|
468
663
|
|
469
|
-
SESSION.
|
664
|
+
unless SESSION.active?
|
665
|
+
pp caller
|
666
|
+
set_mode :running
|
667
|
+
return
|
668
|
+
end
|
669
|
+
# SESSION.check_forked
|
470
670
|
|
471
|
-
while
|
472
|
-
|
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
|
473
680
|
|
474
681
|
cmd, *args = *cmds
|
475
682
|
|
476
683
|
case cmd
|
477
684
|
when :continue
|
478
685
|
break
|
686
|
+
|
479
687
|
when :step
|
480
688
|
step_type = args[0]
|
689
|
+
iter = args[1]
|
690
|
+
|
481
691
|
case step_type
|
482
692
|
when :in
|
483
|
-
|
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
|
+
|
484
703
|
when :next
|
485
704
|
frame = @target_frames.first
|
486
705
|
path = frame.location.absolute_path || "!eval:#{frame.path}"
|
@@ -496,7 +715,7 @@ module DEBUGGER__
|
|
496
715
|
|
497
716
|
depth = @target_frames.first.frame_depth
|
498
717
|
|
499
|
-
step_tp
|
718
|
+
step_tp iter do
|
500
719
|
loc = caller_locations(2, 1).first
|
501
720
|
loc_path = loc.absolute_path || "!eval:#{loc.path}"
|
502
721
|
|
@@ -507,17 +726,39 @@ module DEBUGGER__
|
|
507
726
|
(next_line && loc_path == path &&
|
508
727
|
(loc_lineno = loc.lineno) > line &&
|
509
728
|
loc_lineno <= next_line)
|
510
|
-
|
729
|
+
end
|
730
|
+
break
|
731
|
+
|
511
732
|
when :finish
|
512
733
|
depth = @target_frames.first.frame_depth
|
513
|
-
step_tp
|
734
|
+
step_tp iter do
|
514
735
|
# 3 is debugger's frame count
|
515
736
|
DEBUGGER__.frame_depth - 3 < depth
|
516
|
-
|
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
|
+
|
517
758
|
else
|
518
|
-
raise
|
759
|
+
raise "unknown: #{type}"
|
519
760
|
end
|
520
|
-
|
761
|
+
|
521
762
|
when :eval
|
522
763
|
eval_type, eval_src = *args
|
523
764
|
|
@@ -526,11 +767,16 @@ module DEBUGGER__
|
|
526
767
|
case eval_type
|
527
768
|
when :p
|
528
769
|
result = frame_eval(eval_src)
|
529
|
-
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
|
530
774
|
when :pp
|
531
775
|
result = frame_eval(eval_src)
|
532
|
-
puts "=> "
|
533
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
|
534
780
|
when :call
|
535
781
|
result = frame_eval(eval_src)
|
536
782
|
when :display, :try_display
|
@@ -580,6 +826,7 @@ module DEBUGGER__
|
|
580
826
|
raise "unsupported frame operation: #{arg.inspect}"
|
581
827
|
end
|
582
828
|
event! :result, nil
|
829
|
+
|
583
830
|
when :show
|
584
831
|
type = args.shift
|
585
832
|
|
@@ -594,20 +841,37 @@ module DEBUGGER__
|
|
594
841
|
when :edit
|
595
842
|
show_by_editor(args.first)
|
596
843
|
|
597
|
-
when :
|
598
|
-
|
599
|
-
show_locals
|
600
|
-
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
|
853
|
+
|
854
|
+
when :ivars
|
855
|
+
pat = args.shift
|
856
|
+
show_ivars pat
|
601
857
|
|
602
|
-
when :
|
603
|
-
|
604
|
-
|
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'
|
605
868
|
|
606
869
|
else
|
607
870
|
raise "unknown show param: " + [type, *args].inspect
|
608
871
|
end
|
609
872
|
|
610
873
|
event! :result, nil
|
874
|
+
|
611
875
|
when :breakpoint
|
612
876
|
case args[0]
|
613
877
|
when :method
|
@@ -630,6 +894,55 @@ module DEBUGGER__
|
|
630
894
|
event! :result, nil
|
631
895
|
end
|
632
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
|
+
|
633
946
|
when :dap
|
634
947
|
process_dap args
|
635
948
|
else
|
@@ -637,26 +950,171 @@ module DEBUGGER__
|
|
637
950
|
end
|
638
951
|
end
|
639
952
|
|
640
|
-
rescue SystemExit
|
953
|
+
rescue SuspendReplay, SystemExit
|
641
954
|
raise
|
642
955
|
rescue Exception => e
|
643
956
|
pp ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
|
644
957
|
raise
|
645
|
-
ensure
|
646
|
-
set_mode nil
|
647
958
|
end
|
648
959
|
|
649
|
-
|
650
|
-
|
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
|
651
992
|
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
993
|
+
def enable
|
994
|
+
unless @tp_recorder.enabled?
|
995
|
+
@log.clear
|
996
|
+
@tp_recorder.enable
|
997
|
+
end
|
656
998
|
end
|
657
999
|
|
658
|
-
|
659
|
-
|
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
|
660
1117
|
end
|
1118
|
+
private_constant :Output
|
661
1119
|
end
|
662
1120
|
end
|