debug 1.3.1 → 1.4.0

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,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ return if ENV['RUBY_DEBUG_ENABLE'] == '0'
4
+
3
5
  # skip to load debugger for bundle exec
4
6
 
5
7
  if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
@@ -91,7 +93,7 @@ module DEBUGGER__
91
93
  # [:check, expr] => CheckBreakpoint
92
94
  #
93
95
  @tracers = {}
94
- @th_clients = nil # {Thread => ThreadClient}
96
+ @th_clients = {} # {Thread => ThreadClient}
95
97
  @q_evt = Queue.new
96
98
  @displays = []
97
99
  @tc = nil
@@ -99,17 +101,17 @@ module DEBUGGER__
99
101
  @preset_command = nil
100
102
  @postmortem_hook = nil
101
103
  @postmortem = false
102
- @thread_stopper = nil
103
104
  @intercept_trap_sigint = false
104
105
  @intercepted_sigint_cmd = 'DEFAULT'
105
106
  @process_group = ProcessGroup.new
106
107
  @subsession = nil
107
108
 
108
- @frame_map = {} # {id => [threadId, frame_depth]} for DAP
109
+ @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
109
110
  @var_map = {1 => [:globals], } # {id => ...} for DAP
110
111
  @src_map = {} # {id => src}
111
112
 
112
113
  @script_paths = [File.absolute_path($0)] # for CDP
114
+ @obj_map = {} # { object_id => ... } for CDP
113
115
 
114
116
  @tp_thread_begin = nil
115
117
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
@@ -117,6 +119,8 @@ module DEBUGGER__
117
119
  }
118
120
  @tp_load_script.enable
119
121
 
122
+ @thread_stopper = thread_stopper
123
+
120
124
  activate
121
125
 
122
126
  self.postmortem = CONFIG[:postmortem]
@@ -147,15 +151,15 @@ module DEBUGGER__
147
151
 
148
152
  # Thread management
149
153
  setup_threads
150
- thc = thread_client Thread.current
151
- thc.is_management
154
+ thc = get_thread_client Thread.current
155
+ thc.mark_as_management
152
156
 
153
- if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
154
- thc.is_management
157
+ if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
158
+ thc.mark_as_management
155
159
  end
156
160
 
157
161
  @tp_thread_begin = TracePoint.new(:thread_begin) do |tp|
158
- thread_client
162
+ get_thread_client
159
163
  end
160
164
  @tp_thread_begin.enable
161
165
 
@@ -168,12 +172,12 @@ module DEBUGGER__
168
172
  end
169
173
 
170
174
  def deactivate
171
- thread_client.deactivate
172
- @thread_stopper.disable if @thread_stopper
175
+ get_thread_client.deactivate
176
+ @thread_stopper.disable
173
177
  @tp_load_script.disable
174
178
  @tp_thread_begin.disable
175
- @bps.each{|k, bp| bp.disable}
176
- @th_clients.each{|th, thc| thc.close}
179
+ @bps.each_value{|bp| bp.disable}
180
+ @th_clients.each_value{|thc| thc.close}
177
181
  @tracers.values.each{|t| t.disable}
178
182
  @q_evt.close
179
183
  @ui&.deactivate
@@ -233,12 +237,13 @@ module DEBUGGER__
233
237
  case ev_args.first
234
238
  when :breakpoint
235
239
  bp, i = bp_index ev_args[1]
240
+ clean_bps unless bp
236
241
  @ui.event :suspend_bp, i, bp, tc.id
237
242
  when :trap
238
243
  @ui.event :suspend_trap, sig = ev_args[1], tc.id
239
244
 
240
245
  if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String))
241
- @ui.puts "#{@intercepted_sigint_cmd.inspect} is registerred as SIGINT handler."
246
+ @ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler."
242
247
  @ui.puts "`sigint` command execute it."
243
248
  end
244
249
  else
@@ -418,10 +423,15 @@ module DEBUGGER__
418
423
  # * `fin[ish]`
419
424
  # * Finish this frame. Resume the program until the current frame is finished.
420
425
  # * `fin[ish] <n>`
