debug 1.0.0.beta5 → 1.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
 
3
5
  module DEBUGGER__
@@ -5,7 +7,7 @@ module DEBUGGER__
5
7
  SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
6
8
 
7
9
  def dap_setup bytes
8
- DEBUGGER__.set_config(use_colorize: false)
10
+ CONFIG.set_config no_color: true
9
11
  @seq = 0
10
12
 
11
13
  $stderr.puts '[>]' + bytes if SHOW_PROTOCOL
@@ -35,6 +37,7 @@ module DEBUGGER__
35
37
  },
36
38
  ],
37
39
  supportsExceptionFilterOptions: true,
40
+ supportsStepBack: true,
38
41
 
39
42
  ## Will be supported
40
43
  # supportsExceptionOptions: true,
@@ -48,7 +51,6 @@ module DEBUGGER__
48
51
  # supportsBreakpointLocationsRequest:
49
52
 
50
53
  ## Possible?
51
- # supportsStepBack:
52
54
  # supportsRestartFrame:
53
55
  # supportsCompletionsRequest:
54
56
  # completionTriggerCharacters:
@@ -195,6 +197,12 @@ module DEBUGGER__
195
197
  when 'pause'
196
198
  send_response req
197
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
198
206
 
199
207
  ## query
200
208
  when 'threads'
@@ -210,6 +218,7 @@ module DEBUGGER__
210
218
  'evaluate',
211
219
  'source'
212
220
  @q_msg << req
221
+
213
222
  else
214
223
  raise "Unknown request: #{req.inspect}"
215
224
  end
@@ -218,7 +227,7 @@ module DEBUGGER__
218
227
 
219
228
  ## called by the SESSION thread
220
229
 
221
- def readline
230
+ def readline prompt
222
231
  @q_msg.pop || 'kill!'
223
232
  end
224
233
 
@@ -279,6 +288,13 @@ module DEBUGGER__
279
288
 
280
289
  def process_dap_request req
281
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
+
282
298
  when 'stackTrace'
283
299
  tid = req.dig('arguments', 'threadId')
284
300
  if tc = find_tc(tid)
@@ -363,7 +379,6 @@ module DEBUGGER__
363
379
  else
364
380
  fail_response req, message: 'not found...'
365
381
  end
366
-
367
382
  return :retry
368
383
  else
369
384
  raise "Unknown DAP request: #{req.inspect}"
@@ -436,8 +451,8 @@ module DEBUGGER__
436
451
  when :backtrace
