debug 1.0.0.beta7 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +108 -106
- data/Gemfile +1 -0
- data/README.md +415 -250
- data/Rakefile +2 -1
- data/TODO.md +3 -8
- data/debug.gemspec +1 -0
- data/exe/rdbg +5 -8
- 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 -175
- data/lib/debug/console.rb +75 -70
- 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 +584 -299
- data/lib/debug/{run.rb → start.rb} +1 -1
- 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 +335 -227
- metadata +22 -5
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
|
468
659
|
|
469
|
-
|
660
|
+
def wait_next_action_
|
661
|
+
# assertions
|
662
|
+
raise "@mode is #{@mode}" unless @mode == :waiting
|
663
|
+
|
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
|