debug 1.3.3 → 1.5.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,11 +1,13 @@
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'
6
8
  trace_var(:$0) do |file|
7
9
  trace_var(:$0, nil)
8
- if /-r (#{__dir__}\S+)/ =~ ENV['RUBYOPT']
10
+ if /-r (#{Regexp.escape(__dir__)}\S+)/ =~ ENV['RUBYOPT']
9
11
  lib = $1
10
12
  $LOADED_FEATURES.delete_if{|path| path.start_with?(__dir__)}
11
13
  ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH'] = file
@@ -29,7 +31,7 @@ $LOADED_FEATURES << 'debug.rb'
29
31
  $LOADED_FEATURES << File.expand_path(File.join(__dir__, '..', 'debug.rb'))
30
32
  require 'debug' # invalidate the $LOADED_FEATURE cache
31
33
 
32
- require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
34
+ require 'json' if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
33
35
 
34
36
  class RubyVM::InstructionSequence
35
37
  def traceable_lines_norec lines
@@ -54,23 +56,22 @@ class RubyVM::InstructionSequence
54
56
 
55
57
  def type
56
58
  self.to_a[9]
57
- end
59
+ end unless method_defined?(:type)
58
60
 
59
- def argc
60
- self.to_a[4][:arg_size]
61
- end
62
-
63
- def locals
64
- self.to_a[10]
65
- end
61
+ def parameters_symbols
62
+ ary = self.to_a
63
+ argc = ary[4][:arg_size]
64
+ locals = ary.to_a[10]
65
+ locals[0...argc]
66
+ end unless method_defined?(:parameters_symbols)
66
67
 
67
68
  def last_line
68
69
  self.to_a[4][:code_location][2]
69
- end
70
+ end unless method_defined?(:last_line)
70
71
 
71
72
  def first_line
72
73
  self.to_a[4][:code_location][0]
73
- end
74
+ end unless method_defined?(:first_line)
74
75
  end
75
76
 
76
77
  module DEBUGGER__
@@ -80,8 +81,10 @@ module DEBUGGER__
80
81
  class Session
81
82
  attr_reader :intercepted_sigint_cmd, :process_group
82
83
 
83
- def initialize ui
84
- @ui = ui
84
+ include Color
85
+
86
+ def initialize
87
+ @ui = nil
85
88
  @sr = SourceRepository.new
86
89
  @bps = {} # bp.key => bp
87
90
  # [file, line] => LineBreakpoint
@@ -99,17 +102,17 @@ module DEBUGGER__
99
102
  @preset_command = nil
100
103
  @postmortem_hook = nil
101
104
  @postmortem = false
102
- @thread_stopper = nil
103
105
  @intercept_trap_sigint = false
104
106
  @intercepted_sigint_cmd = 'DEFAULT'
105
107
  @process_group = ProcessGroup.new
106
- @subsession = nil
108
+ @subsession_stack = []
107
109
 
108
- @frame_map = {} # {id => [threadId, frame_depth]} for DAP
110
+ @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
109
111
  @var_map = {1 => [:globals], } # {id => ...} for DAP
110
112
  @src_map = {} # {id => src}
111
113
 
112
- @script_paths = [File.absolute_path($0)] # for CDP
114
+ @scr_id_map = {} # for CDP
115
+ @obj_map = {} # { object_id => ... } for CDP
113
116
 
114
117
  @tp_thread_begin = nil
115
118
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
@@ -117,8 +120,7 @@ module DEBUGGER__
117
120
  }
118
121
  @tp_load_script.enable
119
122
 
120
- activate
121
-
123
+ @thread_stopper = thread_stopper
122
124
  self.postmortem = CONFIG[:postmortem]
123
125
  end
124
126
 
@@ -130,15 +132,13 @@ module DEBUGGER__
130
132
  @bps.has_key? [file, line]
131
133
  end
132
134
 
133
- def activate on_fork: false
135
+ def activate ui = nil, on_fork: false
136
+ @ui = ui if ui
137
+
134
138
  @tp_thread_begin&.disable
135
139
  @tp_thread_begin = nil
136
140
 
137
- if on_fork
138
- @ui.activate self, on_fork: true
139
- else
140
- @ui.activate self, on_fork: false
141
- end
141
+ @ui.activate self, on_fork: on_fork
142
142
 
143
143
  q = Queue.new
144
144
  @session_server = Thread.new do
@@ -147,15 +147,15 @@ module DEBUGGER__
147
147
 
148
148
  # Thread management
149
149
  setup_threads
150
- thc = thread_client Thread.current
151
- thc.is_management
150
+ thc = get_thread_client Thread.current
151
+ thc.mark_as_management
152
152
 
153
- if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
154
- thc.is_management
153
+ if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
154
+ thc.mark_as_management
155
155
  end
156
156
 
157
157
  @tp_thread_begin = TracePoint.new(:thread_begin) do |tp|
158
- thread_client
158
+ get_thread_client
159
159
  end
160
160
  @tp_thread_begin.enable
161
161
 
@@ -168,12 +168,12 @@ module DEBUGGER__
168
168
  end
169
169
 
170
170
  def deactivate
171
- thread_client.deactivate
172
- @thread_stopper.disable if @thread_stopper
171
+ get_thread_client.deactivate
172
+ @thread_stopper.disable
173
173
  @tp_load_script.disable
174
174
  @tp_thread_begin.disable
175
- @bps.each{|k, bp| bp.disable}
176
- @th_clients.each{|th, thc| thc.close}
175
+ @bps.each_value{|bp| bp.disable}
176
+ @th_clients.each_value{|thc| thc.close}
177
177
  @tracers.values.each{|t| t.disable}
178
178
  @q_evt.close
179
179
  @ui&.deactivate
@@ -211,6 +211,7 @@ module DEBUGGER__
211
211
  q << true
212
212
 
213
213
  when :init
214
+ enter_subsession
214
215
  wait_command_loop tc
215
216
 
216
217
  when :load
@@ -233,12 +234,13 @@ module DEBUGGER__
233
234
  case ev_args.first
234
235
  when :breakpoint
235
236
  bp, i = bp_index ev_args[1]
237
+ clean_bps unless bp
236
238
  @ui.event :suspend_bp, i, bp, tc.id
237
239
  when :trap
238
240
  @ui.event :suspend_trap, sig = ev_args[1], tc.id
239
241
 
240
242
  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."
243
+ @ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler."
242
244
  @ui.puts "`sigint` command execute it."
243
245
  end
244
246
  else
@@ -252,7 +254,7 @@ module DEBUGGER__
252
254
  end
253
255
 
254
256
  when :result
255
- raise "[BUG] not in subsession" unless @subsession
257
+ raise "[BUG] not in subsession" if @subsession_stack.empty?
256
258
 
257
259
  case ev_args.first
258
260
  when :try_display
@@ -364,7 +366,7 @@ module DEBUGGER__
364
366
  @ui.puts "(rdbg:#{@preset_command.source}) #{line}"
365
367
  end
366
368
  else
367
- @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
369
+ @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
368
370
  line = @ui.readline prompt
369
371
  end
370
372
 
@@ -418,10 +420,15 @@ module DEBUGGER__
418
420
  # * `fin[ish]`
419
421
  # * Finish this frame. Resume the program until the current frame is finished.
420
422
  # * `fin[ish] <n>`
421
- # * Finish frames, same as `step <n>`.
423
+ # * Finish `<n>`th frames.
422
424
  when 'fin', 'finish'
423
425
  cancel_auto_continue
424
426
  check_postmortem
427
+
428
+ if arg&.to_i == 0
429
+ raise 'finish command with 0 does not make sense.'
430
+ end
431
+
425
432
  step_command :finish, arg
426
433
 
427
434
  # * `c[ontinue]`
@@ -447,7 +454,7 @@ module DEBUGGER__
447
454
  leave_subsession nil
448
455
 
449
456
  # * `kill`
450
- # * Stop the debuggee process with `Kernal#exit!`.
457
+ # * Stop the debuggee process with `Kernel#exit!`.
451
458
  when 'kill'
452
459
  if ask 'Really kill?'
453
460
  exit! (arg || 1).to_i
@@ -461,7 +468,7 @@ module DEBUGGER__
461
468
  exit! (arg || 1).to_i
462
469
 
463
470
  # * `sigint`
464
- # * Execute SIGINT handler registerred by the debuggee.
471
+ # * Execute SIGINT handler registered by the debuggee.
465
472
  # * Note that this command should be used just after stop by `SIGINT`.
466
473
  when 'sigint'
467
474
  begin
@@ -500,6 +507,8 @@ module DEBUGGER__
500
507
  # * break and run `<command>` before stopping.
501
508
  # * `b[reak] ... do: <command>`
502
509
  # * break and run `<command>`, and continue.
510
+ # * `b[reak] ... path: <path>`
511
+ # * break if the path matches to `<path>`. `<path>` can be a regexp with `/regexp/`.
503
512
  # * `b[reak] if: <expr>`
504
513
  # * break if: `<expr>` is true at any lines.
505
514
  # * Note that this feature is super slow.
@@ -526,7 +535,7 @@ module DEBUGGER__
526
535
  require 'json'
527
536
 
528
537
  h = Hash.new{|h, k| h[k] = []}
529
- @bps.each{|key, bp|
538
+ @bps.each_value{|bp|
530
539
  if LineBreakpoint === bp
531
540
  h[bp.path] << {lnum: bp.line}
532
541
  end
@@ -548,6 +557,14 @@ module DEBUGGER__
548
557
 
549
558
  # * `catch <Error>`
550
559
  # * Set breakpoint on raising `<Error>`.
560
+ # * `catch ... if: <expr>`
561
+ # * stops only if `<expr>` is true as well.
562
+ # * `catch ... pre: <command>`
563
+ # * runs `<command>` before stopping.
564
+ # * `catch ... do: <command>`
565
+ # * stops and run `<command>`, and continue.
566
+ # * `catch ... path: <path>`
567
+ # * stops if the exception is raised from a `<path>`. `<path>` can be a regexp with `/regexp/`.
551
568
  when 'catch'
552
569
  check_postmortem
553
570
 
@@ -562,11 +579,19 @@ module DEBUGGER__
562
579
  # * `watch @ivar`
563
580
  # * Stop the execution when the result of current scope's `@ivar` is changed.
564
581
  # * Note that this feature is super slow.
582
+ # * `watch ... if: <expr>`
583
+ # * stops only if `<expr>` is true as well.
584
+ # * `watch ... pre: <command>`
585
+ # * runs `<command>` before stopping.
586
+ # * `watch ... do: <command>`
587
+ # * stops and run `<command>`, and continue.
588
+ # * `watch ... path: <path>`
589
+ # * stops if the path matches `<path>`. `<path>` can be a regexp with `/regexp/`.
565
590
  when 'wat', 'watch'
566
591
  check_postmortem
567
592
 
568
593
  if arg && arg.match?(/\A@\w+/)
569
- @tc << [:breakpoint, :watch, arg]
594
+ repl_add_watch_breakpoint(arg)
570
595
  else
571
596
  show_bps
572
597
  return :retry
@@ -671,8 +696,8 @@ module DEBUGGER__
671
696
  # * Show information about accessible constants except toplevel constants.
672
697
  # * `i[nfo] g[lobal[s]]`
673
698
  # * Show information about global variables
674
- # * `i[nfo] ... </pattern/>`
675
- # * Filter the output with `</pattern/>`.
699
+ # * `i[nfo] ... /regexp/`
700
+ # * Filter the output with `/regexp/`.
676
701
  # * `i[nfo] th[read[s]]`
677
702
  # * Show all threads (same as `th[read]`).
678
703
  when 'i', 'info'
@@ -806,8 +831,8 @@ module DEBUGGER__
806
831
  # * Add an exception tracer. It indicates raising exceptions.
807
832
  # * `trace object <expr>`
808
833
  # * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
809
- # * `trace ... </pattern/>`
810
- # * Indicates only matched events to `</pattern/>` (RegExp).
834
+ # * `trace ... /regexp/`
835
+ # * Indicates only matched events to `/regexp/`.
811
836
  # * `trace ... into: <file>`
812
837
  # * Save trace information into: `<file>`.
813
838
  # * `trace off <num>`
@@ -902,7 +927,7 @@ module DEBUGGER__
902
927
  when nil, 'list', 'l'
903
928
  thread_list
904
929
  when /(\d+)/
905
- thread_switch $1.to_i
930
+ switch_thread $1.to_i
906
931
  else
907
932
  @ui.puts "unknown thread command: #{arg}"
908
933
  end
@@ -950,7 +975,7 @@ module DEBUGGER__
950
975
  when 'open'
951
976
  case arg&.downcase
952
977
  when '', nil
953
- repl_open_unix
978
+ repl_open
954
979
  when 'vscode'
955
980
  repl_open_vscode
956
981
  when /\A(.+):(\d+)\z/
@@ -975,11 +1000,7 @@ module DEBUGGER__
975
1000
  # * `h[elp] <command>`
976
1001
  # * Show help for the given command.
977
1002
  when 'h', 'help', '?'
978
- if arg
979
- show_help arg
980
- else
981
- @ui.puts DEBUGGER__.help
982
- end
1003
+ show_help arg
983
1004
  return :retry
984
1005
 
985
1006
  ### END
@@ -1016,8 +1037,8 @@ module DEBUGGER__
1016
1037
  def repl_open_setup
1017
1038
  @tp_thread_begin.disable
1018
1039
  @ui.activate self
1019
- if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
1020
- thc.is_management
1040
+ if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
1041
+ thc.mark_as_management
1021
1042
  end
1022
1043
  @tp_thread_begin.enable
1023
1044
  end
@@ -1027,14 +1048,14 @@ module DEBUGGER__
1027
1048
  repl_open_setup
1028
1049
  end
1029
1050
 
1030
- def repl_open_unix
1031
- DEBUGGER__.open_unix nonstop: true
1051
+ def repl_open
1052
+ DEBUGGER__.open nonstop: true
1032
1053
  repl_open_setup
1033
1054
  end
1034
1055
 
1035
1056
  def repl_open_vscode
1036
1057
  CONFIG[:open_frontend] = 'vscode'
1037
- repl_open_unix
1058
+ repl_open
1038
1059
  end
1039
1060
 
1040
1061
  def step_command type, arg
@@ -1131,16 +1152,50 @@ module DEBUGGER__
1131
1152
  end
1132
1153
  end
1133
1154
 
1134
- def show_help arg
1135
- DEBUGGER__.helps.each{|cat, cs|
1136
- cs.each{|ws, desc|
1137
- if ws.include? arg
1138
- @ui.puts desc
1139
- return
1155
+ def show_help arg = nil
1156
+ instructions = (DEBUGGER__.commands.keys + DEBUGGER__.commands.values).uniq
1157
+ print_instructions = proc do |desc|
1158
+ desc.split("\n").each do |line|
1159
+ next if line.start_with?(" ") # workaround for step back
1160
+ formatted_line = line.gsub(/[\[\]\*]/, "").strip
1161
+ instructions.each do |inst|
1162
+ if formatted_line.start_with?("`#{inst}")
1163
+ desc.sub!(line, colorize(line, [:CYAN, :BOLD]))
1164
+ end
1165
+ end
1166
+ end
1167
+ @ui.puts desc
1168
+ end
1169
+
1170
+ print_category = proc do |cat|
1171
+ @ui.puts "\n"
1172
+ @ui.puts colorize("### #{cat}", [:GREEN, :BOLD])
1173
+ @ui.puts "\n"
1174
+ end
1175
+
1176
+ DEBUGGER__.helps.each { |cat, cs|
1177
+ # categories
1178
+ if arg.nil?
1179
+ print_category.call(cat)
1180
+ else
1181
+ cs.each { |ws, _|
1182
+ if ws.include?(arg)
1183
+ print_category.call(cat)
1184
+ break
1185
+ end
1186
+ }
1187
+ end
1188
+
1189
+ # instructions
1190
+ cs.each { |ws, desc|
1191
+ if arg.nil? || ws.include?(arg)
1192
+ print_instructions.call(desc.dup)
1193
+ return if arg
1140
1194
  end
1141
1195
  }
1142
1196
  }
1143
- @ui.puts "not found: #{arg}"
1197
+
1198
+ @ui.puts "not found: #{arg}" if arg
1144
1199
  end
1145
1200
 
1146
1201
  def ask msg, default = 'Y'
@@ -1195,6 +1250,12 @@ module DEBUGGER__
1195
1250
  }
1196
1251
  end
1197
1252
 
1253
+ def clean_bps
1254
+ @bps.delete_if{|_k, bp|
1255
+ bp.deleted?
1256
+ }
1257
+ end
1258
+
1198
1259
  def add_bp bp
1199
1260
  # don't repeat commands that add breakpoints
1200
1261
  @repl_prev_line = nil
@@ -1225,7 +1286,7 @@ module DEBUGGER__
1225
1286
  end
1226
1287
  end
1227
1288
 
1228
- BREAK_KEYWORDS = %w(if: do: pre:).freeze
1289
+ BREAK_KEYWORDS = %w(if: do: pre: path:).freeze
1229
1290
 
1230
1291
  def parse_break arg
1231
1292
  mode = :sig
@@ -1238,13 +1299,20 @@ module DEBUGGER__
1238
1299
  end
1239
1300
  }
1240
1301
  expr.default_proc = nil
1241
- expr.transform_values{|v| v.join(' ')}
1302
+ expr = expr.transform_values{|v| v.join(' ')}
1303
+
1304
+ if (path = expr[:path]) && path =~ /\A\/(.*)\/\z/
1305
+ expr[:path] = Regexp.compile($1)
1306
+ end
1307
+
1308
+ expr
1242
1309
  end
1243
1310
 
1244
1311
  def repl_add_breakpoint arg
1245
1312
  expr = parse_break arg.strip
1246
1313
  cond = expr[:if]
1247
1314
  cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1315
+ path = expr[:path]
1248
1316
 
1249
1317
  case expr[:sig]
1250
1318
  when /\A(\d+)\z/
@@ -1252,10 +1320,10 @@ module DEBUGGER__
1252
1320
  when /\A(.+)[:\s+](\d+)\z/
1253
1321
  add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
1254
1322
  when /\A(.+)([\.\#])(.+)\z/
1255
- @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd]
1323
+ @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd, path]
1256
1324
  return :noretry
1257
1325
  when nil
1258
- add_check_breakpoint cond
1326
+ add_check_breakpoint cond, path, cmd
1259
1327
  else
1260
1328
  @ui.puts "Unknown breakpoint format: #{arg}"
1261
1329
  @ui.puts
@@ -1267,18 +1335,28 @@ module DEBUGGER__
1267
1335
  expr = parse_break arg.strip
1268
1336
  cond = expr[:if]
1269
1337
  cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1338
+ path = expr[:path]
1270
1339
 
1271
- bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd)
1340
+ bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
1272
1341
  add_bp bp
1273
1342
  end
1274
1343
 
1344
+ def repl_add_watch_breakpoint arg
1345
+ expr = parse_break arg.strip
1346
+ cond = expr[:if]
1347
+ cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1348
+ path = Regexp.compile(expr[:path]) if expr[:path]
1349
+
1350
+ @tc << [:breakpoint, :watch, expr[:sig], cond, cmd, path]
1351
+ end
1352
+
1275
1353
  def add_catch_breakpoint pat
1276
1354
  bp = CatchBreakpoint.new(pat)
1277
1355
  add_bp bp
1278
1356
  end
1279
1357
 
1280
- def add_check_breakpoint expr
1281
- bp = CheckBreakpoint.new(expr)
1358
+ def add_check_breakpoint cond, path, command
1359
+ bp = CheckBreakpoint.new(cond: cond, path: path, command: command)
1282
1360
  add_bp bp
1283
1361
  end
1284
1362
 
@@ -1291,6 +1369,20 @@ module DEBUGGER__
1291
1369
  @ui.puts e.message
1292
1370
  end
1293
1371
 
1372
+ def clear_line_breakpoints path
1373
+ path = resolve_path(path)
1374
+ @bps.delete_if do |k, bp|
1375
+ if (Array === k) && DEBUGGER__.compare_path(k.first, path)
1376
+ bp.delete
1377
+ end
1378
+ end
1379
+ end
1380
+
1381
+ def add_iseq_breakpoint iseq, **kw
1382
+ bp = ISeqBreakpoint.new(iseq, [:line], **kw)
1383
+ add_bp bp
1384
+ end
1385
+
1294
1386
  # tracers
1295
1387
 
1296
1388
  def add_tracer tracer
@@ -1344,7 +1436,7 @@ module DEBUGGER__
1344
1436
  thcs
1345
1437
  end
1346
1438
 
1347
- def thread_switch n
1439
+ def switch_thread n
1348
1440
  thcs, _unmanaged_ths = update_thread_list
1349
1441
 
1350
1442
  if tc = thcs[n]
@@ -1365,7 +1457,7 @@ module DEBUGGER__
1365
1457
  if tc = prev_clients[th]
1366
1458
  @th_clients[th] = tc
1367
1459
  else
1368
- thread_client_create(th)
1460
+ create_thread_client(th)
1369
1461
  end
1370
1462
  }
1371
1463
  end
@@ -1374,17 +1466,17 @@ module DEBUGGER__
1374
1466
  if @th_clients.has_key? th
1375
1467
  # TODO: NG?
1376
1468
  else
1377
- thread_client_create th
1469
+ create_thread_client th
1378
1470
  end
1379
1471
  end
1380
1472
 
1381
- private def thread_client_create th
1473
+ private def create_thread_client th
1382
1474
  # TODO: Ractor support
1383
1475
  raise "Only session_server can create thread_client" unless Thread.current == @session_server
1384
1476
  @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
1385
1477
  end
1386
1478
 
1387
- private def ask_thread_client th = Thread.current
1479
+ private def ask_thread_client th
1388
1480
  # TODO: Ractor support
1389
1481
  q2 = Queue.new
1390
1482
  # tc, output, ev, @internal_info, *ev_args = evt
@@ -1395,30 +1487,18 @@ module DEBUGGER__
1395
1487
  end
1396
1488
 
1397
1489
  # can be called by other threads
1398
- def thread_client th = Thread.current
1490
+ def get_thread_client th = Thread.current
1399
1491
  if @th_clients.has_key? th
1400
1492
  @th_clients[th]
1401
1493
  else
1402
1494
  if Thread.current == @session_server
1403
- thread_client_create th
1495
+ create_thread_client th
1404
1496
  else
1405
1497
  ask_thread_client th
1406
1498
  end
1407
1499
  end
1408
1500
  end
1409
1501
 
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
1502
  private def running_thread_clients_count
1423
1503
  @th_clients.count{|th, tc|
1424
1504
  next if tc.management?
@@ -1435,15 +1515,27 @@ module DEBUGGER__
1435
1515
  }.compact
1436
1516
  end
1437
1517
 
1518
+ private def thread_stopper
1519
+ TracePoint.new(:line) do
1520
+ # run on each thread
1521
+ tc = ThreadClient.current
1522
+ next if tc.management?
1523
+ next unless tc.running?
1524
+ next if tc == @tc
1525
+
1526
+ tc.on_pause
1527
+ end
1528
+ end
1529
+
1438
1530
  private def stop_all_threads
1439
1531
  return if running_thread_clients_count == 0
1440
1532
 
1441
- stopper = thread_stopper
1533
+ stopper = @thread_stopper
1442
1534
  stopper.enable unless stopper.enabled?
1443
1535
  end
1444
1536
 
1445
1537
  private def restart_all_threads
1446
- stopper = thread_stopper
1538
+ stopper = @thread_stopper
1447
1539
  stopper.disable if stopper.enabled?
1448
1540
 
1449
1541
  waiting_thread_clients.each{|tc|
@@ -1453,27 +1545,38 @@ module DEBUGGER__
1453
1545
  end
1454
1546
 
1455
1547
  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"
1548
+ if !@subsession_stack.empty?
1549
+ DEBUGGER__.info "Enter subsession (nested #{@subsession_stack.size})"
1550
+ else
1551
+ DEBUGGER__.info "Enter subsession"
1552
+ stop_all_threads
1553
+ @process_group.lock
1554
+ end
1555
+
1556
+ @subsession_stack << true
1461
1557
  end
1462
1558
 
1463
1559
  private def leave_subsession type
1464
- DEBUGGER__.info "leave_subsession"
1465
- @process_group.unlock
1466
- restart_all_threads
1560
+ raise '[BUG] leave_subsession: not entered' if @subsession_stack.empty?
1561
+ @subsession_stack.pop
1562
+
1563
+ if @subsession_stack.empty?
1564
+ DEBUGGER__.info "Leave subsession"
1565
+ @process_group.unlock
1566
+ restart_all_threads
1567
+ else
1568
+ DEBUGGER__.info "Leave subsession (nested #{@subsession_stack.size})"
1569
+ end
1570
+
1467
1571
  @tc << type if type
1468
1572
  @tc = nil
1469
- @subsession = false
1470
1573
  rescue Exception => e
1471
- STDERR.puts [e, e.backtrace].inspect
1574
+ STDERR.puts PP.pp([e, e.backtrace], ''.dup)
1472
1575
  raise
1473
1576
  end
1474
1577
 
1475
1578
  def in_subsession?
1476
- @subsession
1579
+ !@subsession_stack.empty?
1477
1580
  end
1478
1581
 
1479
1582
  ## event
@@ -1487,7 +1590,7 @@ module DEBUGGER__
1487
1590
  end
1488
1591
 
1489
1592
  pending_line_breakpoints.each do |_key, bp|
1490
- if bp.path == (iseq.absolute_path || iseq.path)
1593
+ if DEBUGGER__.compare_path(bp.path, (iseq.absolute_path || iseq.path))
1491
1594
  bp.try_activate
1492
1595
  end
1493
1596
  end
@@ -1550,7 +1653,7 @@ module DEBUGGER__
1550
1653
 
1551
1654
  frames = exc.instance_variable_get(:@__debugger_postmortem_frames)
1552
1655
  @postmortem = true
1553
- ThreadClient.current.suspend :postmortem, postmortem_frames: frames
1656
+ ThreadClient.current.suspend :postmortem, postmortem_frames: frames, postmortem_exc: exc
1554
1657
  ensure
1555
1658
  @postmortem = false
1556
1659
  end
@@ -1810,6 +1913,9 @@ module DEBUGGER__
1810
1913
  puts "\nStop by #{args.first}"
1811
1914
  end
1812
1915
  end
1916
+
1917
+ def flush
1918
+ end
1813
1919
  end
1814
1920
 
1815
1921
  # manual configuration methods
@@ -1822,11 +1928,11 @@ module DEBUGGER__
1822
1928
  ::DEBUGGER__::SESSION.add_catch_breakpoint pat
1823
1929
  end
1824
1930
 
1825
- # String for requring location
1931
+ # String for requiring location
1826
1932
  # nil for -r
1827
1933
  def self.require_location
1828
1934
  locs = caller_locations
1829
- dir_prefix = /#{__dir__}/
1935
+ dir_prefix = /#{Regexp.escape(__dir__)}/
1830
1936
 
1831
1937
  locs.each do |loc|
1832
1938
  case loc.absolute_path
@@ -1846,7 +1952,7 @@ module DEBUGGER__
1846
1952
 
1847
1953
  unless defined? SESSION
1848
1954
  require_relative 'local'
1849
- initialize_session UI_LocalConsole.new
1955
+ initialize_session{ UI_LocalConsole.new }
1850
1956
  end
1851
1957
 
1852
1958
  setup_initial_suspend unless nonstop
@@ -1854,8 +1960,9 @@ module DEBUGGER__
1854
1960
 
1855
1961
  def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1856
1962
  CONFIG.set_config(**kw)
1963
+ require_relative 'server'
1857
1964
 
1858
- if port || CONFIG[:open_frontend] == 'chrome'
1965
+ if port || CONFIG[:open_frontend] == 'chrome' || (!::Addrinfo.respond_to?(:unix))
1859
1966
  open_tcp host: host, port: (port || 0), nonstop: nonstop
1860
1967
  else
1861
1968
  open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop
@@ -1869,7 +1976,7 @@ module DEBUGGER__
1869
1976
  if defined? SESSION
1870
1977
  SESSION.reset_ui UI_TcpServer.new(host: host, port: port)
1871
1978
  else
1872
- initialize_session UI_TcpServer.new(host: host, port: port)
1979
+ initialize_session{ UI_TcpServer.new(host: host, port: port) }
1873
1980
  end
1874
1981
 
1875
1982
  setup_initial_suspend unless nonstop
@@ -1882,7 +1989,7 @@ module DEBUGGER__
1882
1989
  if defined? SESSION
1883
1990
  SESSION.reset_ui UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
1884
1991
  else
1885
- initialize_session UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
1992
+ initialize_session{ UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path) }
1886
1993
  end
1887
1994
 
1888
1995
  setup_initial_suspend unless nonstop
@@ -1909,9 +2016,10 @@ module DEBUGGER__
1909
2016
  end
1910
2017
 
1911
2018
  class << self
1912
- define_method :initialize_session do |ui|
2019
+ define_method :initialize_session do |&init_ui|
1913
2020
  DEBUGGER__.info "Session start (pid: #{Process.pid})"
1914
- ::DEBUGGER__.const_set(:SESSION, Session.new(ui))
2021
+ ::DEBUGGER__.const_set(:SESSION, Session.new)
2022
+ SESSION.activate init_ui.call
1915
2023
  load_rc
1916
2024
  end
1917
2025
  end
@@ -1960,13 +2068,17 @@ module DEBUGGER__
1960
2068
  METHOD_ADDED_TRACKER = self.create_method_added_tracker
1961
2069
 
1962
2070
  SHORT_INSPECT_LENGTH = 40
1963
- def self.short_inspect obj, use_short = true
2071
+
2072
+ def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
1964
2073
  str = obj.inspect
1965
- if use_short && str.length > SHORT_INSPECT_LENGTH
1966
- str[0...SHORT_INSPECT_LENGTH] + '...'
2074
+
2075
+ if short && str.length > max_length
2076
+ str[0...max_length] + '...'
1967
2077
  else
1968
2078
  str
1969
2079
  end
2080
+ rescue Exception => e
2081
+ str = "<#inspect raises #{e.inspect}>"
1970
2082
  end
1971
2083
 
1972
2084
  def self.warn msg
@@ -2000,10 +2112,72 @@ module DEBUGGER__
2000
2112
  end
2001
2113
  end
2002
2114
 
2115
+ def self.step_in &b
2116
+ if defined?(SESSION) && SESSION.active?
2117
+ SESSION.add_iseq_breakpoint RubyVM::InstructionSequence.of(b), oneshot: true
2118
+ end
2119
+
2120
+ yield
2121
+ end
2122
+
2123
+ if File.identical?(__FILE__.upcase, __FILE__.downcase)
2124
+ # For case insensitive file system (like Windows)
2125
+ # Note that this check is not enough because case sensitive/insensitive is
2126
+ # depend on the file system. So this check is only roughly estimation.
2127
+
2128
+ def self.compare_path(a, b)
2129
+ a.downcase == b.downcase
2130
+ end
2131
+ else
2132
+ def self.compare_path(a, b)
2133
+ a == b
2134
+ end
2135
+ end
2136
+
2003
2137
  module ForkInterceptor
2004
- def fork(&given_block)
2005
- return super unless defined?(SESSION) && SESSION.active?
2138
+ if Process.respond_to? :_fork
2139
+ def _fork
2140
+ return yield unless defined?(SESSION) && SESSION.active?
2141
+
2142
+ parent_hook, child_hook = __fork_setup_for_debugger
2143
+
2144
+ super.tap do |pid|
2145
+ if pid != 0
2146
+ # after fork: parent
2147
+ parent_hook.call pid
2148
+ else
2149
+ # after fork: child
2150
+ child_hook.call
2151
+ end
2152
+ end
2153
+ end
2154
+ else
2155
+ def fork(&given_block)
2156
+ return yield unless defined?(SESSION) && SESSION.active?
2157
+ parent_hook, child_hook = __fork_setup_for_debugger
2158
+
2159
+ if given_block
2160
+ new_block = proc {
2161
+ # after fork: child
2162
+ child_hook.call
2163
+ given_block.call
2164
+ }
2165
+ super(&new_block).tap{|pid| parent_hook.call(pid)}
2166
+ else
2167
+ super.tap do |pid|
2168
+ if pid
2169
+ # after fork: parent
2170
+ parent_hook.call pid
2171
+ else
2172
+ # after fork: child
2173
+ child_hook.call
2174
+ end
2175
+ end
2176
+ end
2177
+ end
2178
+ end
2006
2179
 
2180
+ private def __fork_setup_for_debugger
2007
2181
  unless fork_mode = CONFIG[:fork_mode]
2008
2182
  if CONFIG[:parent_on_fork]
2009
2183
  fork_mode = :parent
@@ -2050,26 +2224,7 @@ module DEBUGGER__
2050
2224
  }