437
452
  event! :dap_result, :backtrace, req, {
438
453
  stackFrames: @target_frames.map.with_index{|frame, i|
439
- path = frame.path
440
- ref = frame.file_lines unless File.exist?(path)
454
+ path = frame.realpath || frame.path
455
+ ref = frame.file_lines unless path && File.exist?(path)
441
456
 
442
457
  {
443
458
  # id: ??? # filled by SESSION
@@ -455,7 +470,15 @@ module DEBUGGER__
455
470
  when :scopes
456
471
  fid = args.shift
457
472
  frame = @target_frames[fid]
458
- 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
459
482
 
460
483
  event! :dap_result, :scopes, req, scopes: [{
461
484
  name: 'Local variables',
@@ -483,6 +506,10 @@ module DEBUGGER__
483
506
  vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
484
507
  vars.unshift variable('%return', frame.return_value) if frame.has_return_value
485
508
  vars.unshift variable('%self', b.receiver)
509
+ elsif lvars = frame.local_variables
510
+ vars = lvars.map{|var, val|
511
+ variable(var, val)
512
+ }
486
513
  else
487
514
  vars = [variable('%self', frame.self)]
488
515
  vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
data/lib/debug/session.rb CHANGED
@@ -1,4 +1,5 @@
1
- 
1
+ # frozen_string_literal: true
2
+
2
3
  # skip to load debugger for bundle exec
3
4
  return if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
4
5
 
@@ -6,6 +7,7 @@ require_relative 'config'
6
7
  require_relative 'thread_client'
7
8
  require_relative 'source_repository'
8
9
  require_relative 'breakpoint'
10
+ require_relative 'tracer'
9
11
 
10
12
  require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
11
13
 
@@ -52,6 +54,9 @@ class RubyVM::InstructionSequence
52
54
  end
53
55
 
54
56
  module DEBUGGER__
57
+ PresetCommand = Struct.new(:commands, :source, :auto_continue)
58
+ class PostmortemError < RuntimeError; end
59
+
55
60
  class Session
56
61
  def initialize ui
57
62
  @ui = ui
@@ -62,59 +67,125 @@ module DEBUGGER__
62
67
  # "Foo#bar" => MethodBreakpoint
63
68
  # [:watch, ivar] => WatchIVarBreakpoint
64
69
  # [:check, expr] => CheckBreakpoint
65
- @th_clients = {} # {Thread => ThreadClient}
70
+ #
71
+ @tracers = []
72
+ @th_clients = nil # {Thread => ThreadClient}
66
73
  @q_evt = Queue.new
67
74
  @displays = []
68
75
  @tc = nil
69
76
  @tc_id = 0
70
- @initial_commands = []
77
+ @preset_command = nil
78
+ @postmortem_hook = nil
79
+ @postmortem = false
80
+ @thread_stopper = nil
71
81
 
72
82
  @frame_map = {} # {id => [threadId, frame_depth]} for DAP
73
83
  @var_map = {1 => [:globals], } # {id => ...} for DAP
74
84
  @src_map = {} # {id => src}
75
85
 
76
86
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
77
- unless @management_threads.include? Thread.current
78
- ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
79
- end
87
+ ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
80
88
  }
81
89
  @tp_load_script.enable
82
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
83
110
  @session_server = Thread.new do
111
+ Thread.current.name = 'DEBUGGER__::SESSION@server'
84
112
  Thread.current.abort_on_exception = true
85
113
  session_server_main
86
114
  end
87
115
 
88
- @management_threads = [@session_server]
89
- @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
90
-
91
116
  setup_threads
92
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
+
93
131
  @tp_thread_begin = TracePoint.new(:thread_begin){|tp|
94
- unless @management_threads.include?(th = Thread.current)
95
- ThreadClient.current.on_thread_begin th
96
- end
132
+ th = Thread.current
133
+ ThreadClient.current.on_thread_begin th
97
134
  }
98
135
  @tp_thread_begin.enable
99
136
  end
100
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
+
151
+ def reset_ui ui
152
+ @ui.close
153
+ @ui = ui
154
+ end
155
+
156
+ def pop_event
157
+ @q_evt.pop
158
+ end
159
+
101
160
  def session_server_main
102
- while evt = @q_evt.pop
161
+ while evt = pop_event
103
162
  # varible `@internal_info` is only used for test
104
163
  tc, output, ev, @internal_info, *ev_args = evt
105
164
  output.each{|str| @ui.puts str}
106
165
 
107
166
  case ev
167
+ when :init
168
+ wait_command_loop tc
169
+
108
170
  when :load
109
171
  iseq, src = ev_args
110
172
  on_load iseq, src
111
173
  @ui.event :load
112
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
+
113
183
  when :thread_begin
114
184
  th = ev_args.shift
115
185
  on_thread_begin th
116
186
  @ui.event :thread_begin, th
117
187
  tc << :continue
188
+
118
189
  when :suspend
119
190
  case ev_args.first
120
191
  when :breakpoint
@@ -127,16 +198,14 @@ module DEBUGGER__
127
198
  end
128
199
 
129
200
  if @displays.empty?
201
+ stop_all_threads
130
202
  wait_command_loop tc
131
203
  else
132
204
  tc << [:eval, :display, @displays]
133
205
  end
206
+
134
207
  when :result
135
208
  case ev_args.first
136
- when :watch
137
- bp = ev_args[1]
138
- @bps[bp.key] = bp
139
- show_bps bp
140
209
  when :try_display
141
210
  failed_results = ev_args[1]
142
211
  if failed_results.size > 0
@@ -145,14 +214,22 @@ module DEBUGGER__
145
214
  @ui.puts "canceled: #{@displays.pop}"
146
215
  end
147
216
  end
148
- when :method_breakpoint
217
+ stop_all_threads
218
+
219
+ when :method_breakpoint, :watch_breakpoint
149
220
  bp = ev_args[1]
150
221
  if bp
151
- @bps[bp.key] = bp
222
+ add_bp(bp)
152
223
  show_bps bp
153
224
  else
154
225
  # can't make a bp
155
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}"
156
233
  else
157
234
  # ignore
158
235
  end
@@ -165,19 +242,28 @@ module DEBUGGER__
165
242
  end
166
243
  end
167
244
  ensure
168
- @bps.each{|k, bp| bp.disable}
169
- @th_clients.each{|th, thc| thc.close}
245
+ deactivate
170
246
  end
171
247
 
172
- def add_initial_commands cmds
173
- cmds.each{|c|
174
- c.gsub('#.*', '').strip!
175
- @initial_commands << c unless c.empty?
176
- }
248
+ def add_preset_commands name, cmds, kick: true, continue: true
249
+ cs = cmds.map{|c|
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)
260
+ end
261
+
262
+ ThreadClient.current.on_init name if kick
177
263
  end
178
264
 
179
265
  def source iseq
180
- if CONFIG[:use_colorize]
266
+ if !CONFIG[:no_color]
181
267
  @sr.get_colored(iseq)
182
268
  else
183
269
  @sr.get(iseq)
@@ -190,29 +276,46 @@ module DEBUGGER__
190
276
 
191
277
  def wait_command_loop tc
192
278
  @tc = tc
193
- stop_all_threads do
194
- loop do
195
- case wait_command
196
- when :retry
197
- # nothing
198
- else
199
- break
200
- end
201
- rescue Interrupt
202
- retry
279
+
280
+ loop do
281
+ case wait_command
282
+ when :retry
283
+ # nothing
284
+ else
285
+ break
203
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) '
204
298
  end
205
- ensure
206
- @tc = nil
207
299
  end
208
300
 
209
301
  def wait_command
210
- if @initial_commands.empty?
211
- @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
212
- line = @ui.readline
302
+ if @preset_command
303
+ if @preset_command.commands.empty?
304
+ if @preset_command.auto_continue
305
+ @preset_command = nil
306
+ @tc << :continue
307
+ return
308
+ else
309
+ @preset_command = nil
310
+ return :retry
311
+ end
312
+ else
313
+ line = @preset_command.commands.shift
314
+ @ui.puts "(rdbg:#{@preset_command.source}) #{line}"
315
+ end
213
316
  else
214
- line = @initial_commands.shift.strip
215
- @ui.puts "(rdbg:init) #{line}"
317
+ @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
318
+ line = @ui.readline prompt
216
319
  end
217
320
 
218
321
  case line
@@ -246,23 +349,37 @@ module DEBUGGER__
246
349
 
247
350
  # * `s[tep]`
248
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.
249
354
  when 's', 'step'
250
- @tc << [:step, :in]
355
+ cancel_auto_continue
356
+ check_postmortem
357
+ step_command :in, arg
251
358
 
252
359
  # * `n[ext]`
253
360
  # * Step over. Resume the program until next line.
361
+ # * `n[ext] <n>`
362
+ # * Step over, same as `step <n>`.
254
363
  when 'n', 'next'
255
- @tc << [:step, :next]
364
+ cancel_auto_continue
365
+ check_postmortem
366
+ step_command :next, arg
256
367
 
257
368
  # * `fin[ish]`
258
369
  # * Finish this frame. Resume the program until the current frame is finished.
370
+ # * `fin[ish] <n>`
371
+ # * Finish frames, same as `step <n>`.
259
372
  when 'fin', 'finish'
260
- @tc << [:step, :finish]
373
+ cancel_auto_continue
374
+ check_postmortem
375
+ step_command :finish, arg
261
376
 
262
377
  # * `c[ontinue]`
263
378
  # * Resume the program.
264
379
  when 'c', 'continue'
380
+ cancel_auto_continue
265
381
  @tc << :continue
382
+ restart_all_threads
266
383
 
267
384
  # * `q[uit]` or `Ctrl-D`
268
385
  # * Finish debugger (with the debuggee process on non-remote debugging).
@@ -270,6 +387,7 @@ module DEBUGGER__
270
387
  if ask 'Really quit?'
271
388
  @ui.quit arg.to_i
272
389
  @tc << :continue
390
+ restart_all_threads
273
391
  else
274
392
  return :retry
275
393
  end
@@ -278,7 +396,7 @@ module DEBUGGER__
278
396
  # * Same as q[uit] but without the confirmation prompt.
279
397
  when 'q!', 'quit!'
280
398
  @ui.quit arg.to_i
281
- @tc << :continue
399
+ restart_all_threads
282
400
 
283
401
  # * `kill`
284
402
  # * Stop the debuggee process with `Kernal#exit!`.
@@ -306,12 +424,18 @@ module DEBUGGER__
306
424
  # * Set breakpoint on the method `<class>#<name>`.
307
425
  # * `b[reak] <expr>.<name>`
308
426
  # * Set breakpoint on the method `<expr>.<name>`.
309
- # * `b[reak] ... if <expr>`
427
+ # * `b[reak] ... if: <expr>`
310
428
  # * break if `<expr>` is true at specified location.
311
- # * `b[reak] if <expr>`
312
- # * 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.
313
435
  # * Note that this feature is super slow.
314
436
  when 'b', 'break'
437
+ check_postmortem
438
+
315
439
  if arg == nil
316
440
  show_bps
317
441
  return :retry
@@ -328,6 +452,7 @@ module DEBUGGER__
328
452
 
329
453
  # skip
330
454
  when 'bv'
455
+ check_postmortem
331
456
  require 'json'
332
457
 
333
458
  h = Hash.new{|h, k| h[k] = []}
@@ -354,8 +479,10 @@ module DEBUGGER__
354
479
  # * `catch <Error>`
355
480
  # * Set breakpoint on raising `<Error>`.
356
481
  when 'catch'
482
+ check_postmortem
483
+
357
484
  if arg
358
- bp = add_catch_breakpoint arg
485
+ bp = repl_add_catch_breakpoint arg
359
486
  show_bps bp if bp
360
487
  else
361
488
  show_bps
@@ -366,8 +493,10 @@ module DEBUGGER__
366
493
  # * Stop the execution when the result of current scope's `@ivar` is changed.
367
494
  # * Note that this feature is super slow.
368
495
  when 'wat', 'watch'
369
- if arg
370
- @tc << [:eval, :watch, arg]
496
+ check_postmortem
497
+
498
+ if arg && arg.match?(/\A@\w+/)
499
+ @tc << [:breakpoint, :watch, arg]
371
500
  else
372
501
  show_bps
373
502
  return :retry
@@ -378,15 +507,17 @@ module DEBUGGER__
378
507
  # * `del[ete] <bpnum>`
379
508
  # * delete specified breakpoint.
380
509
  when 'del', 'delete'
510
+ check_postmortem
511
+
381
512
  bp =
382
513
  case arg
383
514
  when nil
384
515
  show_bps
385
516
  if ask "Remove all breakpoints?", 'N'
386
- delete_breakpoint
517
+ delete_bp
387
518
  end
388
519
  when /\d+/
389
- delete_breakpoint arg.to_i
520
+ delete_bp arg.to_i
390
521
  else
391
522
  nil
392
523
  end
@@ -397,8 +528,25 @@ module DEBUGGER__
397
528
 
398
529
  # * `bt` or `backtrace`
399
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/`.
400
537
  when 'bt', 'backtrace'
401
- @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
402
550
 
403
551
  # * `l[ist]`
404
552
  # * Show current frame's source code.
@@ -442,25 +590,57 @@ module DEBUGGER__
442
590
 
443
591
  @tc << [:show, :edit, arg]
444
592
 
445
- # * `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]]`
446
596
  # * Show information about the current frame (local variables)
447
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/>`.
448
606
  # * `i[nfo] th[read[s]]`
