debug 1.2.2 → 1.3.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,4 +1,4 @@
1
- # frozen_string_literal: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  # skip to load debugger for bundle exec
4
4
 
@@ -17,6 +17,7 @@ if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
17
17
  return
18
18
  end
19
19
 
20
+ require_relative 'frame_info'
20
21
  require_relative 'config'
21
22
  require_relative 'thread_client'
22
23
  require_relative 'source_repository'
@@ -25,6 +26,7 @@ require_relative 'tracer'
25
26
 
26
27
  # To prevent loading old lib/debug.rb in Ruby 2.6 to 3.0
27
28
  $LOADED_FEATURES << 'debug.rb'
29
+ $LOADED_FEATURES << File.expand_path(File.join(__dir__, '..', 'debug.rb'))
28
30
  require 'debug' # invalidate the $LOADED_FEATURE cache
29
31
 
30
32
  require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
@@ -76,6 +78,8 @@ module DEBUGGER__
76
78
  class PostmortemError < RuntimeError; end
77
79
 
78
80
  class Session
81
+ attr_reader :intercepted_sigint_cmd, :process_group
82
+
79
83
  def initialize ui
80
84
  @ui = ui
81
85
  @sr = SourceRepository.new
@@ -98,11 +102,15 @@ module DEBUGGER__
98
102
  @thread_stopper = nil
99
103
  @intercept_trap_sigint = false
100
104
  @intercepted_sigint_cmd = 'DEFAULT'
105
+ @process_group = ProcessGroup.new
106
+ @subsession = nil
101
107
 
102
108
  @frame_map = {} # {id => [threadId, frame_depth]} for DAP
103
109
  @var_map = {1 => [:globals], } # {id => ...} for DAP
104
110
  @src_map = {} # {id => src}
105
111
 
112
+ @script_paths = [File.absolute_path($0)] # for CDP
113
+
106
114
  @tp_thread_begin = nil
107
115
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
108
116
  ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
@@ -183,95 +191,105 @@ module DEBUGGER__
183
191
 
184
192
  def session_server_main
185
193
  while evt = pop_event
186
- # variable `@internal_info` is only used for test
187
- tc, output, ev, @internal_info, *ev_args = evt
194
+ process_event evt
195
+ end
196
+ ensure
197
+ deactivate
198
+ end
199
+
200
+ def process_event evt
201
+ # variable `@internal_info` is only used for test
202
+ tc, output, ev, @internal_info, *ev_args = evt
203
+ output.each{|str| @ui.puts str} if ev != :suspend
204
+
205
+ case ev
206
+
207
+ when :thread_begin # special event, tc is nil
208
+ th = ev_args.shift
209
+ q = ev_args.shift
210
+ on_thread_begin th
211
+ q << true
212
+
213
+ when :init
214
+ wait_command_loop tc
215
+
216
+ when :load
217
+ iseq, src = ev_args
218
+ on_load iseq, src
219
+ @ui.event :load
220
+ tc << :continue
221
+
222
+ when :trace
223
+ trace_id, msg = ev_args
224
+ if t = @tracers.values.find{|t| t.object_id == trace_id}
225
+ t.puts msg
226
+ end
227
+ tc << :continue
228
+
229
+ when :suspend
230
+ enter_subsession if ev_args.first != :replay
188
231
  output.each{|str| @ui.puts str}
189
232
 
190
- case ev
233
+ case ev_args.first
234
+ when :breakpoint
235
+ bp, i = bp_index ev_args[1]
236
+ @ui.event :suspend_bp, i, bp, tc.id
237
+ when :trap
238
+ @ui.event :suspend_trap, sig = ev_args[1], tc.id
191
239
 
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
240
+ 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."
242
+ @ui.puts "`sigint` command execute it."
243
+ end
244
+ else
245
+ @ui.event :suspended, tc.id
246
+ end
197
247
 
198
- when :init
248
+ if @displays.empty?
199
249
  wait_command_loop tc
250
+ else
251
+ tc << [:eval, :display, @displays]
252
+ end
200
253
 