2051
2225
  end
2052
2226
 
2053
- if given_block
2054
- new_block = proc {
2055
- # after fork: child
2056
- child_hook.call
2057
- given_block.call
2058
- }
2059
- pid = super(&new_block)
2060
- parent_hook.call(pid)
2061
- pid
2062
- else
2063
- if pid = super
2064
- # after fork: parent
2065
- parent_hook.call pid
2066
- else
2067
- # after fork: child
2068
- child_hook.call
2069
- end
2070
-
2071
- pid
2072
- end
2227
+ return parent_hook, child_hook
2073
2228
  end
2074
2229
  end
2075
2230
 
@@ -2086,28 +2241,46 @@ module DEBUGGER__
2086
2241
  end
2087
2242
  end
2088
2243
 
2089
- if RUBY_VERSION >= '3.0.0'
2244
+ if Process.respond_to? :_fork
2245
+ module ::Process
2246
+ class << self
2247
+ prepend ForkInterceptor
2248
+ end
2249
+ end
2250
+
2251
+ # trap
2090
2252
  module ::Kernel
2091
- prepend ForkInterceptor
2092
2253
  prepend TrapInterceptor
2093
2254
  end
2255
+ module ::Signal
2256
+ class << self
2257
+ prepend TrapInterceptor
2258
+ end
2259
+ end
2094
2260
  else
2095
- class ::Object
2096
- include ForkInterceptor
2097
- include TrapInterceptor
2261
+ if RUBY_VERSION >= '3.0.0'
2262
+ module ::Kernel
2263
+ prepend ForkInterceptor
2264
+ prepend TrapInterceptor
2265
+ end
2266
+ else
2267
+ class ::Object
2268
+ include ForkInterceptor
2269
+ include TrapInterceptor
2270
+ end
2098
2271
  end
2099
- end
2100
2272
 
2101
- module ::Kernel
2102
- class << self
2103
- prepend ForkInterceptor
2104
- prepend TrapInterceptor
2273
+ module ::Kernel
2274
+ class << self
2275
+ prepend ForkInterceptor
2276
+ prepend TrapInterceptor
2277
+ end
2105
2278
  end
2106
- end
2107
2279
 
2108
- module ::Process
2109
- class << self
2110
- prepend ForkInterceptor
2280
+ module ::Process
2281
+ class << self
2282
+ prepend ForkInterceptor
2283
+ end
2111
2284
  end
2112
2285
  end
2113
2286