421
- # * Finish frames, same as `step <n>`.
426
+ # * Finish `<n>`th frames.
422
427
  when 'fin', 'finish'
423
428
  cancel_auto_continue
424
429
  check_postmortem
430
+
431
+ if arg&.to_i == 0
432
+ raise 'finish command with 0 does not make sense.'
433
+ end
434
+
425
435
  step_command :finish, arg
426
436
 
427
437
  # * `c[ontinue]`
@@ -447,7 +457,7 @@ module DEBUGGER__
447
457
  leave_subsession nil
448
458
 
449
459
  # * `kill`
450
- # * Stop the debuggee process with `Kernal#exit!`.
460
+ # * Stop the debuggee process with `Kernel#exit!`.
451
461
  when 'kill'
452
462
  if ask 'Really kill?'
453
463
  exit! (arg || 1).to_i
@@ -461,7 +471,7 @@ module DEBUGGER__
461
471
  exit! (arg || 1).to_i
462
472
 
463
473
  # * `sigint`
464
- # * Execute SIGINT handler registerred by the debuggee.
474
+ # * Execute SIGINT handler registered by the debuggee.
465
475
  # * Note that this command should be used just after stop by `SIGINT`.
466
476
  when 'sigint'
467
477
  begin
@@ -500,6 +510,8 @@ module DEBUGGER__
500
510
  # * break and run `<command>` before stopping.
501
511
  # * `b[reak] ... do: <command>`
502
512
  # * break and run `<command>`, and continue.
513
+ # * `b[reak] ... path: <path_regexp>`
514
+ # * break if the triggering event's path matches <path_regexp>.
503
515
  # * `b[reak] if: <expr>`
504
516
  # * break if: `<expr>` is true at any lines.
505
517
  # * Note that this feature is super slow.
@@ -526,7 +538,7 @@ module DEBUGGER__
526
538
  require 'json'
527
539
 
528
540
  h = Hash.new{|h, k| h[k] = []}
