debug 1.0.0.rc2 → 1.2.1

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.
data/lib/debug/session.rb CHANGED
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # skip to load debugger for bundle exec
4
- return if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
4
+
5
+ if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
6
+ trace_var(:$0) do |file|
7
+ trace_var(:$0, nil)
8
+ if /-r (#{__dir__}\S+)/ =~ ENV['RUBYOPT']
9
+ lib = $1
10
+ $LOADED_FEATURES.delete_if{|path| path.start_with?(__dir__)}
11
+ ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH'] = file
12
+ require lib
13
+ ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH'] = nil
14
+ end
15
+ end
16
+
17
+ return
18
+ end
5
19
 
6
20
  require_relative 'config'
7
21
  require_relative 'thread_client'
@@ -9,6 +23,10 @@ require_relative 'source_repository'
9
23
  require_relative 'breakpoint'
10
24
  require_relative 'tracer'
11
25
 
26
+ # To prevent loading old lib/debug.rb in Ruby 2.6 to 3.0
27
+ $LOADED_FEATURES << 'debug.rb'
28
+ require 'debug' # invalidate the $LOADED_FEATURE cache
29
+
12
30
  require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
13
31
 
14
32
  class RubyVM::InstructionSequence
@@ -68,7 +86,7 @@ module DEBUGGER__
68
86
  # [:watch, ivar] => WatchIVarBreakpoint
69
87
  # [:check, expr] => CheckBreakpoint
70
88
  #
71
- @tracers = []
89
+ @tracers = {}
72
90
  @th_clients = nil # {Thread => ThreadClient}
73
91
  @q_evt = Queue.new
74
92
  @displays = []
@@ -78,17 +96,22 @@ module DEBUGGER__
78
96
  @postmortem_hook = nil
79
97
  @postmortem = false
80
98
  @thread_stopper = nil
99
+ @intercept_trap_sigint = false
100
+ @intercepted_sigint_cmd = 'DEFAULT'
81
101
 
82
102
  @frame_map = {} # {id => [threadId, frame_depth]} for DAP
83
103
  @var_map = {1 => [:globals], } # {id => ...} for DAP
84
104
  @src_map = {} # {id => src}
85
105
 
106
+ @tp_thread_begin = nil
86
107
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
87
108
  ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
88
109
  }
89
110
  @tp_load_script.enable
90
111
 
91
112
  activate
113
+
114
+ self.postmortem = CONFIG[:postmortem]
92
115
  end
93
116
 
94
117
  def active?
@@ -99,40 +122,41 @@ module DEBUGGER__
99
122
  @bps.has_key? [file, line]
100
123
  end
101
124
 
102
- def check_forked
103
- unless active?
104
- # TODO: Support it
105
- raise 'DEBUGGER: stop at forked process is not supported yet.'
125
+ def activate on_fork: false
126
+ @tp_thread_begin&.disable
127
+ @tp_thread_begin = nil
128
+
129
+ if on_fork
130
+ @ui.activate self, on_fork: true
131
+ else
132
+ @ui.activate self, on_fork: false
106
133
  end
107
- end
108
134
 
109
- def activate on_fork: false
135
+ q = Queue.new
110
136
  @session_server = Thread.new do
111
137
  Thread.current.name = 'DEBUGGER__::SESSION@server'
112
138
  Thread.current.abort_on_exception = true
113
- session_server_main
114
- end
115
139
 
116
- setup_threads
140
+ # Thread management
141
+ setup_threads
142
+ thc = thread_client Thread.current
143
+ thc.is_management
117
144
 
118
- thc = thread_client @session_server
119
- thc.is_management
145
+ if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
146
+ thc.is_management
147
+ end
120
148
 
121
- if on_fork
122
- @tp_thread_begin.disable
123
- @tp_thread_begin = nil
124
- @ui.activate on_fork: true
125
- end
149
+ @tp_thread_begin = TracePoint.new(:thread_begin) do |tp|
150
+ thread_client
151
+ end
152
+ @tp_thread_begin.enable
126
153
 