201
- when :load
202
- iseq, src = ev_args
203
- on_load iseq, src
204
- @ui.event :load
205
- tc << :continue
254
+ when :result
255
+ raise "[BUG] not in subsession" unless @subsession
206
256
 
207
- when :trace
208
- trace_id, msg = ev_args
209
- if t = @tracers.values.find{|t| t.object_id == trace_id}
210
- t.puts msg
211
- end
212
- tc << :continue
213
-
214
- when :suspend
215
- case ev_args.first
216
- when :breakpoint
217
- bp, i = bp_index ev_args[1]
218
- @ui.event :suspend_bp, i, bp, tc.id
219
- when :trap
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."
257
+ case ev_args.first
258
+ when :try_display
259
+ failed_results = ev_args[1]
260
+ if failed_results.size > 0
261
+ i, _msg = failed_results.last
262
+ if i+1 == @displays.size
263
+ @ui.puts "canceled: #{@displays.pop}"
225
264
  end
226
- else
227
- @ui.event :suspended, tc.id
228
- end
229
-
230
- if @displays.empty?
231
- stop_all_threads
232
- wait_command_loop tc
233
- else
234
- tc << [:eval, :display, @displays]
235
265
  end
236
266
 
237
- when :result
238
- case ev_args.first
239
- when :try_display
240
- failed_results = ev_args[1]
241
- if failed_results.size > 0
242
- i, _msg = failed_results.last
243
- if i+1 == @displays.size
244
- @ui.puts "canceled: #{@displays.pop}"
245
- end
246
- end
247
- stop_all_threads
248
-
249
- when :method_breakpoint, :watch_breakpoint
250
- bp = ev_args[1]
251
- if bp
252
- add_bp(bp)
253
- show_bps bp
254
- else
255
- # can't make a bp
256
- end
257
- when :trace_pass
258
- obj_id = ev_args[1]
259
- obj_inspect = ev_args[2]
260
- opt = ev_args[3]
261
- add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
267
+ when :method_breakpoint, :watch_breakpoint
268
+ bp = ev_args[1]
269
+ if bp
270
+ add_bp(bp)
271
+ show_bps bp
262
272
  else
263
- # ignore
273
+ # can't make a bp
264
274
  end
275
+ when :trace_pass
276
+ obj_id = ev_args[1]
277
+ obj_inspect = ev_args[2]
278
+ opt = ev_args[3]
279
+ add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
280
+ else
281
+ # ignore
282
+ end
265
283
 
266
- wait_command_loop tc
284
+ wait_command_loop tc
267
285
 
268
- when :dap_result
269
- dap_event ev_args # server.rb
270
- wait_command_loop tc
271
- end
286
+ when :dap_result
287
+ dap_event ev_args # server.rb
288
+ wait_command_loop tc
289
+ when :cdp_result
290
+ cdp_event ev_args
291
+ wait_command_loop tc
272
292
  end
273
- ensure
274
- deactivate
275
293
  end
276
294
 
277
295
  def add_preset_commands name, cmds, kick: true, continue: true
@@ -322,6 +340,8 @@ module DEBUGGER__
322
340
  def prompt
323
341
  if @postmortem
324
342
  '(rdbg:postmortem) '
343
+ elsif @process_group.multi?
344
+ "(rdbg@#{process_info}) "
325
345
  else
326
346
  '(rdbg) '
327
347
  end
@@ -333,8 +353,7 @@ module DEBUGGER__
333
353
  if @preset_command.auto_continue
334
354
  @preset_command = nil
335
355
 
336
- @tc << :continue
337
- restart_all_threads
356
+ leave_subsession :continue
338
357
  return
339
358
  else
340
359
  @preset_command = nil
@@ -353,7 +372,7 @@ module DEBUGGER__
353
372
  when String
354
373
  process_command line
355
374
  when Hash
356
- process_dap_request line # defined in server.rb
375
+ process_protocol_request line # defined in server.rb
357
376
  else
358
377
  raise "unexpected input: #{line.inspect}"
359
378
  end