529
- @bps.each{|key, bp|
541
+ @bps.each_value{|bp|
530
542
  if LineBreakpoint === bp
531
543
  h[bp.path] << {lnum: bp.line}
532
544
  end
@@ -548,6 +560,14 @@ module DEBUGGER__
548
560
 
549
561
  # * `catch <Error>`
550
562
  # * Set breakpoint on raising `<Error>`.
563
+ # * `catch ... if: <expr>`
564
+ # * stops only if `<expr>` is true as well.
565
+ # * `catch ... pre: <command>`
566
+ # * runs `<command>` before stopping.
567
+ # * `catch ... do: <command>`
568
+ # * stops and run `<command>`, and continue.
569
+ # * `catch ... path: <path_regexp>`
570
+ # * stops if the exception is raised from a path that matches <path_regexp>.
551
571
  when 'catch'
552
572
  check_postmortem
553
573
 
@@ -562,11 +582,19 @@ module DEBUGGER__
562
582
  # * `watch @ivar`
563
583
  # * Stop the execution when the result of current scope's `@ivar` is changed.
564
584
  # * Note that this feature is super slow.
585
+ # * `watch ... if: <expr>`
586
+ # * stops only if `<expr>` is true as well.
587
+ # * `watch ... pre: <command>`
588
+ # * runs `<command>` before stopping.
589
+ # * `watch ... do: <command>`
590
+ # * stops and run `<command>`, and continue.
591
+ # * `watch ... path: <path_regexp>`
592
+ # * stops if the triggering event's path matches <path_regexp>.
565
593
  when 'wat', 'watch'
566
594
  check_postmortem
567
595
 
568
596
  if arg && arg.match?(/\A@\w+/)
569
- @tc << [:breakpoint, :watch, arg]
597
+ repl_add_watch_breakpoint(arg)
570
598
  else
571
599
  show_bps
572
600
  return :retry
@@ -902,7 +930,7 @@ module DEBUGGER__
902
930
  when nil, 'list', 'l'
903
931
  thread_list
904
932
  when /(\d+)/
905
- thread_switch $1.to_i
933
+ switch_thread $1.to_i
906
934
  else
907
935
  @ui.puts "unknown thread command: #{arg}"
908
936
  end
@@ -1016,8 +1044,8 @@ module DEBUGGER__
1016
1044
  def repl_open_setup
1017
1045
  @tp_thread_begin.disable
1018
1046
  @ui.activate self
1019
- if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
1020
- thc.is_management
1047
+ if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
1048
+ thc.mark_as_management
1021
1049
  end
1022
1050
  @tp_thread_begin.enable
1023
1051
  end
@@ -1195,6 +1223,12 @@ module DEBUGGER__
1195
1223
  }
1196
1224
  end
1197
1225
 
1226
+ def clean_bps
1227
+ @bps.delete_if{|_k, bp|
1228
+ bp.deleted?
1229
+ }
1230
+ end
1231
+
1198
1232
  def add_bp bp
1199
1233
  # don't repeat commands that add breakpoints
1200
1234
  @repl_prev_line = nil
@@ -1225,7 +1259,7 @@ module DEBUGGER__
1225
1259
  end
1226
1260
  end
1227
1261
 
1228
- BREAK_KEYWORDS = %w(if: do: pre:).freeze
1262
+ BREAK_KEYWORDS = %w(if: do: pre: path:).freeze
1229
1263
 
1230
1264
  def parse_break arg
1231
1265
  mode = :sig
@@ -1245,6 +1279,7 @@ module DEBUGGER__
1245
1279
  expr = parse_break arg.strip
1246
1280
  cond = expr[:if]
1247
1281
  cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1282
+ path = Regexp.compile(expr[:path]) if expr[:path]
1248
1283
 
1249
1284
  case expr[:sig]
1250
1285
  when /\A(\d+)\z/
@@ -1252,10 +1287,10 @@ module DEBUGGER__
1252
1287
  when /\A(.+)[:\s+](\d+)\z/
1253
1288
  add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
1254
1289
  when /\A(.+)([\.\#])(.+)\z/
1255
- @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd]
1290
+ @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd, path]
1256
1291
  return :noretry
1257
1292
  when nil
1258
- add_check_breakpoint cond
1293
+ add_check_breakpoint cond, path
1259
1294
  else
1260
1295
  @ui.puts "Unknown breakpoint format: #{arg}"
1261
1296
  @ui.puts
@@ -1267,18 +1302,28 @@ module DEBUGGER__
1267
1302
  expr = parse_break arg.strip
1268
1303
  cond = expr[:if]
1269
1304
  cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1305
+ path = Regexp.compile(expr[:path]) if expr[:path]
1270
1306
 
1271
- bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd)
1307
+ bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
1272
1308
  add_bp bp
1273
1309
  end
1274
1310
 
1311
+ def repl_add_watch_breakpoint arg
1312
+ expr = parse_break arg.strip
1313
+ cond = expr[:if]
1314
+ cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1315
+ path = Regexp.compile(expr[:path]) if expr[:path]
1316
+
1317
+ @tc << [:breakpoint, :watch, expr[:sig], cond, cmd, path]
1318
+ end
1319
+
1275
1320
  def add_catch_breakpoint pat
1276
1321
  bp = CatchBreakpoint.new(pat)
1277
1322
  add_bp bp
1278
1323
  end
1279
1324
 
1280
- def add_check_breakpoint expr
1281
- bp = CheckBreakpoint.new(expr)
1325
+ def add_check_breakpoint expr, path
1326
+ bp = CheckBreakpoint.new(expr, path)
1282
1327
  add_bp bp
1283
1328
  end
1284
1329
 
@@ -1291,6 +1336,11 @@ module DEBUGGER__
1291
1336
  @ui.puts e.message
1292
1337
  end
1293
1338
 
1339
+ def add_iseq_breakpoint iseq, **kw
1340
+ bp = ISeqBreakpoint.new(iseq, [:line], **kw)
1341
+ add_bp bp
1342
+ end
1343
+
1294
1344
  # tracers
1295
1345
 
1296
1346
  def add_tracer tracer