127
- if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
128
- thc.is_management
154
+ # session start
155
+ q << true
156
+ session_server_main
129
157
  end
130
158
 
131
- @tp_thread_begin = TracePoint.new(:thread_begin){|tp|
132
- th = Thread.current
133
- ThreadClient.current.on_thread_begin th
134
- }
135
- @tp_thread_begin.enable
159
+ q.pop
136
160
  end
137
161
 
138
162
  def deactivate
@@ -142,14 +166,14 @@ module DEBUGGER__
142
166
  @tp_thread_begin.disable
143
167
  @bps.each{|k, bp| bp.disable}
144
168
  @th_clients.each{|th, thc| thc.close}
145
- @tracers.each{|t| t.disable}
169
+ @tracers.values.each{|t| t.disable}
146
170
  @q_evt.close
147
171
  @ui&.deactivate
148
172
  @ui = nil
149
173
  end
150
174
 
151
175
  def reset_ui ui
152
- @ui.close
176
+ @ui.deactivate
153
177
  @ui = ui
154
178
  end
155
179
 
@@ -159,11 +183,18 @@ module DEBUGGER__
159
183
 
160
184
  def session_server_main
161
185
  while evt = pop_event
162
- # varible `@internal_info` is only used for test
186
+ # variable `@internal_info` is only used for test
163
187
  tc, output, ev, @internal_info, *ev_args = evt
164
188
  output.each{|str| @ui.puts str}
165
189
 
166
190
  case ev
191
+
192
+ when :thread_begin # special event, tc is nil
193
+ th = ev_args.shift
194
+ q = ev_args.shift
195
+ on_thread_begin th
196
+ q << true
197
+
167
198
  when :init
168
199
  wait_command_loop tc
169
200
 
@@ -175,26 +206,25 @@ module DEBUGGER__
175
206
 
176
207
  when :trace
177
208
  trace_id, msg = ev_args
178
- if t = @tracers.find{|t| t.object_id == trace_id}
209
+ if t = @tracers.values.find{|t| t.object_id == trace_id}
179
210
  t.puts msg
180
211
  end
181
212
  tc << :continue
182
213
 
183
- when :thread_begin
184
- th = ev_args.shift
185
- on_thread_begin th
186
- @ui.event :thread_begin, th
187
- tc << :continue
188
-
189
214
  when :suspend
190
215
  case ev_args.first
191
216
  when :breakpoint
192
217
  bp, i = bp_index ev_args[1]
193
- @ui.event :suspend_bp, i, bp
218
+ @ui.event :suspend_bp, i, bp, tc.id
194
219
  when :trap
195
- @ui.event :suspend_trap, ev_args[1]
220
+ @ui.event :suspend_trap, sig = ev_args[1], tc.id
221
+
222
+ if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String))
223
+ @ui.puts "#{@intercepted_sigint_cmd.inspect} is registerred as SIGINT handler."
224
+ @ui.puts "`sigint` command execute it."
225
+ end
196
226
  else
197
- @ui.event :suspended
227
+ @ui.event :suspended, tc.id
198
228
  end
199
229
 
200
230
  if @displays.empty?
@@ -228,8 +258,7 @@ module DEBUGGER__
228
258
  obj_id = ev_args[1]
229
259
  obj_inspect = ev_args[2]
230
260
  opt = ev_args[3]
231
- @tracers << t = ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
232
- @ui.puts "Enable #{t.to_s}"
261
+ add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
233
262
  else
234
263
  # ignore
235
264
  end
@@ -303,7 +332,9 @@ module DEBUGGER__
303
332
  if @preset_command.commands.empty?
304
333
  if @preset_command.auto_continue
305
334
  @preset_command = nil
335
+
306
336
  @tc << :continue
337
+ restart_all_threads
307
338
  return
308
339
  else
309
340
  @preset_command = nil