449
607
  # * Show all threads (same as `th[read]`).
450
608
  when 'i', 'info'
451
- 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
452
617
  when nil
453
- @tc << [:show, :local]
454
- when 'l', /locals?/
455
- @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]
456
627
  when 'th', /threads?/
457
628
  thread_list
458
629
  return :retry
459
630
  else
631
+ @ui.puts "unrecognized argument for info command: #{arg}"
460
632
  show_help 'info'
461
633
  return :retry
462
634
  end
463
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
+
464
644
  # * `display`
465
645
  # * Show display setting.
466
646
  # * `display <expr>`
@@ -481,9 +661,7 @@ module DEBUGGER__
481
661
  case arg
482
662
  when /(\d+)/
483
663
  if @displays[n = $1.to_i]
484
- if ask "clear \##{n} #{@displays[n]}?"
485
- @displays.delete_at n
486
- end
664
+ @displays.delete_at n
487
665
  end
488
666
  @tc << [:eval, :display, @displays]
489
667
  when nil
@@ -493,28 +671,6 @@ module DEBUGGER__
493
671
  end
494
672
  return :retry
495
673
 
496
- # * `trace [on|off]`
497
- # * enable or disable line tracer.
498
- when 'trace'
499
- case arg
500
- when 'on'
501
- dir = __dir__
502
- @tracer ||= TracePoint.new(:call, :return, :b_call, :b_return, :line, :class, :end){|tp|
503
- next if File.dirname(tp.path) == dir
504
- next if tp.path == '<internal:trace_point>'
505
- # Skip when `JSON.generate` is called during tests
506
- next if tp.binding.eval('self').to_s == 'JSON' and ENV['RUBY_DEBUG_TEST_MODE']
507
- # next if tp.event != :line
508
- @ui.puts pretty_tp(tp)
509
- }
510
- @tracer.enable
511
- when 'off'
512
- @tracer && @tracer.disable
513
- end
514
- enabled = (@tracer && @tracer.enabled?) ? true : false
515
- @ui.puts "Trace #{enabled ? 'on' : 'off'}"
516
- return :retry
517
-
518
674
  ### Frame control
