ed-precompiled_debug 1.11.0-x86_64-linux
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 +7 -0
- data/CONTRIBUTING.md +573 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +996 -0
- data/Rakefile +57 -0
- data/TODO.md +23 -0
- data/debug.gemspec +33 -0
- data/exe/rdbg +53 -0
- data/ext/debug/debug.c +228 -0
- data/ext/debug/extconf.rb +27 -0
- data/ext/debug/iseq_collector.c +93 -0
- data/lib/debug/3.0/debug.so +0 -0
- data/lib/debug/3.1/debug.so +0 -0
- data/lib/debug/3.2/debug.so +0 -0
- data/lib/debug/3.3/debug.so +0 -0
- data/lib/debug/3.4/debug.so +0 -0
- data/lib/debug/abbrev_command.rb +77 -0
- data/lib/debug/breakpoint.rb +556 -0
- data/lib/debug/client.rb +263 -0
- data/lib/debug/color.rb +123 -0
- data/lib/debug/config.rb +592 -0
- data/lib/debug/console.rb +224 -0
- data/lib/debug/dap_custom/traceInspector.rb +336 -0
- data/lib/debug/debug.so +0 -0
- data/lib/debug/frame_info.rb +191 -0
- data/lib/debug/irb_integration.rb +37 -0
- data/lib/debug/local.rb +115 -0
- data/lib/debug/open.rb +13 -0
- data/lib/debug/open_nonstop.rb +15 -0
- data/lib/debug/prelude.rb +50 -0
- data/lib/debug/server.rb +534 -0
- data/lib/debug/server_cdp.rb +1348 -0
- data/lib/debug/server_dap.rb +1108 -0
- data/lib/debug/session.rb +2667 -0
- data/lib/debug/source_repository.rb +150 -0
- data/lib/debug/start.rb +5 -0
- data/lib/debug/thread_client.rb +1457 -0
- data/lib/debug/tracer.rb +241 -0
- data/lib/debug/version.rb +5 -0
- data/lib/debug.rb +9 -0
- data/misc/README.md.erb +660 -0
- metadata +118 -0
@@ -0,0 +1,1457 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'objspace'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
require_relative 'color'
|
7
|
+
|
8
|
+
class ::Thread
|
9
|
+
attr_accessor :debug_thread_client
|
10
|
+
end
|
11
|
+
|
12
|
+
module DEBUGGER__
|
13
|
+
M_INSTANCE_VARIABLES = method(:instance_variables).unbind
|
14
|
+
M_INSTANCE_VARIABLE_GET = method(:instance_variable_get).unbind
|
15
|
+
M_CLASS = method(:class).unbind
|
16
|
+
M_SINGLETON_CLASS = method(:singleton_class).unbind
|
17
|
+
M_KIND_OF_P = method(:kind_of?).unbind
|
18
|
+
M_RESPOND_TO_P = method(:respond_to?).unbind
|
19
|
+
M_METHOD = method(:method).unbind
|
20
|
+
M_OBJECT_ID = method(:object_id).unbind
|
21
|
+
M_NAME = method(:name).unbind
|
22
|
+
|
23
|
+
module SkipPathHelper
|
24
|
+
def skip_path?(path)
|
25
|
+
!path ||
|
26
|
+
DEBUGGER__.skip? ||
|
27
|
+
ThreadClient.current.management? ||
|
28
|
+
skip_internal_path?(path) ||
|
29
|
+
skip_config_skip_path?(path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def skip_config_skip_path?(path)
|
33
|
+
(skip_paths = CONFIG[:skip_path]) && skip_paths.any?{|skip_path| path.match?(skip_path)}
|
34
|
+
end
|
35
|
+
|
36
|
+
def skip_internal_path?(path)
|
37
|
+
path.start_with?(__dir__) || path.delete_prefix('!eval:').start_with?('<internal:')
|
38
|
+
end
|
39
|
+
|
40
|
+
def skip_location?(loc)
|
41
|
+
loc_path = loc.absolute_path || "!eval:#{loc.path}"
|
42
|
+
skip_path?(loc_path)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module GlobalVariablesHelper
|
47
|
+
SKIP_GLOBAL_LIST = %i[$= $KCODE $-K $SAFE $FILENAME].freeze
|
48
|
+
def safe_global_variables
|
49
|
+
global_variables.reject{|name| SKIP_GLOBAL_LIST.include? name }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class ThreadClient
|
54
|
+
def self.current
|
55
|
+
Thread.current.debug_thread_client ||= SESSION.get_thread_client
|
56
|
+
end
|
57
|
+
|
58
|
+
include Color
|
59
|
+
include SkipPathHelper
|
60
|
+
include GlobalVariablesHelper
|
61
|
+
|
62
|
+
attr_reader :thread, :id, :recorder, :check_bp_fulfillment_map
|
63
|
+
|
64
|
+
def location
|
65
|
+
current_frame&.location
|
66
|
+
end
|
67
|
+
|
68
|
+
def assemble_arguments(args)
|
69
|
+
args.map do |arg|
|
70
|
+
"#{colorize_cyan(arg[:name])}=#{arg[:value]}"
|
71
|
+
end.join(", ")
|
72
|
+
end
|
73
|
+
|
74
|
+
def default_frame_formatter frame
|
75
|
+
call_identifier_str =
|
76
|
+
case frame.frame_type
|
77
|
+
when :block
|
78
|
+
level, block_loc = frame.block_identifier
|
79
|
+
args = frame.parameters_info
|
80
|
+
|
81
|
+
if !args.empty?
|
82
|
+
args_str = " {|#{assemble_arguments(args)}|}"
|
83
|
+
end
|
84
|
+
|
85
|
+
"#{colorize_blue("block")}#{args_str} in #{colorize_blue(block_loc + level)}"
|
86
|
+
when :method
|
87
|
+
ci = frame.method_identifier
|
88
|
+
args = frame.parameters_info
|
89
|
+
|
90
|
+
if !args.empty?
|
91
|
+
args_str = "(#{assemble_arguments(args)})"
|
92
|
+
end
|
93
|
+
|
94
|
+
"#{colorize_blue(ci)}#{args_str}"
|
95
|
+
when :c
|
96
|
+
colorize_blue(frame.c_identifier)
|
97
|
+
when :other
|
98
|
+
colorize_blue(frame.other_identifier)
|
99
|
+
end
|
100
|
+
|
101
|
+
location_str = colorize(frame.location_str, [:GREEN])
|
102
|
+
result = "#{call_identifier_str} at #{location_str}"
|
103
|
+
|
104
|
+
if return_str = frame.return_str
|
105
|
+
result += " #=> #{colorize_magenta(return_str)}"
|
106
|
+
end
|
107
|
+
|
108
|
+
result
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialize id, q_evt, q_cmd, thr = Thread.current
|
112
|
+
@is_management = false
|
113
|
+
@id = id
|
114
|
+
@thread = thr
|
115
|
+
@target_frames = nil
|
116
|
+
@q_evt = q_evt
|
117
|
+
@q_cmd = q_cmd
|
118
|
+
@step_tp = nil
|
119
|
+
@output = []
|
120
|
+
@frame_formatter = method(:default_frame_formatter)
|
121
|
+
@var_map = {} # { thread_local_var_id => obj } for DAP
|
122
|
+
@obj_map = {} # { object_id => obj } for CDP
|
123
|
+
@recorder = nil
|
124
|
+
@mode = :waiting
|
125
|
+
@current_frame_index = 0
|
126
|
+
# every thread should maintain its own CheckBreakpoint fulfillment state
|
127
|
+
@check_bp_fulfillment_map = {} # { check_bp => boolean }
|
128
|
+
set_mode :running
|
129
|
+
thr.instance_variable_set(:@__thread_client_id, id)
|
130
|
+
|
131
|
+
::DEBUGGER__.info("Thread \##{@id} is created.")
|
132
|
+
end
|
133
|
+
|
134
|
+
def deactivate
|
135
|
+
@step_tp.disable if @step_tp
|
136
|
+
end
|
137
|
+
|
138
|
+
def management?
|
139
|
+
@is_management
|
140
|
+
end
|
141
|
+
|
142
|
+
def mark_as_management
|
143
|
+
@is_management = true
|
144
|
+
end
|
145
|
+
|
146
|
+
def set_mode mode
|
147
|
+
debug_mode(@mode, mode)
|
148
|
+
# STDERR.puts "#{@mode} => #{mode} @ #{caller.inspect}"
|
149
|
+
# pp caller
|
150
|
+
|
151
|
+
# mode transition check
|
152
|
+
case mode
|
153
|
+
when :running
|
154
|
+
raise "#{mode} is given, but #{mode}" unless self.waiting?
|
155
|
+
when :waiting
|
156
|
+
# TODO: there is waiting -> waiting
|
157
|
+
# raise "#{mode} is given, but #{mode}" unless self.running?
|
158
|
+
else
|
159
|
+
raise "unknown mode: #{mode}"
|
160
|
+
end
|
161
|
+
|
162
|
+
# DEBUGGER__.warn "#{@mode} => #{mode} @ #{self.inspect}"
|
163
|
+
@mode = mode
|
164
|
+
end
|
165
|
+
|
166
|
+
def running?
|
167
|
+
@mode == :running
|
168
|
+
end
|
169
|
+
|
170
|
+
def waiting?
|
171
|
+
@mode == :waiting
|
172
|
+
end
|
173
|
+
|
174
|
+
def name
|
175
|
+
"##{@id} #{@thread.name || @thread.backtrace.last}"
|
176
|
+
end
|
177
|
+
|
178
|
+
def close
|
179
|
+
@q_cmd.close
|
180
|
+
end
|
181
|
+
|
182
|
+
def inspect
|
183
|
+
if bt = @thread.backtrace
|
184
|
+
"#<DBG:TC #{self.id}:#{@mode}@#{bt[-1]}>"
|
185
|
+
else # bt can be nil
|
186
|
+
"#<DBG:TC #{self.id}:#{@mode}>"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def to_s
|
191
|
+
str = "(#{@thread.name || @thread.status})@#{current_frame&.location || @thread.to_s}"
|
192
|
+
str += " (not under control)" unless self.waiting?
|
193
|
+
str
|
194
|
+
end
|
195
|
+
|
196
|
+
def puts str = ''
|
197
|
+
if @recorder&.replaying?
|
198
|
+
prefix = colorize_dim("[replay] ")
|
199
|
+
end
|
200
|
+
case str
|
201
|
+
when nil
|
202
|
+
@output << "\n"
|
203
|
+
when Array
|
204
|
+
str.each{|s| puts s}
|
205
|
+
else
|
206
|
+
@output << "#{prefix}#{str.chomp}\n"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def << req
|
211
|
+
debug_cmd(req)
|
212
|
+
@q_cmd << req
|
213
|
+
end
|
214
|
+
|
215
|
+
def generate_info
|
216
|
+
return unless current_frame
|
217
|
+
|
218
|
+
{ location: current_frame.location_str, line: current_frame.location.lineno }
|
219
|
+
end
|
220
|
+
|
221
|
+
def event! ev, *args
|
222
|
+
debug_event(ev, args)
|
223
|
+
@q_evt << [self, @output, ev, generate_info, *args]
|
224
|
+
@output = []
|
225
|
+
end
|
226
|
+
|
227
|
+
## events
|
228
|
+
|
229
|
+
def wait_reply event_arg
|
230
|
+
return if management?
|
231
|
+
|
232
|
+
set_mode :waiting
|
233
|
+
|
234
|
+
event!(*event_arg)
|
235
|
+
wait_next_action
|
236
|
+
end
|
237
|
+
|
238
|
+
def on_load iseq, eval_src
|
239
|
+
wait_reply [:load, iseq, eval_src]
|
240
|
+
end
|
241
|
+
|
242
|
+
def on_init name
|
243
|
+
wait_reply [:init, name]
|
244
|
+
end
|
245
|
+
|
246
|
+
def on_trace trace_id, msg
|
247
|
+
wait_reply [:trace, trace_id, msg]
|
248
|
+
end
|
249
|
+
|
250
|
+
def on_breakpoint tp, bp
|
251
|
+
suspend tp.event, tp, bp: bp
|
252
|
+
end
|
253
|
+
|
254
|
+
def on_trap sig
|
255
|
+
if waiting?
|
256
|
+
# raise Interrupt
|
257
|
+
else
|
258
|
+
suspend :trap, sig: sig
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def on_pause
|
263
|
+
suspend :pause
|
264
|
+
end
|
265
|
+
|
266
|
+
def suspend event, tp = nil, bp: nil, sig: nil, postmortem_frames: nil, replay_frames: nil, postmortem_exc: nil
|
267
|
+
return if management?
|
268
|
+
debug_suspend(event)
|
269
|
+
|
270
|
+
@current_frame_index = 0
|
271
|
+
|
272
|
+
case
|
273
|
+
when postmortem_frames
|
274
|
+
@target_frames = postmortem_frames
|
275
|
+
@postmortem = true
|
276
|
+
when replay_frames
|
277
|
+
@target_frames = replay_frames
|
278
|
+
else
|
279
|
+
@target_frames = DEBUGGER__.capture_frames(__dir__)
|
280
|
+
end
|
281
|
+
|
282
|
+
cf = @target_frames.first
|
283
|
+
if cf
|
284
|
+
case event
|
285
|
+
when :return, :b_return, :c_return
|
286
|
+
cf.has_return_value = true
|
287
|
+
cf.return_value = tp.return_value
|
288
|
+
end
|
289
|
+
|
290
|
+
if CatchBreakpoint === bp
|
291
|
+
cf.has_raised_exception = true
|
292
|
+
cf.raised_exception = bp.last_exc
|
293
|
+
end
|
294
|
+
|
295
|
+
if postmortem_exc
|
296
|
+
cf.has_raised_exception = true
|
297
|
+
cf.raised_exception = postmortem_exc
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
if event != :pause
|
302
|
+
unless bp&.skip_src
|
303
|
+
show_src
|
304
|
+
show_frames CONFIG[:show_frames]
|
305
|
+
end
|
306
|
+
|
307
|
+
set_mode :waiting
|
308
|
+
|
309
|
+
if bp
|
310
|
+
event! :suspend, :breakpoint, bp.key
|
311
|
+
elsif sig
|
312
|
+
event! :suspend, :trap, sig
|
313
|
+
else
|
314
|
+
event! :suspend, event
|
315
|
+
end
|
316
|
+
else
|
317
|
+
set_mode :waiting
|
318
|
+
end
|
319
|
+
|
320
|
+
wait_next_action
|
321
|
+
end
|
322
|
+
|
323
|
+
def replay_suspend
|
324
|
+
# @recorder.current_position
|
325
|
+
suspend :replay, replay_frames: @recorder.current_frame
|
326
|
+
end
|
327
|
+
|
328
|
+
## control all
|
329
|
+
|
330
|
+
begin
|
331
|
+
TracePoint.new(:raise){}.enable(target_thread: Thread.current)
|
332
|
+
SUPPORT_TARGET_THREAD = true
|
333
|
+
rescue ArgumentError
|
334
|
+
SUPPORT_TARGET_THREAD = false
|
335
|
+
end
|
336
|
+
|
337
|
+
def step_tp iter, events = [:line, :b_return, :return]
|
338
|
+
@step_tp.disable if @step_tp
|
339
|
+
|
340
|
+
thread = Thread.current
|
341
|
+
subsession_id = SESSION.subsession_id
|
342
|
+
|
343
|
+
if SUPPORT_TARGET_THREAD
|
344
|
+
@step_tp = TracePoint.new(*events){|tp|
|
345
|
+
if SESSION.stop_stepping? tp.path, tp.lineno, subsession_id
|
346
|
+
tp.disable
|
347
|
+
next
|
348
|
+
end
|
349
|
+
next if !yield(tp)
|
350
|
+
next if tp.path.start_with?(__dir__)
|
351
|
+
next if tp.path.start_with?('<internal:trace_point>')
|
352
|
+
next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
|
353
|
+
next if skip_internal_path?(tp.path)
|
354
|
+
loc = caller_locations(1, 1).first
|
355
|
+
next if skip_location?(loc)
|
356
|
+
next if iter && (iter -= 1) > 0
|
357
|
+
|
358
|
+
tp.disable
|
359
|
+
suspend tp.event, tp
|
360
|
+
}
|
361
|
+
@step_tp.enable(target_thread: thread)
|
362
|
+
else
|
363
|
+
@step_tp = TracePoint.new(*events){|tp|
|
364
|
+
next if thread != Thread.current
|
365
|
+
if SESSION.stop_stepping? tp.path, tp.lineno, subsession_id
|
366
|
+
tp.disable
|
367
|
+
next
|
368
|
+
end
|
369
|
+
next if !yield(tp)
|
370
|
+
next if tp.path.start_with?(__dir__)
|
371
|
+
next if tp.path.start_with?('<internal:trace_point>')
|
372
|
+
next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
|
373
|
+
next if skip_internal_path?(tp.path)
|
374
|
+
loc = caller_locations(1, 1).first
|
375
|
+
next if skip_location?(loc)
|
376
|
+
next if iter && (iter -= 1) > 0
|
377
|
+
|
378
|
+
tp.disable
|
379
|
+
suspend tp.event, tp
|
380
|
+
}
|
381
|
+
@step_tp.enable
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
## cmd helpers
|
386
|
+
|
387
|
+
if TracePoint.respond_to? :allow_reentry
|
388
|
+
def tp_allow_reentry
|
389
|
+
TracePoint.allow_reentry do
|
390
|
+
yield
|
391
|
+
end
|
392
|
+
rescue RuntimeError => e
|
393
|
+
# on the postmortem mode, it is not stopped in TracePoint
|
394
|
+
if e.message == 'No need to allow reentrance.'
|
395
|
+
yield
|
396
|
+
else
|
397
|
+
raise
|
398
|
+
end
|
399
|
+
end
|
400
|
+
else
|
401
|
+
def tp_allow_reentry
|
402
|
+
yield
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def frame_eval_core src, b, binding_location: false
|
407
|
+
saved_target_frames = @target_frames
|
408
|
+
saved_current_frame_index = @current_frame_index
|
409
|
+
|
410
|
+
if b
|
411
|
+
file, lineno = b.source_location
|
412
|
+
|
413
|
+
tp_allow_reentry do
|
414
|
+
if binding_location
|
415
|
+
b.eval(src, file, lineno)
|
416
|
+
else
|
417
|
+
b.eval(src, "(rdbg)/#{file}")
|
418
|
+
end
|
419
|
+
end
|
420
|
+
else
|
421
|
+
frame_self = current_frame.self
|
422
|
+
|
423
|
+
tp_allow_reentry do
|
424
|
+
frame_self.instance_eval(src)
|
425
|
+
end
|
426
|
+
end
|
427
|
+
ensure
|
428
|
+
@target_frames = saved_target_frames
|
429
|
+
@current_frame_index = saved_current_frame_index
|
430
|
+
end
|
431
|
+
|
432
|
+
SPECIAL_LOCAL_VARS = [
|
433
|
+
[:raised_exception, "_raised"],
|
434
|
+
[:return_value, "_return"],
|
435
|
+
]
|
436
|
+
|
437
|
+
def frame_eval src, re_raise: false, binding_location: false
|
438
|
+
@success_last_eval = false
|
439
|
+
|
440
|
+
b = current_frame&.eval_binding || TOPLEVEL_BINDING
|
441
|
+
|
442
|
+
special_local_variables current_frame do |name, var|
|
443
|
+
b.local_variable_set(name, var) if /\%/ !~ name
|
444
|
+
end
|
445
|
+
|
446
|
+
result = frame_eval_core(src, b, binding_location: binding_location)
|
447
|
+
|
448
|
+
@success_last_eval = true
|
449
|
+
result
|
450
|
+
|
451
|
+
rescue SystemExit
|
452
|
+
raise
|
453
|
+
rescue Exception => e
|
454
|
+
return yield(e) if block_given?
|
455
|
+
|
456
|
+
puts "eval error: #{e}"
|
457
|
+
|
458
|
+
e.backtrace_locations&.each do |loc|
|
459
|
+
break if loc.path == __FILE__
|
460
|
+
puts " #{loc}"
|
461
|
+
end
|
462
|
+
raise if re_raise
|
463
|
+
end
|
464
|
+
|
465
|
+
def get_src(frame,
|
466
|
+
max_lines:,
|
467
|
+
start_line: nil,
|
468
|
+
end_line: nil,
|
469
|
+
dir: +1)
|
470
|
+
if file_lines = frame.file_lines
|
471
|
+
frame_line = frame.location.lineno - 1
|
472
|
+
|
473
|
+
if CONFIG[:no_lineno]
|
474
|
+
lines = file_lines
|
475
|
+
else
|
476
|
+
lines = file_lines.map.with_index do |e, i|
|
477
|
+
cur = i == frame_line ? '=>' : ' '
|
478
|
+
line = colorize_dim('%4d|' % (i+1))
|
479
|
+
"#{cur}#{line} #{e}"
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
unless start_line
|
484
|
+
if frame.show_line
|
485
|
+
if dir > 0
|
486
|
+
start_line = frame.show_line
|
487
|
+
else
|
488
|
+
end_line = frame.show_line - max_lines
|
489
|
+
start_line = [end_line - max_lines, 0].max
|
490
|
+
end
|
491
|
+
else
|
492
|
+
start_line = [frame_line - max_lines/2, 0].max
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
unless end_line
|
497
|
+
end_line = [start_line + max_lines, lines.size].min
|
498
|
+
end
|
499
|
+
|
500
|
+
if start_line != end_line && max_lines
|
501
|
+
[start_line, end_line, lines]
|
502
|
+
end
|
503
|
+
else # no file lines
|
504
|
+
nil
|
505
|
+
end
|
506
|
+
rescue Exception => e
|
507
|
+
p e
|
508
|
+
pp e.backtrace
|
509
|
+
exit!
|
510
|
+
end
|
511
|
+
|
512
|
+
def show_src(frame_index: @current_frame_index, update_line: false, ignore_show_line: false, max_lines: CONFIG[:show_src_lines], **options)
|
513
|
+
if frame = get_frame(frame_index)
|
514
|
+
begin
|
515
|
+
if ignore_show_line
|
516
|
+
prev_show_line = frame.show_line
|
517
|
+
frame.show_line = nil
|
518
|
+
end
|
519
|
+
|
520
|
+
start_line, end_line, lines = *get_src(frame, max_lines: max_lines, **options)
|
521
|
+
|
522
|
+
if start_line
|
523
|
+
if update_line
|
524
|
+
frame.show_line = end_line
|
525
|
+
end
|
526
|
+
|
527
|
+
puts "[#{start_line+1}, #{end_line}] in #{frame.pretty_path}" if !update_line && max_lines != 1
|
528
|
+
puts lines[start_line...end_line]
|
529
|
+
else
|
530
|
+
puts "# No sourcefile available for #{frame.path}"
|
531
|
+
end
|
532
|
+
ensure
|
533
|
+
frame.show_line = prev_show_line if prev_show_line
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
def current_frame
|
539
|
+
get_frame(@current_frame_index)
|
540
|
+
end
|
541
|
+
|
542
|
+
def get_frame(index)
|
543
|
+
if @target_frames
|
544
|
+
@target_frames[index]
|
545
|
+
else
|
546
|
+
nil
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def collect_locals(frame)
|
551
|
+
locals = []
|
552
|
+
|
553
|
+
if s = frame&.self
|
554
|
+
locals << ["%self", s]
|
555
|
+
end
|
556
|
+
special_local_variables frame do |name, val|
|
557
|
+
locals << [name, val]
|
558
|
+
end
|
559
|
+
|
560
|
+
if vars = frame&.local_variables
|
561
|
+
vars.each{|var, val|
|
562
|
+
locals << [var, val]
|
563
|
+
}
|
564
|
+
end
|
565
|
+
|
566
|
+
locals
|
567
|
+
end
|
568
|
+
|
569
|
+
## cmd: show
|
570
|
+
|
571
|
+
def special_local_variables frame
|
572
|
+
SPECIAL_LOCAL_VARS.each do |mid, name|
|
573
|
+
next unless frame&.send("has_#{mid}")
|
574
|
+
name = name.sub('_', '%') if frame.eval_binding.local_variable_defined?(name)
|
575
|
+
yield name, frame.send(mid)
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
def show_locals pat
|
580
|
+
collect_locals(current_frame).each do |var, val|
|
581
|
+
puts_variable_info(var, val, pat)
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
def show_ivars pat, expr = nil
|
586
|
+
if expr && !expr.empty?
|
587
|
+
_self = frame_eval(expr);
|
588
|
+
elsif _self = current_frame&.self
|
589
|
+
else
|
590
|
+
_self = nil
|
591
|
+
end
|
592
|
+
|
593
|
+
if _self
|
594
|
+
M_INSTANCE_VARIABLES.bind_call(_self).sort.each{|iv|
|
595
|
+
value = M_INSTANCE_VARIABLE_GET.bind_call(_self, iv)
|
596
|
+
puts_variable_info iv, value, pat
|
597
|
+
}
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
def iter_consts c, names = {}
|
602
|
+
c.constants(false).sort.each{|name|
|
603
|
+
next if names.has_key? name
|
604
|
+
names[name] = nil
|
605
|
+
begin
|
606
|
+
value = c.const_get(name)
|
607
|
+
rescue Exception => e
|
608
|
+
value = e
|
609
|
+
end
|
610
|
+
yield name, value
|
611
|
+
}
|
612
|
+
end
|
613
|
+
|
614
|
+
def get_consts expr = nil, only_self: false, &block
|
615
|
+
if expr && !expr.empty?
|
616
|
+
begin
|
617
|
+
_self = frame_eval(expr, re_raise: true)
|
618
|
+
rescue Exception
|
619
|
+
# ignore
|
620
|
+
else
|
621
|
+
if M_KIND_OF_P.bind_call(_self, Module)
|
622
|
+
iter_consts _self, &block
|
623
|
+
return
|
624
|
+
else
|
625
|
+
puts "#{_self.inspect} (by #{expr}) is not a Module."
|
626
|
+
end
|
627
|
+
end
|
628
|
+
elsif _self = current_frame&.self
|
629
|
+
cs = {}
|
630
|
+
if M_KIND_OF_P.bind_call(_self, Module)
|
631
|
+
cs[_self] = :self
|
632
|
+
else
|
633
|
+
_self = M_CLASS.bind_call(_self)
|
634
|
+
cs[_self] = :self unless only_self
|
635
|
+
end
|
636
|
+
|
637
|
+
unless only_self
|
638
|
+
_self.ancestors.each{|c| break if c == Object; cs[c] = :ancestors}
|
639
|
+
if b = current_frame&.binding
|
640
|
+
b.eval('::Module.nesting').each{|c| cs[c] = :nesting unless cs.has_key? c}
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
names = {}
|
645
|
+
|
646
|
+
cs.each{|c, _|
|
647
|
+
iter_consts c, names, &block
|
648
|
+
}
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
def show_consts pat, expr = nil, only_self: false
|
653
|
+
get_consts expr, only_self: only_self do |name, value|
|
654
|
+
puts_variable_info name, value, pat
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
def show_globals pat
|
659
|
+
safe_global_variables.sort.each{|name|
|
660
|
+
next if SKIP_GLOBAL_LIST.include? name
|
661
|
+
|
662
|
+
value = eval(name.to_s)
|
663
|
+
puts_variable_info name, value, pat
|
664
|
+
}
|
665
|
+
end
|
666
|
+
|
667
|
+
def puts_variable_info label, obj, pat
|
668
|
+
return if pat && pat !~ label
|
669
|
+
|
670
|
+
begin
|
671
|
+
inspected = DEBUGGER__.safe_inspect(obj)
|
672
|
+
rescue Exception => e
|
673
|
+
inspected = e.inspect
|
674
|
+
end
|
675
|
+
mono_info = "#{label} = #{inspected}"
|
676
|
+
|
677
|
+
w = SESSION::width
|
678
|
+
|
679
|
+
if mono_info.length >= w
|
680
|
+
maximum_value_width = w - "#{label} = ".length
|
681
|
+
valstr = truncate(inspected, width: maximum_value_width)
|
682
|
+
else
|
683
|
+
valstr = colored_inspect(obj, width: 2 ** 30)
|
684
|
+
valstr = inspected if valstr.lines.size > 1
|
685
|
+
end
|
686
|
+
|
687
|
+
info = "#{colorize_cyan(label)} = #{valstr}"
|
688
|
+
|
689
|
+
puts info
|
690
|
+
end
|
691
|
+
|
692
|
+
def truncate(string, width:)
|
693
|
+
if string.start_with?("#<")
|
694
|
+
string[0 .. (width-5)] + '...>'
|
695
|
+
else
|
696
|
+
string[0 .. (width-4)] + '...'
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
### cmd: show edit
|
701
|
+
|
702
|
+
def show_by_editor path = nil
|
703
|
+
unless path
|
704
|
+
if current_frame
|
705
|
+
path = current_frame.path
|
706
|
+
else
|
707
|
+
return # can't get path
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
if File.exist?(path)
|
712
|
+
if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR'])
|
713
|
+
puts "command: #{editor}"
|
714
|
+
puts " path: #{path}"
|
715
|
+
require 'shellwords'
|
716
|
+
system(*Shellwords.split(editor), path)
|
717
|
+
else
|
718
|
+
puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']"
|
719
|
+
end
|
720
|
+
else
|
721
|
+
puts "Can not find file: #{path}"
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
### cmd: show frames
|
726
|
+
|
727
|
+
def show_frames max = nil, pattern = nil
|
728
|
+
if @target_frames && (max ||= @target_frames.size) > 0
|
729
|
+
frames = []
|
730
|
+
@target_frames.each_with_index{|f, i|
|
731
|
+
# we need to use FrameInfo#matchable_location because #location_str is for display
|
732
|
+
# and it may change based on configs (e.g. use_short_path)
|
733
|
+
next if pattern && !(f.name.match?(pattern) || f.matchable_location.match?(pattern))
|
734
|
+
# avoid using skip_path? because we still want to display internal frames
|
735
|
+
next if skip_config_skip_path?(f.matchable_location)
|
736
|
+
|
737
|
+
frames << [i, f]
|
738
|
+
}
|
739
|
+
|
740
|
+
size = frames.size
|
741
|
+
max.times{|i|
|
742
|
+
break unless frames[i]
|
743
|
+
index, frame = frames[i]
|
744
|
+
puts frame_str(index, frame: frame)
|
745
|
+
}
|
746
|
+
puts " # and #{size - max} frames (use `bt' command for all frames)" if max < size
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
def show_frame i=0
|
751
|
+
puts frame_str(i)
|
752
|
+
end
|
753
|
+
|
754
|
+
def frame_str(i, frame: @target_frames[i])
|
755
|
+
cur_str = (@current_frame_index == i ? '=>' : ' ')
|
756
|
+
prefix = "#{cur_str}##{i}"
|
757
|
+
frame_string = @frame_formatter.call(frame)
|
758
|
+
"#{prefix}\t#{frame_string}"
|
759
|
+
end
|
760
|
+
|
761
|
+
### cmd: show outline
|
762
|
+
|
763
|
+
def show_outline expr
|
764
|
+
begin
|
765
|
+
obj = frame_eval(expr, re_raise: true)
|
766
|
+
rescue Exception
|
767
|
+
# ignore
|
768
|
+
else
|
769
|
+
o = Output.new(@output)
|
770
|
+
|
771
|
+
locals = current_frame&.local_variables
|
772
|
+
|
773
|
+
klass = M_CLASS.bind_call(obj)
|
774
|
+
klass = obj if Class == klass || Module == klass
|
775
|
+
|
776
|
+
o.dump("constants", obj.constants) if M_RESPOND_TO_P.bind_call(obj, :constants)
|
777
|
+
outline_method(o, klass, obj)
|
778
|
+
o.dump("instance variables", M_INSTANCE_VARIABLES.bind_call(obj))
|
779
|
+
o.dump("class variables", klass.class_variables)
|
780
|
+
o.dump("locals", locals.keys) if locals
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
def outline_method(o, klass, obj)
|
785
|
+
begin
|
786
|
+
singleton_class = M_SINGLETON_CLASS.bind_call(obj)
|
787
|
+
rescue TypeError
|
788
|
+
singleton_class = nil
|
789
|
+
end
|
790
|
+
|
791
|
+
maps = class_method_map((singleton_class || klass).ancestors)
|
792
|
+
maps.each do |mod, methods|
|
793
|
+
name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods"
|
794
|
+
o.dump(name, methods)
|
795
|
+
end
|
796
|
+
end
|
797
|
+
|
798
|
+
def class_method_map(classes)
|
799
|
+
dumped = Array.new
|
800
|
+
classes.reject { |mod| mod >= Object }.map do |mod|
|
801
|
+
methods = mod.public_instance_methods(false).select do |m|
|
802
|
+
dumped.push(m) unless dumped.include?(m)
|
803
|
+
end
|
804
|
+
[mod, methods]
|
805
|
+
end.reverse
|
806
|
+
end
|
807
|
+
|
808
|
+
## cmd: breakpoint
|
809
|
+
|
810
|
+
# TODO: support non-ASCII Constant name
|
811
|
+
def constant_name? name
|
812
|
+
case name
|
813
|
+
when /\A::\b/
|
814
|
+
constant_name? $~.post_match
|
815
|
+
when /\A[A-Z]\w*/
|
816
|
+
post = $~.post_match
|
817
|
+
if post.empty?
|
818
|
+
true
|
819
|
+
else
|
820
|
+
constant_name? post
|
821
|
+
end
|
822
|
+
else
|
823
|
+
false
|
824
|
+
end
|
825
|
+
end
|
826
|
+
|
827
|
+
def make_breakpoint args
|
828
|
+
case args.first
|
829
|
+
when :method
|
830
|
+
klass_name, op, method_name, cond, cmd, path = args[1..]
|
831
|
+
bp = MethodBreakpoint.new(current_frame&.eval_binding || TOPLEVEL_BINDING, klass_name, op, method_name, cond: cond, command: cmd, path: path)
|
832
|
+
begin
|
833
|
+
bp.enable
|
834
|
+
rescue NameError => e
|
835
|
+
if bp.klass
|
836
|
+
puts "Unknown method name: \"#{e.name}\""
|
837
|
+
else
|
838
|
+
# klass_name can not be evaluated
|
839
|
+
if constant_name? klass_name
|
840
|
+
puts "Unknown constant name: \"#{e.name}\""
|
841
|
+
else
|
842
|
+
# only Class name is allowed
|
843
|
+
puts "Not a constant name: \"#{klass_name}\""
|
844
|
+
bp = nil
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
Session.activate_method_added_trackers if bp
|
849
|
+
rescue Exception => e
|
850
|
+
puts e.inspect
|
851
|
+
bp = nil
|
852
|
+
end
|
853
|
+
|
854
|
+
bp
|
855
|
+
when :watch
|
856
|
+
ivar, object, result, cond, command, path = args[1..]
|
857
|
+
WatchIVarBreakpoint.new(ivar, object, result, cond: cond, command: command, path: path)
|
858
|
+
else
|
859
|
+
raise "unknown breakpoint: #{args}"
|
860
|
+
end
|
861
|
+
end
|
862
|
+
|
863
|
+
class SuspendReplay < Exception
|
864
|
+
end
|
865
|
+
|
866
|
+
if ::Fiber.respond_to?(:blocking)
|
867
|
+
private def fiber_blocking
|
868
|
+
::Fiber.blocking{yield}
|
869
|
+
end
|
870
|
+
else
|
871
|
+
private def fiber_blocking
|
872
|
+
yield
|
873
|
+
end
|
874
|
+
end
|
875
|
+
|
876
|
+
def wait_next_action
|
877
|
+
fiber_blocking{wait_next_action_}
|
878
|
+
rescue SuspendReplay
|
879
|
+
replay_suspend
|
880
|
+
end
|
881
|
+
|
882
|
+
def wait_next_action_
|
883
|
+
# assertions
|
884
|
+
raise "@mode is #{@mode}" if !waiting?
|
885
|
+
|
886
|
+
unless SESSION.active?
|
887
|
+
pp caller
|
888
|
+
set_mode :running
|
889
|
+
return
|
890
|
+
end
|
891
|
+
|
892
|
+
while true
|
893
|
+
begin
|
894
|
+
set_mode :waiting if !waiting?
|
895
|
+
cmds = @q_cmd.pop
|
896
|
+
# pp [self, cmds: cmds]
|
897
|
+
|
898
|
+
break unless cmds
|
899
|
+
ensure
|
900
|
+
set_mode :running
|
901
|
+
end
|
902
|
+
|
903
|
+
cmd, *args = *cmds
|
904
|
+
|
905
|
+
case cmd
|
906
|
+
when :continue
|
907
|
+
break
|
908
|
+
|
909
|
+
when :step
|
910
|
+
step_type = args[0]
|
911
|
+
iter = args[1]
|
912
|
+
|
913
|
+
case step_type
|
914
|
+
when :in
|
915
|
+
iter = iter || 1
|
916
|
+
if @recorder&.replaying?
|
917
|
+
@recorder.step_forward iter
|
918
|
+
raise SuspendReplay
|
919
|
+
else
|
920
|
+
step_tp iter do
|
921
|
+
true
|
922
|
+
end
|
923
|
+
break
|
924
|
+
end
|
925
|
+
|
926
|
+
when :next
|
927
|
+
frame = @target_frames.first
|
928
|
+
path = frame.location.absolute_path || "!eval:#{frame.path}"
|
929
|
+
line = frame.location.lineno
|
930
|
+
label = frame.location.base_label
|
931
|
+
|
932
|
+
if frame.iseq
|
933
|
+
frame.iseq.traceable_lines_norec(lines = {})
|
934
|
+
next_line = lines.keys.bsearch{|e| e > line}
|
935
|
+
if !next_line && (last_line = frame.iseq.last_line) > line
|
936
|
+
next_line = last_line
|
937
|
+
end
|
938
|
+
end
|
939
|
+
|
940
|
+
depth = @target_frames.first.frame_depth
|
941
|
+
|
942
|
+
step_tp iter do |tp|
|
943
|
+
loc = caller_locations(2, 1).first
|
944
|
+
loc_path = loc.absolute_path || "!eval:#{loc.path}"
|
945
|
+
loc_label = loc.base_label
|
946
|
+
loc_depth = DEBUGGER__.frame_depth - 3
|
947
|
+
|
948
|
+
case
|
949
|
+
when loc_depth == depth && loc_label == label
|
950
|
+
true
|
951
|
+
when loc_depth < depth
|
952
|
+
# lower stack depth
|
953
|
+
true
|
954
|
+
when (next_line &&
|
955
|
+
loc_path == path &&
|
956
|
+
(loc_lineno = loc.lineno) > line &&
|
957
|
+
loc_lineno <= next_line)
|
958
|
+
# different frame (maybe block) but the line is before next_line
|
959
|
+
true
|
960
|
+
end
|
961
|
+
end
|
962
|
+
break
|
963
|
+
|
964
|
+
when :finish
|
965
|
+
finish_frames = (iter || 1) - 1
|
966
|
+
frame = @target_frames.first
|
967
|
+
goal_depth = frame.frame_depth - finish_frames - (frame.has_return_value ? 1 : 0)
|
968
|
+
|
969
|
+
step_tp nil, [:return, :b_return] do
|
970
|
+
DEBUGGER__.frame_depth - 3 <= goal_depth ? true : false
|
971
|
+
end
|
972
|
+
break
|
973
|
+
|
974
|
+
when :until
|
975
|
+
location = iter&.strip
|
976
|
+
frame = @target_frames.first
|
977
|
+
depth = frame.frame_depth - (frame.has_return_value ? 1 : 0)
|
978
|
+
target_location_label = frame.location.base_label
|
979
|
+
|
980
|
+
case location
|
981
|
+
when nil, /\A(?:(.+):)?(\d+)\z/
|
982
|
+
no_loc = !location
|
983
|
+
file = $1 || frame.location.path
|
984
|
+
line = ($2 || frame.location.lineno + 1).to_i
|
985
|
+
|
986
|
+
step_tp nil, [:line, :return] do |tp|
|
987
|
+
if tp.event == :line
|
988
|
+
next false if no_loc && depth < DEBUGGER__.frame_depth - 3
|
989
|
+
next false unless tp.path.end_with?(file)
|
990
|
+
next false unless tp.lineno >= line
|
991
|
+
true
|
992
|
+
else
|
993
|
+
true if depth >= DEBUGGER__.frame_depth - 3 &&
|
994
|
+
caller_locations(2, 1).first.base_label == target_location_label
|
995
|
+
# TODO: imcomplete condition
|
996
|
+
end
|
997
|
+
end
|
998
|
+
else
|
999
|
+
pat = location
|
1000
|
+
if /\A\/(.+)\/\z/ =~ pat
|
1001
|
+
pat = Regexp.new($1)
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
step_tp nil, [:call, :c_call, :return] do |tp|
|
1005
|
+
case tp.event
|
1006
|
+
when :call, :c_call
|
1007
|
+
true if pat === tp.callee_id.to_s
|
1008
|
+
else # :return, :b_return
|
1009
|
+
true if depth >= DEBUGGER__.frame_depth - 3 &&
|
1010
|
+
caller_locations(2, 1).first.base_label == target_location_label
|
1011
|
+
# TODO: imcomplete condition
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
break
|
1017
|
+
|
1018
|
+
when :back
|
1019
|
+
iter = iter || 1
|
1020
|
+
if @recorder&.can_step_back?
|
1021
|
+
unless @recorder.backup_frames
|
1022
|
+
@recorder.backup_frames = @target_frames
|
1023
|
+
end
|
1024
|
+
@recorder.step_back iter
|
1025
|
+
raise SuspendReplay
|
1026
|
+
else
|
1027
|
+
puts "Can not step back more."
|
1028
|
+
event! :result, nil
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
when :reset
|
1032
|
+
if @recorder&.replaying?
|
1033
|
+
@recorder.step_reset
|
1034
|
+
raise SuspendReplay
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
else
|
1038
|
+
raise "unknown: #{type}"
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
when :eval
|
1042
|
+
eval_type, eval_src = *args
|
1043
|
+
|
1044
|
+
result_type = nil
|
1045
|
+
|
1046
|
+
case eval_type
|
1047
|
+
when :p
|
1048
|
+
result = frame_eval(eval_src)
|
1049
|
+
puts "=> " + color_pp(result, 2 ** 30)
|
1050
|
+
if alloc_path = ObjectSpace.allocation_sourcefile(result)
|
1051
|
+
puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}"
|
1052
|
+
end
|
1053
|
+
when :pp
|
1054
|
+
result = frame_eval(eval_src)
|
1055
|
+
puts color_pp(result, SESSION.width)
|
1056
|
+
if alloc_path = ObjectSpace.allocation_sourcefile(result)
|
1057
|
+
puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}"
|
1058
|
+
end
|
1059
|
+
when :call
|
1060
|
+
result = frame_eval(eval_src)
|
1061
|
+
when :display, :try_display
|
1062
|
+
failed_results = []
|
1063
|
+
eval_src.each_with_index{|src, i|
|
1064
|
+
result = frame_eval(src){|e|
|
1065
|
+
failed_results << [i, e.message]
|
1066
|
+
"<error: #{e.message}>"
|
1067
|
+
}
|
1068
|
+
puts "#{i}: #{src} = #{result}"
|
1069
|
+
}
|
1070
|
+
|
1071
|
+
result_type = eval_type
|
1072
|
+
result = failed_results
|
1073
|
+
else
|
1074
|
+
raise "unknown error option: #{args.inspect}"
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
event! :result, result_type, result
|
1078
|
+
when :frame
|
1079
|
+
type, arg = *args
|
1080
|
+
case type
|
1081
|
+
when :up
|
1082
|
+
if @current_frame_index + 1 < @target_frames.size
|
1083
|
+
@current_frame_index += 1
|
1084
|
+
show_src max_lines: CONFIG[:show_src_lines_frame]
|
1085
|
+
show_frame(@current_frame_index)
|
1086
|
+
end
|
1087
|
+
when :down
|
1088
|
+
if @current_frame_index > 0
|
1089
|
+
@current_frame_index -= 1
|
1090
|
+
show_src max_lines: CONFIG[:show_src_lines_frame]
|
1091
|
+
show_frame(@current_frame_index)
|
1092
|
+
end
|
1093
|
+
when :set
|
1094
|
+
if arg
|
1095
|
+
index = arg.to_i
|
1096
|
+
if index >= 0 && index < @target_frames.size
|
1097
|
+
@current_frame_index = index
|
1098
|
+
else
|
1099
|
+
puts "out of frame index: #{index}"
|
1100
|
+
end
|
1101
|
+
end
|
1102
|
+
show_src max_lines: CONFIG[:show_src_lines_frame]
|
1103
|
+
show_frame(@current_frame_index)
|
1104
|
+
else
|
1105
|
+
raise "unsupported frame operation: #{arg.inspect}"
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
event! :result, nil
|
1109
|
+
|
1110
|
+
when :show
|
1111
|
+
type = args.shift
|
1112
|
+
|
1113
|
+
case type
|
1114
|
+
when :backtrace
|
1115
|
+
max_lines, pattern = *args
|
1116
|
+
show_frames max_lines, pattern
|
1117
|
+
|
1118
|
+
when :list
|
1119
|
+
show_src(update_line: true, **(args.first || {}))
|
1120
|
+
|
1121
|
+
when :whereami
|
1122
|
+
show_src ignore_show_line: true
|
1123
|
+
show_frames CONFIG[:show_frames]
|
1124
|
+
|
1125
|
+
when :edit
|
1126
|
+
show_by_editor(args.first)
|
1127
|
+
|
1128
|
+
when :default
|
1129
|
+
pat = args.shift
|
1130
|
+
show_locals pat
|
1131
|
+
show_ivars pat
|
1132
|
+
show_consts pat, only_self: true
|
1133
|
+
|
1134
|
+
when :locals
|
1135
|
+
pat = args.shift
|
1136
|
+
show_locals pat
|
1137
|
+
|
1138
|
+
when :ivars
|
1139
|
+
pat = args.shift
|
1140
|
+
expr = args.shift
|
1141
|
+
show_ivars pat, expr
|
1142
|
+
|
1143
|
+
when :consts
|
1144
|
+
pat = args.shift
|
1145
|
+
expr = args.shift
|
1146
|
+
show_consts pat, expr
|
1147
|
+
|
1148
|
+
when :globals
|
1149
|
+
pat = args.shift
|
1150
|
+
show_globals pat
|
1151
|
+
|
1152
|
+
when :outline
|
1153
|
+
show_outline args.first || 'self'
|
1154
|
+
|
1155
|
+
else
|
1156
|
+
raise "unknown show param: " + [type, *args].inspect
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
event! :result, nil
|
1160
|
+
|
1161
|
+
when :breakpoint
|
1162
|
+
case args[0]
|
1163
|
+
when :method
|
1164
|
+
bp = make_breakpoint args
|
1165
|
+
event! :result, :method_breakpoint, bp
|
1166
|
+
when :watch
|
1167
|
+
ivar, cond, command, path = args[1..]
|
1168
|
+
result = frame_eval(ivar)
|
1169
|
+
|
1170
|
+
if @success_last_eval
|
1171
|
+
object =
|
1172
|
+
if b = current_frame.binding
|
1173
|
+
b.receiver
|
1174
|
+
else
|
1175
|
+
current_frame.self
|
1176
|
+
end
|
1177
|
+
bp = make_breakpoint [:watch, ivar, object, result, cond, command, path]
|
1178
|
+
event! :result, :watch_breakpoint, bp
|
1179
|
+
else
|
1180
|
+
event! :result, nil
|
1181
|
+
end
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
when :trace
|
1185
|
+
case args.shift
|
1186
|
+
when :object
|
1187
|
+
begin
|
1188
|
+
obj = frame_eval args.shift, re_raise: true
|
1189
|
+
opt = args.shift
|
1190
|
+
obj_inspect = DEBUGGER__.safe_inspect(obj)
|
1191
|
+
|
1192
|
+
width = 50
|
1193
|
+
|
1194
|
+
if obj_inspect.length >= width
|
1195
|
+
obj_inspect = truncate(obj_inspect, width: width)
|
1196
|
+
end
|
1197
|
+
|
1198
|
+
event! :result, :trace_pass, M_OBJECT_ID.bind_call(obj), obj_inspect, opt
|
1199
|
+
rescue => e
|
1200
|
+
puts e.message
|
1201
|
+
event! :result, nil
|
1202
|
+
end
|
1203
|
+
else
|
1204
|
+
raise "unreachable"
|
1205
|
+
end
|
1206
|
+
|
1207
|
+
when :record
|
1208
|
+
case args[0]
|
1209
|
+
when nil
|
1210
|
+
# ok
|
1211
|
+
when :on
|
1212
|
+
# enable recording
|
1213
|
+
if !@recorder
|
1214
|
+
@recorder = Recorder.new
|
1215
|
+
end
|
1216
|
+
@recorder.enable
|
1217
|
+
when :off
|
1218
|
+
if @recorder&.enabled?
|
1219
|
+
@recorder.disable
|
1220
|
+
end
|
1221
|
+
else
|
1222
|
+
raise "unknown: #{args.inspect}"
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
if @recorder&.enabled?
|
1226
|
+
puts "Recorder for #{Thread.current}: on (#{@recorder.log.size} records)"
|
1227
|
+
else
|
1228
|
+
puts "Recorder for #{Thread.current}: off"
|
1229
|
+
end
|
1230
|
+
event! :result, nil
|
1231
|
+
|
1232
|
+
when :quit
|
1233
|
+
sleep # wait for SystemExit
|
1234
|
+
when :dap
|
1235
|
+
process_dap args
|
1236
|
+
when :cdp
|
1237
|
+
process_cdp args
|
1238
|
+
else
|
1239
|
+
raise [cmd, *args].inspect
|
1240
|
+
end
|
1241
|
+
end
|
1242
|
+
|
1243
|
+
rescue SuspendReplay, SystemExit, Interrupt
|
1244
|
+
raise
|
1245
|
+
rescue Exception => e
|
1246
|
+
STDERR.puts e.cause.inspect
|
1247
|
+
STDERR.puts e.inspect
|
1248
|
+
Thread.list.each{|th|
|
1249
|
+
STDERR.puts "@@@ #{th}"
|
1250
|
+
th.backtrace.each{|b|
|
1251
|
+
STDERR.puts " > #{b}"
|
1252
|
+
}
|
1253
|
+
}
|
1254
|
+
p ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
|
1255
|
+
raise
|
1256
|
+
ensure
|
1257
|
+
@returning = false
|
1258
|
+
end
|
1259
|
+
|
1260
|
+
def debug_event(ev, args)
|
1261
|
+
DEBUGGER__.debug{
|
1262
|
+
args = args.map { |arg| DEBUGGER__.safe_inspect(arg) }
|
1263
|
+
"#{inspect} sends Event { type: #{ev.inspect}, args: #{args} } to Session"
|
1264
|
+
}
|
1265
|
+
end
|
1266
|
+
|
1267
|
+
def debug_mode(old_mode, new_mode)
|
1268
|
+
DEBUGGER__.debug{
|
1269
|
+
"#{inspect} changes mode (#{old_mode} -> #{new_mode})"
|
1270
|
+
}
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
def debug_cmd(cmds)
|
1274
|
+
DEBUGGER__.debug{
|
1275
|
+
cmd, *args = *cmds
|
1276
|
+
args = args.map { |arg| DEBUGGER__.safe_inspect(arg) }
|
1277
|
+
"#{inspect} receives Cmd { type: #{cmd.inspect}, args: #{args} } from Session"
|
1278
|
+
}
|
1279
|
+
end
|
1280
|
+
|
1281
|
+
def debug_suspend(event)
|
1282
|
+
DEBUGGER__.debug{
|
1283
|
+
"#{inspect} is suspended for #{event.inspect}"
|
1284
|
+
}
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
class Recorder
|
1288
|
+
attr_reader :log, :index
|
1289
|
+
attr_accessor :backup_frames
|
1290
|
+
|
1291
|
+
include SkipPathHelper
|
1292
|
+
|
1293
|
+
def initialize
|
1294
|
+
@log = []
|
1295
|
+
@index = 0
|
1296
|
+
@backup_frames = nil
|
1297
|
+
thread = Thread.current
|
1298
|
+
|
1299
|
+
@tp_recorder ||= TracePoint.new(:line){|tp|
|
1300
|
+
next unless Thread.current == thread
|
1301
|
+
# can't be replaced by skip_location
|
1302
|
+
next if skip_internal_path?(tp.path)
|
1303
|
+
loc = caller_locations(1, 1).first
|
1304
|
+
next if skip_location?(loc)
|
1305
|
+
|
1306
|
+
frames = DEBUGGER__.capture_frames(__dir__)
|
1307
|
+
frames.each{|frame|
|
1308
|
+
if b = frame.binding
|
1309
|
+
frame.binding = nil
|
1310
|
+
frame._local_variables = b.local_variables.map{|name|
|
1311
|
+
[name, b.local_variable_get(name)]
|
1312
|
+
}.to_h
|
1313
|
+
frame._callee = b.eval('__callee__')
|
1314
|
+
end
|
1315
|
+
}
|
1316
|
+
append(frames)
|
1317
|
+
}
|
1318
|
+
end
|
1319
|
+
|
1320
|
+
def append frames
|
1321
|
+
@log << frames
|
1322
|
+
end
|
1323
|
+
|
1324
|
+
def enable
|
1325
|
+
unless @tp_recorder.enabled?
|
1326
|
+
@log.clear
|
1327
|
+
@tp_recorder.enable
|
1328
|
+
end
|
1329
|
+
end
|
1330
|
+
|
1331
|
+
def disable
|
1332
|
+
if @tp_recorder.enabled?
|
1333
|
+
@log.clear
|
1334
|
+
@tp_recorder.disable
|
1335
|
+
end
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
def enabled?
|
1339
|
+
@tp_recorder.enabled?
|
1340
|
+
end
|
1341
|
+
|
1342
|
+
def step_back iter
|
1343
|
+
@index += iter
|
1344
|
+
if @index > @log.size
|
1345
|
+
@index = @log.size
|
1346
|
+
end
|
1347
|
+
end
|
1348
|
+
|
1349
|
+
def step_forward iter
|
1350
|
+
@index -= iter
|
1351
|
+
if @index < 0
|
1352
|
+
@index = 0
|
1353
|
+
end
|
1354
|
+
end
|
1355
|
+
|
1356
|
+
def step_reset
|
1357
|
+
@index = 0
|
1358
|
+
@backup_frames = nil
|
1359
|
+
end
|
1360
|
+
|
1361
|
+
def replaying?
|
1362
|
+
@index > 0
|
1363
|
+
end
|
1364
|
+
|
1365
|
+
def can_step_back?
|
1366
|
+
log.size > @index
|
1367
|
+
end
|
1368
|
+
|
1369
|
+
def log_index
|
1370
|
+
@log.size - @index
|
1371
|
+
end
|
1372
|
+
|
1373
|
+
def current_frame
|
1374
|
+
if @index == 0
|
1375
|
+
f = @backup_frames
|
1376
|
+
@backup_frames = nil
|
1377
|
+
f
|
1378
|
+
else
|
1379
|
+
frames = @log[log_index]
|
1380
|
+
frames
|
1381
|
+
end
|
1382
|
+
end
|
1383
|
+
|
1384
|
+
# for debugging
|
1385
|
+
def current_position
|
1386
|
+
puts "INDEX: #{@index}"
|
1387
|
+
li = log_index
|
1388
|
+
@log.each_with_index{|frame, i|
|
1389
|
+
loc = frame.first&.location
|
1390
|
+
prefix = i == li ? "=> " : ' '
|
1391
|
+
puts "#{prefix} #{loc}"
|
1392
|
+
}
|
1393
|
+
end
|
1394
|
+
end
|
1395
|
+
|
1396
|
+
# copied from irb
|
1397
|
+
class Output
|
1398
|
+
include Color
|
1399
|
+
|
1400
|
+
MARGIN = " "
|
1401
|
+
|
1402
|
+
def initialize(output)
|
1403
|
+
@output = output
|
1404
|
+
@line_width = screen_width - MARGIN.length # right padding
|
1405
|
+
end
|
1406
|
+
|
1407
|
+
def dump(name, strs)
|
1408
|
+
strs = strs.sort
|
1409
|
+
return if strs.empty?
|
1410
|
+
|
1411
|
+
line = "#{colorize_blue(name)}: "
|
1412
|
+
|
1413
|
+
# Attempt a single line
|
1414
|
+
if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length)
|
1415
|
+
line += strs.join(MARGIN)
|
1416
|
+
@output << line
|
1417
|
+
return
|
1418
|
+
end
|
1419
|
+
|
1420
|
+
# Multi-line
|
1421
|
+
@output << line
|
1422
|
+
|
1423
|
+
# Dump with the largest # of columns that fits on a line
|
1424
|
+
cols = strs.size
|
1425
|
+
until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1
|
1426
|
+
cols -= 1
|
1427
|
+
end
|
1428
|
+
widths = col_widths(strs, cols: cols)
|
1429
|
+
strs.each_slice(cols) do |ss|
|
1430
|
+
@output << ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join
|
1431
|
+
end
|
1432
|
+
end
|
1433
|
+
|
1434
|
+
private
|
1435
|
+
|
1436
|
+
def fits_on_line?(strs, cols:, offset: 0)
|
1437
|
+
width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1)
|
1438
|
+
width <= @line_width - offset
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
def col_widths(strs, cols:)
|
1442
|
+
cols.times.map do |col|
|
1443
|
+
(col...strs.size).step(cols).map do |i|
|
1444
|
+
strs[i].length
|
1445
|
+
end.max
|
1446
|
+
end
|
1447
|
+
end
|
1448
|
+
|
1449
|
+
def screen_width
|
1450
|
+
SESSION.width
|
1451
|
+
rescue Errno::EINVAL # in `winsize': Invalid argument - <STDIN>
|
1452
|
+
80
|
1453
|
+
end
|
1454
|
+
end
|
1455
|
+
private_constant :Output
|
1456
|
+
end
|
1457
|
+
end
|