@@ -412,6 +443,29 @@ module DEBUGGER__
412
443
  when 'kill!'
413
444
  exit! (arg || 1).to_i
414
445
 
446
+ # * `sigint`
447
+ # * Execute SIGINT handler registerred by the debuggee.
448
+ # * Note that this command should be used just after stop by `SIGINT`.
449
+ when 'sigint'
450
+ begin
451
+ case cmd = @intercepted_sigint_cmd
452
+ when nil, 'IGNORE', :IGNORE, 'DEFAULT', :DEFAULT
453
+ # ignore
454
+ when String
455
+ eval(cmd)
456
+ when Proc
457
+ cmd.call
458
+ end
459
+
460
+ @tc << :continue
461
+ restart_all_threads
462
+
463
+ rescue Exception => e
464
+ @ui.puts "Exception: #{e}"
465
+ @ui.puts e.backtrace.map{|line| " #{e}"}
466
+ return :retry
467
+ end
468
+
415
469
  ### Breakpoint
416
470
 
417
471
  # * `b[reak]`
@@ -596,7 +650,7 @@ module DEBUGGER__
596
650
  # * Show information about the current frame (local variables)
597
651
  # * It includes `self` as `%self` and a return value as `%return`.
598
652
  # * `i[nfo] i[var[s]]` or `i[nfo] instance`
599
- # * Show information about insttance variables about `self`.
653
+ # * Show information about instance variables about `self`.
600
654
  # * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
601
655
  # * Show information about accessible constants except toplevel constants.
602
656
  # * `i[nfo] g[lobal[s]]`
@@ -702,10 +756,16 @@ module DEBUGGER__
702
756
  when 'pp'
703
757
  @tc << [:eval, :pp, arg.to_s]
704
758
 
705
- # * `e[val] <expr>`
759
+ # * `eval <expr>`
706
760
  # * Evaluate `<expr>` on the current frame.
707
- when 'e', 'eval', 'call'
708
- @tc << [:eval, :call, arg]
761
+ when 'eval', 'call'
762
+ if arg == nil || arg.empty?
763
+ show_help 'eval'
764
+ @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead."
765
+ return :retry
766
+ else
767
+ @tc << [:eval, :call, arg]
768
+ end
709
769
 
710
770
  # * `irb`
711
771
  # * Invoke `irb` on the current frame.
@@ -714,7 +774,7 @@ module DEBUGGER__
714
774
  @ui.puts "not supported on the remote console."
715
775
  return :retry
716
776
  end
717
- @tc << [:eval, :call, 'binding.irb']
777
+ @tc << [:eval, :irb]
718
778
 
719
779
  # don't repeat irb command
720
780
  @repl_prev_line = nil
@@ -752,32 +812,29 @@ module DEBUGGER__
752
812
  case arg
753
813
  when nil
754
814
  @ui.puts 'Tracers:'
755
- @tracers.each_with_index{|t, i|
815
+ @tracers.values.each_with_index{|t, i|
756
816
  @ui.puts "* \##{i} #{t}"
757
817
  }
758
818
  @ui.puts
759
819
  return :retry
760
820
 
761
821
  when /\Aline\z/
762
- @tracers << t = LineTracer.new(@ui, pattern: pattern, into: into)
763
- @ui.puts "Enable #{t.to_s}"
822
+ add_tracer LineTracer.new(@ui, pattern: pattern, into: into)
764
823
  return :retry
765
824
 
766
825
  when /\Acall\z/
767
- @tracers << t = CallTracer.new(@ui, pattern: pattern, into: into)
768
- @ui.puts "Enable #{t.to_s}"
826
+ add_tracer CallTracer.new(@ui, pattern: pattern, into: into)
769
827
  return :retry
770
828
 
771
829
  when /\Aexception\z/
772
- @tracers << t = ExceptionTracer.new(@ui, pattern: pattern, into: into)
773
- @ui.puts "Enable #{t.to_s}"
830
+ add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into)
774
831
  return :retry
