debug 1.0.0.beta8 → 1.0.0.rc1

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
@@ -246,7 +315,7 @@ module DEBUGGER__
246
315
  end
247
316
  else
248
317
  @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
249
- line = @ui.readline
318
+ line = @ui.readline prompt
250
319
  end
251
320
 
252
321
  case line
@@ -280,27 +349,37 @@ module DEBUGGER__
280
349
 
281
350
  # * `s[tep]`
282
351
  # * Step in. Resume the program until next breakable point.
352
+ # * `s[tep] <n>`
353
+ # * Step in, resume the program at `<n>`th breakable point.
283
354
  when 's', 'step'
284
355
  cancel_auto_continue
285
- @tc << [:step, :in]
356
+ check_postmortem
357
+ step_command :in, arg
286
358
 
287
359
  # * `n[ext]`
288
360
  # * Step over. Resume the program until next line.
361
+ # * `n[ext] <n>`
362
+ # * Step over, same as `step <n>`.
289
363
  when 'n', 'next'
290
364
  cancel_auto_continue
291
- @tc << [:step, :next]
365
+ check_postmortem
366
+ step_command :next, arg
292
367
 
293
368
  # * `fin[ish]`
294
369
  # * Finish this frame. Resume the program until the current frame is finished.
370
+ # * `fin[ish] <n>`
371
+ # * Finish frames, same as `step <n>`.
295
372
  when 'fin', 'finish'
296
373
  cancel_auto_continue
297
- @tc << [:step, :finish]
374
+ check_postmortem
375
+ step_command :finish, arg
298
376
 
299
377
  # * `c[ontinue]`
300
378
  # * Resume the program.
301
379
  when 'c', 'continue'
302
380
  cancel_auto_continue
303
381
  @tc << :continue
382
+ restart_all_threads
304
383
 
305
384
  # * `q[uit]` or `Ctrl-D`
306
385
  # * Finish debugger (with the debuggee process on non-remote debugging).
@@ -308,6 +387,7 @@ module DEBUGGER__
308
387
  if ask 'Really quit?'
309
388
  @ui.quit arg.to_i
310
389
  @tc << :continue
390
+ restart_all_threads
311
391
  else
312
392
  return :retry
313
393
  end
@@ -316,7 +396,7 @@ module DEBUGGER__
316
396
  # * Same as q[uit] but without the confirmation prompt.
317
397
  when 'q!', 'quit!'
318
398
  @ui.quit arg.to_i
319
- @tc << :continue
399
+ restart_all_threads
320
400
 
321
401
  # * `kill`
322
402
  # * Stop the debuggee process with `Kernal#exit!`.
@@ -354,6 +434,8 @@ module DEBUGGER__
354
434
  # * break if: `<expr>` is true at any lines.
355
435
  # * Note that this feature is super slow.
356
436
  when 'b', 'break'
437
+ check_postmortem
438
+
357
439
  if arg == nil
358
440
  show_bps
359
441
  return :retry
@@ -370,6 +452,7 @@ module DEBUGGER__
370
452
 
371
453
  # skip
372
454
  when 'bv'
455
+ check_postmortem
373
456
  require 'json'
374
457
 
375
458
  h = Hash.new{|h, k| h[k] = []}
@@ -396,8 +479,10 @@ module DEBUGGER__
396
479
  # * `catch <Error>`
397
480
  # * Set breakpoint on raising `<Error>`.
398
481
  when 'catch'
482
+ check_postmortem
483
+
399
484
  if arg
400
- bp = add_catch_breakpoint arg
485
+ bp = repl_add_catch_breakpoint arg
401
486
  show_bps bp if bp
402
487
  else
403
488
  show_bps
@@ -408,6 +493,8 @@ module DEBUGGER__
408
493
  # * Stop the execution when the result of current scope's `@ivar` is changed.
409
494
  # * Note that this feature is super slow.
410
495
  when 'wat', 'watch'
496
+ check_postmortem
497
+
411
498
  if arg && arg.match?(/\A@\w+/)
