debug 1.0.0.rc2 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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