775
832
 
776
833
  when /\Aobject\s+(.+)/
777
834
  @tc << [:trace, :object, $1.strip, {pattern: pattern, into: into}]
778
835
 
779
836
  when /\Aoff\s+(\d+)\z/
780
- if t = @tracers[$1.to_i]
837
+ if t = @tracers.values[$1.to_i]
781
838
  t.disable
782
839
  @ui.puts "Disable #{t.to_s}"
783
840
  else
@@ -786,7 +843,7 @@ module DEBUGGER__
786
843
  return :retry
787
844
 
788
845
  when /\Aoff(\s+(line|call|exception|object))?\z/
789
- @tracers.each{|t|
846
+ @tracers.values.each{|t|
790
847
  if $2.nil? || t.type == $2
791
848
  t.disable
792
849
  @ui.puts "Disable #{t.to_s}"
@@ -1145,6 +1202,11 @@ module DEBUGGER__
1145
1202
  add_bp bp
1146
1203
  end
1147
1204
 
1205
+ def add_catch_breakpoint pat
1206
+ bp = CatchBreakpoint.new(pat)
1207
+ add_bp bp
1208
+ end
1209
+
1148
1210
  def add_check_breakpoint expr
1149
1211
  bp = CheckBreakpoint.new(expr)
1150
1212
  add_bp bp
@@ -1159,6 +1221,20 @@ module DEBUGGER__
1159
1221
  @ui.puts e.message
1160
1222
  end
1161
1223
 
1224
+ # tracers
1225
+
1226
+ def add_tracer tracer
1227
+ # don't repeat commands that add tracers
1228
+ @repl_prev_line = nil
1229
+ if @tracers.has_key? tracer.key
1230
+ tracer.disable
1231
+ @ui.puts "Duplicated tracer: #{tracer}"
1232
+ else
1233
+ @tracers[tracer.key] = tracer
1234
+ @ui.puts "Enable #{tracer}"
1235
+ end
1236
+ end
1237
+
1162
1238
  # threads
1163
1239
 
1164
1240
  def update_thread_list
@@ -1211,10 +1287,6 @@ module DEBUGGER__
1211
1287
  thread_list
1212
1288
  end
1213
1289
 
1214
- def thread_client_create th
1215
- @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
1216
- end
1217
-
1218
1290
  def setup_threads
1219
1291
  @th_clients = {}
1220
1292
 
@@ -1225,18 +1297,38 @@ module DEBUGGER__
1225
1297
 
1226
1298
  def on_thread_begin th
1227
1299
  if @th_clients.has_key? th
1228
- # OK
1229
- else
1230
1300
  # TODO: NG?
1301
+ else
1231
1302
  thread_client_create th
1232
1303
  end
1233
1304
  end
1234
1305
 
1235
- def thread_client thr = Thread.current
1236
- if @th_clients.has_key? thr
1237
- @th_clients[thr]
1306
+ private def thread_client_create th
1307
+ # TODO: Ractor support
1308
+ raise "Only session_server can create thread_client" unless Thread.current == @session_server
1309
+ @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
1310
+ end
1311
+
1312
+ private def ask_thread_client th = Thread.current
1313
+ # TODO: Ractor support
1314
+ q2 = Queue.new
1315
+ # tc, output, ev, @internal_info, *ev_args = evt
1316
+ @q_evt << [nil, [], :thread_begin, nil, th, q2]
1317
+ q2.pop
1318
+
1319
+ @th_clients[th] or raise "unexpected error"
1320
+ end
1321
+
1322
+ # can be called by other threads
1323
+ def thread_client th = Thread.current
1324
+ if @th_clients.has_key? th
1325
+ @th_clients[th]
1238
1326
  else
1239
- @th_clients[thr] = thread_client_create(thr)
1327
+ if Thread.current == @session_server
1328
+ thread_client_create th
1329
+ else
1330
+ ask_thread_client th
1331
+ end
1240
1332
  end
1241
1333
  end
1242
1334
 
@@ -1355,7 +1447,10 @@ module DEBUGGER__
1355
1447
  end
1356
1448
  end
1357
1449
 
1358
- def enter_postmortem_session frames
1450
+ def enter_postmortem_session exc
1451
+ return unless exc.instance_variable_defined? :@__debugger_postmortem_frames
1452
+
1453
+ frames = exc.instance_variable_get(:@__debugger_postmortem_frames)
1359
1454
  @postmortem = true
1360
1455
  ThreadClient.current.suspend :postmortem, postmortem_frames: frames
1361
1456
  ensure
@@ -1368,7 +1463,7 @@ module DEBUGGER__
1368
1463
  @postmortem_hook = TracePoint.new(:raise){|tp|
1369
1464
  exc = tp.raised_exception
1370
1465
  frames = DEBUGGER__.capture_frames(__dir__)
1371
- exc.instance_variable_set(:@postmortem_frames, frames)
1466
+ exc.instance_variable_set(:@__debugger_postmortem_frames, frames)
1372
1467
  }
1373
1468
  at_exit{
1374
1469
  @postmortem_hook.disable
@@ -1380,7 +1475,7 @@ module DEBUGGER__
1380
1475
  @ui.puts exc.backtrace.map{|e| ' ' + e}
1381
1476
  @ui.puts "\n"
1382
1477
 
1383
- enter_postmortem_session exc.instance_variable_get(:@postmortem_frames)
1478
+ enter_postmortem_session exc
1384
1479
  rescue SystemExit
1385
1480
  exit!
1386
1481
  rescue Exception => e
@@ -1400,6 +1495,30 @@ module DEBUGGER__
1400
1495
  end
1401
1496
  end
1402
1497
  end
1498
+
1499
+ def save_int_trap cmd
1500
+ prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, cmd
1501
+ prev
1502
+ end
1503
+
1504
+ attr_reader :intercepted_sigint_cmd
1505
+
1506
+ def intercept_trap_sigint?
1507
+ @intercept_trap_sigint
1508
+ end
1509
+
1510
+ def intercept_trap_sigint flag, &b
1511
+ prev = @intercept_trap_sigint
1512
+ @intercept_trap_sigint = flag
1513
+ yield
1514
+ ensure
1515
+ @intercept_trap_sigint = prev
1516
+ end
1517
+
1518
+ def intercept_trap_sigint_start prev
1519
+ @intercept_trap_sigint = true
1520
+ @intercepted_sigint_cmd = prev
1521
+ end
1403
1522
  end
1404
1523
 
1405
1524
  class UI_Base
@@ -1494,7 +1613,13 @@ module DEBUGGER__
1494
1613
 
1495
1614
  def self.setup_initial_suspend
1496
1615
  if !CONFIG[:nonstop]
1497
- if loc = ::DEBUGGER__.require_location
1616
+ case
1617
+ when CONFIG[:stop_at_load]
1618
+ add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, hook_call: false
1619
+ nil # stop here
1620
+ when path = ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH']
1621
+ add_line_breakpoint path, 0, oneshot: true, hook_call: false
1622
+ when loc = ::DEBUGGER__.require_location
1498
1623
  # require 'debug/start' or 'debug'
1499
1624
  add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
1500
1625
  else
@@ -1506,29 +1631,8 @@ module DEBUGGER__
1506
1631
 
1507
1632
  class << self
1508
1633
  define_method :initialize_session do |ui|
1509
- DEBUGGER__.warn "Session start (pid: #{Process.pid})"
1510
-
1634
+ DEBUGGER__.info "Session start (pid: #{Process.pid})"
1511
1635
  ::DEBUGGER__.const_set(:SESSION, Session.new(ui))
1512
-
1513
- # default breakpoints
1514
-
1515
- # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
1516
-
1517
- Binding.module_eval do
1518
- def break pre: nil, do: nil
1519
- return unless SESSION.active?
1520
-
1521
- if pre || (do_expr = binding.local_variable_get(:do))
1522
- cmds = ['binding.break', pre, do_expr]
1523
- end
1524
-
1525
- ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, command: cmds
1526
- true
1527
- end
1528
- alias b break
1529
- # alias bp break
1530
- end
1531
-
1532
1636
  load_rc
1533
1637
  end
1534
1638
  end
@@ -1586,14 +1690,6 @@ module DEBUGGER__
1586
1690
  end
1587
1691
  end
1588
1692
 
1589
- LOG_LEVELS = {
1590
- UNKNOWN: 0,
1591
- FATAL: 1,
1592
- ERROR: 2,
1593
- WARN: 3,
1594
- INFO: 4,
1595
- }.freeze
1596
-
1597
1693
  def self.warn msg
1598
1694
  log :WARN, msg
1599
1695
  end
@@ -1638,7 +1734,18 @@ module DEBUGGER__
1638
1734
 
1639
1735
  at_exit{
1640
1736
  trap(:SIGINT, :IGNORE)
1641
- Process.waitpid(child_pid)
1737
+
1738
+ # only check child process from its parent
1739
+ if Process.pid == parent_pid
1740
+ begin
1741
+ # sending a null signal to see if the child is still alive
1742
+ Process.kill(0, child_pid)
1743
+ # if the child is still alive, wait for it
1744
+ Process.waitpid(child_pid)
1745
+ rescue Errno::ESRCH
1746
+ # if the child process has died, do nothing
1747
+ end
1748
+ end
1642
1749
  }
1643
1750
  }
1644
1751
  child_hook = -> {
@@ -1670,8 +1777,36 @@ module DEBUGGER__
1670
1777
  end
1671
1778
  end
1672
1779
 
1673
- class ::Object
1674
- include ForkInterceptor
1780
+ module TrapInterceptor
1781
+ def trap sig, *command, &command_proc
1782
+ case sig&.to_sym
1783
+ when :INT, :SIGINT
1784
+ if defined?(SESSION) && SESSION.active? && SESSION.intercept_trap_sigint?
1785
+ return SESSION.save_int_trap(command.empty? ? command_proc : command.first)
1786
+ end
1787
+ end
1788
+
1789
+ super
1790
+ end
1791
+ end
1792
+
1793
+ if RUBY_VERSION >= '3.0.0'
1794
+ module ::Kernel
1795
+ prepend ForkInterceptor
1796
+ prepend TrapInterceptor
1797
+ end
1798
+ else
1799
+ class ::Object
1800
+ include ForkInterceptor
1801
+ include TrapInterceptor
1802
+ end
1803
+ end
1804
+
1805
+ module ::Kernel
1806
+ class << self
1807
+ prepend ForkInterceptor
1808
+ prepend TrapInterceptor
1809
+ end
1675
1810
  end
1676
1811
 
1677
1812
  module ::Process
@@ -1679,5 +1814,29 @@ module DEBUGGER__
1679
1814
  prepend ForkInterceptor
1680
1815
  end
1681
1816
  end
1817
+
1818
+ module ::Signal
1819
+ class << self
1820
+ prepend TrapInterceptor
1821
+ end
1822
+ end
1823
+ end
1824
+
1825
+ module Kernel
1826
+ def debugger pre: nil, do: nil
1827
+ return if !defined?(::DEBUGGER__::SESSION) || !::DEBUGGER__::SESSION.active?
1828
+
1829
+ if pre || (do_expr = binding.local_variable_get(:do))
1830
+ cmds = ['binding.break', pre, do_expr]
1831
+ end
1832
+
1833
+ ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, command: cmds
1834
+ self
1835
+ end
1836
+ end
1837
+
1838
+ class Binding
1839
+ alias break debugger
1840
+ alias b debugger
1682
1841
  end
1683
1842