debug 1.0.0.beta6 → 1.0.0.rc2

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,50 +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
 
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
149
+ end
150
+
104
151
  def reset_ui ui
105
152
  @ui.close
106
153
  @ui = ui
107
- @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
154
+ end
155
+
156
+ def pop_event
157
+ @q_evt.pop
108
158
  end
109
159
 
110
160
  def session_server_main
111
- while evt = @q_evt.pop
161
+ while evt = pop_event
112
162
  # varible `@internal_info` is only used for test
113
163
  tc, output, ev, @internal_info, *ev_args = evt
114
164
  output.each{|str| @ui.puts str}
@@ -116,16 +166,26 @@ module DEBUGGER__
116
166
  case ev
117
167
  when :init
118
168
  wait_command_loop tc
169
+
119
170
  when :load
120
171
  iseq, src = ev_args
121
172
  on_load iseq, src
122
173
  @ui.event :load
123
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
+
124
183
  when :thread_begin
125
184
  th = ev_args.shift
126
185
  on_thread_begin th
127
186
  @ui.event :thread_begin, th
128
187
  tc << :continue
188
+
129
189
  when :suspend
130
190
  case ev_args.first
131
191
  when :breakpoint
@@ -138,10 +198,12 @@ module DEBUGGER__
138
198
  end
139
199
 
140
200
  if @displays.empty?
201
+ stop_all_threads
141
202
  wait_command_loop tc
142
203
  else
143
204
  tc << [:eval, :display, @displays]
144
205
  end
206
+
145
207
  when :result
146
208
  case ev_args.first
147
209
  when :try_display
@@ -152,14 +214,22 @@ module DEBUGGER__
152
214
  @ui.puts "canceled: #{@displays.pop}"
153
215
  end
154
216
  end
217
+ stop_all_threads
218
+
155
219
  when :method_breakpoint, :watch_breakpoint
156
220
  bp = ev_args[1]
157
221
  if bp
158
- add_breakpoint(bp)
222
+ add_bp(bp)
159
223
  show_bps bp
160
224
  else
161
225
  # can't make a bp
162
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}"
163
233
  else
164
234
  # ignore
165
235
  end
@@ -172,24 +242,24 @@ module DEBUGGER__
172
242
  end
173
243
  end
174
244
  ensure
175
- @bps.each{|k, bp| bp.disable}
176
- @th_clients.each{|th, thc| thc.close}
245
+ deactivate
177
246
  end
178
247
 
179
248
  def add_preset_commands name, cmds, kick: true, continue: true
180
249
  cs = cmds.map{|c|
181
- c = c.strip.gsub(/\A\s*\#.*/, '').strip
182
- c unless c.empty?
183
- }.compact
184
-
185
- unless cs.empty?
186
- if @preset_command
187
- @preset_command.commands += cs
188
- else
189
- @preset_command = PresetCommand.new(cs, name, continue)
190
- end
191
- 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)
192
260
  end
261
+
262
+ ThreadClient.current.on_init name if kick
193
263
  end
194
264
 
195
265
  def source iseq
@@ -206,20 +276,26 @@ module DEBUGGER__
206
276
 
207
277
  def wait_command_loop tc
208
278
  @tc = tc
209
- stop_all_threads do
210
- loop do
211
- case wait_command
212
- when :retry
213
- # nothing
214
- else
215
- break
216
- end
217
- rescue Interrupt
218
- retry
279
+
280
+ loop do
281
+ case wait_command
282
+ when :retry
283
+ # nothing
284
+ else
285
+ break
219
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) '
220
298
  end
221
- ensure
222
- @tc = nil
223
299
  end
224
300
 
225
301
  def wait_command
@@ -239,7 +315,7 @@ module DEBUGGER__
239
315
  end
240
316
  else
241
317
  @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
242
- line = @ui.readline
318
+ line = @ui.readline prompt
243
319
  end
244
320
 
245
321
  case line
@@ -273,27 +349,37 @@ module DEBUGGER__
273
349
 
274
350
  # * `s[tep]`
275
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.
276
354
  when 's', 'step'
277
355
  cancel_auto_continue
278
- @tc << [:step, :in]
356
+ check_postmortem
357
+ step_command :in, arg
279
358
 
280
359
  # * `n[ext]`
281
360
  # * Step over. Resume the program until next line.