412
499
  @tc << [:breakpoint, :watch, arg]
413
500
  else
@@ -420,15 +507,17 @@ module DEBUGGER__
420
507
  # * `del[ete] <bpnum>`
421
508
  # * delete specified breakpoint.
422
509
  when 'del', 'delete'
510
+ check_postmortem
511
+
423
512
  bp =
424
513
  case arg
425
514
  when nil
426
515
  show_bps
427
516
  if ask "Remove all breakpoints?", 'N'
428
- delete_breakpoint
517
+ delete_bp
429
518
  end
430
519
  when /\d+/
431
- delete_breakpoint arg.to_i
520
+ delete_bp arg.to_i
432
521
  else
433
522
  nil
434
523
  end
@@ -501,25 +590,57 @@ module DEBUGGER__
501
590
 
502
591
  @tc << [:show, :edit, arg]
503
592
 
504
- # * `i[nfo]`, `i[nfo] l[ocal[s]]`
593
+ # * `i[nfo]`
594
+ # * Show information about current frame (local/instance variables and defined constants).
595
+ # * `i[nfo] l[ocal[s]]`
505
596
  # * Show information about the current frame (local variables)
506
597
  # * It includes `self` as `%self` and a return value as `%return`.
598
+ # * `i[nfo] i[var[s]]` or `i[nfo] instance`
599
+ # * Show information about insttance variables about `self`.
600
+ # * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
601
+ # * Show information about accessible constants except toplevel constants.
602
+ # * `i[nfo] g[lobal[s]]`
603
+ # * Show information about global variables
604
+ # * `i[nfo] ... </pattern/>`
605
+ # * Filter the output with `</pattern/>`.
507
606
  # * `i[nfo] th[read[s]]`
508
607
  # * Show all threads (same as `th[read]`).
509
608
  when 'i', 'info'
510
- case arg
609
+ if /\/(.+)\/\z/ =~ arg
610
+ pat = Regexp.compile($1)
611
+ sub = $~.pre_match.strip
612
+ else
613
+ sub = arg
614
+ end
615
+
616
+ case sub
511
617
  when nil
512
- @tc << [:show, :local]
513
- when 'l', /locals?/
514
- @tc << [:show, :local]
618
+ @tc << [:show, :default, pat] # something useful
619
+ when 'l', /^locals?/
620
+ @tc << [:show, :locals, pat]
621
+ when 'i', /^ivars?/i, /^instance[_ ]variables?/i
622
+ @tc << [:show, :ivars, pat]
623
+ when 'c', /^consts?/i, /^constants?/i
624
+ @tc << [:show, :consts, pat]
625
+ when 'g', /^globals?/i, /^global[_ ]variables?/i
626
+ @tc << [:show, :globals, pat]
515
627
  when 'th', /threads?/
516
628
  thread_list
517
629
  return :retry
518
630
  else
631
+ @ui.puts "unrecognized argument for info command: #{arg}"
519
632
  show_help 'info'
520
633
  return :retry
521
634
  end
522
635
 
636
+ # * `o[utline]` or `ls`
637
+ # * Show you available methods, constants, local variables, and instance variables in the current scope.
638
+ # * `o[utline] <expr>` or `ls <expr>`
639
+ # * Show you available methods and instance variables of the given object.
640
+ # * If the object is a class/module, it also lists its constants.
641
+ when 'outline', 'o', 'ls'
642
+ @tc << [:show, :outline, arg]
643
+
523
644
  # * `display`
524
645
  # * Show display setting.
525
646
  # * `display <expr>`
@@ -598,6 +719,105 @@ module DEBUGGER__
598
719
  # don't repeat irb command
599
720
  @repl_prev_line = nil
600
721
 