@@ -409,16 +428,14 @@ module DEBUGGER__
409
428
  # * Resume the program.
410
429
  when 'c', 'continue'
411
430
  cancel_auto_continue
412
- @tc << :continue
413
- restart_all_threads
431
+ leave_subsession :continue
414
432
 
415
433
  # * `q[uit]` or `Ctrl-D`
416
434
  # * Finish debugger (with the debuggee process on non-remote debugging).
417
435
  when 'q', 'quit'
418
436
  if ask 'Really quit?'
419
437
  @ui.quit arg.to_i
420
- @tc << :continue
421
- restart_all_threads
438
+ leave_subsession :continue
422
439
  else
423
440
  return :retry
424
441
  end
@@ -427,7 +444,7 @@ module DEBUGGER__
427
444
  # * Same as q[uit] but without the confirmation prompt.
428
445
  when 'q!', 'quit!'
429
446
  @ui.quit arg.to_i
430
- restart_all_threads
447
+ leave_subsession nil
431
448
 
432
449
  # * `kill`
433
450
  # * Stop the debuggee process with `Kernal#exit!`.
@@ -457,8 +474,7 @@ module DEBUGGER__
457
474
  cmd.call
458
475
  end
459
476
 
460
- @tc << :continue
461
- restart_all_threads
477
+ leave_subsession :continue
462
478
 
463
479
  rescue Exception => e
464
480
  @ui.puts "Exception: #{e}"
@@ -722,8 +738,8 @@ module DEBUGGER__
722
738
  if ask "clear all?", 'N'
723
739
  @displays.clear
724
740
  end
741
+ return :retry
725
742
  end
726
- return :retry
727
743
 
728
744
  ### Frame control
729
745
 
@@ -922,6 +938,36 @@ module DEBUGGER__
922
938
  end
923
939
  return :retry
924
940
 
941
+ # * `open`
942
+ # * open debuggee port on UNIX domain socket and wait for attaching.
943
+ # * Note that `open` command is EXPERIMENTAL.
944
+ # * `open [<host>:]<port>`
945
+ # * open debuggee port on TCP/IP with given `[<host>:]<port>` and wait for attaching.
946
+ # * `open vscode`
947
+ # * open debuggee port for VSCode and launch VSCode if available.
948
+ # * `open chrome`
949
+ # * open debuggee port for Chrome and wait for attaching.
950
+ when 'open'
951
+ case arg&.downcase
952
+ when '', nil
953
+ repl_open_unix
954
+ when 'vscode'
955
+ repl_open_vscode
956
+ when /\A(.+):(\d+)\z/
957
+ repl_open_tcp $1, $2.to_i
958
+ when /\A(\d+)z/
959
+ repl_open_tcp nil, $1.to_i
960
+ when 'tcp'
961
+ repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
962
+ when 'chrome', 'cdp'
963
+ CONFIG[:open_frontend] = 'chrome'
964
+ repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
965
+ else
966
+ raise "Unknown arg: #{arg}"
967
+ end
968
+
969
+ return :retry
970
+
925
971
  ### Help
926
972
 
927
973
  # * `h[elp]`
@@ -967,14 +1013,38 @@ module DEBUGGER__
967
1013
  return :retry
968
1014
  end
969
1015
 
1016
+ def repl_open_setup
1017
+ @tp_thread_begin.disable
1018
+ @ui.activate self
1019
+ if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
1020
+ thc.is_management
1021
+ end
1022
+ @tp_thread_begin.enable
1023
+ end
1024
+
1025
+ def repl_open_tcp host, port, **kw
1026
+ DEBUGGER__.open_tcp host: host, port: port, nonstop: true, **kw
1027
+ repl_open_setup
1028
+ end
1029
+
1030
+ def repl_open_unix
1031
+ DEBUGGER__.open_unix nonstop: true
1032
+ repl_open_setup
1033
+ end
1034
+
1035
+ def repl_open_vscode
1036
+ CONFIG[:open_frontend] = 'vscode'
1037
+ repl_open_unix
1038
+ end
1039
+
970
1040
  def step_command type, arg