@@ -1344,7 +1394,7 @@ module DEBUGGER__
1344
1394
  thcs
1345
1395
  end
1346
1396
 
1347
- def thread_switch n
1397
+ def switch_thread n
1348
1398
  thcs, _unmanaged_ths = update_thread_list
1349
1399
 
1350
1400
  if tc = thcs[n]
@@ -1358,14 +1408,14 @@ module DEBUGGER__
1358
1408
  end
1359
1409
 
1360
1410
  def setup_threads
1361
- prev_clients = @th_clients || {}
1411
+ prev_clients = @th_clients
1362
1412
  @th_clients = {}
1363
1413
 
1364
1414
  Thread.list.each{|th|
1365
1415
  if tc = prev_clients[th]
1366
1416
  @th_clients[th] = tc
1367
1417
  else
1368
- thread_client_create(th)
1418
+ create_thread_client(th)
1369
1419
  end
1370
1420
  }
1371
1421
  end
@@ -1374,17 +1424,17 @@ module DEBUGGER__
1374
1424
  if @th_clients.has_key? th
1375
1425
  # TODO: NG?
1376
1426
  else
1377
- thread_client_create th
1427
+ create_thread_client th
1378
1428
  end
1379
1429
  end
1380
1430
 
1381
- private def thread_client_create th
1431
+ private def create_thread_client th
1382
1432
  # TODO: Ractor support
1383
1433
  raise "Only session_server can create thread_client" unless Thread.current == @session_server
1384
1434
  @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
1385
1435
  end
1386
1436
 
1387
- private def ask_thread_client th = Thread.current
1437
+ private def ask_thread_client th
1388
1438
  # TODO: Ractor support
1389
1439
  q2 = Queue.new
1390
1440
  # tc, output, ev, @internal_info, *ev_args = evt
@@ -1395,30 +1445,18 @@ module DEBUGGER__
1395
1445
  end
1396
1446
 
1397
1447
  # can be called by other threads
1398
- def thread_client th = Thread.current
1448
+ def get_thread_client th = Thread.current
1399
1449
  if @th_clients.has_key? th
1400
1450
  @th_clients[th]
1401
1451
  else
1402
1452
  if Thread.current == @session_server
1403
- thread_client_create th
1453
+ create_thread_client th
1404
1454
  else
1405
1455
  ask_thread_client th
1406
1456
  end
1407
1457
  end
1408
1458
  end
1409
1459
 
1410
- private def thread_stopper
1411
- @thread_stopper ||= TracePoint.new(:line) do
1412
- # run on each thread
1413
- tc = ThreadClient.current
1414
- next if tc.management?
1415
- next unless tc.running?
1416
- next if tc == @tc
1417
-
1418
- tc.on_pause
1419
- end
1420
- end
1421
-
1422
1460
  private def running_thread_clients_count
1423
1461
  @th_clients.count{|th, tc|
1424
1462
  next if tc.management?
@@ -1435,15 +1473,27 @@ module DEBUGGER__
1435
1473
  }.compact
1436
1474
  end
1437
1475
 
1476
+ private def thread_stopper
1477
+ TracePoint.new(:line) do
1478
+ # run on each thread
1479
+ tc = ThreadClient.current
1480
+ next if tc.management?
1481
+ next unless tc.running?
1482
+ next if tc == @tc
1483
+
1484
+ tc.on_pause
1485
+ end
1486
+ end
1487
+
1438
1488
  private def stop_all_threads
1439
1489
  return if running_thread_clients_count == 0
1440
1490
 
1441
- stopper = thread_stopper
1491
+ stopper = @thread_stopper
1442
1492
  stopper.enable unless stopper.enabled?
1443
1493
  end
1444
1494
 
1445
1495
  private def restart_all_threads
1446
- stopper = thread_stopper
1496
+ stopper = @thread_stopper
1447
1497
  stopper.disable if stopper.enabled?
1448
1498
 
