debug 1.0.0.beta5 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+