debug 1.0.0.beta8 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,7 +7,7 @@ module DEBUGGER__
7
7
  SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
8
8
 
9
9
  def dap_setup bytes
10
- DEBUGGER__.set_config(no_color: true)
10
+ CONFIG.set_config no_color: true
11
11
  @seq = 0
12
12
 
13
13
  $stderr.puts '[>]' + bytes if SHOW_PROTOCOL
@@ -37,6 +37,7 @@ module DEBUGGER__
37
37
  },
38
38
  ],
39
39
  supportsExceptionFilterOptions: true,
40
+ supportsStepBack: true,
40
41
 
41
42
  ## Will be supported
42
43
  # supportsExceptionOptions: true,
@@ -50,7 +51,6 @@ module DEBUGGER__
50
51
  # supportsBreakpointLocationsRequest:
51
52
 
52
53
  ## Possible?
53
- # supportsStepBack:
54
54
  # supportsRestartFrame:
55
55
  # supportsCompletionsRequest:
56
56
  # completionTriggerCharacters:
@@ -153,8 +153,8 @@ module DEBUGGER__
153
153
  when 'setFunctionBreakpoints'
154
154
  send_response req
155
155
  when 'setExceptionBreakpoints'
156
- filters = args.dig('filterOptions').map{|bp_info|
157
- case bp_info.dig('filterId')
156
+ process_filter = ->(filter_id) {
157
+ case filter_id
158
158
  when 'any'
159
159
  bp = SESSION.add_catch_breakpoint 'Exception'
160
160
  when 'RuntimeError'
@@ -163,10 +163,19 @@ module DEBUGGER__
163
163
  bp = nil
164
164
  end
165
165
  {
166
- verifiled: bp ? true : false,
166
+ verified: bp ? true : false,
167
167
  message: bp.inspect,
168
168
  }
169
169
  }
170
+
171
+ filters = args.fetch('filters').map {|filter_id|
172
+ process_filter.call(filter_id)
173
+ }
174
+
175
+ filters += args.fetch('filterOptions', {}).map{|bp_info|
176
+ process_filter.call(bp_info.dig('filterId'))
177
+ }
178
+
170
179
  send_response req, breakpoints: filters
171
180
  when 'configurationDone'
172
181
  send_response req
@@ -197,6 +206,12 @@ module DEBUGGER__
197
206
  when 'pause'
198
207
  send_response req
199
208
  Process.kill(:SIGINT, Process.pid)
209
+ when 'reverseContinue'
210
+ send_response req,
211
+ success: false, message: 'cancelled',
212
+ result: "Reverse Continue is not supported. Only \"Step back\" is supported."
213
+ when 'stepBack'
214
+ @q_msg << req
200
215
 
201
216
  ## query
202
217
  when 'threads'
@@ -212,6 +227,7 @@ module DEBUGGER__
212
227
  'evaluate',
213
228
  'source'
214
229
  @q_msg << req
230
+
215
231
  else
216
232
  raise "Unknown request: #{req.inspect}"
217
233
  end
@@ -220,7 +236,7 @@ module DEBUGGER__
220
236
 
221
237
  ## called by the SESSION thread
222
238
 
223
- def readline
239
+ def readline prompt
224
240
  @q_msg.pop || 'kill!'
225
241
  end
226
242
 
@@ -281,6 +297,13 @@ module DEBUGGER__
281
297
 
282
298
  def process_dap_request req
283
299
  case req['command']
300
+ when 'stepBack'
301
+ if @tc.recorder&.can_step_back?
302
+ @tc << [:step, :back]
303
+ else
304
+ fail_response req, message: 'cancelled'
305
+ end
306
+
284
307
  when 'stackTrace'
285
308
  tid = req.dig('arguments', 'threadId')
286
309
  if tc = find_tc(tid)
@@ -340,7 +363,7 @@ module DEBUGGER__
340
363
  fail_response req
341
364
  end
342
365
  else
343
- raise "Uknown type: #{ref.inspect}"
366
+ raise "Unknown type: #{ref.inspect}"
344
367
  end
345
368
  else
346
369
  fail_response req
@@ -365,7 +388,6 @@ module DEBUGGER__
365
388
  else
366
389
  fail_response req, message: 'not found...'
367
390
  end
368
-
369
391
  return :retry
370
392
  else
371
393
  raise "Unknown DAP request: #{req.inspect}"
@@ -438,8 +460,8 @@ module DEBUGGER__
438
460
  when :backtrace
439
461
  event! :dap_result, :backtrace, req, {
440
462
  stackFrames: @target_frames.map.with_index{|frame, i|
441
- path = frame.realpath
442
- ref = frame.file_lines unless File.exist?(path)
463
+ path = frame.realpath || frame.path
464
+ ref = frame.file_lines unless path && File.exist?(path)
443
465
 
444
466
  {
445
467
  # id: ??? # filled by SESSION
@@ -457,7 +479,15 @@ module DEBUGGER__
457
479
  when :scopes
458
480
  fid = args.shift
459
481
  frame = @target_frames[fid]
460
- lnum = frame.binding ? frame.binding.local_variables.size : 0
482
+
483
+ lnum =
484
+ if frame.binding
485
+ frame.binding.local_variables.size
486
+ elsif vars = frame.local_variables
487
+ vars.size
488
+ else
489
+ 0
490
+ end
461
491
 
462
492
  event! :dap_result, :scopes, req, scopes: [{
463
493
  name: 'Local variables',
@@ -485,6 +515,10 @@ module DEBUGGER__
485
515
  vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
486
516
  vars.unshift variable('%return', frame.return_value) if frame.has_return_value
487
517
  vars.unshift variable('%self', b.receiver)
518
+ elsif lvars = frame.local_variables
519
+ vars = lvars.map{|var, val|
520
+ variable(var, val)
521
+ }
488
522
  else
489
523
  vars = [variable('%self', frame.self)]
490
524
  vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
@@ -555,7 +589,7 @@ module DEBUGGER__
555
589
  end
556
590
  event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
557
591
  else
558
- raise "Unkown req: #{args.inspect}"
592
+ raise "Unknown req: #{args.inspect}"
559
593
  end
560
594
  end
561
595
 
data/lib/debug/session.rb CHANGED
@@ -1,12 +1,31 @@
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'
8
22
  require_relative 'source_repository'
9
23
  require_relative 'breakpoint'
24
+ require_relative 'tracer'
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
10
29
 
11
30
  require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
12
31
 
@@ -54,6 +73,7 @@ end
54
73
 
55
74
  module DEBUGGER__
56
75
  PresetCommand = Struct.new(:commands, :source, :auto_continue)
76
+ class PostmortemError < RuntimeError; end
57
77
 
58
78
  class Session
59
79
  def initialize ui
@@ -65,71 +85,125 @@ module DEBUGGER__
65
85
  # "Foo#bar" => MethodBreakpoint
66
86
  # [:watch, ivar] => WatchIVarBreakpoint
67
87
  # [:check, expr] => CheckBreakpoint
68
- @th_clients = {} # {Thread => ThreadClient}
88
+ #
89
+ @tracers = []
90
+ @th_clients = nil # {Thread => ThreadClient}
69
91
  @q_evt = Queue.new
70
92
  @displays = []
71
93
  @tc = nil
72
94
  @tc_id = 0
73
95
  @preset_command = nil
96
+ @postmortem_hook = nil
97
+ @postmortem = false
98
+ @thread_stopper = nil
74
99
 
75
100
  @frame_map = {} # {id => [threadId, frame_depth]} for DAP
76
101
  @var_map = {1 => [:globals], } # {id => ...} for DAP
77
102
  @src_map = {} # {id => src}
78
103
 
79
104
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
80
- unless @management_threads.include? Thread.current
81
- ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
82
- end
105
+ ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
83
106
  }
84
107
  @tp_load_script.enable
85
108
 
109
+ activate
110
+ end
111
+
112
+ def active?
113
+ !@q_evt.closed?
114
+ end
115
+
116
+ def break_at? file, line
117
+ @bps.has_key? [file, line]
118
+ end
119
+
120
+ def check_forked
121
+ unless active?
122
+ # TODO: Support it
123
+ raise 'DEBUGGER: stop at forked process is not supported yet.'
124
+ end
125
+ end
126
+
127
+ def activate on_fork: false
86
128
  @session_server = Thread.new do
129
+ Thread.current.name = 'DEBUGGER__::SESSION@server'
87
130
  Thread.current.abort_on_exception = true
88
131
  session_server_main
89
132
  end
90
133
 
91
- @management_threads = [@session_server]
92
- @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
93
-
94
134
  setup_threads
95
135
 
136
+ thc = thread_client @session_server
137
+ thc.is_management
138
+
139
+ if on_fork
140
+ @tp_thread_begin.disable
141
+ @tp_thread_begin = nil
142
+ @ui.activate on_fork: true
143
+ end
144
+
145
+ if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
146
+ thc.is_management
147
+ end
148
+
96
149
  @tp_thread_begin = TracePoint.new(:thread_begin){|tp|
97
- unless @management_threads.include?(th = Thread.current)
98
- ThreadClient.current.on_thread_begin th
99
- end
150
+ th = Thread.current
151
+ ThreadClient.current.on_thread_begin th
100
152
  }
101
153
  @tp_thread_begin.enable
102
154
  end
103
155
 
104
- def active?
105
- @ui ? true : false
156
+ def deactivate
157
+ thread_client.deactivate
158
+ @thread_stopper.disable if @thread_stopper
159
+ @tp_load_script.disable
160
+ @tp_thread_begin.disable
161
+ @bps.each{|k, bp| bp.disable}
162
+ @th_clients.each{|th, thc| thc.close}
163
+ @tracers.each{|t| t.disable}
164
+ @q_evt.close
165
+ @ui&.deactivate
166
+ @ui = nil
106
167
  end
107
168
 
108
169
  def reset_ui ui
109
170
  @ui.close
110
171
  @ui = ui
111
- @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
172
+ end
173
+
174
+ def pop_event
175
+ @q_evt.pop
112
176
  end
113
177
 
114
178
  def session_server_main
115
- while evt = @q_evt.pop
116
- # varible `@internal_info` is only used for test
179
+ while evt = pop_event
180
+ # variable `@internal_info` is only used for test
117
181
  tc, output, ev, @internal_info, *ev_args = evt
118
182
  output.each{|str| @ui.puts str}
119
183
 
120
184
  case ev
121
185
  when :init
122
186
  wait_command_loop tc
187
+
123
188
  when :load
124
189
  iseq, src = ev_args
125
190
  on_load iseq, src
126
191
  @ui.event :load
127
192
  tc << :continue
193
+
194
+ when :trace
195
+ trace_id, msg = ev_args
196
+ if t = @tracers.find{|t| t.object_id == trace_id}
197
+ t.puts msg
198
+ end
199
+ tc << :continue
200
+
128
201
  when :thread_begin
129
202
  th = ev_args.shift
130
203
  on_thread_begin th
131
204
  @ui.event :thread_begin, th
132
205
  tc << :continue
206
+
133
207
  when :suspend
134
208
  case ev_args.first
135
209
  when :breakpoint
@@ -142,10 +216,12 @@ module DEBUGGER__
142
216
  end
143
217
 
144
218
  if @displays.empty?
219
+ stop_all_threads
145
220
  wait_command_loop tc
146
221
  else
147
222
  tc << [:eval, :display, @displays]
148
223
  end
224
+
149
225
  when :result
150
226
  case ev_args.first
151
227
  when :try_display
@@ -156,14 +232,22 @@ module DEBUGGER__
156
232
  @ui.puts "canceled: #{@displays.pop}"
157
233
  end
158
234
  end
235
+ stop_all_threads
236
+
159
237
  when :method_breakpoint, :watch_breakpoint
160
238
  bp = ev_args[1]
161
239
  if bp
162
- add_breakpoint(bp)
240
+ add_bp(bp)
163
241
  show_bps bp
164
242
  else
165
243
  # can't make a bp
166
244
  end
245
+ when :trace_pass
246
+ obj_id = ev_args[1]
247
+ obj_inspect = ev_args[2]
248
+ opt = ev_args[3]
249
+ @tracers << t = ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
250
+ @ui.puts "Enable #{t.to_s}"
167
251
  else
168
252
  # ignore
169
253
  end
@@ -176,27 +260,24 @@ module DEBUGGER__
176
260
  end
177
261
  end
178
262
  ensure
179
- @tp_load_script.disable
180
- @tp_thread_begin.disable
181
- @bps.each{|k, bp| bp.disable}
182
- @th_clients.each{|th, thc| thc.close}
183
- @ui = nil
263
+ deactivate
184
264
  end
185
265
 
186
266
  def add_preset_commands name, cmds, kick: true, continue: true
187
267
  cs = cmds.map{|c|
188
- c = c.strip.gsub(/\A\s*\#.*/, '').strip
189
- c unless c.empty?
190
- }.compact
191
-
192
- unless cs.empty?
193
- if @preset_command
194
- @preset_command.commands += cs
195
- else
196
- @preset_command = PresetCommand.new(cs, name, continue)
197
- end
198
- ThreadClient.current.on_init name if kick
268
+ c.each_line.map{|line|
269
+ line = line.strip.gsub(/\A\s*\#.*/, '').strip
270
+ line unless line.empty?
271
+ }.compact
272
+ }.flatten.compact
273
+
274
+ if @preset_command && !@preset_command.commands.empty?
275
+ @preset_command.commands += cs
276
+ else
277
+ @preset_command = PresetCommand.new(cs, name, continue)
199
278
  end
279
+
280
+ ThreadClient.current.on_init name if kick
200
281
  end
201
282
 
202
283
  def source iseq
@@ -213,20 +294,26 @@ module DEBUGGER__
213
294
 
214
295
  def wait_command_loop tc
215
296
  @tc = tc
216
- stop_all_threads do
217
- loop do
218
- case wait_command
219
- when :retry
220
- # nothing
221
- else
222
- break
223
- end
224
- rescue Interrupt
225
- retry
297
+
298
+ loop do
299
+ case wait_command
300
+ when :retry
301
+ # nothing
302
+ else
303
+ break
226
304
  end
305
+ rescue Interrupt
306
+ @ui.puts "\n^C"
307
+ retry
308
+ end
309
+ end
310
+
311
+ def prompt
312
+ if @postmortem
313
+ '(rdbg:postmortem) '
314
+ else
315
+ '(rdbg) '
227
316
  end
228
- ensure
229
- @tc = nil
230
317
  end
231
318
 
232
319
  def wait_command
@@ -234,7 +321,9 @@ module DEBUGGER__
234
321
  if @preset_command.commands.empty?
235
322
  if @preset_command.auto_continue
236
323
  @preset_command = nil
324
+
237
325
  @tc << :continue
326
+ restart_all_threads
238
327
  return
239
328
  else
240
329
  @preset_command = nil
@@ -246,7 +335,7 @@ module DEBUGGER__
246
335
  end
247
336
  else
248
337
  @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
249
- line = @ui.readline
338
+ line = @ui.readline prompt
250
339
  end
251
340
 
252
341
  case line
@@ -280,27 +369,37 @@ module DEBUGGER__
280
369
 
281
370
  # * `s[tep]`
282
371
  # * Step in. Resume the program until next breakable point.
372
+ # * `s[tep] <n>`
373
+ # * Step in, resume the program at `<n>`th breakable point.
283
374
  when 's', 'step'
284
375
  cancel_auto_continue
285
- @tc << [:step, :in]
376
+ check_postmortem
377
+ step_command :in, arg
286
378
 
287
379
  # * `n[ext]`
288
380
  # * Step over. Resume the program until next line.
381
+ # * `n[ext] <n>`
382
+ # * Step over, same as `step <n>`.
289
383
  when 'n', 'next'
290
384
  cancel_auto_continue
291
- @tc << [:step, :next]
385
+ check_postmortem
386
+ step_command :next, arg
292
387
 
293
388
  # * `fin[ish]`
294
389
  # * Finish this frame. Resume the program until the current frame is finished.
390
+ # * `fin[ish] <n>`
391
+ # * Finish frames, same as `step <n>`.
295
392
  when 'fin', 'finish'
296
393
  cancel_auto_continue
297
- @tc << [:step, :finish]
394
+ check_postmortem
395
+ step_command :finish, arg
298
396
 
299
397
  # * `c[ontinue]`
300
398
  # * Resume the program.
301
399
  when 'c', 'continue'
302
400
  cancel_auto_continue
303
401
  @tc << :continue
402
+ restart_all_threads
304
403
 
305
404
  # * `q[uit]` or `Ctrl-D`
306
405
  # * Finish debugger (with the debuggee process on non-remote debugging).
@@ -308,6 +407,7 @@ module DEBUGGER__
308
407
  if ask 'Really quit?'
309
408
  @ui.quit arg.to_i
310
409
  @tc << :continue
410
+ restart_all_threads
311
411
  else
312
412
  return :retry
313
413
  end
@@ -316,7 +416,7 @@ module DEBUGGER__
316
416
  # * Same as q[uit] but without the confirmation prompt.
317
417
  when 'q!', 'quit!'
318
418
  @ui.quit arg.to_i
319
- @tc << :continue
419
+ restart_all_threads
320
420
 
321
421
  # * `kill`
322
422
  # * Stop the debuggee process with `Kernal#exit!`.
@@ -354,6 +454,8 @@ module DEBUGGER__
354
454
  # * break if: `<expr>` is true at any lines.
355
455
  # * Note that this feature is super slow.
356
456
  when 'b', 'break'
457
+ check_postmortem
458
+
357
459
  if arg == nil
358
460
  show_bps
359
461
  return :retry
@@ -370,6 +472,7 @@ module DEBUGGER__
370
472
 
371
473
  # skip
372
474
  when 'bv'
475
+ check_postmortem
373
476
  require 'json'
374
477
 
375
478
  h = Hash.new{|h, k| h[k] = []}
@@ -396,8 +499,10 @@ module DEBUGGER__
396
499
  # * `catch <Error>`
397
500
  # * Set breakpoint on raising `<Error>`.
398
501
  when 'catch'
502
+ check_postmortem
503
+
399
504
  if arg
400
- bp = add_catch_breakpoint arg
505
+ bp = repl_add_catch_breakpoint arg
401
506
  show_bps bp if bp
402
507
  else
403
508
  show_bps
@@ -408,6 +513,8 @@ module DEBUGGER__
408
513
  # * Stop the execution when the result of current scope's `@ivar` is changed.
409
514
  # * Note that this feature is super slow.
410
515
  when 'wat', 'watch'
516
+ check_postmortem
517
+
411
518
  if arg && arg.match?(/\A@\w+/)
412
519
  @tc << [:breakpoint, :watch, arg]
413
520
  else
@@ -420,15 +527,17 @@ module DEBUGGER__
420
527
  # * `del[ete] <bpnum>`
421
528
  # * delete specified breakpoint.
422
529
  when 'del', 'delete'
530
+ check_postmortem
531
+
423
532
  bp =
424
533
  case arg
425
534
  when nil
426
535
  show_bps
427
536
  if ask "Remove all breakpoints?", 'N'
428
- delete_breakpoint
537
+ delete_bp
429
538
  end
430
539
  when /\d+/
431
- delete_breakpoint arg.to_i
540
+ delete_bp arg.to_i
432
541
  else
433
542
  nil
434
543
  end
@@ -501,25 +610,57 @@ module DEBUGGER__
501
610
 
502
611
  @tc << [:show, :edit, arg]
503
612
 
504
- # * `i[nfo]`, `i[nfo] l[ocal[s]]`
613
+ # * `i[nfo]`
614
+ # * Show information about current frame (local/instance variables and defined constants).
615
+ # * `i[nfo] l[ocal[s]]`
505
616
  # * Show information about the current frame (local variables)
506
617
  # * It includes `self` as `%self` and a return value as `%return`.
618
+ # * `i[nfo] i[var[s]]` or `i[nfo] instance`
619
+ # * Show information about instance variables about `self`.
620
+ # * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
621
+ # * Show information about accessible constants except toplevel constants.
622
+ # * `i[nfo] g[lobal[s]]`
623
+ # * Show information about global variables
624
+ # * `i[nfo] ... </pattern/>`
625
+ # * Filter the output with `</pattern/>`.
507
626
  # * `i[nfo] th[read[s]]`
508
627
  # * Show all threads (same as `th[read]`).
509
628
  when 'i', 'info'
510
- case arg
629
+ if /\/(.+)\/\z/ =~ arg
630
+ pat = Regexp.compile($1)
631
+ sub = $~.pre_match.strip
632
+ else
633
+ sub = arg
634
+ end
635
+
636
+ case sub
511
637
  when nil
512
- @tc << [:show, :local]
513
- when 'l', /locals?/
514
- @tc << [:show, :local]
638
+ @tc << [:show, :default, pat] # something useful
639
+ when 'l', /^locals?/
640
+ @tc << [:show, :locals, pat]
641
+ when 'i', /^ivars?/i, /^instance[_ ]variables?/i
642
+ @tc << [:show, :ivars, pat]
643
+ when 'c', /^consts?/i, /^constants?/i
644
+ @tc << [:show, :consts, pat]
645
+ when 'g', /^globals?/i, /^global[_ ]variables?/i
646
+ @tc << [:show, :globals, pat]
515
647
  when 'th', /threads?/
516
648
  thread_list
517
649
  return :retry
518
650
  else
651
+ @ui.puts "unrecognized argument for info command: #{arg}"
519
652
  show_help 'info'
520
653
  return :retry
521
654
  end
522
655
 
656
+ # * `o[utline]` or `ls`
657
+ # * Show you available methods, constants, local variables, and instance variables in the current scope.
658
+ # * `o[utline] <expr>` or `ls <expr>`
659
+ # * Show you available methods and instance variables of the given object.
660
+ # * If the object is a class/module, it also lists its constants.
661
+ when 'outline', 'o', 'ls'
662
+ @tc << [:show, :outline, arg]
663
+
523
664
  # * `display`
524
665
  # * Show display setting.
525
666
  # * `display <expr>`
@@ -581,10 +722,16 @@ module DEBUGGER__
581
722
  when 'pp'
582
723
  @tc << [:eval, :pp, arg.to_s]
583
724
 
584
- # * `e[val] <expr>`
725
+ # * `eval <expr>`
585
726
  # * Evaluate `<expr>` on the current frame.
586
- when 'e', 'eval', 'call'
587
- @tc << [:eval, :call, arg]
727
+ when 'eval', 'call'
728
+ if arg == nil || arg.empty?
729
+ show_help 'eval'
730
+ @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead."
731
+ return :retry
732
+ else
733
+ @tc << [:eval, :call, arg]
734
+ end
588
735
 
589
736
  # * `irb`
590
737
  # * Invoke `irb` on the current frame.
@@ -598,6 +745,105 @@ module DEBUGGER__
598
745
  # don't repeat irb command
599
746
  @repl_prev_line = nil
600
747
 
748
+ ### Trace
749
+ # * `trace`
750
+ # * Show available tracers list.
751
+ # * `trace line`
752
+ # * Add a line tracer. It indicates line events.
753
+ # * `trace call`
754
+ # * Add a call tracer. It indicate call/return events.
755
+ # * `trace exception`
756
+ # * Add an exception tracer. It indicates raising exceptions.
757
+ # * `trace object <expr>`
758
+ # * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
759
+ # * `trace ... </pattern/>`
760
+ # * Indicates only matched events to `</pattern/>` (RegExp).
761
+ # * `trace ... into: <file>`
762
+ # * Save trace information into: `<file>`.
763
+ # * `trace off <num>`
764
+ # * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
765
+ # * `trace off [line|call|pass]`
766
+ # * Disable all tracers. If `<type>` is provided, disable specified type tracers.
767
+ when 'trace'
768
+ if (re = /\s+into:\s*(.+)/) =~ arg
769
+ into = $1
770
+ arg.sub!(re, '')
771
+ end
772
+
773
+ if (re = /\s\/(.+)\/\z/) =~ arg
774
+ pattern = $1
775
+ arg.sub!(re, '')
776
+ end
777
+
778
+ case arg
779
+ when nil
780
+ @ui.puts 'Tracers:'
781
+ @tracers.each_with_index{|t, i|
782
+ @ui.puts "* \##{i} #{t}"
783
+ }
784
+ @ui.puts
785
+ return :retry
786
+
787
+ when /\Aline\z/
788
+ @tracers << t = LineTracer.new(@ui, pattern: pattern, into: into)
789
+ @ui.puts "Enable #{t.to_s}"
790
+ return :retry
791
+
792
+ when /\Acall\z/
793
+ @tracers << t = CallTracer.new(@ui, pattern: pattern, into: into)
794
+ @ui.puts "Enable #{t.to_s}"
795
+ return :retry
796
+
797
+ when /\Aexception\z/
798
+ @tracers << t = ExceptionTracer.new(@ui, pattern: pattern, into: into)
799
+ @ui.puts "Enable #{t.to_s}"
800
+ return :retry
801
+
802
+ when /\Aobject\s+(.+)/
803
+ @tc << [:trace, :object, $1.strip, {pattern: pattern, into: into}]
804
+
805
+ when /\Aoff\s+(\d+)\z/
806
+ if t = @tracers[$1.to_i]
807
+ t.disable
808
+ @ui.puts "Disable #{t.to_s}"
809
+ else
810
+ @ui.puts "Unmatched: #{$1}"
811
+ end
812
+ return :retry
813
+
814
+ when /\Aoff(\s+(line|call|exception|object))?\z/
815
+ @tracers.each{|t|
816
+ if $2.nil? || t.type == $2
817
+ t.disable
818
+ @ui.puts "Disable #{t.to_s}"
819
+ end
820
+ }
821
+ return :retry
822
+
823
+ else
824
+ @ui.puts "Unknown trace option: #{arg.inspect}"
825
+ return :retry
826
+ end
827
+
828
+ # Record
829
+ # * `record`
830
+ # * Show recording status.
831
+ # * `record [on|off]`
832
+ # * Start/Stop recording.
833
+ # * `step back`
834
+ # * Start replay. Step back with the last execution log.
835
+ # * `s[tep]` does stepping forward with the last log.
836
+ # * `step reset`
837
+ # * Stop replay .
838
+ when 'record'
839
+ case arg
840
+ when nil, 'on', 'off'
841
+ @tc << [:record, arg&.to_sym]
842
+ else
843
+ @ui.puts "unknown command: #{arg}"
844
+ return :retry
845
+ end
846
+
601
847
  ### Thread control
602
848
 
603
849
  # * `th[read]`
@@ -630,13 +876,28 @@ module DEBUGGER__
630
876
  config_command arg
631
877
  return :retry
632
878
 
879
+ # * `source <file>`
880
+ # * Evaluate lines in `<file>` as debug commands.
881
+ when 'source'
882
+ if arg
883
+ begin
884
+ cmds = File.readlines(path = File.expand_path(arg))
885
+ add_preset_commands path, cmds, kick: true, continue: false
886
+ rescue Errno::ENOENT
887
+ @ui.puts "File not found: #{arg}"
888
+ end
889
+ else
890
+ show_help 'source'
891
+ end
892
+ return :retry
893
+
633
894
  ### Help
634
895
 
635
896
  # * `h[elp]`
636
897
  # * Show help for all commands.
637
898
  # * `h[elp] <command>`
638
899
  # * Show help for the given command.
639
- when 'h', 'help'
900
+ when 'h', 'help', '?'
640
901
  if arg
641
902
  show_help arg
642
903
  else
@@ -646,21 +907,56 @@ module DEBUGGER__
646
907
 
647
908
  ### END
648
909
  else
649
- @ui.puts "unknown command: #{line}"
910
+ @tc << [:eval, :pp, line]
911
+ =begin
650
912
  @repl_prev_line = nil
913
+ @ui.puts "unknown command: #{line}"
914
+ begin
915
+ require 'did_you_mean'
916
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: DEBUGGER__.commands)
917
+ correction = spell_checker.correct(line.split(/\s/).first || '')
918
+ @ui.puts "Did you mean? #{correction.join(' or ')}" unless correction.empty?
919
+ rescue LoadError
920
+ # Don't use D
921
+ end
651
922
  return :retry
923
+ =end
652
924
  end
653
925
 
654
926
  rescue Interrupt
655
927
  return :retry
656
928
  rescue SystemExit
657
929
  raise
930
+ rescue PostmortemError => e
931
+ @ui.puts e.message
932
+ return :retry
658
933
  rescue Exception => e
659
934
  @ui.puts "[REPL ERROR] #{e.inspect}"
660
935
  @ui.puts e.backtrace.map{|e| ' ' + e}
661
936
  return :retry
662
937
  end
663
938
 
939
+ def step_command type, arg
940
+ case arg
941
+ when nil
942
+ @tc << [:step, type]
943
+ restart_all_threads
944
+ when /\A\d+\z/
945
+ @tc << [:step, type, arg.to_i]
946
+ restart_all_threads
947
+ when /\Aback\z/, /\Areset\z/
948
+ if type != :in
949
+ @ui.puts "only `step #{arg}` is supported."
950
+ :retry
951
+ else
952
+ @tc << [:step, arg.to_sym]
953
+ end
954
+ else
955
+ @ui.puts "Unknown option: #{arg}"
956
+ :retry
957
+ end
958
+ end
959
+
664
960
  def config_show key
665
961
  key = key.to_sym
666
962
  if CONFIG_SET[key]
@@ -682,9 +978,9 @@ module DEBUGGER__
682
978
  if CONFIG_SET[key = key.to_sym]
683
979
  begin
684
980
  if append
685
- DEBUGGER__.append_config(key, val)
981
+ CONFIG.append_config(key, val)
686
982
  else
687
- DEBUGGER__.set_config({key => val})
983
+ CONFIG[key] = val
688
984
  end
689
985
  rescue => e
690
986
  @ui.puts e.message
@@ -703,7 +999,7 @@ module DEBUGGER__
703
999
 
704
1000
  when /\Aunset\s+(.+)\z/
705
1001
  if CONFIG_SET[key = $1.to_sym]
706
- DEBUGGER__.set_config({key => nil})
1002
+ CONFIG[key] = nil
707
1003
  end
708
1004
  config_show key
709
1005
 
@@ -758,51 +1054,7 @@ module DEBUGGER__
758
1054
  end
759
1055
  end
760
1056
 
761
- def msig klass, receiver
762
- if klass.singleton_class?
763
- "#{receiver}."
764
- else
765
- "#{klass}#"
766
- end
767
- end
768
-
769
- def pretty_tp tp
770
- loc = "#{tp.path}:#{tp.lineno}"
771
- level = caller.size
772
-
773
- info =
774
- case tp.event
775
- when :line
776
- "line at #{loc}"
777
- when :call, :c_call
778
- klass = tp.defined_class
779
- "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} at #{loc}"
780
- when :return, :c_return
781
- klass = tp.defined_class
782
- "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} => #{tp.return_value.inspect} at #{loc}"
783
- when :b_call
784
- "b_call at #{loc}"
785
- when :b_return
786
- "b_return => #{tp.return_value} at #{loc}"
787
- when :class
788
- "class #{tp.self} at #{loc}"
789
- when :end
790
- "class #{tp.self} end at #{loc}"
791
- else
792
- "#{tp.event} at #{loc}"
793
- end
794
-
795
- case tp.event
796
- when :call, :b_call, :return, :b_return, :class, :end
797
- level -= 1
798
- end
799
-
800
- "Tracing:#{' ' * level} #{info}"
801
- rescue => e
802
- p e
803
- pp e.backtrace
804
- exit!
805
- end
1057
+ # breakpoint management
806
1058
 
807
1059
  def iterate_bps
808
1060
  deleted_bps = []
@@ -834,7 +1086,29 @@ module DEBUGGER__
834
1086
  nil
835
1087
  end
836
1088
 
837
- def delete_breakpoint arg = nil
1089
+ def rehash_bps
1090
+ bps = @bps.values
1091
+ @bps.clear
1092
+ bps.each{|bp|
1093
+ add_bp bp
1094
+ }
1095
+ end
1096
+
1097
+ def add_bp bp
1098
+ # don't repeat commands that add breakpoints
1099
+ @repl_prev_line = nil
1100
+
1101
+ if @bps.has_key? bp.key
1102
+ unless bp.duplicable?
1103
+ @ui.puts "duplicated breakpoint: #{bp}"
1104
+ bp.disable
1105
+ end
1106
+ else
1107
+ @bps[bp.key] = bp
1108
+ end
1109
+ end
1110
+
1111
+ def delete_bp arg = nil
838
1112
  case arg
839
1113
  when nil
840
1114
  @bps.each{|key, bp| bp.delete}
@@ -873,14 +1147,14 @@ module DEBUGGER__
873
1147
 
874
1148
  case expr[:sig]
875
1149
  when /\A(\d+)\z/
876
- add_line_breakpoint @tc.location.path, $1.to_i, cond: expr[:if], command: cmd
1150
+ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd
877
1151
  when /\A(.+)[:\s+](\d+)\z/
878
- add_line_breakpoint $1, $2.to_i, cond: expr[:if], command: cmd
1152
+ add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
879
1153
  when /\A(.+)([\.\#])(.+)\z/
880
- @tc << [:breakpoint, :method, $1, $2, $3, expr[:if], cmd]
1154
+ @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd]
881
1155
  return :noretry
882
1156
  when nil
883
- add_check_breakpoint expr[:if]
1157
+ add_check_breakpoint cond
884
1158
  else
885
1159
  @ui.puts "Unknown breakpoint format: #{arg}"
886
1160
  @ui.puts
@@ -888,6 +1162,34 @@ module DEBUGGER__
888
1162
  end
889
1163
  end
890
1164
 
1165
+ def repl_add_catch_breakpoint arg
1166
+ expr = parse_break arg.strip
1167
+ cond = expr[:if]
1168
+ cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1169
+
1170
+ bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd)
1171
+ add_bp bp
1172
+ end
1173
+
1174
+ def add_catch_breakpoint pat
1175
+ bp = CatchBreakpoint.new(pat)
1176
+ add_bp bp
1177
+ end
1178
+
1179
+ def add_check_breakpoint expr
1180
+ bp = CheckBreakpoint.new(expr)
1181
+ add_bp bp
1182
+ end
1183
+
1184
+ def add_line_breakpoint file, line, **kw
1185
+ file = resolve_path(file)
1186
+ bp = LineBreakpoint.new(file, line, **kw)
1187
+
1188
+ add_bp bp
1189
+ rescue Errno::ENOENT => e
1190
+ @ui.puts e.message
1191
+ end
1192
+
891
1193
  # threads
892
1194
 
893
1195
  def update_thread_list
@@ -896,17 +1198,15 @@ module DEBUGGER__
896
1198
  unmanaged = []
897
1199
 
898
1200
  list.each{|th|
899
- case
900
- when th == Thread.current
901
- # ignore
902
- when @management_threads.include?(th)
903
- # ignore
904
- when @th_clients.has_key?(th)
905
- thcs << @th_clients[th]
1201
+ if thc = @th_clients[th]
1202
+ if !thc.management?
1203
+ thcs << thc
1204
+ end
906
1205
  else
907
1206
  unmanaged << th
908
1207
  end
909
1208
  }
1209
+
910
1210
  return thcs.sort_by{|thc| thc.id}, unmanaged
911
1211
  end
912
1212
 
@@ -933,7 +1233,7 @@ module DEBUGGER__
933
1233
  thcs, _unmanaged_ths = update_thread_list
934
1234
 
935
1235
  if tc = thcs[n]
936
- if tc.mode
1236
+ if tc.waiting?
937
1237
  @tc = tc
938
1238
  else
939
1239
  @ui.puts "#{tc.thread} is not controllable yet."
@@ -947,11 +1247,11 @@ module DEBUGGER__
947
1247
  end
948
1248
 
949
1249
  def setup_threads
950
- stop_all_threads do
951
- Thread.list.each{|th|
952
- thread_client_create(th)
953
- }
954
- end
1250
+ @th_clients = {}
1251
+
1252
+ Thread.list.each{|th|
1253
+ thread_client_create(th)
1254
+ }
955
1255
  end
956
1256
 
957
1257
  def on_thread_begin th
@@ -963,8 +1263,7 @@ module DEBUGGER__
963
1263
  end
964
1264
  end
965
1265
 
966
- def thread_client
967
- thr = Thread.current
1266
+ def thread_client thr = Thread.current
968
1267
  if @th_clients.has_key? thr
969
1268
  @th_clients[thr]
970
1269
  else
@@ -972,84 +1271,67 @@ module DEBUGGER__
972
1271
  end
973
1272
  end
974
1273
 
975
- def stop_all_threads
976
- current = Thread.current
1274
+ private def thread_stopper
1275
+ @thread_stopper ||= TracePoint.new(:line) do
1276
+ # run on each thread
1277
+ tc = ThreadClient.current
1278
+ next if tc.management?
1279
+ next unless tc.running?
1280
+ next if tc == @tc
977
1281
 
978
- if Thread.list.size > 1
979
- TracePoint.new(:line) do
980
- th = Thread.current
981
- if current == th || @management_threads.include?(th)
982
- next
983
- else
984
- tc = ThreadClient.current
985
- tc.on_pause
986
- end
987
- end.enable do
988
- yield
989
- ensure
990
- @th_clients.each{|thr, tc|
991
- case thr
992
- when current, (@tc && @tc.thread)
993
- next
994
- else
995
- tc << :continue if thr != Thread.current
996
- end
997
- }
998
- end
999
- else
1000
- yield
1282
+ tc.on_pause
1001
1283
  end
1002
1284
  end
1003
1285
 
1004
- ## event
1005
-
1006
- def on_load iseq, src
1007
- DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1008
- @sr.add iseq, src
1009
-
1010
- pending_line_breakpoints do |bp|
1011
- if bp.path == (iseq.absolute_path || iseq.path)
1012
- bp.try_activate
1013
- end
1014
- end
1286
+ private def running_thread_clients_count
1287
+ @th_clients.count{|th, tc|
1288
+ next if tc.management?
1289
+ next unless tc.running?
1290
+ true
1291
+ }
1015
1292
  end
1016
1293
 
1017
- # breakpoint management
1294
+ private def waiting_thread_clients
1295
+ @th_clients.map{|th, tc|
1296
+ next if tc.management?
1297
+ next unless tc.waiting?
1298
+ tc
1299
+ }.compact
1300
+ end
1018
1301
 
1019
- def add_breakpoint bp
1020
- # don't repeat commands that add breakpoints
1021
- @repl_prev_line = nil
1302
+ private def stop_all_threads
1303
+ return if running_thread_clients_count == 0
1022
1304
 
1023
- if @bps.has_key? bp.key
1024
- unless bp.duplicable?
1025
- @ui.puts "duplicated breakpoint: #{bp}"
1026
- bp.disable
1027
- end
1028
- else
1029
- @bps[bp.key] = bp
1030
- end
1305
+ stopper = thread_stopper
1306
+ stopper.enable unless stopper.enabled?
1031
1307
  end
1032
1308
 
1033
- def rehash_bps
1034
- bps = @bps.values
1035
- @bps.clear
1036
- bps.each{|bp|
1037
- add_breakpoint bp
1309
+ private def restart_all_threads
1310
+ stopper = thread_stopper
1311
+ stopper.disable if stopper.enabled?
1312
+
1313
+ waiting_thread_clients.each{|tc|
1314
+ next if @tc == tc
1315
+ tc << :continue
1038
1316
  }
1317
+ @tc = nil
1039
1318
  end
1040
1319
 
1041
- def break? file, line
1042
- @bps.has_key? [file, line]
1043
- end
1320
+ ## event
1044
1321
 
1045
- def add_catch_breakpoint arg
1046
- bp = CatchBreakpoint.new(arg)
1047
- add_breakpoint bp
1048
- end
1322
+ def on_load iseq, src
1323
+ DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1324
+ @sr.add iseq, src
1049
1325
 
1050
- def add_check_breakpoint expr
1051
- bp = CheckBreakpoint.new(expr)
1052
- add_breakpoint bp
1326
+ pending_line_breakpoints = @bps.find_all do |key, bp|
1327
+ LineBreakpoint === bp && !bp.iseq
1328
+ end
1329
+
1330
+ pending_line_breakpoints.each do |_key, bp|
1331
+ if bp.path == (iseq.absolute_path || iseq.path)
1332
+ bp.try_activate
1333
+ end
1334
+ end
1053
1335
  end
1054
1336
 
1055
1337
  def resolve_path file
@@ -1070,23 +1352,6 @@ module DEBUGGER__
1070
1352
  raise
1071
1353
  end
1072
1354
 
1073
- def add_line_breakpoint file, line, **kw
1074
- file = resolve_path(file)
1075
- bp = LineBreakpoint.new(file, line, **kw)
1076
-
1077
- add_breakpoint bp
1078
- rescue Errno::ENOENT => e
1079
- @ui.puts e.message
1080
- end
1081
-
1082
- def pending_line_breakpoints
1083
- @bps.find_all do |key, bp|
1084
- LineBreakpoint === bp && !bp.iseq
1085
- end.each do |key, bp|
1086
- yield bp
1087
- end
1088
- end
1089
-
1090
1355
  def method_added tp
1091
1356
  b = tp.binding
1092
1357
  if var_name = b.local_variables.first
@@ -1115,10 +1380,55 @@ module DEBUGGER__
1115
1380
  @ui.width
1116
1381
  end
1117
1382
 
1118
- def check_forked
1119
- unless @session_server.status
1120
- # TODO: Support it
1121
- raise 'DEBUGGER: stop at forked process is not supported yet.'
1383
+ def check_postmortem
1384
+ if @postmortem
1385
+ raise PostmortemError, "Can not use this command on postmortem mode."
1386
+ end
1387
+ end
1388
+
1389
+ def enter_postmortem_session frames
1390
+ @postmortem = true
1391
+ ThreadClient.current.suspend :postmortem, postmortem_frames: frames
1392
+ ensure
1393
+ @postmortem = false
1394
+ end
1395
+
1396
+ def postmortem=(is_enable)
1397
+ if is_enable
1398
+ unless @postmortem_hook
1399
+ @postmortem_hook = TracePoint.new(:raise){|tp|
1400
+ exc = tp.raised_exception
1401
+ frames = DEBUGGER__.capture_frames(__dir__)
1402
+ exc.instance_variable_set(:@postmortem_frames, frames)
1403
+ }
1404
+ at_exit{
1405
+ @postmortem_hook.disable
1406
+ if CONFIG[:postmortem] && (exc = $!) != nil
1407
+ exc = exc.cause while exc.cause
1408
+
1409
+ begin
1410
+ @ui.puts "Enter postmortem mode with #{exc.inspect}"
1411
+ @ui.puts exc.backtrace.map{|e| ' ' + e}
1412
+ @ui.puts "\n"
1413
+
1414
+ enter_postmortem_session exc.instance_variable_get(:@postmortem_frames)
1415
+ rescue SystemExit
1416
+ exit!
1417
+ rescue Exception => e
1418
+ @ui = STDERR unless @ui
1419
+ @ui.puts "Error while postmortem console: #{e.inspect}"
1420
+ end
1421
+ end
1422
+ }
1423
+ end
1424
+
1425
+ if !@postmortem_hook.enabled?
1426
+ @postmortem_hook.enable
1427
+ end
1428
+ else
1429
+ if @postmortem_hook && @postmortem_hook.enabled?
1430
+ @postmortem_hook.disable
1431
+ end
1122
1432
  end
1123
1433
  end
1124
1434
  end
@@ -1165,18 +1475,18 @@ module DEBUGGER__
1165
1475
  # start methods
1166
1476
 
1167
1477
  def self.start nonstop: false, **kw
1168
- set_config(kw)
1478
+ CONFIG.set_config(**kw)
1169
1479
 
1170
1480
  unless defined? SESSION
1171
- require_relative 'console'
1172
- initialize_session UI_Console.new
1481
+ require_relative 'local'
1482
+ initialize_session UI_LocalConsole.new
1173
1483
  end
1174
1484
 
1175
1485
  setup_initial_suspend unless nonstop
1176
1486
  end
1177
1487
 
1178
- def self.open host: nil, port: ::DEBUGGER__::CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1179
- set_config(kw)
1488
+ def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1489
+ CONFIG.set_config(**kw)
1180
1490
 
1181
1491
  if port
1182
1492
  open_tcp host: host, port: port, nonstop: nonstop
@@ -1186,7 +1496,7 @@ module DEBUGGER__
1186
1496
  end
1187
1497
 
1188
1498
  def self.open_tcp host: nil, port:, nonstop: false, **kw
1189
- set_config(kw)
1499
+ CONFIG.set_config(**kw)
1190
1500
  require_relative 'server'
1191
1501
 
1192
1502
  if defined? SESSION
@@ -1199,7 +1509,7 @@ module DEBUGGER__
1199
1509
  end
1200
1510
 
1201
1511
  def self.open_unix sock_path: nil, sock_dir: nil, nonstop: false, **kw
1202
- set_config(kw)
1512
+ CONFIG.set_config(**kw)
1203
1513
  require_relative 'server'
1204
1514
 
1205
1515
  if defined? SESSION
@@ -1214,42 +1524,24 @@ module DEBUGGER__
1214
1524
  # boot utilities
1215
1525
 
1216
1526
  def self.setup_initial_suspend
1217
- if !::DEBUGGER__::CONFIG[:nonstop]
1218
- if loc = ::DEBUGGER__.require_location
1219
- # require 'debug/console' or 'debug'
1527
+ if !CONFIG[:nonstop]
1528
+ case
1529
+ when path = ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH']
1530
+ add_line_breakpoint path, 0, oneshot: true, hook_call: false
1531
+ when loc = ::DEBUGGER__.require_location
1532
+ # require 'debug/start' or 'debug'
1220
1533
  add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
1221
1534
  else
1222
1535
  # -r
1223
- add_line_breakpoint $0, 1, oneshot: true, hook_call: false
1536
+ add_line_breakpoint $0, 0, oneshot: true, hook_call: false
1224
1537
  end
1225
1538
  end
1226
1539
  end
1227
1540
 
1228
1541
  class << self
1229
1542
  define_method :initialize_session do |ui|
1230
- DEBUGGER__.warn "Session start (pid: #{Process.pid})"
1231
-
1543
+ DEBUGGER__.info "Session start (pid: #{Process.pid})"
1232
1544
  ::DEBUGGER__.const_set(:SESSION, Session.new(ui))
1233
-
1234
- # default breakpoints
1235
-
1236
- # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
1237
-
1238
- Binding.module_eval do
1239
- def break pre: nil, do: nil
1240
- return unless SESSION.active?
1241
-
1242
- if pre || (do_expr = binding.local_variable_get(:do))
1243
- cmds = ['binding.break', pre, do_expr]
1244
- end
1245
-
1246
- ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, command: cmds
1247
- true
1248
- end
1249
- alias b break
1250
- # alias bp break
1251
- end
1252
-
1253
1545
  load_rc
1254
1546
  end
1255
1547
  end
@@ -1258,10 +1550,10 @@ module DEBUGGER__
1258
1550
  [[File.expand_path('~/.rdbgrc'), true],
1259
1551
  [File.expand_path('~/.rdbgrc.rb'), true],
1260
1552
  # ['./.rdbgrc', true], # disable because of security concern
1261
- [::DEBUGGER__::CONFIG[:init_script], false],
1553
+ [CONFIG[:init_script], false],
1262
1554
  ].each{|(path, rc)|
1263
1555
  next unless path
1264
- next if rc && ::DEBUGGER__::CONFIG[:no_rc] # ignore rc
1556
+ next if rc && CONFIG[:no_rc] # ignore rc
1265
1557
 
1266
1558
  if File.file? path
1267
1559
  if path.end_with?('.rb')
@@ -1275,62 +1567,12 @@ module DEBUGGER__
1275
1567
  }
1276
1568
 
1277
1569
  # given debug commands
1278
- if ::DEBUGGER__::CONFIG[:commands]
1279
- cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
1570
+ if CONFIG[:commands]
1571
+ cmds = CONFIG[:commands].split(';;')
1280
1572
  ::DEBUGGER__::SESSION.add_preset_commands "commands", cmds, kick: false, continue: false
1281
1573
  end
1282
1574
  end
1283
1575
 
1284
- def self.parse_help
1285
- helps = Hash.new{|h, k| h[k] = []}
1286
- desc = cat = nil
1287
- cmds = []
1288
-
1289
- File.read(__FILE__).each_line do |line|
1290
- case line
1291
- when /\A\s*### (.+)/
1292
- cat = $1
1293
- break if $1 == 'END'
1294
- when /\A when (.+)/
1295
- next unless cat
1296
- next unless desc
1297
- ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
1298
- helps[cat] << [ws, desc]
1299
- desc = nil
1300
- cmds.concat ws
1301
- when /\A\s+# (\s*\*.+)/
1302
- if desc
1303
- desc << "\n" + $1
1304
- else
1305
- desc = $1
1306
- end
1307
- end
1308
- end
1309
- @commands = cmds
1310
- @helps = helps
1311
- end
1312
-
1313
- def self.helps
1314
- (defined?(@helps) && @helps) || parse_help
1315
- end
1316
-
1317
- def self.commands
1318
- (defined?(@commands) && @commands) || (parse_help; @commands)
1319
- end
1320
-
1321
- def self.help
1322
- r = []
1323
- self.helps.each{|cat, cmds|
1324
- r << "### #{cat}"
1325
- r << ''
1326
- cmds.each{|ws, desc|
1327
- r << desc
1328
- }
1329
- r << ''
1330
- }
1331
- r.join("\n")
1332
- end
1333
-
1334
1576
  class ::Module
1335
1577
  undef method_added
1336
1578
  def method_added mid; end
@@ -1357,14 +1599,6 @@ module DEBUGGER__
1357
1599
  end
1358
1600
  end
1359
1601
 
1360
- LOG_LEVELS = {
1361
- UNKNOWN: 0,
1362
- FATAL: 1,
1363
- ERROR: 2,
1364
- WARN: 3,
1365
- INFO: 4,
1366
- }.freeze
1367
-
1368
1602
  def self.warn msg
1369
1603
  log :WARN, msg
1370
1604
  end
@@ -1386,4 +1620,97 @@ module DEBUGGER__
1386
1620
  end
1387
1621
  end
1388
1622
  end
1623
+
1624
+ module ForkInterceptor
1625
+ def fork(&given_block)
1626
+ return super unless defined?(SESSION) && SESSION.active?
1627
+
1628
+ # before fork
1629
+ if CONFIG[:parent_on_fork]
1630
+ parent_hook = -> child_pid {
1631
+ # Do nothing
1632
+ }
1633
+ child_hook = -> {
1634
+ DEBUGGER__.warn "Detaching after fork from child process #{Process.pid}"
1635
+ SESSION.deactivate
1636
+ }
1637
+ else
1638
+ parent_pid = Process.pid
1639
+
1640
+ parent_hook = -> child_pid {
1641
+ DEBUGGER__.warn "Detaching after fork from parent process #{Process.pid}"
1642
+ SESSION.deactivate
1643
+
1644
+ at_exit{
1645
+ trap(:SIGINT, :IGNORE)
1646
+ Process.waitpid(child_pid)
1647
+ }
1648
+ }
1649
+ child_hook = -> {
1650
+ DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
1651
+ SESSION.activate on_fork: true
1652
+ }
1653
+ end
1654
+
1655
+ if given_block
1656
+ new_block = proc {
1657
+ # after fork: child
1658
+ child_hook.call
1659
+ given_block.call
1660
+ }
1661
+ pid = super(&new_block)
1662
+ parent_hook.call(pid)
1663
+ pid
1664
+ else
1665
+ if pid = super
1666
+ # after fork: parent
1667
+ parent_hook.call pid
1668
+ else
1669
+ # after fork: child
1670
+ child_hook.call
1671
+ end
1672
+
1673
+ pid
1674
+ end
1675
+ end
1676
+ end
1677
+
1678
+ class ::Object
1679
+ include ForkInterceptor
1680
+ end
1681
+
1682
+ module ::Process
1683
+ class << self
1684
+ prepend ForkInterceptor
1685
+ end
1686
+ end
1687
+ end
1688
+
1689
+ class Binding
1690
+ def break pre: nil, do: nil
1691
+ return if !defined?(::DEBUGGER__::SESSION) || !::DEBUGGER__::SESSION.active?
1692
+
1693
+ if pre || (do_expr = binding.local_variable_get(:do))
1694
+ cmds = ['binding.break', pre, do_expr]
1695
+ end
1696
+
1697
+ ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, command: cmds
1698
+ self
1699
+ end
1700
+ alias b break
1701
+ end
1702
+
1703
+ module Kernel
1704
+ if RUBY_VERSION >= '2.7.0'
1705
+ eval <<~RUBY, binding, __FILE__, __LINE__
1706
+ def debugger(...)
1707
+ binding.break(...)
1708
+ end
1709
+ RUBY
1710
+ else
1711
+ def debugger pre: nil, do: nil
1712
+ b = binding
1713
+ b.break pre: pre, do: b.local_variable_get(:do)
1714
+ end
1715
+ end
1389
1716
  end