1449
1499
  waiting_thread_clients.each{|tc|
@@ -1550,11 +1600,41 @@ module DEBUGGER__
1550
1600
 
1551
1601
  frames = exc.instance_variable_get(:@__debugger_postmortem_frames)
1552
1602
  @postmortem = true
1553
- ThreadClient.current.suspend :postmortem, postmortem_frames: frames
1603
+ ThreadClient.current.suspend :postmortem, postmortem_frames: frames, postmortem_exc: exc
1554
1604
  ensure
1555
1605
  @postmortem = false
1556
1606
  end
1557
1607
 
1608
+ def capture_exception_frames *exclude_path
1609
+ postmortem_hook = TracePoint.new(:raise){|tp|
1610
+ exc = tp.raised_exception
1611
+ frames = DEBUGGER__.capture_frames(__dir__)
1612
+
1613
+ exclude_path.each{|ex|
1614
+ if Regexp === ex
1615
+ frames.delete_if{|e| ex =~ e.path}
1616
+ else
1617
+ frames.delete_if{|e| e.path.start_with? ex.to_s}
1618
+ end
1619
+ }
1620
+ exc.instance_variable_set(:@__debugger_postmortem_frames, frames)
1621
+ }
1622
+ postmortem_hook.enable
1623
+
1624
+ begin
1625
+ yield
1626
+ nil
1627
+ rescue Exception => e
1628
+ if e.instance_variable_defined? :@__debugger_postmortem_frames
1629
+ e
1630
+ else
1631
+ raise
1632
+ end
1633
+ ensure
1634
+ postmortem_hook.disable
1635
+ end
1636
+ end
1637
+
1558
1638
  def postmortem=(is_enable)
1559
1639
  if is_enable
1560
1640
  unless @postmortem_hook
@@ -1792,7 +1872,7 @@ module DEBUGGER__
1792
1872
  ::DEBUGGER__::SESSION.add_catch_breakpoint pat
1793
1873
  end
1794
1874
 
1795
- # String for requring location
1875
+ # String for requiring location
1796
1876
  # nil for -r
1797
1877
  def self.require_location
1798
1878
  locs = caller_locations
@@ -1930,13 +2010,17 @@ module DEBUGGER__
1930
2010
  METHOD_ADDED_TRACKER = self.create_method_added_tracker
1931
2011
 
1932
2012
  SHORT_INSPECT_LENGTH = 40
1933
- def self.short_inspect obj, use_short = true
2013
+
2014
+ def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
1934
2015
  str = obj.inspect
1935
- if use_short && str.length > SHORT_INSPECT_LENGTH
1936
- str[0...SHORT_INSPECT_LENGTH] + '...'
2016
+
2017
+ if short && str.length > max_length
2018
+ str[0...max_length] + '...'
1937
2019
  else
1938
2020
  str
1939
2021
  end
2022
+ rescue Exception => e
2023
+ str = "<#inspect raises #{e.inspect}>"
1940
2024
  end
1941
2025
 
1942
2026
  def self.warn msg
@@ -1970,6 +2054,14 @@ module DEBUGGER__
1970
2054
  end
1971
2055
  end
1972
2056
 
2057
+ def self.step_in &b
2058
+ if defined?(SESSION) && SESSION.active?
2059
+ SESSION.add_iseq_breakpoint RubyVM::InstructionSequence.of(b), oneshot: true
2060
+ end
2061
+
2062
+ yield
2063
+ end
2064
+
1973
2065
  module ForkInterceptor
1974
2066
  def fork(&given_block)
1975
2067
  return super unless defined?(SESSION) && SESSION.active?
@@ -40,12 +40,10 @@ module DEBUGGER__
40
40
  end
41
41
 
42
42
  private def add_path path
43
- begin
44
- src = File.read(path)
45
- src = src.gsub("\r\n", "\n") # CRLF -> LF
46
- @files[path] = SrcInfo.new(src.lines)
47
- rescue SystemCallError
48
- end
43
+ src = File.read(path)
44
+ src = src.gsub("\r\n", "\n") # CRLF -> LF
45
+ @files[path] = SrcInfo.new(src.lines)
46
+ rescue SystemCallError
49
47
  end
50
48
 
51
49
  private def get_si iseq