361
+ # * `n[ext] <n>`
362
+ # * Step over, same as `step <n>`.
282
363
  when 'n', 'next'
283
364
  cancel_auto_continue
284
- @tc << [:step, :next]
365
+ check_postmortem
366
+ step_command :next, arg
285
367
 
286
368
  # * `fin[ish]`
287
369
  # * Finish this frame. Resume the program until the current frame is finished.
370
+ # * `fin[ish] <n>`
371
+ # * Finish frames, same as `step <n>`.
288
372
  when 'fin', 'finish'
289
373
  cancel_auto_continue
290
- @tc << [:step, :finish]
374
+ check_postmortem
375
+ step_command :finish, arg
291
376
 
292
377
  # * `c[ontinue]`
293
378
  # * Resume the program.
294
379
  when 'c', 'continue'
295
380
  cancel_auto_continue
296
381
  @tc << :continue
382
+ restart_all_threads
297
383
 
298
384
  # * `q[uit]` or `Ctrl-D`
299
385
  # * Finish debugger (with the debuggee process on non-remote debugging).
@@ -301,6 +387,7 @@ module DEBUGGER__
301
387
  if ask 'Really quit?'
302
388
  @ui.quit arg.to_i
303
389
  @tc << :continue
390
+ restart_all_threads
304
391
  else
305
392
  return :retry
306
393
  end
@@ -309,7 +396,7 @@ module DEBUGGER__
309
396
  # * Same as q[uit] but without the confirmation prompt.
310
397
  when 'q!', 'quit!'
311
398
  @ui.quit arg.to_i
312
- @tc << :continue
399
+ restart_all_threads
313
400
 
314
401
  # * `kill`
315
402
  # * Stop the debuggee process with `Kernal#exit!`.
@@ -337,12 +424,18 @@ module DEBUGGER__
337
424
  # * Set breakpoint on the method `<class>#<name>`.
338
425
  # * `b[reak] <expr>.<name>`
339
426
  # * Set breakpoint on the method `<expr>.<name>`.
340
- # * `b[reak] ... if <expr>`
427
+ # * `b[reak] ... if: <expr>`
341
428
  # * break if `<expr>` is true at specified location.
342
- # * `b[reak] if <expr>`
343
- # * break if `<expr>` is true at any lines.
429
+ # * `b[reak] ... pre: <command>`
430
+ # * break and run `<command>` before stopping.
431
+ # * `b[reak] ... do: <command>`
432
+ # * break and run `<command>`, and continue.
433
+ # * `b[reak] if: <expr>`
434
+ # * break if: `<expr>` is true at any lines.
344
435
  # * Note that this feature is super slow.
345
436
  when 'b', 'break'
437
+ check_postmortem
438
+
346
439
  if arg == nil
347
440
  show_bps
348
441
  return :retry
@@ -359,6 +452,7 @@ module DEBUGGER__
359
452
 
360
453
  # skip
361
454
  when 'bv'
455
+ check_postmortem
362
456
  require 'json'
363
457
 
364
458
  h = Hash.new{|h, k| h[k] = []}
@@ -385,8 +479,10 @@ module DEBUGGER__
385
479
  # * `catch <Error>`
386
480
  # * Set breakpoint on raising `<Error>`.
387
481
  when 'catch'
482
+ check_postmortem
483
+
388
484
  if arg
389
- bp = add_catch_breakpoint arg
485
+ bp = repl_add_catch_breakpoint arg
390
486
  show_bps bp if bp
391
487
  else
392
488
  show_bps
@@ -397,6 +493,8 @@ module DEBUGGER__
397
493
  # * Stop the execution when the result of current scope's `@ivar` is changed.
398
494
  # * Note that this feature is super slow.
399
495
  when 'wat', 'watch'
496
+ check_postmortem
497
+
400
498
  if arg && arg.match?(/\A@\w+/)
401
499
  @tc << [:breakpoint, :watch, arg]
402
500
  else
@@ -409,15 +507,17 @@ module DEBUGGER__
409
507
  # * `del[ete] <bpnum>`
410
508
  # * delete specified breakpoint.
411
509
  when 'del', 'delete'
510
+ check_postmortem
511
+
412
512
  bp =
413
513
  case arg
414
514
  when nil
415
515
  show_bps