519
675
 
520
676
  # * `f[rame]`
@@ -560,6 +716,108 @@ module DEBUGGER__
560
716
  end
561
717
  @tc << [:eval, :call, 'binding.irb']
562
718
 
719
+ # don't repeat irb command
720
+ @repl_prev_line = nil
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
+
563
821
  ### Thread control
564
822
 
565
823
  # * `th[read]`
@@ -577,13 +835,43 @@ module DEBUGGER__
577
835
  end
578
836
  return :retry
579
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
+
580
868
  ### Help
581
869
 
582
870
  # * `h[elp]`
583
871
  # * Show help for all commands.
584
872
  # * `h[elp] <command>`
585
873
  # * Show help for the given command.
586
- when 'h', 'help'
874
+ when 'h', 'help', '?'
587
875
  if arg
588
876
  show_help arg
589
877
  else
@@ -593,21 +881,129 @@ module DEBUGGER__
593
881
 
594
882
  ### END
595
883
  else
596
- @ui.puts "unknown command: #{line}"
884
+ @tc << [:eval, :pp, line]
885
+ =begin
597
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
598
896
  return :retry
897
+ =end
599
898
  end
600
899
 
601
900
  rescue Interrupt
602
901
  return :retry
603
902
  rescue SystemExit
604
903
  raise