722
+ ### Trace
723
+ # * `trace`
724
+ # * Show available tracers list.
725
+ # * `trace line`
726
+ # * Add a line tracer. It indicates line events.
727
+ # * `trace call`
728
+ # * Add a call tracer. It indicate call/return events.
729
+ # * `trace exception`
730
+ # * Add an exception tracer. It indicates raising exceptions.
731
+ # * `trace object <expr>`
732
+ # * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
733
+ # * `trace ... </pattern/>`
734
+ # * Indicates only matched events to `</pattern/>` (RegExp).
735
+ # * `trace ... into: <file>`
736
+ # * Save trace information into: `<file>`.
737
+ # * `trace off <num>`
738
+ # * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
739
+ # * `trace off [line|call|pass]`
740
+ # * Disable all tracers. If `<type>` is provided, disable specified type tracers.
741
+ when 'trace'
742
+ if (re = /\s+into:\s*(.+)/) =~ arg
743
+ into = $1
744
+ arg.sub!(re, '')
745
+ end
746
+
747
+ if (re = /\s\/(.+)\/\z/) =~ arg
748
+ pattern = $1
749
+ arg.sub!(re, '')
750
+ end
751
+
752
+ case arg
753
+ when nil
754
+ @ui.puts 'Tracers:'
755
+ @tracers.each_with_index{|t, i|
756
+ @ui.puts "* \##{i} #{t}"
757
+ }
758
+ @ui.puts
759
+ return :retry
760
+
761
+ when /\Aline\z/
762
+ @tracers << t = LineTracer.new(@ui, pattern: pattern, into: into)
763
+ @ui.puts "Enable #{t.to_s}"
764
+ return :retry
765
+
766
+ when /\Acall\z/
767
+ @tracers << t = CallTracer.new(@ui, pattern: pattern, into: into)
768
+ @ui.puts "Enable #{t.to_s}"
769
+ return :retry
770
+
771
+ when /\Aexception\z/
772
+ @tracers << t = ExceptionTracer.new(@ui, pattern: pattern, into: into)
773
+ @ui.puts "Enable #{t.to_s}"
774
+ return :retry
775
+
776
+ when /\Aobject\s+(.+)/
777
+ @tc << [:trace, :object, $1.strip, {pattern: pattern, into: into}]
778
+
779
+ when /\Aoff\s+(\d+)\z/
780
+ if t = @tracers[$1.to_i]
781
+ t.disable
782
+ @ui.puts "Disable #{t.to_s}"
783
+ else
784
+ @ui.puts "Unmatched: #{$1}"
785
+ end
786
+ return :retry
787
+
788
+ when /\Aoff(\s+(line|call|exception|object))?\z/
789
+ @tracers.each{|t|
790
+ if $2.nil? || t.type == $2
791
+ t.disable
792
+ @ui.puts "Disable #{t.to_s}"
793
+ end
794
+ }
795
+ return :retry
796
+
797
+ else
798
+ @ui.puts "Unknown trace option: #{arg.inspect}"
799
+ return :retry
800
+ end
801
+
802
+ # Record
803
+ # * `record`
804
+ # * Show recording status.
805
+ # * `record [on|off]`
806
+ # * Start/Stop recording.
807
+ # * `step back`
808
+ # * Start replay. Step back with the last execution log.
809
+ # * `s[tep]` does stepping forward with the last log.
810
+ # * `step reset`
811
+ # * Stop replay .
812
+ when 'record'
813
+ case arg
814
+ when nil, 'on', 'off'
815
+ @tc << [:record, arg&.to_sym]
816
+ else
817
+ @ui.puts "unknown command: #{arg}"
818
+ return :retry
819
+ end
820
+
601
821
  ### Thread control
602
822
 
603
823
  # * `th[read]`
@@ -630,13 +850,28 @@ module DEBUGGER__
630
850
  config_command arg
631
851
  return :retry
632
852
 
853
+ # * `source <file>`
854
+ # * Evaluate lines in `<file>` as debug commands.
855
+ when 'source'
856
+ if arg
857
+ begin
858
+ cmds = File.readlines(path = File.expand_path(arg))
859
+ add_preset_commands path, cmds, kick: true, continue: false
860
+ rescue Errno::ENOENT
861
+ @ui.puts "File not found: #{arg}"
862
+ end
863
+ else
864
+ show_help 'source'
865
+ end
866
+ return :retry
867
+
633
868
  ### Help
