debug 1.0.0.beta8 → 1.1.0

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