904
+ rescue PostmortemError => e
905
+ @ui.puts e.message
906
+ return :retry
605
907
  rescue Exception => e
606
908
  @ui.puts "[REPL ERROR] #{e.inspect}"
607
909
  @ui.puts e.backtrace.map{|e| ' ' + e}
608
910
  return :retry
609
911
  end
610
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
+
1001
+ def cancel_auto_continue
1002
+ if @preset_command&.auto_continue
1003
+ @preset_command.auto_continue = false
1004
+ end
1005
+ end
1006
+
611
1007
  def show_help arg
612
1008
  DEBUGGER__.helps.each{|cat, cs|
613
1009
  cs.each{|ws, desc|
@@ -632,51 +1028,7 @@ module DEBUGGER__
632
1028
  end
633
1029
  end
634
1030
 
635
- def msig klass, receiver
636
- if klass.singleton_class?
637
- "#{receiver}."
638
- else
639
- "#{klass}#"
640
- end
641
- end
642
-
643
- def pretty_tp tp
644
- loc = "#{tp.path}:#{tp.lineno}"
645
- level = caller.size
646
-
647
- info =
648
- case tp.event
649
- when :line
650
- "line at #{loc}"
651
- when :call, :c_call
652
- klass = tp.defined_class
653
- "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} at #{loc}"
654
- when :return, :c_return
655
- klass = tp.defined_class
656
- "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} => #{tp.return_value.inspect} at #{loc}"
657
- when :b_call
658
- "b_call at #{loc}"
659
- when :b_return
660
- "b_return => #{tp.return_value} at #{loc}"
661
- when :class
662
- "class #{tp.self} at #{loc}"
663
- when :end
664
- "class #{tp.self} end at #{loc}"
665
- else
666
- "#{tp.event} at #{loc}"
667
- end
668
-
669
- case tp.event
670
- when :call, :b_call, :return, :b_return, :class, :end
671
- level -= 1
672
- end
673
-
674
- "Tracing:#{' ' * level} #{info}"
675
- rescue => e
676
- p e
677
- pp e.backtrace
678
- exit!
679
- end
1031
+ # breakpoint management
680
1032
 
681
1033
  def iterate_bps
682
1034
  deleted_bps = []
@@ -708,7 +1060,29 @@ module DEBUGGER__
708
1060
  nil
709
1061
  end
710
1062
 
711
- 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
712
1086
  case arg
713
1087
  when nil
714
1088
  @bps.each{|key, bp| bp.delete}
@@ -724,26 +1098,34 @@ module DEBUGGER__
724
1098
  end
725
1099
  end
726
1100
 
727
- def repl_add_breakpoint arg
728
- arg.strip!
1101
+ BREAK_KEYWORDS = %w(if: do: pre:).freeze
729
1102
 
730
- case arg
731
- when /\Aif\s+(.+)\z/
732
- cond = $1
733
- when /(.+?)\s+if\s+(.+)\z/
734
- sig = $1
735
- cond = $2
736
- else
737
- sig = arg
738
- 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
739
1116
 
740
- case sig
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]
1121
+
1122
+ case expr[:sig]
741
1123
  when /\A(\d+)\z/
742
- 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
743
1125
  when /\A(.+)[:\s+](\d+)\z/
744
- add_line_breakpoint $1, $2.to_i, cond: cond
1126
+ add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
745
1127
  when /\A(.+)([\.\#])(.+)\z/
746
- @tc << [:breakpoint, :method, $1, $2, $3, cond]
1128
+ @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd]
747
1129
  return :noretry
748
1130
  when nil
749
1131
  add_check_breakpoint cond
@@ -754,6 +1136,29 @@ module DEBUGGER__
754
1136
  end
755
1137
  end
756
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
+
757
1162
  # threads
758
1163
 
759
1164
  def update_thread_list
@@ -762,17 +1167,15 @@ module DEBUGGER__
762
1167
  unmanaged = []
763
1168
 
764
1169
  list.each{|th|
765
- case
766
- when th == Thread.current
767
- # ignore
768
- when @management_threads.include?(th)
769
- # ignore
770
- when @th_clients.has_key?(th)
771
- thcs << @th_clients[th]
1170
+ if thc = @th_clients[th]
1171
+ if !thc.management?
1172
+ thcs << thc
1173
+ end
772
1174
  else
773
1175
  unmanaged << th
774
1176
  end
775
1177
  }
1178
+
776
1179
  return thcs.sort_by{|thc| thc.id}, unmanaged
777
1180
  end
778
1181
 
@@ -799,7 +1202,7 @@ module DEBUGGER__
799
1202
  thcs, _unmanaged_ths = update_thread_list
800
1203
 
801
1204
  if tc = thcs[n]
802
- if tc.mode
1205
+ if tc.waiting?
803
1206
  @tc = tc
804
1207
  else
805
1208
  @ui.puts "#{tc.thread} is not controllable yet."
@@ -813,11 +1216,11 @@ module DEBUGGER__
813
1216
  end
814
1217
 
815
1218
  def setup_threads
816
- stop_all_threads do
817
- Thread.list.each{|th|
818
- thread_client_create(th)
819
- }
820
- end
1219
+ @th_clients = {}
1220
+
1221
+ Thread.list.each{|th|
1222
+ thread_client_create(th)
1223
+ }
821
1224
  end
822
1225
 
823
1226
  def on_thread_begin th
@@ -829,8 +1232,7 @@ module DEBUGGER__
829
1232
  end
830
1233
  end
831
1234
 
832
- def thread_client
833
- thr = Thread.current
1235
+ def thread_client thr = Thread.current
834
1236
  if @th_clients.has_key? thr
835
1237
  @th_clients[thr]
836
1238
  else
@@ -838,77 +1240,67 @@ module DEBUGGER__
838
1240
  end
839
1241
  end
840
1242
 
841
- def stop_all_threads
842
- 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
843
1250
 
844
- if Thread.list.size > 1
845
- TracePoint.new(:line) do
846
- th = Thread.current
847
- if current == th || @management_threads.include?(th)
848
- next
849
- else
850
- tc = ThreadClient.current
851
- tc.on_pause
852
- end
853
- end.enable do
854
- yield
855
- ensure
856
- @th_clients.each{|thr, tc|
857
- case thr
858
- when current, (@tc && @tc.thread)
859
- next
860
- else
861
- tc << :continue if thr != Thread.current
862
- end
863
- }
864
- end
865
- else
866
- yield
1251
+ tc.on_pause
867
1252
  end
868
1253
  end
869
1254
 
870
- ## event
871
-
872
- def on_load iseq, src
873
- @sr.add iseq, src
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
+ }
1261
+ end
874
1262
 
875
- pending_line_breakpoints do |bp|
876
- if bp.path == (iseq.absolute_path || iseq.path)
877
- bp.try_activate
878
- end
879
- end
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
880
1269
  end
881
1270
 
882
- # breakpoint management
1271
+ private def stop_all_threads
1272
+ return if running_thread_clients_count == 0
883
1273
 
884
- def add_breakpoint bp
885
- if @bps.has_key? bp.key
886
- @ui.puts "duplicated breakpoint: #{bp}"
887
- else
888
- @bps[bp.key] = bp
889
- end
1274
+ stopper = thread_stopper
1275
+ stopper.enable unless stopper.enabled?
890
1276
  end
891
1277
 
892
- def rehash_bps
893
- bps = @bps.values
894
- @bps.clear
895
- bps.each{|bp|
896
- 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
897
1285
  }
1286
+ @tc = nil
898
1287
  end
899
1288
 
900
- def break? file, line
901
- @bps.has_key? [file, line]
902
- end
1289
+ ## event
903
1290
 
904
- def add_catch_breakpoint arg
905
- bp = CatchBreakpoint.new(arg)
906
- add_breakpoint bp
907
- end
1291
+ def on_load iseq, src
1292
+ DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1293
+ @sr.add iseq, src
908
1294
 
909
- def add_check_breakpoint expr
910
- bp = CheckBreakpoint.new(expr)
911
- 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
912
1304
  end
913
1305
 
914
1306
  def resolve_path file
@@ -929,22 +1321,6 @@ module DEBUGGER__
929
1321
  raise
930
1322
  end
931
1323
 
932
- def add_line_breakpoint file, line, **kw
933
- file = resolve_path(file)
934
- bp = LineBreakpoint.new(file, line, **kw)
935
- add_breakpoint bp
936
- rescue Errno::ENOENT => e
937
- @ui.puts e.message
938
- end
939
-
940
- def pending_line_breakpoints
941
- @bps.find_all do |key, bp|
942
- LineBreakpoint === bp && !bp.iseq
943
- end.each do |key, bp|
944
- yield bp
945
- end
946
- end
947
-
948
1324
  def method_added tp
949
1325
  b = tp.binding
950
1326
  if var_name = b.local_variables.first
@@ -956,7 +1332,7 @@ module DEBUGGER__
956
1332
  when MethodBreakpoint
957
1333
  if bp.method.nil?
958
1334
  if bp.sig_method_name == mid.to_s
959
- bp.try_enable(quiet: true)
1335
+ bp.try_enable(added: true)
960
1336
  end
961
1337
  end
962
1338
 
@@ -973,10 +1349,55 @@ module DEBUGGER__
973
1349
  @ui.width
974
1350
  end
975
1351
 
976
- def check_forked
977
- unless @session_server.status
978
- # TODO: Support it
979
- 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
980
1401
  end
981
1402
  end
982
1403
  end
@@ -1022,44 +1443,71 @@ module DEBUGGER__
1022
1443
 
1023
1444
  # start methods
1024
1445
 
1025
- def self.console **kw
1026
- set_config(kw)
1027
-
1028
- require_relative 'console'
1446
+ def self.start nonstop: false, **kw
1447
+ CONFIG.set_config(**kw)
1029
1448
 
1030
- initialize_session UI_Console.new
1449
+ unless defined? SESSION
1450
+ require_relative 'local'
1451
+ initialize_session UI_LocalConsole.new
1452
+ end
1031
1453
 
1032
- @prev_handler = trap(:SIGINT){
1033
- ThreadClient.current.on_trap :SIGINT
1034
- }
1454
+ setup_initial_suspend unless nonstop
1035
1455
  end
1036
1456
 
1037
- def self.open host: nil, port: ::DEBUGGER__::CONFIG[:port], sock_path: nil, sock_dir: nil, **kw
1038
- 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)
1039
1459
 