634
869
 
635
870
  # * `h[elp]`
636
871
  # * Show help for all commands.
637
872
  # * `h[elp] <command>`
638
873
  # * Show help for the given command.
639
- when 'h', 'help'
874
+ when 'h', 'help', '?'
640
875
  if arg
641
876
  show_help arg
642
877
  else
@@ -646,21 +881,56 @@ module DEBUGGER__
646
881
 
647
882
  ### END
648
883
  else
649
- @ui.puts "unknown command: #{line}"
884
+ @tc << [:eval, :pp, line]
885
+ =begin
650
886
  @repl_prev_line = nil
887
+ @ui.puts "unknown command: #{line}"
888
+ begin
889
+ require 'did_you_mean'
890
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: DEBUGGER__.commands)
891
+ correction = spell_checker.correct(line.split(/\s/).first || '')
892
+ @ui.puts "Did you mean? #{correction.join(' or ')}" unless correction.empty?
893
+ rescue LoadError
894
+ # Don't use D
895
+ end
651
896
  return :retry
897
+ =end
652
898
  end
653
899
 
654
900
  rescue Interrupt
655
901
  return :retry
656
902
  rescue SystemExit
657
903
  raise
904
+ rescue PostmortemError => e
905
+ @ui.puts e.message
906
+ return :retry
658
907
  rescue Exception => e
659
908
  @ui.puts "[REPL ERROR] #{e.inspect}"
660
909
  @ui.puts e.backtrace.map{|e| ' ' + e}
661
910
  return :retry
662
911
  end
663
912
 
913
+ def step_command type, arg
914
+ case arg
915
+ when nil
916
+ @tc << [:step, type]
917
+ restart_all_threads
918
+ when /\A\d+\z/
919
+ @tc << [:step, type, arg.to_i]
920
+ restart_all_threads
921
+ when /\Aback\z/, /\Areset\z/
922
+ if type != :in
923
+ @ui.puts "only `step #{arg}` is supported."
924
+ :retry
925
+ else
926
+ @tc << [:step, arg.to_sym]
927
+ end
928
+ else
929
+ @ui.puts "Unknown option: #{arg}"
930
+ :retry
931
+ end
932
+ end
933
+
664
934
  def config_show key
665
935
  key = key.to_sym
666
936
  if CONFIG_SET[key]
@@ -682,9 +952,9 @@ module DEBUGGER__
682
952
  if CONFIG_SET[key = key.to_sym]
683
953
  begin
684
954
  if append
685
- DEBUGGER__.append_config(key, val)
955
+ CONFIG.append_config(key, val)
686
956
  else
687
- DEBUGGER__.set_config({key => val})
957
+ CONFIG[key] = val
688
958
  end
689
959
  rescue => e
690
960
  @ui.puts e.message
@@ -703,7 +973,7 @@ module DEBUGGER__
703
973
 
704
974
  when /\Aunset\s+(.+)\z/
705
975
  if CONFIG_SET[key = $1.to_sym]
706
- DEBUGGER__.set_config({key => nil})
976
+ CONFIG[key] = nil
707
977
  end
708
978
  config_show key
709
979
 
@@ -758,51 +1028,7 @@ module DEBUGGER__
758
1028
  end
759
1029
  end
760
1030
 
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
1031
+ # breakpoint management
806
1032
 
807
1033
  def iterate_bps
808
1034
  deleted_bps = []
@@ -834,7 +1060,29 @@ module DEBUGGER__
834
1060
  nil
835
1061
  end
836
1062
 
