debug 1.0.0.beta7 → 1.0.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.
@@ -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:
@@ -197,6 +197,12 @@ module DEBUGGER__
197
197
  when 'pause'
198
198
  send_response req
199
199
  Process.kill(:SIGINT, Process.pid)
200
+ when 'reverseContinue'
201
+ send_response req,
202
+ success: false, message: 'cancelled',
203
+ result: "Reverse Continue is not supported. Only \"Step back\" is supported."
204
+ when 'stepBack'
205
+ @q_msg << req
200
206
 
201
207
  ## query
202
208
  when 'threads'
@@ -212,6 +218,7 @@ module DEBUGGER__
212
218
  'evaluate',
213
219
  'source'
214
220
  @q_msg << req
221
+
215
222
  else
216
223
  raise "Unknown request: #{req.inspect}"
217
224
  end
@@ -220,7 +227,7 @@ module DEBUGGER__
220
227
 
221
228
  ## called by the SESSION thread
222
229
 
223
- def readline
230
+ def readline prompt
224
231
  @q_msg.pop || 'kill!'
225
232
  end
226
233
 
@@ -281,6 +288,13 @@ module DEBUGGER__
281
288
 
282
289
  def process_dap_request req
283
290
  case req['command']
291
+ when 'stepBack'
292
+ if @tc.recorder&.can_step_back?
293
+ @tc << [:step, :back]
294
+ else
295
+ fail_response req, message: 'cancelled'
296
+ end
297
+
284
298
  when 'stackTrace'
285
299
  tid = req.dig('arguments', 'threadId')
286
300
  if tc = find_tc(tid)
@@ -365,7 +379,6 @@ module DEBUGGER__
365
379
  else
366
380
  fail_response req, message: 'not found...'
367
381
  end
368
-
369
382
  return :retry
370
383
  else
371
384
  raise "Unknown DAP request: #{req.inspect}"
@@ -438,8 +451,8 @@ module DEBUGGER__
438
451
  when :backtrace