1040
1460
  if port
1041
- open_tcp host: host, port: port
1461
+ open_tcp host: host, port: port, nonstop: nonstop
1042
1462
  else
1043
- open_unix sock_path: sock_path, sock_dir: sock_dir
1463
+ open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop
1044
1464
  end
1045
1465
  end
1046
1466
 
1047
- def self.open_tcp host: nil, port:, **kw
1048
- set_config(kw)
1467
+ def self.open_tcp host: nil, port:, nonstop: false, **kw
1468
+ CONFIG.set_config(**kw)
1049
1469
  require_relative 'server'
1050
- initialize_session UI_TcpServer.new(host: host, port: port)
1470
+
1471
+ if defined? SESSION
1472
+ SESSION.reset_ui UI_TcpServer.new(host: host, port: port)
1473
+ else
1474
+ initialize_session UI_TcpServer.new(host: host, port: port)
1475
+ end
1476
+
1477
+ setup_initial_suspend unless nonstop
1051
1478
  end
1052
1479
 
1053
- def self.open_unix sock_path: nil, sock_dir: nil, **kw
1054
- set_config(kw)
1480
+ def self.open_unix sock_path: nil, sock_dir: nil, nonstop: false, **kw
1481
+ CONFIG.set_config(**kw)
1055
1482
  require_relative 'server'