837
- def delete_breakpoint arg = nil
1063
+ def rehash_bps
1064
+ bps = @bps.values
1065
+ @bps.clear
1066
+ bps.each{|bp|
1067
+ add_bp bp
1068
+ }
1069
+ end
1070
+
1071
+ def add_bp bp
1072
+ # don't repeat commands that add breakpoints
1073
+ @repl_prev_line = nil
1074
+
1075
+ if @bps.has_key? bp.key
1076
+ unless bp.duplicable?
1077
+ @ui.puts "duplicated breakpoint: #{bp}"
1078
+ bp.disable
1079
+ end
1080
+ else
1081
+ @bps[bp.key] = bp
1082
+ end
1083
+ end
1084
+
1085
+ def delete_bp arg = nil
838
1086
  case arg
839
1087
  when nil
840
1088
  @bps.each{|key, bp| bp.delete}
@@ -873,14 +1121,14 @@ module DEBUGGER__
873
1121
 
874
1122
  case expr[:sig]
875
1123
  when /\A(\d+)\z/
876
- add_line_breakpoint @tc.location.path, $1.to_i, cond: expr[:if], command: cmd
1124
+ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd
877
1125
  when /\A(.+)[:\s+](\d+)\z/
878
- add_line_breakpoint $1, $2.to_i, cond: expr[:if], command: cmd
1126
+ add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
879
1127
  when /\A(.+)([\.\#])(.+)\z/
880
- @tc << [:breakpoint, :method, $1, $2, $3, expr[:if], cmd]
1128
+ @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd]
881
1129
  return :noretry
882
1130
  when nil
883
- add_check_breakpoint expr[:if]
1131
+ add_check_breakpoint cond
884
1132
  else
885
1133
  @ui.puts "Unknown breakpoint format: #{arg}"
886
1134
  @ui.puts
@@ -888,6 +1136,29 @@ module DEBUGGER__
888
1136
  end
889
1137
  end
890
1138
 
1139
+ def repl_add_catch_breakpoint arg
1140
+ expr = parse_break arg.strip
1141
+ cond = expr[:if]
1142
+ cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1143
+
1144
+ bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd)
1145
+ add_bp bp
1146
+ end
1147
+
1148
+ def add_check_breakpoint expr
1149
+ bp = CheckBreakpoint.new(expr)
1150
+ add_bp bp
1151
+ end
1152
+
1153
+ def add_line_breakpoint file, line, **kw
1154
+ file = resolve_path(file)
1155
+ bp = LineBreakpoint.new(file, line, **kw)
1156
+
1157
+ add_bp bp
1158
+ rescue Errno::ENOENT => e
1159
+ @ui.puts e.message
1160
+ end
1161
+
891
1162
  # threads
892
1163
 
893
1164
  def update_thread_list
@@ -896,17 +1167,15 @@ module DEBUGGER__
896
1167
  unmanaged = []
897
1168
 
898
1169
  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]
1170
+ if thc = @th_clients[th]
1171
+ if !thc.management?
1172
+ thcs << thc
1173
+ end
906
1174
  else
907
1175
  unmanaged << th
908
1176
  end
909
1177
  }
1178
+
910
1179
  return thcs.sort_by{|thc| thc.id}, unmanaged
911
1180
  end
912
1181
 
@@ -933,7 +1202,7 @@ module DEBUGGER__
933
1202
  thcs, _unmanaged_ths = update_thread_list
934
1203
 
935
1204
  if tc = thcs[n]
936
- if tc.mode
1205
+ if tc.waiting?
937
1206
  @tc = tc
938
1207
  else
939
1208
  @ui.puts "#{tc.thread} is not controllable yet."
@@ -947,11 +1216,11 @@ module DEBUGGER__
947
1216
  end
948
1217
 
949
1218
  def setup_threads
950
- stop_all_threads do
951
- Thread.list.each{|th|
952
- thread_client_create(th)
953
- }
954
- end
1219
+ @th_clients = {}
1220
+
1221
+ Thread.list.each{|th|
1222
+ thread_client_create(th)
1223
+ }
955
1224
  end
956
1225
 
957
1226
  def on_thread_begin th