416
516
  if ask "Remove all breakpoints?", 'N'
417
- delete_breakpoint
517
+ delete_bp
418
518
  end
419
519
  when /\d+/
420
- delete_breakpoint arg.to_i
520
+ delete_bp arg.to_i
421
521
  else
422
522
  nil
423
523
  end
@@ -428,8 +528,25 @@ module DEBUGGER__
428
528
 
429
529
  # * `bt` or `backtrace`
430
530
  # * Show backtrace (frame) information.
531
+ # * `bt <num>` or `backtrace <num>`
532
+ # * Only shows first `<num>` frames.
533
+ # * `bt /regexp/` or `backtrace /regexp/`
534
+ # * Only shows frames with method name or location info that matches `/regexp/`.
535
+ # * `bt <num> /regexp/` or `backtrace <num> /regexp/`
536
+ # * Only shows first `<num>` frames with method name or location info that matches `/regexp/`.
431
537
  when 'bt', 'backtrace'
432
- @tc << [:show, :backtrace]
538
+ case arg
539
+ when /\A(\d+)\z/
540
+ @tc << [:show, :backtrace, arg.to_i, nil]
541
+ when /\A\/(.*)\/\z/
542
+ pattern = $1
543
+ @tc << [:show, :backtrace, nil, Regexp.compile(pattern)]
544
+ when /\A(\d+)\s+\/(.*)\/\z/
545
+ max, pattern = $1, $2
546
+ @tc << [:show, :backtrace, max.to_i, Regexp.compile(pattern)]
547
+ else
548
+ @tc << [:show, :backtrace, nil, nil]
549
+ end
433
550
 
434
551
  # * `l[ist]`
435
552
  # * Show current frame's source code.
@@ -473,25 +590,57 @@ module DEBUGGER__
473
590
 
474
591
  @tc << [:show, :edit, arg]
475
592
 
476
- # * `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]]`
477
596
  # * Show information about the current frame (local variables)