971
1041
  case arg
972
- when nil
973
- @tc << [:step, type]
974
- restart_all_threads
975
- when /\A\d+\z/
976
- @tc << [:step, type, arg.to_i]
977
- restart_all_threads
1042
+ when nil, /\A\d+\z/
1043
+ if type == :in && @tc.recorder&.replaying?
1044
+ @tc << [:step, type, arg&.to_i]
1045
+ else
1046
+ leave_subsession [:step, type, arg&.to_i]
1047
+ end
978
1048
  when /\Aback\z/, /\Areset\z/
979
1049
  if type != :in
980
1050
  @ui.puts "only `step #{arg}` is supported."
@@ -1288,10 +1358,15 @@ module DEBUGGER__
1288
1358
  end
1289
1359
 
1290
1360
  def setup_threads
1361
+ prev_clients = @th_clients || {}
1291
1362
  @th_clients = {}
1292
1363
 
1293
1364
  Thread.list.each{|th|
1294
- thread_client_create(th)
1365
+ if tc = prev_clients[th]
1366
+ @th_clients[th] = tc
1367
+ else
1368
+ thread_client_create(th)
1369
+ end
1295
1370
  }
1296
1371
  end
1297
1372
 
@@ -1375,7 +1450,30 @@ module DEBUGGER__
1375
1450
  next if @tc == tc
1376
1451
  tc << :continue
1377
1452
  }
1453
+ end
1454
+
1455
+ private def enter_subsession
1456
+ raise "already in subsession" if @subsession
1457
+ @subsession = true
1458
+ stop_all_threads
1459
+ @process_group.lock
1460
+ DEBUGGER__.info "enter_subsession"
1461
+ end
1462
+
1463
+ private def leave_subsession type
1464
+ DEBUGGER__.info "leave_subsession"
1465
+ @process_group.unlock
1466
+ restart_all_threads
1467
+ @tc << type if type
1378
1468
  @tc = nil
1469
+ @subsession = false
1470
+ rescue Exception => e
1471
+ STDERR.puts [e, e.backtrace].inspect
1472
+ raise
1473
+ end
1474
+
1475
+ def in_subsession?
1476
+ @subsession
1379
1477
  end
1380
1478
 
1381
1479
  ## event
@@ -1501,8 +1599,6 @@ module DEBUGGER__
1501
1599
  prev
1502
1600
  end
1503
1601
 
1504
- attr_reader :intercepted_sigint_cmd
1505
-
1506
1602
  def intercept_trap_sigint?
1507
1603
  @intercept_trap_sigint
1508
1604
  end
@@ -1519,6 +1615,159 @@ module DEBUGGER__
1519
1615
  @intercept_trap_sigint = true
1520
1616
  @intercepted_sigint_cmd = prev
1521
1617
  end