@@ -963,8 +1232,7 @@ module DEBUGGER__
963
1232
  end
964
1233
  end
965
1234
 
966
- def thread_client
967
- thr = Thread.current
1235
+ def thread_client thr = Thread.current
968
1236
  if @th_clients.has_key? thr
969
1237
  @th_clients[thr]
970
1238
  else
@@ -972,84 +1240,67 @@ module DEBUGGER__
972
1240
  end
973
1241
  end
974
1242
 
975
- def stop_all_threads
976
- current = Thread.current
1243
+ private def thread_stopper
1244
+ @thread_stopper ||= TracePoint.new(:line) do
1245
+ # run on each thread
1246
+ tc = ThreadClient.current
1247
+ next if tc.management?
1248
+ next unless tc.running?
1249
+ next if tc == @tc
977
1250
 
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
1251
+ tc.on_pause
1001
1252
  end
1002
1253
  end
1003
1254
 
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
1255
+ private def running_thread_clients_count
1256
+ @th_clients.count{|th, tc|
1257
+ next if tc.management?
1258
+ next unless tc.running?
1259
+ true
1260
+ }
1015
1261
  end
1016
1262
 
1017
- # breakpoint management
1263
+ private def waiting_thread_clients
1264
+ @th_clients.map{|th, tc|
1265
+ next if tc.management?
1266
+ next unless tc.waiting?
1267
+ tc
1268
+ }.compact
1269
+ end
1018
1270
 
1019
- def add_breakpoint bp
1020
- # don't repeat commands that add breakpoints
1021
- @repl_prev_line = nil
1271
+ private def stop_all_threads
1272
+ return if running_thread_clients_count == 0
1022
1273
 
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
1274
+ stopper = thread_stopper
1275
+ stopper.enable unless stopper.enabled?
1031
1276
  end
1032
1277
 