478
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/>`.
479
606
  # * `i[nfo] th[read[s]]`
480
607
  # * Show all threads (same as `th[read]`).
481
608
  when 'i', 'info'
482
- 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
483
617
  when nil
484
- @tc << [:show, :local]
485
- when 'l', /locals?/
486
- @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]
487
627
  when 'th', /threads?/
488
628
  thread_list
489
629
  return :retry
490
630
  else
631
+ @ui.puts "unrecognized argument for info command: #{arg}"
491
632
  show_help 'info'
492
633
  return :retry
493
634
  end
494
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
+
495
644
  # * `display`
496
645
  # * Show display setting.
497
646
  # * `display <expr>`
@@ -522,28 +671,6 @@ module DEBUGGER__
522
671
  end
523
672
  return :retry
524
673
 
525
- # * `trace [on|off]`
526
- # * enable or disable line tracer.
527
- when 'trace'
528
- case arg
529
- when 'on'
530
- dir = __dir__
531
- @tracer ||= TracePoint.new(:call, :return, :b_call, :b_return, :line, :class, :end){|tp|
532
- next if File.dirname(tp.path) == dir
533
- next if tp.path == '<internal:trace_point>'
534
- # Skip when `JSON.generate` is called during tests
535
- next if tp.binding.eval('self').to_s == 'JSON' and ENV['RUBY_DEBUG_TEST_MODE']
536
- # next if tp.event != :line
537
- @ui.puts pretty_tp(tp)
538
- }
539
- @tracer.enable
540
- when 'off'
541
- @tracer && @tracer.disable
542
- end
543
- enabled = (@tracer && @tracer.enabled?) ? true : false
544
- @ui.puts "Trace #{enabled ? 'on' : 'off'}"
545
- return :retry
546
-
547
674
  ### Frame control
548
675
 
549
676
  # * `f[rame]`
@@ -592,6 +719,105 @@ module DEBUGGER__
592
719
  # don't repeat irb command
593
720
  @repl_prev_line = nil
594
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
+
595
821
  ### Thread control
596
822
 
597
823
  # * `th[read]`
@@ -609,13 +835,43 @@ module DEBUGGER__
609
835
  end
610
836
  return :retry
611
837
 
838
+ ### Configuration
839
+ # * `config`
840
+ # * Show all configuration with description.
841
+ # * `config <name>`
842
+ # * Show current configuration of <name>.
843
+ # * `config set <name> <val>` or `config <name> = <val>`
844
+ # * Set <name> to <val>.
845
+ # * `config append <name> <val>` or `config <name> << <val>`
846
+ # * Append `<val>` to `<name>` if it is an array.
847
+ # * `config unset <name>`
848
+ # * Set <name> to default.
849
+ when 'config'
850
+ config_command arg
851
+ return :retry
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
+
612
868
  ### Help
613
869
 
614
870
  # * `h[elp]`
615
871
  # * Show help for all commands.
616
872
  # * `h[elp] <command>`
617
873
  # * Show help for the given command.
618
- when 'h', 'help'
874
+ when 'h', 'help', '?'
619
875
  if arg
620
876
  show_help arg
621
877
  else
@@ -625,21 +881,123 @@ module DEBUGGER__
625
881
 
626
882
  ### END
627
883
  else
628
- @ui.puts "unknown command: #{line}"
884
+ @tc << [:eval, :pp, line]
885
+ =begin
629
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
630
896
  return :retry
897
+ =end
631
898
  end
632
899
 
633
900
  rescue Interrupt
634
901
  return :retry
635
902
  rescue SystemExit
636
903
  raise
904
+ rescue PostmortemError => e
905
+ @ui.puts e.message
906
+ return :retry
637
907
  rescue Exception => e
638
908
  @ui.puts "[REPL ERROR] #{e.inspect}"
639
909
  @ui.puts e.backtrace.map{|e| ' ' + e}
640
910
  return :retry
641
911
  end
642
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
+
934
+ def config_show key
935
+ key = key.to_sym
936
+ if CONFIG_SET[key]
937
+ v = CONFIG[key]
938
+ kv = "#{key} = #{v.nil? ? '(default)' : v.inspect}"
939
+ desc = CONFIG_SET[key][1]
940
+ line = "%-30s \# %s" % [kv, desc]
941
+ if line.size > SESSION.width
942
+ @ui.puts "\# #{desc}\n#{kv}"
943
+ else
944
+ @ui.puts line
945
+ end
946
+ else
947
+ @ui.puts "Unknown configuration: #{key}. 'config' shows all configurations."
948
+ end
949
+ end
950
+
951
+ def config_set key, val, append: false
952
+ if CONFIG_SET[key = key.to_sym]
953
+ begin
954
+ if append
955
+ CONFIG.append_config(key, val)
956
+ else
957
+ CONFIG[key] = val
958
+ end
959
+ rescue => e
960
+ @ui.puts e.message
961
+ end
962
+ end
963
+
964
+ config_show key
965
+ end
966
+
967
+ def config_command arg
968
+ case arg
969
+ when nil
970
+ CONFIG_SET.each do |k, _|
971
+ config_show k
972
+ end
973
+
974
+ when /\Aunset\s+(.+)\z/
975
+ if CONFIG_SET[key = $1.to_sym]
976
+ CONFIG[key] = nil
977
+ end
978
+ config_show key
979
+
980
+ when /\A(\w+)\s*=\s*(.+)\z/
981
+ config_set $1, $2
982
+
983
+ when /\A\s*set\s+(\w+)\s+(.+)\z/
984
+ config_set $1, $2
985
+
986
+ when /\A(\w+)\s*<<\s*(.+)\z/
987
+ config_set $1, $2, append: true
988
+
989
+ when /\A\s*append\s+(\w+)\s+(.+)\z/
990
+ config_set $1, $2
991
+
992
+ when /\A(\w+)\z/
993
+ config_show $1
994
+
995
+ else
996
+ @ui.puts "Can not parse parameters: #{arg}"
997
+ end
998
+ end
999
+
1000
+
643
1001
  def cancel_auto_continue
644
1002
  if @preset_command&.auto_continue
645
1003
  @preset_command.auto_continue = false
@@ -670,51 +1028,7 @@ module DEBUGGER__
670
1028
  end
671
1029
  end
672
1030
 
673
- def msig klass, receiver
674
- if klass.singleton_class?
675
- "#{receiver}."
676
- else
677
- "#{klass}#"
678
- end
679
- end
680
-
681
- def pretty_tp tp
682
- loc = "#{tp.path}:#{tp.lineno}"
683
- level = caller.size
684
-
685
- info =
686
- case tp.event
687
- when :line
688
- "line at #{loc}"
689
- when :call, :c_call
690
- klass = tp.defined_class
691
- "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} at #{loc}"
692
- when :return, :c_return
693
- klass = tp.defined_class
694
- "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} => #{tp.return_value.inspect} at #{loc}"
695
- when :b_call
696
- "b_call at #{loc}"
697
- when :b_return
698
- "b_return => #{tp.return_value} at #{loc}"
699
- when :class
700
- "class #{tp.self} at #{loc}"
701
- when :end
702
- "class #{tp.self} end at #{loc}"
703
- else
704
- "#{tp.event} at #{loc}"
705
- end
706
-
707
- case tp.event
708
- when :call, :b_call, :return, :b_return, :class, :end
709
- level -= 1
710
- end
711
-
712
- "Tracing:#{' ' * level} #{info}"
713
- rescue => e
714
- p e
715
- pp e.backtrace
716
- exit!
717
- end
1031
+ # breakpoint management
718
1032
 
719
1033
  def iterate_bps
720
1034
  deleted_bps = []
@@ -746,7 +1060,29 @@ module DEBUGGER__
746
1060
  nil
747
1061
  end
748
1062
 
749
- 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
750
1086
  case arg
751
1087
  when nil
752
1088
  @bps.each{|key, bp| bp.delete}
@@ -762,26 +1098,34 @@ module DEBUGGER__
762
1098
  end
763
1099
  end
764
1100
 
765
- def repl_add_breakpoint arg
766
- arg.strip!
1101
+ BREAK_KEYWORDS = %w(if: do: pre:).freeze
767
1102
 
768
- case arg
769
- when /\Aif\s+(.+)\z/
770
- cond = $1
771
- when /(.+?)\s+if\s+(.+)\z/
772
- sig = $1
773
- cond = $2
774
- else
775
- sig = arg
776
- end
1103
+ def parse_break arg
1104
+ mode = :sig
1105
+ expr = Hash.new{|h, k| h[k] = []}
1106
+ arg.split(' ').each{|w|
1107
+ if BREAK_KEYWORDS.any?{|pat| w == pat}
1108
+ mode = w[0..-2].to_sym
1109
+ else
1110
+ expr[mode] << w
1111
+ end
1112
+ }
1113
+ expr.default_proc = nil
1114
+ expr.transform_values{|v| v.join(' ')}
1115
+ end
1116
+
1117
+ def repl_add_breakpoint arg
1118
+ expr = parse_break arg.strip
1119
+ cond = expr[:if]
1120
+ cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
777
1121
 
778
- case sig
1122
+ case expr[:sig]
779
1123
  when /\A(\d+)\z/
780
- add_line_breakpoint @tc.location.path, $1.to_i, cond: cond
1124
+ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd
781
1125
  when /\A(.+)[:\s+](\d+)\z/
782
- add_line_breakpoint $1, $2.to_i, cond: cond
1126
+ add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
783
1127
  when /\A(.+)([\.\#])(.+)\z/
784
- @tc << [:breakpoint, :method, $1, $2, $3, cond]
1128
+ @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd]
785
1129
  return :noretry
786
1130
  when nil
787
1131
  add_check_breakpoint cond
@@ -792,6 +1136,29 @@ module DEBUGGER__
792
1136
  end
793
1137
  end
794
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
+
795
1162
  # threads
796
1163
 
797
1164
  def update_thread_list
@@ -800,17 +1167,15 @@ module DEBUGGER__
800
1167
  unmanaged = []
801
1168
 
802
1169
  list.each{|th|
803
- case
804
- when th == Thread.current
805
- # ignore
806
- when @management_threads.include?(th)
807
- # ignore
808
- when @th_clients.has_key?(th)
809
- thcs << @th_clients[th]
1170
+ if thc = @th_clients[th]
1171
+ if !thc.management?
1172
+ thcs << thc
1173
+ end
810
1174
  else
811
1175
  unmanaged << th
812
1176
  end
813
1177
  }
1178
+
814
1179
  return thcs.sort_by{|thc| thc.id}, unmanaged
815
1180
  end
816
1181
 
@@ -837,7 +1202,7 @@ module DEBUGGER__
837
1202
  thcs, _unmanaged_ths = update_thread_list
838
1203
 
839
1204
  if tc = thcs[n]
840
- if tc.mode
1205
+ if tc.waiting?
841
1206
  @tc = tc
842
1207
  else
843
1208
  @ui.puts "#{tc.thread} is not controllable yet."
@@ -851,11 +1216,11 @@ module DEBUGGER__
851
1216
  end
852
1217
 
853
1218
  def setup_threads
854
- stop_all_threads do
855
- Thread.list.each{|th|
856
- thread_client_create(th)
857
- }
858
- end
1219
+ @th_clients = {}
1220
+
1221
+ Thread.list.each{|th|
1222
+ thread_client_create(th)
1223
+ }
859
1224
  end
860
1225
 
861
1226
  def on_thread_begin th
@@ -867,8 +1232,7 @@ module DEBUGGER__
867
1232
  end
868
1233
  end
869
1234
 
870
- def thread_client
871
- thr = Thread.current
1235
+ def thread_client thr = Thread.current
872
1236
  if @th_clients.has_key? thr
873
1237
  @th_clients[thr]
874
1238
  else
@@ -876,83 +1240,67 @@ module DEBUGGER__
876
1240
  end
877
1241
  end
878
1242
 
879
- def stop_all_threads
880
- 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
881
1250
 
882
- if Thread.list.size > 1
883
- TracePoint.new(:line) do
884
- th = Thread.current
885
- if current == th || @management_threads.include?(th)
886
- next
887
- else
888
- tc = ThreadClient.current
889
- tc.on_pause
890
- end
891
- end.enable do
892
- yield
893
- ensure
894
- @th_clients.each{|thr, tc|
895
- case thr
896
- when current, (@tc && @tc.thread)
897
- next
898
- else
899
- tc << :continue if thr != Thread.current
900
- end
901
- }
902
- end
903
- else
904
- yield
1251
+ tc.on_pause
905
1252
  end
906
1253
  end
907
1254
 
908
- ## event
909
-
910
- def on_load iseq, src
911
- @sr.add iseq, src
912
-
913
- pending_line_breakpoints do |bp|
914
- if bp.path == (iseq.absolute_path || iseq.path)
915
- bp.try_activate
916
- end
917
- 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
+ }
918
1261
  end
919
1262
 
920
- # 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
921
1270
 
922
- def add_breakpoint bp
923
- if @bps.has_key? bp.key
924
- unless bp.duplicable?
925
- @ui.puts "duplicated breakpoint: #{bp}"
926
- bp.disable
927
- end
928
- else
929
- @bps[bp.key] = bp
930
- end
1271
+ private def stop_all_threads
1272
+ return if running_thread_clients_count == 0
931
1273
 
932
- # don't repeat commands that add breakpoints
933
- @repl_prev_line = nil
1274
+ stopper = thread_stopper
1275
+ stopper.enable unless stopper.enabled?
934
1276
  end
935
1277
 
936
- def rehash_bps
937
- bps = @bps.values
938
- @bps.clear
939
- bps.each{|bp|
940
- 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
941
1285
  }
1286
+ @tc = nil
942
1287
  end
943
1288
 
944
- def break? file, line
945
- @bps.has_key? [file, line]
946
- end
1289
+ ## event
947
1290
 
948
- def add_catch_breakpoint arg
949
- bp = CatchBreakpoint.new(arg)
950
- add_breakpoint bp
951
- end
1291
+ def on_load iseq, src
1292
+ DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1293
+ @sr.add iseq, src
952
1294
 
953
- def add_check_breakpoint expr
954
- bp = CheckBreakpoint.new(expr)
955
- 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
956
1304
  end
957
1305
 
958
1306
  def resolve_path file
@@ -973,23 +1321,6 @@ module DEBUGGER__
973
1321
  raise
974
1322
  end
975
1323
 
976
- def add_line_breakpoint file, line, **kw
977
- file = resolve_path(file)
978
- bp = LineBreakpoint.new(file, line, **kw)
979
-
980
- add_breakpoint bp
981
- rescue Errno::ENOENT => e
982
- @ui.puts e.message
983
- end
984
-
985
- def pending_line_breakpoints
986
- @bps.find_all do |key, bp|
987
- LineBreakpoint === bp && !bp.iseq
988
- end.each do |key, bp|
989
- yield bp
990
- end
991
- end
992
-
993
1324
  def method_added tp
994
1325
  b = tp.binding
995
1326
  if var_name = b.local_variables.first
@@ -1001,7 +1332,7 @@ module DEBUGGER__
1001
1332
  when MethodBreakpoint
1002
1333
  if bp.method.nil?
1003
1334
  if bp.sig_method_name == mid.to_s
1004
- bp.try_enable(quiet: true)
1335
+ bp.try_enable(added: true)
1005
1336
  end
1006
1337
  end
1007
1338
 
@@ -1018,10 +1349,55 @@ module DEBUGGER__
1018
1349
  @ui.width
1019
1350
  end
1020
1351
 
1021
- def check_forked
1022
- unless @session_server.status
1023
- # TODO: Support it
1024
- 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
1025
1401
  end
1026
1402
  end
1027
1403
  end
@@ -1068,18 +1444,18 @@ module DEBUGGER__
1068
1444
  # start methods
1069
1445
 
1070
1446
  def self.start nonstop: false, **kw
1071
- set_config(kw)
1447
+ CONFIG.set_config(**kw)
1072
1448
 
1073
1449
  unless defined? SESSION
1074
- require_relative 'console'
1075
- initialize_session UI_Console.new
1450
+ require_relative 'local'
1451
+ initialize_session UI_LocalConsole.new
1076
1452
  end
1077
1453
 
1078
1454
  setup_initial_suspend unless nonstop
1079
1455
  end
1080
1456
 
1081
- def self.open host: nil, port: ::DEBUGGER__::CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1082
- 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)
1083
1459
 
1084
1460
  if port
1085
1461
  open_tcp host: host, port: port, nonstop: nonstop
@@ -1089,7 +1465,7 @@ module DEBUGGER__
1089
1465
  end
1090
1466
 
1091
1467
  def self.open_tcp host: nil, port:, nonstop: false, **kw
1092
- set_config(kw)
1468
+ CONFIG.set_config(**kw)
1093
1469
  require_relative 'server'
1094
1470
 
1095
1471
  if defined? SESSION
@@ -1102,7 +1478,7 @@ module DEBUGGER__
1102
1478
  end
1103
1479
 
1104
1480
  def self.open_unix sock_path: nil, sock_dir: nil, nonstop: false, **kw
1105
- set_config(kw)
1481
+ CONFIG.set_config(**kw)
1106
1482
  require_relative 'server'
1107
1483
 
1108
1484
  if defined? SESSION
@@ -1117,13 +1493,13 @@ module DEBUGGER__
1117
1493
  # boot utilities
1118
1494
 
1119
1495
  def self.setup_initial_suspend
1120
- if !::DEBUGGER__::CONFIG[:nonstop]
1496
+ if !CONFIG[:nonstop]
1121
1497
  if loc = ::DEBUGGER__.require_location
1122
- # require 'debug/console' or 'debug'
1498
+ # require 'debug/start' or 'debug'
1123
1499
  add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
1124
1500
  else
1125
1501
  # -r
1126
- add_line_breakpoint $0, 1, oneshot: true, hook_call: false
1502
+ add_line_breakpoint $0, 0, oneshot: true, hook_call: false
1127
1503
  end
1128
1504
  end
1129
1505
  end
@@ -1139,20 +1515,18 @@ module DEBUGGER__
1139
1515
  # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
1140
1516
 
1141
1517
  Binding.module_eval do
1142
- def bp command: nil, nonstop: nil
1143
- if command
1144
- cmds = command.split(";;")
1145
- # nonstop
1146
- # nil, true -> auto_continue
1147
- # false -> stop
1148
- SESSION.add_preset_commands 'binding.bp(command:)', cmds,
1149
- kick: false,
1150
- continue: nonstop == false ? false : true
1518
+ def break pre: nil, do: nil
1519
+ return unless SESSION.active?
1520
+
1521
+ if pre || (do_expr = binding.local_variable_get(:do))
1522
+ cmds = ['binding.break', pre, do_expr]
1151
1523
  end
1152
1524
 
1153
- ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true
1525
+ ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, command: cmds
1154
1526
  true
1155
1527
  end
1528
+ alias b break
1529
+ # alias bp break
1156
1530
  end
1157
1531
 
1158
1532
  load_rc
@@ -1163,10 +1537,10 @@ module DEBUGGER__
1163
1537
  [[File.expand_path('~/.rdbgrc'), true],
1164
1538
  [File.expand_path('~/.rdbgrc.rb'), true],
1165
1539
  # ['./.rdbgrc', true], # disable because of security concern
1166
- [::DEBUGGER__::CONFIG[:init_script], false],
1540
+ [CONFIG[:init_script], false],
1167
1541
  ].each{|(path, rc)|
1168
1542
  next unless path
1169
- next if rc && ::DEBUGGER__::CONFIG[:no_rc] # ignore rc
1543
+ next if rc && CONFIG[:no_rc] # ignore rc
1170
1544
 
1171
1545
  if File.file? path
1172
1546
  if path.end_with?('.rb')
@@ -1180,62 +1554,12 @@ module DEBUGGER__
1180
1554
  }
1181
1555
 
1182
1556
  # given debug commands
1183
- if ::DEBUGGER__::CONFIG[:commands]
1184
- cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
1557
+ if CONFIG[:commands]
1558
+ cmds = CONFIG[:commands].split(';;')
1185
1559
  ::DEBUGGER__::SESSION.add_preset_commands "commands", cmds, kick: false, continue: false
1186
1560
  end
1187
1561
  end
1188
1562
 
1189
- def self.parse_help
1190
- helps = Hash.new{|h, k| h[k] = []}
1191
- desc = cat = nil
1192
- cmds = []
1193
-
1194
- File.read(__FILE__).each_line do |line|
1195
- case line
1196
- when /\A\s*### (.+)/
1197
- cat = $1
1198
- break if $1 == 'END'
1199
- when /\A when (.+)/
1200
- next unless cat
1201
- next unless desc
1202
- ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
1203
- helps[cat] << [ws, desc]
1204
- desc = nil
1205
- cmds.concat ws
1206
- when /\A\s+# (\s*\*.+)/
1207
- if desc
1208
- desc << "\n" + $1
1209
- else
1210
- desc = $1
1211
- end
1212
- end
1213
- end
1214
- @commands = cmds
1215
- @helps = helps
1216
- end
1217
-
1218
- def self.helps
1219
- (defined?(@helps) && @helps) || parse_help
1220
- end
1221
-
1222
- def self.commands
1223
- (defined?(@commands) && @commands) || (parse_help; @commands)
1224
- end
1225
-
1226
- def self.help
1227
- r = []
1228
- self.helps.each{|cat, cmds|
1229
- r << "### #{cat}"
1230
- r << ''
1231
- cmds.each{|ws, desc|
1232
- r << desc
1233
- }
1234
- r << ''
1235
- }
1236
- r.join("\n")
1237
- end
1238
-
1239
1563
  class ::Module
1240
1564
  undef method_added
1241
1565
  def method_added mid; end
@@ -1262,12 +1586,98 @@ module DEBUGGER__
1262
1586
  end
1263
1587
  end
1264
1588
 
1265
- def self.warn msg, level = :warn
1266
- case level
1267
- when :warn
1268
- STDERR.puts "DEBUGGER: #{msg}" unless CONFIG[:quiet]
1269
- when :error
1270
- STDERR.puts "DEBUGGER: #{msg}"
1589
+ LOG_LEVELS = {
1590
+ UNKNOWN: 0,
1591
+ FATAL: 1,
1592
+ ERROR: 2,
1593
+ WARN: 3,
1594
+ INFO: 4,
1595
+ }.freeze
1596
+
1597
+ def self.warn msg
1598
+ log :WARN, msg
1599
+ end
1600
+
1601
+ def self.info msg
1602
+ log :INFO, msg
1603
+ end
1604
+
1605
+ def self.log level, msg
1606
+ lv = LOG_LEVELS[level]
1607
+ config_lv = LOG_LEVELS[CONFIG[:log_level] || :WARN]
1608
+
1609
+ if lv <= config_lv
1610
+ if level == :WARN
1611
+ # :WARN on debugger is general information
1612
+ STDERR.puts "DEBUGGER: #{msg}"
1613
+ else
1614
+ STDERR.puts "DEBUGGER (#{level}): #{msg}"
1615
+ end
1616
+ end
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
1271
1680
  end
1272
1681
  end
1273
1682
  end
1683
+