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.
@@ -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