1033
- def rehash_bps
1034
- bps = @bps.values
1035
- @bps.clear
1036
- bps.each{|bp|
1037
- add_breakpoint bp
1278
+ private def restart_all_threads
1279
+ stopper = thread_stopper
1280
+ stopper.disable if stopper.enabled?
1281
+
1282
+ waiting_thread_clients.each{|tc|
1283
+ next if @tc == tc
1284
+ tc << :continue
1038
1285
  }
1286
+ @tc = nil
1039
1287
  end
1040
1288
 
1041
- def break? file, line
1042
- @bps.has_key? [file, line]
1043
- end
1289
+ ## event
1044
1290
 
1045
- def add_catch_breakpoint arg
1046
- bp = CatchBreakpoint.new(arg)
1047
- add_breakpoint bp
1048
- end
1291
+ def on_load iseq, src
1292
+ DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1293
+ @sr.add iseq, src
1049
1294
 
1050
- def add_check_breakpoint expr
1051
- bp = CheckBreakpoint.new(expr)
1052
- add_breakpoint bp
1295
+ pending_line_breakpoints = @bps.find_all do |key, bp|
1296
+ LineBreakpoint === bp && !bp.iseq
1297
+ end
1298
+
1299
+ pending_line_breakpoints.each do |_key, bp|
1300
+ if bp.path == (iseq.absolute_path || iseq.path)
1301
+ bp.try_activate
1302
+ end
1303
+ end
1053
1304
  end
1054
1305
 
1055
1306
  def resolve_path file
@@ -1070,23 +1321,6 @@ module DEBUGGER__
1070
1321
  raise
1071
1322
  end
1072
1323
 
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
1324
  def method_added tp
1091
1325
  b = tp.binding
1092
1326
  if var_name = b.local_variables.first
@@ -1115,10 +1349,55 @@ module DEBUGGER__
1115
1349
  @ui.width
1116
1350
  end
1117
1351
 
1118
- def check_forked
1119
- unless @session_server.status
1120
- # TODO: Support it
1121
- raise 'DEBUGGER: stop at forked process is not supported yet.'
1352
+ def check_postmortem
1353
+ if @postmortem
1354
+ raise PostmortemError, "Can not use this command on postmortem mode."
1355
+ end
1356
+ end
1357
+
1358
+ def enter_postmortem_session frames
1359
+ @postmortem = true
1360
+ ThreadClient.current.suspend :postmortem, postmortem_frames: frames
1361
+ ensure
1362
+ @postmortem = false
1363
+ end
1364
+
1365
+ def postmortem=(is_enable)
1366
+ if is_enable
1367
+ unless @postmortem_hook
1368
+ @postmortem_hook = TracePoint.new(:raise){|tp|
1369
+ exc = tp.raised_exception
1370
+ frames = DEBUGGER__.capture_frames(__dir__)
1371
+ exc.instance_variable_set(:@postmortem_frames, frames)
1372
+ }
1373
+ at_exit{
1374
+ @postmortem_hook.disable
1375
+ if CONFIG[:postmortem] && (exc = $!) != nil
1376
+ exc = exc.cause while exc.cause
1377
+
1378
+ begin
1379
+ @ui.puts "Enter postmortem mode with #{exc.inspect}"
1380
+ @ui.puts exc.backtrace.map{|e| ' ' + e}
1381
+ @ui.puts "\n"
1382
+
1383
+ enter_postmortem_session exc.instance_variable_get(:@postmortem_frames)
1384
+ rescue SystemExit
1385
+ exit!
1386
+ rescue Exception => e
1387
+ @ui = STDERR unless @ui
1388
+ @ui.puts "Error while postmortem console: #{e.inspect}"
1389
+ end
1390
+ end
1391
+ }
1392
+ end
1393
+
1394
+ if !@postmortem_hook.enabled?
1395
+ @postmortem_hook.enable
1396
+ end
1397
+ else
1398
+ if @postmortem_hook && @postmortem_hook.enabled?
1399
+ @postmortem_hook.disable
1400
+ end
1122
1401
  end
1123
1402
  end
1124
1403
  end
@@ -1165,18 +1444,18 @@ module DEBUGGER__
1165
1444
  # start methods
1166
1445
 
1167
1446
  def self.start nonstop: false, **kw
1168
- set_config(kw)
1447
+ CONFIG.set_config(**kw)
1169
1448
 
1170
1449
  unless defined? SESSION
1171
- require_relative 'console'
1172
- initialize_session UI_Console.new
1450
+ require_relative 'local'
1451
+ initialize_session UI_LocalConsole.new
1173
1452
  end
1174
1453
 
1175
1454
  setup_initial_suspend unless nonstop
1176
1455
  end
1177
1456
 
1178
- def self.open host: nil, port: ::DEBUGGER__::CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1179
- set_config(kw)
1457
+ def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1458
+ CONFIG.set_config(**kw)
1180
1459
 
1181
1460
  if port
1182
1461
  open_tcp host: host, port: port, nonstop: nonstop
@@ -1186,7 +1465,7 @@ module DEBUGGER__
1186
1465
  end
1187
1466
 
1188
1467
  def self.open_tcp host: nil, port:, nonstop: false, **kw
1189
- set_config(kw)
1468
+ CONFIG.set_config(**kw)
1190
1469
  require_relative 'server'
1191
1470
 
1192
1471
  if defined? SESSION
@@ -1199,7 +1478,7 @@ module DEBUGGER__
1199
1478
  end
1200
1479
 
1201
1480
  def self.open_unix sock_path: nil, sock_dir: nil, nonstop: false, **kw
1202
- set_config(kw)
1481
+ CONFIG.set_config(**kw)
1203
1482
  require_relative 'server'
1204
1483
 
1205
1484
  if defined? SESSION
@@ -1214,13 +1493,13 @@ module DEBUGGER__
1214
1493
  # boot utilities
1215
1494
 
1216
1495
  def self.setup_initial_suspend
1217
- if !::DEBUGGER__::CONFIG[:nonstop]
1496
+ if !CONFIG[:nonstop]
1218
1497
  if loc = ::DEBUGGER__.require_location
1219
- # require 'debug/console' or 'debug'
1498
+ # require 'debug/start' or 'debug'
1220
1499
  add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
1221
1500
  else
1222
1501
  # -r
1223
- add_line_breakpoint $0, 1, oneshot: true, hook_call: false
1502
+ add_line_breakpoint $0, 0, oneshot: true, hook_call: false
1224
1503
  end
1225
1504
  end
1226
1505
  end
@@ -1258,10 +1537,10 @@ module DEBUGGER__
1258
1537
  [[File.expand_path('~/.rdbgrc'), true],
1259
1538
  [File.expand_path('~/.rdbgrc.rb'), true],
1260
1539
  # ['./.rdbgrc', true], # disable because of security concern
1261
- [::DEBUGGER__::CONFIG[:init_script], false],
1540
+ [CONFIG[:init_script], false],
1262
1541
  ].each{|(path, rc)|
1263
1542
  next unless path
1264
- next if rc && ::DEBUGGER__::CONFIG[:no_rc] # ignore rc
1543
+ next if rc && CONFIG[:no_rc] # ignore rc
1265
1544
 
1266
1545
  if File.file? path
1267
1546
  if path.end_with?('.rb')
@@ -1275,62 +1554,12 @@ module DEBUGGER__
1275
1554
  }
1276
1555
 
1277
1556
  # given debug commands
1278
- if ::DEBUGGER__::CONFIG[:commands]
1279
- cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
1557
+ if CONFIG[:commands]
1558
+ cmds = CONFIG[:commands].split(';;')
1280
1559
  ::DEBUGGER__::SESSION.add_preset_commands "commands", cmds, kick: false, continue: false
1281
1560
  end
1282
1561
  end
1283
1562
 
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
1563
  class ::Module
1335
1564
  undef method_added
1336
1565
  def method_added mid; end
@@ -1386,4 +1615,69 @@ module DEBUGGER__
1386
1615
  end
1387
1616
  end
1388
1617
  end
1618
+
1619
+ module ForkInterceptor
1620
+ def fork(&given_block)
1621
+ return super unless defined?(SESSION) && SESSION.active?
1622
+
1623
+ # before fork
1624
+ if CONFIG[:parent_on_fork]
1625
+ parent_hook = -> child_pid {
1626
+ # Do nothing
1627
+ }
1628
+ child_hook = -> {
1629
+ DEBUGGER__.warn "Detaching after fork from child process #{Process.pid}"
1630
+ SESSION.deactivate
1631
+ }
1632
+ else
1633
+ parent_pid = Process.pid
1634
+
1635
+ parent_hook = -> child_pid {
1636
+ DEBUGGER__.warn "Detaching after fork from parent process #{Process.pid}"
1637
+ SESSION.deactivate
1638
+
1639
+ at_exit{
1640
+ trap(:SIGINT, :IGNORE)
1641
+ Process.waitpid(child_pid)
1642
+ }
1643
+ }
1644
+ child_hook = -> {
1645
+ DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
1646
+ SESSION.activate on_fork: true
1647
+ }
1648
+ end
1649
+
1650
+ if given_block
1651
+ new_block = proc {
1652
+ # after fork: child
1653
+ child_hook.call
1654
+ given_block.call
1655
+ }
1656
+ pid = super(&new_block)
1657
+ parent_hook.call(pid)
1658
+ pid
1659
+ else
1660
+ if pid = super
1661
+ # after fork: parent
1662
+ parent_hook.call pid
1663
+ else
1664
+ # after fork: child
1665
+ child_hook.call
1666
+ end
1667
+
1668
+ pid
1669
+ end
1670
+ end
1671
+ end
1672
+
1673
+ class ::Object
1674
+ include ForkInterceptor
1675
+ end
1676
+
1677
+ module ::Process
1678
+ class << self
1679
+ prepend ForkInterceptor
1680
+ end
1681
+ end
1389
1682
  end
1683
+