debug 1.0.0.beta8 → 1.0.0.rc1

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
@@ -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
+