1056
- initialize_session UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
1483
+
1484
+ if defined? SESSION
1485
+ SESSION.reset_ui UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
1486
+ else
1487
+ initialize_session UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
1488
+ end
1489
+
1490
+ setup_initial_suspend unless nonstop
1057
1491
  end
1058
1492
 
1059
1493
  # boot utilities
1060
1494
 
1495
+ def self.setup_initial_suspend
1496
+ if !CONFIG[:nonstop]
1497
+ if loc = ::DEBUGGER__.require_location
1498
+ # require 'debug/start' or 'debug'
1499
+ add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
1500
+ else
1501
+ # -r
1502
+ add_line_breakpoint $0, 0, oneshot: true, hook_call: false
1503
+ end
1504
+ end
1505
+ end
1506
+
1061
1507
  class << self
1062
1508
  define_method :initialize_session do |ui|
1509
+ DEBUGGER__.warn "Session start (pid: #{Process.pid})"
1510
+
1063
1511
  ::DEBUGGER__.const_set(:SESSION, Session.new(ui))
1064
1512
 
1065
1513
  # default breakpoints
@@ -1067,25 +1515,18 @@ module DEBUGGER__
1067
1515
  # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
1068
1516
 
1069
1517
  Binding.module_eval do
1070
- def bp command: nil
1071
- if command
1072
- cmds = command.split(";;")
1073
- SESSION.add_initial_commands cmds
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]
1074
1523
  end