1618
+
1619
+ def intercept_trap_sigint_end
1620
+ @intercept_trap_sigint = false
1621
+ prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, nil
1622
+ prev
1623
+ end
1624
+
1625
+ def process_info
1626
+ if @process_group.multi?
1627
+ "#{$0}\##{Process.pid}"
1628
+ end
1629
+ end
1630
+
1631
+ def before_fork need_lock = true
1632
+ if need_lock
1633
+ @process_group.multi_process!
1634
+ end
1635
+ end
1636
+
1637
+ def after_fork_parent
1638
+ @ui.after_fork_parent
1639
+ end
1640
+ end
1641
+
1642
+ class ProcessGroup
1643
+ def initialize
1644
+ @lock_file = nil
1645
+ end
1646
+
1647
+ def locked?
1648
+ true
1649
+ end
1650
+
1651
+ def trylock
1652
+ true
1653
+ end
1654
+
1655
+ def lock
1656
+ true
1657
+ end
1658
+
1659
+ def unlock
1660
+ true
1661
+ end
1662
+
1663
+ def sync
1664
+ yield
1665
+ end
1666
+
1667
+ def after_fork
1668
+ end
1669
+
1670
+ def multi?
1671
+ @lock_file
1672
+ end
1673
+
1674
+ def multi_process!
1675
+ require 'tempfile'
1676
+ @lock_tempfile = Tempfile.open("ruby-debug-lock-")
1677
+ @lock_tempfile.close
1678
+ extend MultiProcessGroup
1679
+ end
1680
+ end
1681
+
1682
+ module MultiProcessGroup
1683
+ def multi_process!
1684
+ end
1685
+
1686
+ def after_fork child: true
1687
+ if child || !@lock_file
1688
+ @m = Mutex.new
1689
+ @lock_level = 0
1690
+ @lock_file = open(@lock_tempfile.path, 'w')
1691
+ end
1692
+ end
1693
+
1694
+ def info msg
1695
+ DEBUGGER__.info "#{msg} (#{@lock_level})" # #{caller.first(1).map{|bt| bt.sub(__dir__, '')}}"
1696
+ end
1697
+
1698
+ def locked?
1699
+ # DEBUGGER__.info "locked? #{@lock_level}"
1700
+ @lock_level > 0
1701
+ end
1702
+
1703
+ private def lock_level_up
1704
+ raise unless @m.owned?
1705
+ @lock_level += 1
1706
+ end
1707
+
1708
+ private def lock_level_down
1709
+ raise unless @m.owned?
1710
+ raise "@lock_level underflow: #{@lock_level}" if @lock_level < 1
1711
+ @lock_level -= 1
1712
+ end
1713
+
1714
+ private def trylock
1715
+ @m.synchronize do
1716
+ if locked?
1717
+ lock_level_up
1718
+ info "Try lock, already locked"
1719
+ true
1720
+ else
1721
+ case r = @lock_file.flock(File::LOCK_EX | File::LOCK_NB)
1722
+ when 0
1723
+ lock_level_up
1724
+ info "Try lock with file: success"
1725
+ true
1726
+ when false
1727
+ info "Try lock with file: failed"
1728
+ false
1729
+ else
1730
+ raise "unknown flock result: #{r.inspect}"
1731
+ end
1732
+ end
1733
+ end
1734
+ end
1735
+
1736
+ def lock
1737
+ unless trylock
1738
+ @m.synchronize do
1739
+ if locked?
1740
+ lock_level_up
1741
+ else
1742
+ info "Lock: block"
1743
+ @lock_file.flock(File::LOCK_EX)
1744
+ lock_level_up
1745
+ end
1746
+ end
1747
+
1748
+ info "Lock: success"
1749
+ end
1750
+ end
1751
+
1752
+ def unlock
1753
+ @m.synchronize do
1754
+ raise "lock file is not opened (#{@lock_file.inspect})" if @lock_file.closed?
1755
+ lock_level_down
1756
+ @lock_file.flock(File::LOCK_UN) unless locked?
1757
+ info "Unlocked"
1758
+ end
1759
+ end
1760
+
1761
+ def sync &b
1762
+ info "sync"
1763
+
1764
+ lock
1765
+ begin
1766
+ b.call if b
1767
+ ensure
1768
+ unlock
1769
+ end
1770
+ end
1522
1771
  end
1523
1772
 
1524
1773
  class UI_Base
@@ -1576,8 +1825,8 @@ module DEBUGGER__
1576
1825
  def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1577
1826
  CONFIG.set_config(**kw)
1578
1827
 
1579
- if port
1580
- open_tcp host: host, port: port, nonstop: nonstop
1828
+ if port || CONFIG[:open_frontend] == 'chrome'
1829
+ open_tcp host: host, port: (port || 0), nonstop: nonstop
1581
1830
  else
1582
1831
  open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop
1583
1832
  end
@@ -1699,15 +1948,24 @@ module DEBUGGER__
1699
1948
  end
1700
1949
 
1701
1950
  def self.log level, msg
1951
+ @logfile = STDERR unless defined? @logfile
1952
+
1702
1953
  lv = LOG_LEVELS[level]
1703
1954
  config_lv = LOG_LEVELS[CONFIG[:log_level] || :WARN]
