debug 1.0.0.beta7 → 1.0.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:
@@ -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
+