1075
1524
 
1076
- ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true
1525
+ ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, command: cmds
1077
1526
  true
1078
1527
  end
1079
- end
1080
-
1081
- if !::DEBUGGER__::CONFIG[:nonstop]
1082
- if loc = ::DEBUGGER__.require_location
1083
- # require 'debug/console' or 'debug'
1084
- add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
1085
- else
1086
- # -r
1087
- add_line_breakpoint $0, 1, oneshot: true, hook_call: false
1088
- end
1528
+ alias b break
1529
+ # alias bp break
1089
1530
  end
1090
1531
 
1091
1532
  load_rc
@@ -1093,80 +1534,30 @@ module DEBUGGER__
1093
1534
  end
1094
1535
 
1095
1536
  def self.load_rc
1096
- ['./rdbgrc.rb', File.expand_path('~/.rdbgrc.rb')].each{|path|
1097
- if File.file? path
1098
- load path
1099
- end
1100
- }
1101
-
1102
- # debug commands file
1103
- [init_script = ::DEBUGGER__::CONFIG[:init_script],
1104
- './.rdbgrc',
1105
- File.expand_path('~/.rdbgrc')].each{|path|
1106
- next unless path
1537
+ [[File.expand_path('~/.rdbgrc'), true],
1538
+ [File.expand_path('~/.rdbgrc.rb'), true],
1539
+ # ['./.rdbgrc', true], # disable because of security concern
1540
+ [CONFIG[:init_script], false],
1541
+ ].each{|(path, rc)|
1542
+ next unless path
1543
+ next if rc && CONFIG[:no_rc] # ignore rc
1107
1544
 
1108
1545
  if File.file? path
1109
- ::DEBUGGER__::SESSION.add_initial_commands File.readlines(path)
1110
- elsif path == init_script
1546
+ if path.end_with?('.rb')
1547
+ load path
1548
+ else
1549
+ ::DEBUGGER__::SESSION.add_preset_commands path, File.readlines(path)
1550
+ end
1551
+ elsif !rc
1111
1552
  warn "Not found: #{path}"
1112
1553
  end
1113
1554
  }
1114
1555
 
1115
1556
  # given debug commands
1116
- if ::DEBUGGER__::CONFIG[:commands]
1117
- cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
1118
- ::DEBUGGER__::SESSION.add_initial_commands cmds
1119
- end
1120
- end
1121
-
1122
- def self.parse_help
1123
- helps = Hash.new{|h, k| h[k] = []}
1124
- desc = cat = nil
1125
- cmds = []
1126
-
1127
- File.read(__FILE__).each_line do |line|
1128
- case line
1129
- when /\A\s*### (.+)/
1130
- cat = $1
1131
- break if $1 == 'END'
1132
- when /\A when (.+)/
1133
- next unless cat
1134
- next unless desc
1135
- ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
1136
- helps[cat] << [ws, desc]
1137
- desc = nil
1138
- cmds.concat ws
1139
- when /\A\s+# (\s*\*.+)/
1140
- if desc
1141
- desc << "\n" + $1
1142
- else
1143
- desc = $1
1144
- end
1145
- end
1557
+ if CONFIG[:commands]
1558
+ cmds = CONFIG[:commands].split(';;')
1559
+ ::DEBUGGER__::SESSION.add_preset_commands "commands", cmds, kick: false, continue: false
1146
1560
  end
1147
- @commands = cmds
1148
- @helps = helps
1149
- end
1150
-
1151
- def self.helps
1152
- (defined?(@helps) && @helps) || parse_help
1153
- end
1154
-
1155
- def self.commands
1156
- (defined?(@commands) && @commands) || (parse_help; @commands)
1157
- end
1158
-
1159
- def self.help
1160
- r = []
1161
- self.helps.each{|cat, cmds|
1162
- r << "### #{cat}"
1163
- r << ''
1164
- cmds.each{|ws, desc|
1165
- r << desc
1166
- }
1167
- r << ''
1168
- }
1169
- r.join("\n")
1170
1561
  end
1171
1562
 
1172
1563
  class ::Module
@@ -1194,4 +1585,99 @@ module DEBUGGER__
1194
1585
  str
1195
1586
  end
1196
1587
  end
1588
+
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
1680
+ end
1681
+ end
1197
1682
  end
1683
+