1704
1955
 
1956
+ if defined? SESSION
1957
+ pi = SESSION.process_info
1958
+ process_info = pi ? "[#{pi}]" : nil
1959
+ end
1960
+
1705
1961
  if lv <= config_lv
1706
1962
  if level == :WARN
1707
1963
  # :WARN on debugger is general information
1708
- STDERR.puts "DEBUGGER: #{msg}"
1964
+ @logfile.puts "DEBUGGER#{process_info}: #{msg}"
1965
+ @logfile.flush
1709
1966
  else
1710
- STDERR.puts "DEBUGGER (#{level}): #{msg}"
1967
+ @logfile.puts "DEBUGGER#{process_info} (#{level}): #{msg}"
1968
+ @logfile.flush
1711
1969
  end
1712
1970
  end
1713
1971
  end
@@ -1716,8 +1974,19 @@ module DEBUGGER__
1716
1974
  def fork(&given_block)
1717
1975
  return super unless defined?(SESSION) && SESSION.active?
1718
1976
 
1977
+ unless fork_mode = CONFIG[:fork_mode]
1978
+ if CONFIG[:parent_on_fork]
1979
+ fork_mode = :parent
1980
+ else
1981
+ fork_mode = :both
1982
+ end
1983
+ end
1984
+
1985
+ parent_pid = Process.pid
1986
+
1719
1987
  # before fork
1720
- if CONFIG[:parent_on_fork]
1988
+ case fork_mode
1989
+ when :parent
1721
1990
  parent_hook = -> child_pid {
1722
1991
  # Do nothing
1723
1992
  }
@@ -1725,31 +1994,28 @@ module DEBUGGER__
1725
1994
  DEBUGGER__.warn "Detaching after fork from child process #{Process.pid}"
1726
1995
  SESSION.deactivate
1727
1996
  }
1728
- else
1729
- parent_pid = Process.pid
1997
+ when :child
1998
+ SESSION.before_fork false
1730
1999
 
1731
2000
  parent_hook = -> child_pid {
1732
2001
  DEBUGGER__.warn "Detaching after fork from parent process #{Process.pid}"
2002
+ SESSION.after_fork_parent
1733
2003
  SESSION.deactivate
2004
+ }
2005
+ child_hook = -> {
2006
+ DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2007
+ SESSION.activate on_fork: true
2008
+ }
2009
+ when :both
2010
+ SESSION.before_fork
1734
2011
 
1735
- at_exit{
1736
- trap(:SIGINT, :IGNORE)
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
1749
- }
2012
+ parent_hook = -> child_pid {
2013
+ SESSION.process_group.after_fork
2014
+ SESSION.after_fork_parent
1750
2015
  }
1751
2016
  child_hook = -> {
1752
2017
  DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2018
+ SESSION.process_group.after_fork child: true
1753
2019
  SESSION.activate on_fork: true
1754
2020
  }
1755
2021
  end
@@ -1823,20 +2089,21 @@ module DEBUGGER__
1823
2089
  end
1824
2090
 
1825
2091
  module Kernel
1826
- def debugger pre: nil, do: nil
2092
+ def debugger pre: nil, do: nil, up_level: 0
1827
2093
  return if !defined?(::DEBUGGER__::SESSION) || !::DEBUGGER__::SESSION.active?
1828
2094
 
1829
2095
  if pre || (do_expr = binding.local_variable_get(:do))
1830
2096
  cmds = ['binding.break', pre, do_expr]
1831
2097
  end
1832
2098
 
1833
- ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, command: cmds
2099
+ loc = caller_locations(up_level, 1).first; ::DEBUGGER__.add_line_breakpoint loc.path, loc.lineno + 1, oneshot: true, command: cmds
1834
2100
  self
1835
2101
  end
2102
+
2103
+ alias bb debugger if ENV['RUBY_DEBUG_BB']
1836
2104
  end
1837
2105
 
1838
2106
  class Binding
1839
2107
  alias break debugger
1840
2108
  alias b debugger
1841
2109
  end
1842
-