439
452
  event! :dap_result, :backtrace, req, {
440
453
  stackFrames: @target_frames.map.with_index{|frame, i|
441
- path = frame.realpath
442
- ref = frame.file_lines unless File.exist?(path)
454
+ path = frame.realpath || frame.path
455
+ ref = frame.file_lines unless path && File.exist?(path)
443
456
 
444
457
  {
445
458
  # id: ??? # filled by SESSION
@@ -457,7 +470,15 @@ module DEBUGGER__
457
470
  when :scopes
458
471
  fid = args.shift
459
472
  frame = @target_frames[fid]
460
- lnum = frame.binding ? frame.binding.local_variables.size : 0
473
+
474
+ lnum =
475
+ if frame.binding
476
+ frame.binding.local_variables.size
477
+ elsif vars = frame.local_variables
478
+ vars.size
479
+ else
480
+ 0
481
+ end
461
482
 
462
483
  event! :dap_result, :scopes, req, scopes: [{
463
484
  name: 'Local variables',
@@ -485,6 +506,10 @@ module DEBUGGER__
485
506
  vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
486
507
  vars.unshift variable('%return', frame.return_value) if frame.has_return_value
487
508
  vars.unshift variable('%self', b.receiver)
509
+ elsif lvars = frame.local_variables
510
+ vars = lvars.map{|var, val|
511
+ variable(var, val)
512
+ }
488
513
  else
489
514
  vars = [variable('%self', frame.self)]
490
515
  vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
data/lib/debug/session.rb CHANGED
@@ -7,6 +7,7 @@ require_relative 'config'
7
7
  require_relative 'thread_client'
8
8
  require_relative 'source_repository'
9
9
  require_relative 'breakpoint'
10
+ require_relative 'tracer'
10
11
 
11
12
  require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
12
13
 
@@ -54,6 +55,7 @@ end
54
55
 
55
56
  module DEBUGGER__
56
57
  PresetCommand = Struct.new(:commands, :source, :auto_continue)
58
+ class PostmortemError < RuntimeError; end
57
59
 
58
60
  class Session
59
61
  def initialize ui
@@ -65,54 +67,98 @@ module DEBUGGER__
65
67
  # "Foo#bar" => MethodBreakpoint
66
68
  # [:watch, ivar] => WatchIVarBreakpoint
67
69
  # [:check, expr] => CheckBreakpoint
68
- @th_clients = {} # {Thread => ThreadClient}
70
+ #
71
+ @tracers = []
72
+ @th_clients = nil # {Thread => ThreadClient}
69
73
  @q_evt = Queue.new
70
74
  @displays = []
71
75
  @tc = nil
72
76
  @tc_id = 0
73
77
  @preset_command = nil
78
+ @postmortem_hook = nil
79
+ @postmortem = false
80
+ @thread_stopper = nil
74
81
 
75
82
  @frame_map = {} # {id => [threadId, frame_depth]} for DAP
76
83
  @var_map = {1 => [:globals], } # {id => ...} for DAP
77
84
  @src_map = {} # {id => src}
78
85
 
79
86
  @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
87
+ ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
83
88
  }
84
89
  @tp_load_script.enable
85
90
 
91
+ activate
92
+ end
93
+
94
+ def active?
95
+ !@q_evt.closed?
96
+ end
97
+
98
+ def break_at? file, line
99
+ @bps.has_key? [file, line]
100
+ end
101
+
102
+ def check_forked
103
+ unless active?
104
+ # TODO: Support it
105
+ raise 'DEBUGGER: stop at forked process is not supported yet.'
106
+ end
107
+ end
108
+
109
+ def activate on_fork: false
86
110
  @session_server = Thread.new do
111
+ Thread.current.name = 'DEBUGGER__::SESSION@server'
87
112
  Thread.current.abort_on_exception = true
88
113
  session_server_main
89
114
  end
90
115
 
91
- @management_threads = [@session_server]
92
- @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
93
-
94
116
  setup_threads
95
117
 
118
+ thc = thread_client @session_server
119
+ thc.is_management
120
+
121
+ if on_fork
122
+ @tp_thread_begin.disable
123
+ @tp_thread_begin = nil
124
+ @ui.activate on_fork: true
125
+ end
126
+
127
+ if @ui.respond_to?(:reader_thread) && thc = thread_client(@ui.reader_thread)
128
+ thc.is_management
129
+ end
130
+
96
131
  @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
132
+ th = Thread.current
133
+ ThreadClient.current.on_thread_begin th
100
134
  }
101
135
  @tp_thread_begin.enable
102
136
  end
103
137
 
104
- def active?
105
- @ui ? true : false
138
+ def deactivate
139
+ thread_client.deactivate
140
+ @thread_stopper.disable if @thread_stopper
141
+ @tp_load_script.disable
142
+ @tp_thread_begin.disable
143
+ @bps.each{|k, bp| bp.disable}
144
+ @th_clients.each{|th, thc| thc.close}
145
+ @tracers.each{|t| t.disable}
146
+ @q_evt.close
147
+ @ui&.deactivate
148
+ @ui = nil
106
149
  end
107
150
 
108
151
  def reset_ui ui
109
152
  @ui.close
110
153
  @ui = ui
111
- @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
154
+ end
155
+
156
+ def pop_event
157
+ @q_evt.pop
112
158
  end
113
159
 
114
160
  def session_server_main
115
- while evt = @q_evt.pop
161
+ while evt = pop_event
116
162
  # varible `@internal_info` is only used for test
117
163
  tc, output, ev, @internal_info, *ev_args = evt
118
164
  output.each{|str| @ui.puts str}
@@ -120,16 +166,26 @@ module DEBUGGER__
120
166
  case ev
121
167
  when :init
122
168
  wait_command_loop tc
169
+
123
170
  when :load
124
171
  iseq, src = ev_args
125
172
  on_load iseq, src
126
173
  @ui.event :load
127
174
  tc << :continue
175
+
176
+ when :trace
177
+ trace_id, msg = ev_args
178
+ if t = @tracers.find{|t| t.object_id == trace_id}
179
+ t.puts msg
180
+ end
181
+ tc << :continue
182
+
128
183
  when :thread_begin
129
184
  th = ev_args.shift
130
185
  on_thread_begin th
131
186
  @ui.event :thread_begin, th
132
187
  tc << :continue
188
+
133
189
  when :suspend
134
190
  case ev_args.first
135
191
  when :breakpoint
@@ -142,10 +198,12 @@ module DEBUGGER__
142
198
  end
143
199
 
144
200
  if @displays.empty?
201
+ stop_all_threads
145
202
  wait_command_loop tc
146
203
  else
147
204
  tc << [:eval, :display, @displays]
148
205
  end
206
+
149
207
  when :result
150
208
  case ev_args.first
151
209
  when :try_display
@@ -156,14 +214,22 @@ module DEBUGGER__
156
214
  @ui.puts "canceled: #{@displays.pop}"
157
215
  end
158
216
  end
217
+ stop_all_threads
218
+
159
219
  when :method_breakpoint, :watch_breakpoint
160
220
  bp = ev_args[1]
161
221
  if bp
162
- add_breakpoint(bp)
222
+ add_bp(bp)
163
223
  show_bps bp
164
224
  else
165
225
  # can't make a bp
166
226
  end
227
+ when :trace_pass
228
+ obj_id = ev_args[1]
229
+ obj_inspect = ev_args[2]
230
+ opt = ev_args[3]
231
+ @tracers << t = ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
232
+ @ui.puts "Enable #{t.to_s}"
167
233
  else
168
234
  # ignore
169
235
  end
@@ -176,27 +242,24 @@ module DEBUGGER__
176
242
  end
177
243
  end
178
244
  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
245
+ deactivate
184
246
  end
185
247
 
186
248
  def add_preset_commands name, cmds, kick: true, continue: true
187
249
  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
250
+ c.each_line.map{|line|
251
+ line = line.strip.gsub(/\A\s*\#.*/, '').strip
252
+ line unless line.empty?
253
+ }.compact
254
+ }.flatten.compact
255
+
256
+ if @preset_command && !@preset_command.commands.empty?
257
+ @preset_command.commands += cs
258
+ else
259
+ @preset_command = PresetCommand.new(cs, name, continue)
199
260
  end
261
+
262
+ ThreadClient.current.on_init name if kick
200
263
  end
201
264
 
202
265
  def source iseq
@@ -213,20 +276,26 @@ module DEBUGGER__
213
276
 
214
277
  def wait_command_loop tc
215
278
  @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
279
+
280
+ loop do
281
+ case wait_command
282
+ when :retry
283
+ # nothing
284
+ else
285
+ break
226
286
  end
287
+ rescue Interrupt
288
+ @ui.puts "\n^C"
289
+ retry
290
+ end
291
+ end
292
+
293
+ def prompt
294
+ if @postmortem
295
+ '(rdbg:postmortem) '
296
+ else
297
+ '(rdbg) '
227
298
  end
228
- ensure
229
- @tc = nil
230
299
  end
231
300
 
232
301
  def wait_command
@@ -234,7 +303,9 @@ module DEBUGGER__
234
303
  if @preset_command.commands.empty?
235
304
  if @preset_command.auto_continue
236
305
  @preset_command = nil
306
+
237
307
  @tc << :continue
308
+ restart_all_threads
238
309
  return
239
310
  else
240
311
  @preset_command = nil
@@ -246,7 +317,7 @@ module DEBUGGER__
246
317
  end
247
318
  else
248
319
  @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
249
- line = @ui.readline
320
+ line = @ui.readline prompt
250
321
  end
251
322
 
252
323
  case line
@@ -280,27 +351,37 @@ module DEBUGGER__
280
351
 
281
352
  # * `s[tep]`
282
353
  # * Step in. Resume the program until next breakable point.
354
+ # * `s[tep] <n>`
355
+ # * Step in, resume the program at `<n>`th breakable point.
283
356
  when 's', 'step'
284
357
  cancel_auto_continue
285
- @tc << [:step, :in]
358
+ check_postmortem
359
+ step_command :in, arg
286
360
 
287
361
  # * `n[ext]`
288
362
  # * Step over. Resume the program until next line.
363
+ # * `n[ext] <n>`
364
+ # * Step over, same as `step <n>`.
289
365
  when 'n', 'next'
290
366
  cancel_auto_continue
291
- @tc << [:step, :next]
367
+ check_postmortem
368
+ step_command :next, arg
292
369
 
293
370
  # * `fin[ish]`
294
371
  # * Finish this frame. Resume the program until the current frame is finished.
372
+ # * `fin[ish] <n>`
373
+ # * Finish frames, same as `step <n>`.
295
374
  when 'fin', 'finish'
296
375
  cancel_auto_continue
297
- @tc << [:step, :finish]
376
+ check_postmortem
377
+ step_command :finish, arg
298
378
 
299
379
  # * `c[ontinue]`
300
380
  # * Resume the program.
301
381
  when 'c', 'continue'
302
382
  cancel_auto_continue
303
383
  @tc << :continue
384
+ restart_all_threads
304
385
 
305
386
  # * `q[uit]` or `Ctrl-D`
306
387
  # * Finish debugger (with the debuggee process on non-remote debugging).
@@ -308,6 +389,7 @@ module DEBUGGER__
308
389
  if ask 'Really quit?'
309
390
  @ui.quit arg.to_i
310
391
  @tc << :continue
392
+ restart_all_threads
311
393
  else
312
394
  return :retry
313
395
  end
@@ -316,7 +398,7 @@ module DEBUGGER__
316
398
  # * Same as q[uit] but without the confirmation prompt.
317
399
  when 'q!', 'quit!'
318
400
  @ui.quit arg.to_i
319
- @tc << :continue
401
+ restart_all_threads
320
402
 
321
403
  # * `kill`
322
404
  # * Stop the debuggee process with `Kernal#exit!`.
@@ -354,6 +436,8 @@ module DEBUGGER__
354
436
  # * break if: `<expr>` is true at any lines.
355
437
  # * Note that this feature is super slow.
356
438
  when 'b', 'break'
439
+ check_postmortem
440
+
357
441
  if arg == nil
358
442
  show_bps
359
443
  return :retry
@@ -370,6 +454,7 @@ module DEBUGGER__
370
454
 
371
455
  # skip
372
456
  when 'bv'
457
+ check_postmortem
373
458
  require 'json'
374
459
 
375
460
  h = Hash.new{|h, k| h[k] = []}
@@ -396,8 +481,10 @@ module DEBUGGER__
396
481
  # * `catch <Error>`
397
482
  # * Set breakpoint on raising `<Error>`.
398
483
  when 'catch'
484
+ check_postmortem
485
+
399
486
  if arg
400
- bp = add_catch_breakpoint arg
487
+ bp = repl_add_catch_breakpoint arg
401
488
  show_bps bp if bp
402
489
  else
403
490
  show_bps
@@ -408,6 +495,8 @@ module DEBUGGER__
408
495
  # * Stop the execution when the result of current scope's `@ivar` is changed.
409
496
  # * Note that this feature is super slow.
410
497
  when 'wat', 'watch'
498
+ check_postmortem
499
+
411
500
  if arg && arg.match?(/\A@\w+/)
412
501
  @tc << [:breakpoint, :watch, arg]
413
502
  else
@@ -420,15 +509,17 @@ module DEBUGGER__
420
509
  # * `del[ete] <bpnum>`
421
510
  # * delete specified breakpoint.
422
511
  when 'del', 'delete'
512
+ check_postmortem
513
+
423
514
  bp =
424
515
  case arg
425
516
  when nil
426
517
  show_bps
427
518
  if ask "Remove all breakpoints?", 'N'
428
- delete_breakpoint
519
+ delete_bp
429
520
  end
430
521
  when /\d+/
431
- delete_breakpoint arg.to_i
522
+ delete_bp arg.to_i
432
523
  else
433
524
  nil
434
525
  end
@@ -501,25 +592,57 @@ module DEBUGGER__
501
592
 
502
593
  @tc << [:show, :edit, arg]
503
594
 
504
- # * `i[nfo]`, `i[nfo] l[ocal[s]]`
595
+ # * `i[nfo]`
596
+ # * Show information about current frame (local/instance variables and defined constants).
597
+ # * `i[nfo] l[ocal[s]]`
505
598
  # * Show information about the current frame (local variables)
506
599
  # * It includes `self` as `%self` and a return value as `%return`.
600
+ # * `i[nfo] i[var[s]]` or `i[nfo] instance`
601
+ # * Show information about insttance variables about `self`.
602
+ # * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
603
+ # * Show information about accessible constants except toplevel constants.
604
+ # * `i[nfo] g[lobal[s]]`
605
+ # * Show information about global variables
606
+ # * `i[nfo] ... </pattern/>`
607
+ # * Filter the output with `</pattern/>`.
507
608
  # * `i[nfo] th[read[s]]`
508
609
  # * Show all threads (same as `th[read]`).
509
610
  when 'i', 'info'
510
- case arg
611
+ if /\/(.+)\/\z/ =~ arg
612
+ pat = Regexp.compile($1)
613
+ sub = $~.pre_match.strip
614
+ else
615
+ sub = arg
616
+ end
617
+
618
+ case sub
511
619
  when nil
512
- @tc << [:show, :local]
513
- when 'l', /locals?/
514
- @tc << [:show, :local]
620
+ @tc << [:show, :default, pat] # something useful
621
+ when 'l', /^locals?/
622
+ @tc << [:show, :locals, pat]
623
+ when 'i', /^ivars?/i, /^instance[_ ]variables?/i
624
+ @tc << [:show, :ivars, pat]
625
+ when 'c', /^consts?/i, /^constants?/i
626
+ @tc << [:show, :consts, pat]
627
+ when 'g', /^globals?/i, /^global[_ ]variables?/i
628
+ @tc << [:show, :globals, pat]
515
629
  when 'th', /threads?/
516
630
  thread_list
517
631
  return :retry
518
632
  else
633
+ @ui.puts "unrecognized argument for info command: #{arg}"
519
634
  show_help 'info'
520
635
  return :retry
521
636
  end
522
637
 
638
+ # * `o[utline]` or `ls`
639
+ # * Show you available methods, constants, local variables, and instance variables in the current scope.
640
+ # * `o[utline] <expr>` or `ls <expr>`
641
+ # * Show you available methods and instance variables of the given object.
642
+ # * If the object is a class/module, it also lists its constants.
643
+ when 'outline', 'o', 'ls'
644
+ @tc << [:show, :outline, arg]
645
+
523
646
  # * `display`
524
647
  # * Show display setting.
525
648
  # * `display <expr>`
@@ -550,28 +673,6 @@ module DEBUGGER__
550
673
  end
551
674
  return :retry
552
675
 
553
- # * `trace [on|off]`
554
- # * enable or disable line tracer.
555
- when 'trace'
556
- case arg
557
- when 'on'
558
- dir = __dir__
559
- @tracer ||= TracePoint.new(:call, :return, :b_call, :b_return, :line, :class, :end){|tp|
560
- next if File.dirname(tp.path) == dir
561
- next if tp.path == '<internal:trace_point>'
562
- # Skip when `JSON.generate` is called during tests
563
- next if tp.binding.eval('self').to_s == 'JSON' and ENV['RUBY_DEBUG_TEST_MODE']
564
- # next if tp.event != :line
565
- @ui.puts pretty_tp(tp)
566
- }
567
- @tracer.enable
568
- when 'off'
569
- @tracer && @tracer.disable
570
- end
571
- enabled = (@tracer && @tracer.enabled?) ? true : false
572
- @ui.puts "Trace #{enabled ? 'on' : 'off'}"
573
- return :retry
574
-
575
676
  ### Frame control
576
677
 
577
678
  # * `f[rame]`
@@ -603,10 +704,16 @@ module DEBUGGER__
603
704
  when 'pp'
604
705
  @tc << [:eval, :pp, arg.to_s]
605
706
 
606
- # * `e[val] <expr>`
707
+ # * `eval <expr>`
607
708
  # * Evaluate `<expr>` on the current frame.
608
- when 'e', 'eval', 'call'
609
- @tc << [:eval, :call, arg]
709
+ when 'eval', 'call'
710
+ if arg == nil || arg.empty?
711
+ show_help 'eval'
712
+ @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead."
713
+ return :retry
714
+ else
715
+ @tc << [:eval, :call, arg]
716
+ end
610
717
 
611
718
  # * `irb`
612
719
  # * Invoke `irb` on the current frame.
@@ -620,6 +727,105 @@ module DEBUGGER__
620
727
  # don't repeat irb command
621
728
  @repl_prev_line = nil
622
729
 
730
+ ### Trace
731
+ # * `trace`
732
+ # * Show available tracers list.
733
+ # * `trace line`
734
+ # * Add a line tracer. It indicates line events.
735
+ # * `trace call`
736
+ # * Add a call tracer. It indicate call/return events.
737
+ # * `trace exception`
738
+ # * Add an exception tracer. It indicates raising exceptions.
739
+ # * `trace object <expr>`
740
+ # * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
741
+ # * `trace ... </pattern/>`
742
+ # * Indicates only matched events to `</pattern/>` (RegExp).
743
+ # * `trace ... into: <file>`
744
+ # * Save trace information into: `<file>`.
745
+ # * `trace off <num>`
746
+ # * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
747
+ # * `trace off [line|call|pass]`
748
+ # * Disable all tracers. If `<type>` is provided, disable specified type tracers.
749
+ when 'trace'
750
+ if (re = /\s+into:\s*(.+)/) =~ arg
751
+ into = $1
752
+ arg.sub!(re, '')
753
+ end
754
+
755
+ if (re = /\s\/(.+)\/\z/) =~ arg
756
+ pattern = $1
757
+ arg.sub!(re, '')
758
+ end
759
+
760
+ case arg
761
+ when nil
762
+ @ui.puts 'Tracers:'
763
+ @tracers.each_with_index{|t, i|
764
+ @ui.puts "* \##{i} #{t}"
765
+ }
766
+ @ui.puts
767
+ return :retry
768
+
769
+ when /\Aline\z/
770
+ @tracers << t = LineTracer.new(@ui, pattern: pattern, into: into)
771
+ @ui.puts "Enable #{t.to_s}"
772
+ return :retry
773
+
774
+ when /\Acall\z/
775
+ @tracers << t = CallTracer.new(@ui, pattern: pattern, into: into)
776
+ @ui.puts "Enable #{t.to_s}"
777
+ return :retry
778
+
779
+ when /\Aexception\z/
780
+ @tracers << t = ExceptionTracer.new(@ui, pattern: pattern, into: into)
781
+ @ui.puts "Enable #{t.to_s}"
782
+ return :retry
783
+
784
+ when /\Aobject\s+(.+)/
785
+ @tc << [:trace, :object, $1.strip, {pattern: pattern, into: into}]
786
+
787
+ when /\Aoff\s+(\d+)\z/
788
+ if t = @tracers[$1.to_i]
789
+ t.disable
790
+ @ui.puts "Disable #{t.to_s}"
791
+ else
792
+ @ui.puts "Unmatched: #{$1}"
793
+ end
794
+ return :retry
795
+
796
+ when /\Aoff(\s+(line|call|exception|object))?\z/
797
+ @tracers.each{|t|
798
+ if $2.nil? || t.type == $2
799
+ t.disable
800
+ @ui.puts "Disable #{t.to_s}"
801
+ end
802
+ }
803
+ return :retry
804
+
805
+ else
806
+ @ui.puts "Unknown trace option: #{arg.inspect}"
807
+ return :retry
808
+ end
809
+
810
+ # Record
811
+ # * `record`
812
+ # * Show recording status.
813
+ # * `record [on|off]`
814
+ # * Start/Stop recording.
815
+ # * `step back`
816
+ # * Start replay. Step back with the last execution log.
817
+ # * `s[tep]` does stepping forward with the last log.
818
+ # * `step reset`
819
+ # * Stop replay .
820
+ when 'record'
821
+ case arg
822
+ when nil, 'on', 'off'
823
+ @tc << [:record, arg&.to_sym]
824
+ else
825
+ @ui.puts "unknown command: #{arg}"
826
+ return :retry
827
+ end
828
+
623
829
  ### Thread control
624
830
 
625
831
  # * `th[read]`
@@ -652,13 +858,28 @@ module DEBUGGER__
652
858
  config_command arg
653
859
  return :retry
654
860
 
861
+ # * `source <file>`
862
+ # * Evaluate lines in `<file>` as debug commands.
863
+ when 'source'
864
+ if arg
865
+ begin
866
+ cmds = File.readlines(path = File.expand_path(arg))
867
+ add_preset_commands path, cmds, kick: true, continue: false
868
+ rescue Errno::ENOENT
869
+ @ui.puts "File not found: #{arg}"
870
+ end
871
+ else
872
+ show_help 'source'
873
+ end
874
+ return :retry
875
+
655
876
  ### Help
656
877
 
657
878
  # * `h[elp]`
658
879
  # * Show help for all commands.
659
880
  # * `h[elp] <command>`
660
881
  # * Show help for the given command.
661
- when 'h', 'help'
882
+ when 'h', 'help', '?'
662
883
  if arg
663
884
  show_help arg
664
885
  else
@@ -668,21 +889,56 @@ module DEBUGGER__
668
889
 
669
890
  ### END
670
891
  else
671
- @ui.puts "unknown command: #{line}"
892
+ @tc << [:eval, :pp, line]
893
+ =begin
672
894
  @repl_prev_line = nil
895
+ @ui.puts "unknown command: #{line}"
896
+ begin
897
+ require 'did_you_mean'
898
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: DEBUGGER__.commands)
899
+ correction = spell_checker.correct(line.split(/\s/).first || '')
900
+ @ui.puts "Did you mean? #{correction.join(' or ')}" unless correction.empty?
901
+ rescue LoadError
902
+ # Don't use D
903
+ end
673
904
  return :retry
905
+ =end
674
906
  end
675
907
 
676
908
  rescue Interrupt
677
909
  return :retry
678
910
  rescue SystemExit
679
911
  raise
912
+ rescue PostmortemError => e
913
+ @ui.puts e.message
914
+ return :retry
680
915
  rescue Exception => e
681
916
  @ui.puts "[REPL ERROR] #{e.inspect}"
682
917
  @ui.puts e.backtrace.map{|e| ' ' + e}
683
918
  return :retry
684
919
  end
685
920
 
921
+ def step_command type, arg
922
+ case arg
923
+ when nil
924
+ @tc << [:step, type]
925
+ restart_all_threads
926
+ when /\A\d+\z/
927
+ @tc << [:step, type, arg.to_i]
928
+ restart_all_threads
929
+ when /\Aback\z/, /\Areset\z/
930
+ if type != :in
931
+ @ui.puts "only `step #{arg}` is supported."
932
+ :retry
933
+ else
934
+ @tc << [:step, arg.to_sym]
935
+ end
936
+ else
937
+ @ui.puts "Unknown option: #{arg}"
938
+ :retry
939
+ end
940
+ end
941
+
686
942
  def config_show key
687
943
  key = key.to_sym
688
944
  if CONFIG_SET[key]
@@ -704,9 +960,9 @@ module DEBUGGER__
704
960
  if CONFIG_SET[key = key.to_sym]
705
961
  begin
706
962
  if append
707
- DEBUGGER__.append_config(key, val)
963
+ CONFIG.append_config(key, val)
708
964
  else
709
- DEBUGGER__.set_config({key => val})
965
+ CONFIG[key] = val
710
966
  end
711
967
  rescue => e
712
968
  @ui.puts e.message
@@ -725,7 +981,7 @@ module DEBUGGER__
725
981
 
726
982
  when /\Aunset\s+(.+)\z/
727
983
  if CONFIG_SET[key = $1.to_sym]
728
- DEBUGGER__.set_config({key => nil})
984
+ CONFIG[key] = nil
729
985
  end
730
986
  config_show key
731
987
 
@@ -780,51 +1036,7 @@ module DEBUGGER__
780
1036
  end
781
1037
  end
782
1038
 
783
- def msig klass, receiver
784
- if klass.singleton_class?
785
- "#{receiver}."
786
- else
787
- "#{klass}#"
788
- end
789
- end
790
-
791
- def pretty_tp tp
792
- loc = "#{tp.path}:#{tp.lineno}"
793
- level = caller.size
794
-
795
- info =
796
- case tp.event
797
- when :line
798
- "line at #{loc}"
799
- when :call, :c_call
800
- klass = tp.defined_class
801
- "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} at #{loc}"
802
- when :return, :c_return
803
- klass = tp.defined_class
804
- "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} => #{tp.return_value.inspect} at #{loc}"
805
- when :b_call
806
- "b_call at #{loc}"
807
- when :b_return
808
- "b_return => #{tp.return_value} at #{loc}"
809
- when :class
810
- "class #{tp.self} at #{loc}"
811
- when :end
812
- "class #{tp.self} end at #{loc}"
813
- else
814
- "#{tp.event} at #{loc}"
815
- end
816
-
817
- case tp.event
818
- when :call, :b_call, :return, :b_return, :class, :end
819
- level -= 1
820
- end
821
-
822
- "Tracing:#{' ' * level} #{info}"
823
- rescue => e
824
- p e
825
- pp e.backtrace
826
- exit!
827
- end
1039
+ # breakpoint management
828
1040
 
829
1041
  def iterate_bps
830
1042
  deleted_bps = []
@@ -856,7 +1068,29 @@ module DEBUGGER__
856
1068
  nil
857
1069
  end
858
1070
 
859
- def delete_breakpoint arg = nil
1071
+ def rehash_bps
1072
+ bps = @bps.values
1073
+ @bps.clear
1074
+ bps.each{|bp|
1075
+ add_bp bp
1076
+ }
1077
+ end
1078
+
1079
+ def add_bp bp
1080
+ # don't repeat commands that add breakpoints
1081
+ @repl_prev_line = nil
1082
+
1083
+ if @bps.has_key? bp.key
1084
+ unless bp.duplicable?
1085
+ @ui.puts "duplicated breakpoint: #{bp}"
1086
+ bp.disable
1087
+ end
1088
+ else
1089
+ @bps[bp.key] = bp
1090
+ end
1091
+ end
1092
+
1093
+ def delete_bp arg = nil
860
1094
  case arg
861
1095
  when nil
862
1096
  @bps.each{|key, bp| bp.delete}
@@ -895,14 +1129,14 @@ module DEBUGGER__
895
1129
 
896
1130
  case expr[:sig]
897
1131
  when /\A(\d+)\z/
898
- add_line_breakpoint @tc.location.path, $1.to_i, cond: expr[:if], command: cmd
1132
+ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd
899
1133
  when /\A(.+)[:\s+](\d+)\z/
900
- add_line_breakpoint $1, $2.to_i, cond: expr[:if], command: cmd
1134
+ add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
901
1135
  when /\A(.+)([\.\#])(.+)\z/
902
- @tc << [:breakpoint, :method, $1, $2, $3, expr[:if], cmd]
1136
+ @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd]
903
1137
  return :noretry
904
1138
  when nil
905
- add_check_breakpoint expr[:if]
1139
+ add_check_breakpoint cond
906
1140
  else
907
1141
  @ui.puts "Unknown breakpoint format: #{arg}"
908
1142
  @ui.puts
@@ -910,6 +1144,34 @@ module DEBUGGER__
910
1144
  end
911
1145
  end
912
1146
 
1147
+ def repl_add_catch_breakpoint arg
1148
+ expr = parse_break arg.strip
1149
+ cond = expr[:if]
1150
+ cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1151
+
1152
+ bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd)
1153
+ add_bp bp
1154
+ end
1155
+
1156
+ def add_catch_breakpoint pat
1157
+ bp = CatchBreakpoint.new(pat)
1158
+ add_bp bp
1159
+ end
1160
+
1161
+ def add_check_breakpoint expr
1162
+ bp = CheckBreakpoint.new(expr)
1163
+ add_bp bp
1164
+ end
1165
+
1166
+ def add_line_breakpoint file, line, **kw
1167
+ file = resolve_path(file)
1168
+ bp = LineBreakpoint.new(file, line, **kw)
1169
+
1170
+ add_bp bp
1171
+ rescue Errno::ENOENT => e
1172
+ @ui.puts e.message
1173
+ end
1174
+
913
1175
  # threads
914
1176
 
915
1177
  def update_thread_list
@@ -918,17 +1180,15 @@ module DEBUGGER__
918
1180
  unmanaged = []
919
1181
 
920
1182
  list.each{|th|
921
- case
922
- when th == Thread.current
923
- # ignore
924
- when @management_threads.include?(th)
925
- # ignore
926
- when @th_clients.has_key?(th)
927
- thcs << @th_clients[th]
1183
+ if thc = @th_clients[th]
1184
+ if !thc.management?
1185
+ thcs << thc
1186
+ end
928
1187
  else
929
1188
  unmanaged << th
930
1189
  end
931
1190
  }
1191
+
932
1192
  return thcs.sort_by{|thc| thc.id}, unmanaged
933
1193
  end
934
1194
 
@@ -955,7 +1215,7 @@ module DEBUGGER__
955
1215
  thcs, _unmanaged_ths = update_thread_list
956
1216
 
957
1217
  if tc = thcs[n]
958
- if tc.mode
1218
+ if tc.waiting?
959
1219
  @tc = tc
960
1220
  else
961
1221
  @ui.puts "#{tc.thread} is not controllable yet."
@@ -969,11 +1229,11 @@ module DEBUGGER__
969
1229
  end
970
1230
 
971
1231
  def setup_threads
972
- stop_all_threads do
973
- Thread.list.each{|th|
974
- thread_client_create(th)
975
- }
976
- end
1232
+ @th_clients = {}
1233
+
1234
+ Thread.list.each{|th|
1235
+ thread_client_create(th)
1236
+ }
977
1237
  end
978
1238
 
979
1239
  def on_thread_begin th
@@ -985,8 +1245,7 @@ module DEBUGGER__
985
1245
  end
986
1246
  end
987
1247
 
988
- def thread_client
989
- thr = Thread.current
1248
+ def thread_client thr = Thread.current
990
1249
  if @th_clients.has_key? thr
991
1250
  @th_clients[thr]
992
1251
  else
@@ -994,84 +1253,67 @@ module DEBUGGER__
994
1253
  end
995
1254
  end
996
1255
 
997
- def stop_all_threads
998
- current = Thread.current
1256
+ private def thread_stopper
1257
+ @thread_stopper ||= TracePoint.new(:line) do
1258
+ # run on each thread
1259
+ tc = ThreadClient.current
1260
+ next if tc.management?
1261
+ next unless tc.running?
1262
+ next if tc == @tc
999
1263
 
1000
- if Thread.list.size > 1
1001
- TracePoint.new(:line) do
1002
- th = Thread.current
1003
- if current == th || @management_threads.include?(th)
1004
- next
1005
- else
1006
- tc = ThreadClient.current
1007
- tc.on_pause
1008
- end
1009
- end.enable do
1010
- yield
1011
- ensure
1012
- @th_clients.each{|thr, tc|
1013
- case thr
1014
- when current, (@tc && @tc.thread)
1015
- next
1016
- else
1017
- tc << :continue if thr != Thread.current
1018
- end
1019
- }
1020
- end
1021
- else
1022
- yield
1264
+ tc.on_pause
1023
1265
  end
1024
1266
  end
1025
1267
 
1026
- ## event
1027
-
1028
- def on_load iseq, src
1029
- DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1030
- @sr.add iseq, src
1031
-
1032
- pending_line_breakpoints do |bp|
1033
- if bp.path == (iseq.absolute_path || iseq.path)
1034
- bp.try_activate
1035
- end
1036
- end
1268
+ private def running_thread_clients_count
1269
+ @th_clients.count{|th, tc|
1270
+ next if tc.management?
1271
+ next unless tc.running?
1272
+ true
1273
+ }
1037
1274
  end
1038
1275
 
1039
- # breakpoint management
1276
+ private def waiting_thread_clients
1277
+ @th_clients.map{|th, tc|
1278
+ next if tc.management?
1279
+ next unless tc.waiting?
1280
+ tc
1281
+ }.compact
1282
+ end
1040
1283
 
1041
- def add_breakpoint bp
1042
- # don't repeat commands that add breakpoints
1043
- @repl_prev_line = nil
1284
+ private def stop_all_threads
1285
+ return if running_thread_clients_count == 0
1044
1286
 
1045
- if @bps.has_key? bp.key
1046
- unless bp.duplicable?
1047
- @ui.puts "duplicated breakpoint: #{bp}"
1048
- bp.disable
1049
- end
1050
- else
1051
- @bps[bp.key] = bp
1052
- end
1287
+ stopper = thread_stopper
1288
+ stopper.enable unless stopper.enabled?
1053
1289
  end
1054
1290
 
1055
- def rehash_bps
1056
- bps = @bps.values
1057
- @bps.clear
1058
- bps.each{|bp|
1059
- add_breakpoint bp
1291
+ private def restart_all_threads
1292
+ stopper = thread_stopper
1293
+ stopper.disable if stopper.enabled?
1294
+
1295
+ waiting_thread_clients.each{|tc|
1296
+ next if @tc == tc
1297
+ tc << :continue
1060
1298
  }
1299
+ @tc = nil
1061
1300
  end
1062
1301
 
1063
- def break? file, line
1064
- @bps.has_key? [file, line]
1065
- end
1302
+ ## event
1066
1303
 
1067
- def add_catch_breakpoint arg
1068
- bp = CatchBreakpoint.new(arg)
1069
- add_breakpoint bp
1070
- end
1304
+ def on_load iseq, src
1305
+ DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1306
+ @sr.add iseq, src
1071
1307
 
1072
- def add_check_breakpoint expr
1073
- bp = CheckBreakpoint.new(expr)
1074
- add_breakpoint bp
1308
+ pending_line_breakpoints = @bps.find_all do |key, bp|
1309
+ LineBreakpoint === bp && !bp.iseq
1310
+ end
1311
+
1312
+ pending_line_breakpoints.each do |_key, bp|
1313
+ if bp.path == (iseq.absolute_path || iseq.path)
1314
+ bp.try_activate
1315
+ end
1316
+ end
1075
1317
  end
1076
1318
 
1077
1319
  def resolve_path file
@@ -1092,23 +1334,6 @@ module DEBUGGER__
1092
1334
  raise
1093
1335
  end
1094
1336
 
1095
- def add_line_breakpoint file, line, **kw
1096
- file = resolve_path(file)
1097
- bp = LineBreakpoint.new(file, line, **kw)
1098
-
1099
- add_breakpoint bp
1100
- rescue Errno::ENOENT => e
1101
- @ui.puts e.message
1102
- end
1103
-
1104
- def pending_line_breakpoints
1105
- @bps.find_all do |key, bp|
1106
- LineBreakpoint === bp && !bp.iseq
1107
- end.each do |key, bp|
1108
- yield bp
1109
- end
1110
- end
1111
-
1112
1337
  def method_added tp
1113
1338
  b = tp.binding
1114
1339
  if var_name = b.local_variables.first
@@ -1137,10 +1362,55 @@ module DEBUGGER__
1137
1362
  @ui.width
1138
1363
  end
1139
1364
 
1140
- def check_forked
1141
- unless @session_server.status
1142
- # TODO: Support it
1143
- raise 'DEBUGGER: stop at forked process is not supported yet.'
1365
+ def check_postmortem
1366
+ if @postmortem
1367
+ raise PostmortemError, "Can not use this command on postmortem mode."
1368
+ end
1369
+ end
1370
+
1371
+ def enter_postmortem_session frames
1372
+ @postmortem = true
1373
+ ThreadClient.current.suspend :postmortem, postmortem_frames: frames
1374
+ ensure
1375
+ @postmortem = false
1376
+ end
1377
+
1378
+ def postmortem=(is_enable)
1379
+ if is_enable
1380
+ unless @postmortem_hook
1381
+ @postmortem_hook = TracePoint.new(:raise){|tp|
1382
+ exc = tp.raised_exception
1383
+ frames = DEBUGGER__.capture_frames(__dir__)
1384
+ exc.instance_variable_set(:@postmortem_frames, frames)
1385
+ }
1386
+ at_exit{
1387
+ @postmortem_hook.disable
1388
+ if CONFIG[:postmortem] && (exc = $!) != nil
1389
+ exc = exc.cause while exc.cause
1390
+
1391
+ begin
1392
+ @ui.puts "Enter postmortem mode with #{exc.inspect}"
1393
+ @ui.puts exc.backtrace.map{|e| ' ' + e}
1394
+ @ui.puts "\n"
1395
+
1396
+ enter_postmortem_session exc.instance_variable_get(:@postmortem_frames)
1397
+ rescue SystemExit
1398
+ exit!
1399
+ rescue Exception => e
1400
+ @ui = STDERR unless @ui
1401
+ @ui.puts "Error while postmortem console: #{e.inspect}"
1402
+ end
1403
+ end
1404
+ }
1405
+ end
1406
+
1407
+ if !@postmortem_hook.enabled?
1408
+ @postmortem_hook.enable
1409
+ end
1410
+ else
1411
+ if @postmortem_hook && @postmortem_hook.enabled?
1412
+ @postmortem_hook.disable
1413
+ end
1144
1414
  end
1145
1415
  end
1146
1416
  end
@@ -1187,18 +1457,18 @@ module DEBUGGER__
1187
1457
  # start methods
1188
1458
 
1189
1459
  def self.start nonstop: false, **kw
1190
- set_config(kw)
1460
+ CONFIG.set_config(**kw)
1191
1461
 
1192
1462
  unless defined? SESSION
1193
- require_relative 'console'
1194
- initialize_session UI_Console.new
1463
+ require_relative 'local'
1464
+ initialize_session UI_LocalConsole.new
1195
1465
  end
1196
1466
 
1197
1467
  setup_initial_suspend unless nonstop
1198
1468
  end
1199
1469
 
1200
- def self.open host: nil, port: ::DEBUGGER__::CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1201
- set_config(kw)
1470
+ def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1471
+ CONFIG.set_config(**kw)
1202
1472
 
1203
1473
  if port
1204
1474
  open_tcp host: host, port: port, nonstop: nonstop
@@ -1208,7 +1478,7 @@ module DEBUGGER__
1208
1478
  end
1209
1479
 
1210
1480
  def self.open_tcp host: nil, port:, nonstop: false, **kw
1211
- set_config(kw)
1481
+ CONFIG.set_config(**kw)
1212
1482
  require_relative 'server'
1213
1483
 
1214
1484
  if defined? SESSION
@@ -1221,7 +1491,7 @@ module DEBUGGER__
1221
1491
  end
1222
1492
 
1223
1493
  def self.open_unix sock_path: nil, sock_dir: nil, nonstop: false, **kw
1224
- set_config(kw)
1494
+ CONFIG.set_config(**kw)
1225
1495
  require_relative 'server'
1226
1496
 
1227
1497
  if defined? SESSION
@@ -1236,13 +1506,13 @@ module DEBUGGER__
1236
1506
  # boot utilities
1237
1507
 
1238
1508
  def self.setup_initial_suspend
1239
- if !::DEBUGGER__::CONFIG[:nonstop]
1509
+ if !CONFIG[:nonstop]
1240
1510
  if loc = ::DEBUGGER__.require_location
1241
- # require 'debug/console' or 'debug'
1511
+ # require 'debug/start' or 'debug'
1242
1512
  add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
1243
1513
  else
1244
1514
  # -r
1245
- add_line_breakpoint $0, 1, oneshot: true, hook_call: false
1515
+ add_line_breakpoint $0, 0, oneshot: true, hook_call: false
1246
1516
  end
1247
1517
  end
1248
1518
  end
@@ -1280,10 +1550,10 @@ module DEBUGGER__
1280
1550
  [[File.expand_path('~/.rdbgrc'), true],
1281
1551
  [File.expand_path('~/.rdbgrc.rb'), true],
1282
1552
  # ['./.rdbgrc', true], # disable because of security concern
1283
- [::DEBUGGER__::CONFIG[:init_script], false],
1553
+ [CONFIG[:init_script], false],
1284
1554
  ].each{|(path, rc)|
1285
1555
  next unless path
1286
- next if rc && ::DEBUGGER__::CONFIG[:no_rc] # ignore rc
1556
+ next if rc && CONFIG[:no_rc] # ignore rc
1287
1557
 
1288
1558
  if File.file? path
1289
1559
  if path.end_with?('.rb')
@@ -1297,62 +1567,12 @@ module DEBUGGER__
1297
1567
  }
1298
1568
 
1299
1569
  # given debug commands
1300
- if ::DEBUGGER__::CONFIG[:commands]
1301
- cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
1570
+ if CONFIG[:commands]
1571
+ cmds = CONFIG[:commands].split(';;')
1302
1572
  ::DEBUGGER__::SESSION.add_preset_commands "commands", cmds, kick: false, continue: false
1303
1573
  end
1304
1574
  end
1305
1575
 
1306
- def self.parse_help
1307
- helps = Hash.new{|h, k| h[k] = []}
1308
- desc = cat = nil
1309
- cmds = []
1310
-
1311
- File.read(__FILE__).each_line do |line|
1312
- case line
1313
- when /\A\s*### (.+)/
1314
- cat = $1
1315
- break if $1 == 'END'
1316
- when /\A when (.+)/
1317
- next unless cat
1318
- next unless desc
1319
- ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
1320
- helps[cat] << [ws, desc]
1321
- desc = nil
1322
- cmds.concat ws
1323
- when /\A\s+# (\s*\*.+)/
1324
- if desc
1325
- desc << "\n" + $1
1326
- else
1327
- desc = $1
1328
- end
1329
- end
1330
- end
1331
- @commands = cmds
1332
- @helps = helps
1333
- end
1334
-
1335
- def self.helps
1336
- (defined?(@helps) && @helps) || parse_help
1337
- end
1338
-
1339
- def self.commands
1340
- (defined?(@commands) && @commands) || (parse_help; @commands)
1341
- end
1342
-
1343
- def self.help
1344
- r = []
1345
- self.helps.each{|cat, cmds|
1346
- r << "### #{cat}"
1347
- r << ''
1348
- cmds.each{|ws, desc|
1349
- r << desc
1350
- }
1351
- r << ''
1352
- }
1353
- r.join("\n")
1354
- end
1355
-
1356
1576
  class ::Module
1357
1577
  undef method_added
1358
1578
  def method_added mid; end
@@ -1408,4 +1628,69 @@ module DEBUGGER__
1408
1628
  end
1409
1629
  end
1410
1630
  end
1631
+
1632
+ module ForkInterceptor
1633
+ def fork(&given_block)
1634
+ return super unless defined?(SESSION) && SESSION.active?
1635
+
1636
+ # before fork
1637
+ if CONFIG[:parent_on_fork]
1638
+ parent_hook = -> child_pid {
1639
+ # Do nothing
1640
+ }
1641
+ child_hook = -> {
1642
+ DEBUGGER__.warn "Detaching after fork from child process #{Process.pid}"
1643
+ SESSION.deactivate
1644
+ }
1645
+ else
1646
+ parent_pid = Process.pid
1647
+
1648
+ parent_hook = -> child_pid {
1649
+ DEBUGGER__.warn "Detaching after fork from parent process #{Process.pid}"
1650
+ SESSION.deactivate
1651
+
1652
+ at_exit{
1653
+ trap(:SIGINT, :IGNORE)
1654
+ Process.waitpid(child_pid)
1655
+ }
1656
+ }
1657
+ child_hook = -> {
1658
+ DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
1659
+ SESSION.activate on_fork: true
1660
+ }
1661
+ end
1662
+
1663
+ if given_block
1664
+ new_block = proc {
1665
+ # after fork: child
1666
+ child_hook.call
1667
+ given_block.call
1668
+ }
1669
+ pid = super(&new_block)
1670
+ parent_hook.call(pid)
1671
+ pid
1672
+ else
1673
+ if pid = super
1674
+ # after fork: parent
1675
+ parent_hook.call pid
1676
+ else
1677
+ # after fork: child
1678
+ child_hook.call
1679
+ end
1680
+
1681
+ pid
1682
+ end
1683
+ end
1684
+ end
1685
+
1686
+ class ::Object
1687
+ include ForkInterceptor
1688
+ end
1689
+
1690
+ module ::Process
1691
+ class << self
1692
+ prepend ForkInterceptor
1693
+ end
1694
+ end
1411
1695
  end
1696
+