debug 0.2.0 → 1.0.0.beta3
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile +4 -5
- data/LICENSE.txt +0 -0
- data/README.md +442 -27
- data/Rakefile +29 -0
- data/debug.gemspec +10 -5
- data/exe/rdbg +34 -0
- data/ext/debug/debug.c +118 -0
- data/ext/debug/extconf.rb +2 -0
- data/ext/debug/iseq_collector.c +91 -0
- data/lib/debug.rb +1 -1111
- data/lib/debug/bp.vim +68 -0
- data/lib/debug/breakpoint.rb +374 -0
- data/lib/debug/client.rb +128 -0
- data/lib/debug/config.rb +125 -0
- data/lib/debug/console.rb +89 -0
- data/lib/debug/open.rb +10 -0
- data/lib/debug/run.rb +2 -0
- data/lib/debug/server.rb +226 -0
- data/lib/debug/session.rb +1071 -0
- data/lib/debug/source_repository.rb +27 -0
- data/lib/debug/thread_client.rb +618 -0
- data/lib/debug/version.rb +3 -0
- data/misc/README.md.erb +324 -0
- metadata +34 -14
@@ -0,0 +1,1071 @@
|
|
1
|
+
module DEBUGGER__
|
2
|
+
# used in thread_client.c
|
3
|
+
FrameInfo = Struct.new(:location, :self, :binding, :iseq, :class, :frame_depth,
|
4
|
+
:has_return_value, :return_value, :show_line)
|
5
|
+
end
|
6
|
+
|
7
|
+
if File.exist? File.join(__dir__, 'debug.so')
|
8
|
+
require_relative 'debug.so'
|
9
|
+
else
|
10
|
+
require "debug/debug"
|
11
|
+
end
|
12
|
+
|
13
|
+
require_relative 'source_repository'
|
14
|
+
require_relative 'breakpoint'
|
15
|
+
require_relative 'thread_client'
|
16
|
+
require_relative 'config'
|
17
|
+
|
18
|
+
class RubyVM::InstructionSequence
|
19
|
+
def traceable_lines_norec lines
|
20
|
+
code = self.to_a[13]
|
21
|
+
line = 0
|
22
|
+
code.each{|e|
|
23
|
+
case e
|
24
|
+
when Integer
|
25
|
+
line = e
|
26
|
+
when Symbol
|
27
|
+
if /\ARUBY_EVENT_/ =~ e.to_s
|
28
|
+
lines[line] = [e, *lines[line]]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def traceable_lines_rec lines
|
35
|
+
self.each_child{|ci| ci.traceable_lines_rec(lines)}
|
36
|
+
traceable_lines_norec lines
|
37
|
+
end
|
38
|
+
|
39
|
+
def type
|
40
|
+
self.to_a[9]
|
41
|
+
end
|
42
|
+
|
43
|
+
def argc
|
44
|
+
self.to_a[4][:arg_size]
|
45
|
+
end
|
46
|
+
|
47
|
+
def locals
|
48
|
+
self.to_a[10]
|
49
|
+
end
|
50
|
+
|
51
|
+
def last_line
|
52
|
+
self.to_a[4][:code_location][2]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module DEBUGGER__
|
57
|
+
class Session
|
58
|
+
def initialize ui
|
59
|
+
@ui = ui
|
60
|
+
@sr = SourceRepository.new
|
61
|
+
@bps = {} # bp.key => bp
|
62
|
+
# [file, line] => LineBreakpoint
|
63
|
+
# "Error" => CatchBreakpoint
|
64
|
+
# Method => MethodBreakpoint
|
65
|
+
# [:watch, expr] => WatchExprBreakpoint
|
66
|
+
# [:check, expr] => CheckBreakpoint
|
67
|
+
@th_clients = {} # {Thread => ThreadClient}
|
68
|
+
@q_evt = Queue.new
|
69
|
+
@displays = []
|
70
|
+
@tc = nil
|
71
|
+
@tc_id = 0
|
72
|
+
@initial_commands = []
|
73
|
+
|
74
|
+
@tp_load_script = TracePoint.new(:script_compiled){|tp|
|
75
|
+
ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
|
76
|
+
}.enable
|
77
|
+
|
78
|
+
@session_server = Thread.new do
|
79
|
+
Thread.current.abort_on_exception = true
|
80
|
+
|
81
|
+
while evt = @q_evt.pop
|
82
|
+
tc, output, ev, *ev_args = evt
|
83
|
+
output.each{|str| @ui.puts str}
|
84
|
+
|
85
|
+
case ev
|
86
|
+
when :load
|
87
|
+
iseq, src = ev_args
|
88
|
+
on_load iseq, src
|
89
|
+
tc << :continue
|
90
|
+
when :thread_begin
|
91
|
+
th = ev_args.shift
|
92
|
+
on_thread_begin th
|
93
|
+
tc << :continue
|
94
|
+
when :suspend
|
95
|
+
case ev_args.first
|
96
|
+
when :breakpoint
|
97
|
+
bp, i = bp_index ev_args[1]
|
98
|
+
if bp
|
99
|
+
@ui.puts "\nStop by \##{i} #{bp}"
|
100
|
+
end
|
101
|
+
when :trap
|
102
|
+
@ui.puts ''
|
103
|
+
@ui.puts "\nStop by #{ev_args[1]}"
|
104
|
+
end
|
105
|
+
|
106
|
+
if @displays.empty?
|
107
|
+
wait_command_loop tc
|
108
|
+
else
|
109
|
+
tc << [:eval, :display, @displays]
|
110
|
+
end
|
111
|
+
when :result
|
112
|
+
case ev_args.first
|
113
|
+
when :watch
|
114
|
+
bp = ev_args[1]
|
115
|
+
@bps[bp.key] = bp
|
116
|
+
show_bps bp
|
117
|
+
when :try_display
|
118
|
+
failed_results = ev_args[1]
|
119
|
+
if failed_results.size > 0
|
120
|
+
i, msg = failed_results.last
|
121
|
+
if i+1 == @displays.size
|
122
|
+
@ui.puts "canceled: #{@displays.pop}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
when :method_breakpoint
|
126
|
+
bp = ev_args[1]
|
127
|
+
if bp
|
128
|
+
@bps[bp.key] = bp
|
129
|
+
show_bps bp
|
130
|
+
else
|
131
|
+
# can't make a bp
|
132
|
+
end
|
133
|
+
else
|
134
|
+
# ignore
|
135
|
+
end
|
136
|
+
|
137
|
+
wait_command_loop tc
|
138
|
+
end
|
139
|
+
end
|
140
|
+
ensure
|
141
|
+
@bps.each{|k, bp| bp.disable}
|
142
|
+
@th_clients.each{|th, thc| thc.close}
|
143
|
+
end
|
144
|
+
|
145
|
+
@management_threads = [@session_server]
|
146
|
+
@management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
|
147
|
+
|
148
|
+
setup_threads
|
149
|
+
|
150
|
+
@tp_thread_begin = TracePoint.new(:thread_begin){|tp|
|
151
|
+
ThreadClient.current.on_thread_begin Thread.current
|
152
|
+
}.enable
|
153
|
+
end
|
154
|
+
|
155
|
+
def add_initial_commands cmds
|
156
|
+
cmds.each{|c|
|
157
|
+
c.gsub('#.*', '').strip!
|
158
|
+
@initial_commands << c unless c.empty?
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
def source path
|
163
|
+
@sr.get(path)
|
164
|
+
end
|
165
|
+
|
166
|
+
def inspect
|
167
|
+
"DEBUGGER__::SESSION"
|
168
|
+
end
|
169
|
+
|
170
|
+
def wait_command_loop tc
|
171
|
+
@tc = tc
|
172
|
+
stop_all_threads do
|
173
|
+
loop do
|
174
|
+
case wait_command
|
175
|
+
when :retry
|
176
|
+
# nothing
|
177
|
+
else
|
178
|
+
break
|
179
|
+
end
|
180
|
+
rescue Interrupt
|
181
|
+
retry
|
182
|
+
end
|
183
|
+
end
|
184
|
+
ensure
|
185
|
+
@tc = nil
|
186
|
+
end
|
187
|
+
|
188
|
+
def wait_command
|
189
|
+
if @initial_commands.empty?
|
190
|
+
line = @ui.readline
|
191
|
+
else
|
192
|
+
line = @initial_commands.shift.strip
|
193
|
+
@ui.puts "(rdbg:init) #{line}"
|
194
|
+
end
|
195
|
+
|
196
|
+
if line.empty?
|
197
|
+
if @repl_prev_line
|
198
|
+
line = @repl_prev_line
|
199
|
+
else
|
200
|
+
return :retry
|
201
|
+
end
|
202
|
+
else
|
203
|
+
@repl_prev_line = line
|
204
|
+
end
|
205
|
+
|
206
|
+
/([^\s]+)(?:\s+(.+))?/ =~ line
|
207
|
+
cmd, arg = $1, $2
|
208
|
+
|
209
|
+
# p cmd: [cmd, *arg]
|
210
|
+
|
211
|
+
case cmd
|
212
|
+
### Control flow
|
213
|
+
|
214
|
+
# * `s[tep]`
|
215
|
+
# * Step in. Resume the program until next breakable point.
|
216
|
+
when 's', 'step'
|
217
|
+
@tc << [:step, :in]
|
218
|
+
|
219
|
+
# * `n[ext]`
|
220
|
+
# * Step over. Resume the program until next line.
|
221
|
+
when 'n', 'next'
|
222
|
+
@tc << [:step, :next]
|
223
|
+
|
224
|
+
# * `fin[ish]`
|
225
|
+
# * Finish this frame. Resume the program until the current frame is finished.
|
226
|
+
when 'fin', 'finish'
|
227
|
+
@tc << [:step, :finish]
|
228
|
+
|
229
|
+
# * `c[ontinue]`
|
230
|
+
# * Resume the program.
|
231
|
+
when 'c', 'continue'
|
232
|
+
@tc << :continue
|
233
|
+
|
234
|
+
# * `q[uit]` or `Ctrl-D`
|
235
|
+
# * Finish debugger (with the debuggee process on non-remote debugging).
|
236
|
+
when 'q', 'quit'
|
237
|
+
if ask 'Really quit?'
|
238
|
+
@ui.quit arg.to_i
|
239
|
+
@tc << :continue
|
240
|
+
else
|
241
|
+
return :retry
|
242
|
+
end
|
243
|
+
|
244
|
+
# * `q[uit]!`
|
245
|
+
# * Same as q[uit] but without the confirmation prompt.
|
246
|
+
when 'q!', 'quit!'
|
247
|
+
@ui.quit arg.to_i
|
248
|
+
@tc << :continue
|
249
|
+
|
250
|
+
# * `kill`
|
251
|
+
# * Stop the debuggee process with `Kernal#exit!`.
|
252
|
+
when 'kill'
|
253
|
+
if ask 'Really kill?'
|
254
|
+
exit! (arg || 1).to_i
|
255
|
+
else
|
256
|
+
return :retry
|
257
|
+
end
|
258
|
+
|
259
|
+
# * `kill!`
|
260
|
+
# * Same as kill but without the confirmation prompt.
|
261
|
+
when 'kill!'
|
262
|
+
exit! (arg || 1).to_i
|
263
|
+
|
264
|
+
### Breakpoint
|
265
|
+
|
266
|
+
# * `b[reak]`
|
267
|
+
# * Show all breakpoints.
|
268
|
+
# * `b[reak] <line>`
|
269
|
+
# * Set breakpoint on `<line>` at the current frame's file.
|
270
|
+
# * `b[reak] <file>:<line>` or `<file> <line>`
|
271
|
+
# * Set breakpoint on `<file>:<line>`.
|
272
|
+
# * `b[reak] <class>#<name>`
|
273
|
+
# * Set breakpoint on the method `<class>#<name>`.
|
274
|
+
# * `b[reak] <expr>.<name>`
|
275
|
+
# * Set breakpoint on the method `<expr>.<name>`.
|
276
|
+
# * `b[reak] ... if <expr>`
|
277
|
+
# * break if `<expr>` is true at specified location.
|
278
|
+
# * `b[reak] if <expr>`
|
279
|
+
# * break if `<expr>` is true at any lines.
|
280
|
+
# * Note that this feature is super slow.
|
281
|
+
when 'b', 'break'
|
282
|
+
if arg == nil
|
283
|
+
show_bps
|
284
|
+
return :retry
|
285
|
+
else
|
286
|
+
case bp = repl_add_breakpoint(arg)
|
287
|
+
when :noretry
|
288
|
+
when nil
|
289
|
+
return :retry
|
290
|
+
else
|
291
|
+
show_bps bp
|
292
|
+
return :retry
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# skip
|
297
|
+
when 'bv'
|
298
|
+
require 'json'
|
299
|
+
|
300
|
+
h = Hash.new{|h, k| h[k] = []}
|
301
|
+
@bps.each{|key, bp|
|
302
|
+
if LineBreakpoint === bp
|
303
|
+
h[bp.path] << {lnum: bp.line}
|
304
|
+
end
|
305
|
+
}
|
306
|
+
if h.empty?
|
307
|
+
# TODO: clean?
|
308
|
+
else
|
309
|
+
open(".rdb_breakpoints.json", 'w'){|f| JSON.dump(h, f)}
|
310
|
+
end
|
311
|
+
|
312
|
+
vimsrc = File.join(__dir__, 'bp.vim')
|
313
|
+
system("vim -R -S #{vimsrc} #{@tc.location.path}")
|
314
|
+
|
315
|
+
if File.exist?(".rdb_breakpoints.json")
|
316
|
+
pp JSON.load(File.read(".rdb_breakpoints.json"))
|
317
|
+
end
|
318
|
+
|
319
|
+
return :retry
|
320
|
+
|
321
|
+
# * `catch <Error>`
|
322
|
+
# * Set breakpoint on raising `<Error>`.
|
323
|
+
when 'catch'
|
324
|
+
if arg
|
325
|
+
bp = add_catch_breakpoint arg
|
326
|
+
show_bps bp if bp
|
327
|
+
else
|
328
|
+
show_bps
|
329
|
+
end
|
330
|
+
return :retry
|
331
|
+
|
332
|
+
# * `watch <expr>`
|
333
|
+
# * Stop the execution when the result of `<expr>` is changed.
|
334
|
+
# * Note that this feature is super slow.
|
335
|
+
when 'wat', 'watch'
|
336
|
+
if arg
|
337
|
+
@tc << [:eval, :watch, arg]
|
338
|
+
else
|
339
|
+
show_bps
|
340
|
+
return :retry
|
341
|
+
end
|
342
|
+
|
343
|
+
# * `del[ete]`
|
344
|
+
# * delete all breakpoints.
|
345
|
+
# * `del[ete] <bpnum>`
|
346
|
+
# * delete specified breakpoint.
|
347
|
+
when 'del', 'delete'
|
348
|
+
bp =
|
349
|
+
case arg
|
350
|
+
when nil
|
351
|
+
show_bps
|
352
|
+
if ask "Remove all breakpoints?", 'N'
|
353
|
+
delete_breakpoint
|
354
|
+
end
|
355
|
+
when /\d+/
|
356
|
+
delete_breakpoint arg.to_i
|
357
|
+
else
|
358
|
+
nil
|
359
|
+
end
|
360
|
+
@ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
|
361
|
+
return :retry
|
362
|
+
|
363
|
+
### Information
|
364
|
+
|
365
|
+
# * `bt` or `backtrace`
|
366
|
+
# * Show backtrace (frame) information.
|
367
|
+
when 'bt', 'backtrace'
|
368
|
+
@tc << [:show, :backtrace]
|
369
|
+
|
370
|
+
# * `l[ist]`
|
371
|
+
# * Show current frame's source code.
|
372
|
+
# * Next `list` command shows the successor lines.
|
373
|
+
# * `l[ist] -`
|
374
|
+
# * Show predecessor lines as opposed to the `list` command.
|
375
|
+
# * `l[ist] <start>` or `l[ist] <start>-<end>`
|
376
|
+
# * Show current frame's source code from the line <start> to <end> if given.
|
377
|
+
when 'l', 'list'
|
378
|
+
case arg ? arg.strip : nil
|
379
|
+
when /\A(\d+)\z/
|
380
|
+
@tc << [:show, :list, {start_line: arg.to_i - 1}]
|
381
|
+
when /\A-\z/
|
382
|
+
@tc << [:show, :list, {dir: -1}]
|
383
|
+
when /\A(\d+)-(\d+)\z/
|
384
|
+
@tc << [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
|
385
|
+
when nil
|
386
|
+
@tc << [:show, :list]
|
387
|
+
else
|
388
|
+
@ui.puts "Can not handle list argument: #{arg}"
|
389
|
+
return :retry
|
390
|
+
end
|
391
|
+
|
392
|
+
# * `edit`
|
393
|
+
# * Open the current file on the editor (use `EDITOR` environment variable).
|
394
|
+
# * Note that edited file will not be reloaded.
|
395
|
+
# * `edit <file>`
|
396
|
+
# * Open <file> on the editor.
|
397
|
+
when 'edit'
|
398
|
+
if @ui.remote?
|
399
|
+
@ui.puts "not supported on the remote console."
|
400
|
+
return :retry
|
401
|
+
end
|
402
|
+
|
403
|
+
begin
|
404
|
+
arg = resolve_path(arg) if arg
|
405
|
+
rescue Errno::ENOENT
|
406
|
+
@ui.puts "not found: #{arg}"
|
407
|
+
return :retry
|
408
|
+
end
|
409
|
+
|
410
|
+
@tc << [:show, :edit, arg]
|
411
|
+
|
412
|
+
# * `i[nfo]`
|
413
|
+
# * Show information about the current frame (local variables)
|
414
|
+
# * It includes `self` as `%self` and a return value as `%return`.
|
415
|
+
# * `i[nfo] <expr>`
|
416
|
+
# * Show information about the result of <expr>.
|
417
|
+
when 'i', 'info'
|
418
|
+
case arg
|
419
|
+
when nil
|
420
|
+
@tc << [:show, :local]
|
421
|
+
else
|
422
|
+
@tc << [:show, :object_info, arg]
|
423
|
+
end
|
424
|
+
|
425
|
+
# * `display`
|
426
|
+
# * Show display setting.
|
427
|
+
# * `display <expr>`
|
428
|
+
# * Show the result of `<expr>` at every suspended timing.
|
429
|
+
when 'display'
|
430
|
+
if arg && !arg.empty?
|
431
|
+
@displays << arg
|
432
|
+
@tc << [:eval, :try_display, @displays]
|
433
|
+
else
|
434
|
+
@tc << [:eval, :display, @displays]
|
435
|
+
end
|
436
|
+
|
437
|
+
# * `undisplay`
|
438
|
+
# * Remove all display settings.
|
439
|
+
# * `undisplay <displaynum>`
|
440
|
+
# * Remove a specified display setting.
|
441
|
+
when 'undisplay'
|
442
|
+
case arg
|
443
|
+
when /(\d+)/
|
444
|
+
if @displays[n = $1.to_i]
|
445
|
+
if ask "clear \##{n} #{@displays[n]}?"
|
446
|
+
@displays.delete_at n
|
447
|
+
end
|
448
|
+
end
|
449
|
+
@tc << [:eval, :display, @displays]
|
450
|
+
when nil
|
451
|
+
if ask "clear all?", 'N'
|
452
|
+
@displays.clear
|
453
|
+
end
|
454
|
+
end
|
455
|
+
return :retry
|
456
|
+
|
457
|
+
# * `trace [on|off]`
|
458
|
+
# * enable or disable line tracer.
|
459
|
+
when 'trace'
|
460
|
+
case arg
|
461
|
+
when 'on'
|
462
|
+
dir = __dir__
|
463
|
+
@tracer ||= TracePoint.new(){|tp|
|
464
|
+
next if File.dirname(tp.path) == dir
|
465
|
+
next if tp.path == '<internal:trace_point>'
|
466
|
+
# next if tp.event != :line
|
467
|
+
@ui.puts pretty_tp(tp)
|
468
|
+
}
|
469
|
+
@tracer.enable
|
470
|
+
when 'off'
|
471
|
+
@tracer && @tracer.disable
|
472
|
+
end
|
473
|
+
enabled = (@tracer && @tracer.enabled?) ? true : false
|
474
|
+
@ui.puts "Trace #{enabled ? 'on' : 'off'}"
|
475
|
+
return :retry
|
476
|
+
|
477
|
+
### Frame control
|
478
|
+
|
479
|
+
# * `f[rame]`
|
480
|
+
# * Show the current frame.
|
481
|
+
# * `f[rame] <framenum>`
|
482
|
+
# * Specify a current frame. Evaluation are run on specified frame.
|
483
|
+
when 'frame', 'f'
|
484
|
+
@tc << [:frame, :set, arg]
|
485
|
+
|
486
|
+
# * `up`
|
487
|
+
# * Specify the upper frame.
|
488
|
+
when 'up'
|
489
|
+
@tc << [:frame, :up]
|
490
|
+
|
491
|
+
# * `down`
|
492
|
+
# * Specify the lower frame.
|
493
|
+
when 'down'
|
494
|
+
@tc << [:frame, :down]
|
495
|
+
|
496
|
+
### Evaluate
|
497
|
+
|
498
|
+
# * `p <expr>`
|
499
|
+
# * Evaluate like `p <expr>` on the current frame.
|
500
|
+
when 'p'
|
501
|
+
@tc << [:eval, :p, arg.to_s]
|
502
|
+
|
503
|
+
# * `pp <expr>`
|
504
|
+
# * Evaluate like `pp <expr>` on the current frame.
|
505
|
+
when 'pp'
|
506
|
+
@tc << [:eval, :pp, arg.to_s]
|
507
|
+
|
508
|
+
# * `e[val] <expr>`
|
509
|
+
# * Evaluate `<expr>` on the current frame.
|
510
|
+
when 'e', 'eval', 'call'
|
511
|
+
@tc << [:eval, :call, arg]
|
512
|
+
|
513
|
+
# * `irb`
|
514
|
+
# * Invoke `irb` on the current frame.
|
515
|
+
when 'irb'
|
516
|
+
if @ui.remote?
|
517
|
+
@ui.puts "not supported on the remote console."
|
518
|
+
return :retry
|
519
|
+
end
|
520
|
+
@tc << [:eval, :call, 'binding.irb']
|
521
|
+
|
522
|
+
### Thread control
|
523
|
+
|
524
|
+
# * `th[read]`
|
525
|
+
# * Show all threads.
|
526
|
+
# * `th[read] <thnum>`
|
527
|
+
# * Switch thread specified by `<thnum>`.
|
528
|
+
when 'th', 'thread'
|
529
|
+
case arg
|
530
|
+
when nil, 'list', 'l'
|
531
|
+
thread_list
|
532
|
+
when /(\d+)/
|
533
|
+
thread_switch $1.to_i
|
534
|
+
else
|
535
|
+
@ui.puts "unknown thread command: #{arg}"
|
536
|
+
end
|
537
|
+
return :retry
|
538
|
+
|
539
|
+
### Help
|
540
|
+
|
541
|
+
# * `h[elp]`
|
542
|
+
# * Show help for all commands.
|
543
|
+
# * `h[elp] <command>`
|
544
|
+
# * Show help for the given command.
|
545
|
+
when 'h', 'help'
|
546
|
+
if arg
|
547
|
+
show_help arg
|
548
|
+
else
|
549
|
+
@ui.puts DEBUGGER__.help
|
550
|
+
end
|
551
|
+
return :retry
|
552
|
+
|
553
|
+
### END
|
554
|
+
else
|
555
|
+
@ui.puts "unknown command: #{line}"
|
556
|
+
@repl_prev_line = nil
|
557
|
+
return :retry
|
558
|
+
end
|
559
|
+
|
560
|
+
rescue Interrupt
|
561
|
+
return :retry
|
562
|
+
rescue SystemExit
|
563
|
+
raise
|
564
|
+
rescue Exception => e
|
565
|
+
@ui.puts "[REPL ERROR] #{e.inspect}"
|
566
|
+
@ui.puts e.backtrace.map{|e| ' ' + e}
|
567
|
+
return :retry
|
568
|
+
end
|
569
|
+
|
570
|
+
def show_help arg
|
571
|
+
DEBUGGER__.helps.each{|cat, cs|
|
572
|
+
cs.each{|ws, desc|
|
573
|
+
if ws.include? arg
|
574
|
+
@ui.puts desc
|
575
|
+
return
|
576
|
+
end
|
577
|
+
}
|
578
|
+
}
|
579
|
+
@ui.puts "not found: #{arg}"
|
580
|
+
end
|
581
|
+
|
582
|
+
def ask msg, default = 'Y'
|
583
|
+
opts = '[y/n]'.tr(default.downcase, default)
|
584
|
+
input = @ui.ask("#{msg} #{opts} ")
|
585
|
+
input = default if input.empty?
|
586
|
+
case input
|
587
|
+
when 'y', 'Y'
|
588
|
+
true
|
589
|
+
else
|
590
|
+
false
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
def msig klass, receiver
|
595
|
+
if klass.singleton_class?
|
596
|
+
"#{receiver}."
|
597
|
+
else
|
598
|
+
"#{klass}#"
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
def pretty_tp tp
|
603
|
+
loc = "#{tp.path}:#{tp.lineno}"
|
604
|
+
level = caller.size
|
605
|
+
|
606
|
+
info =
|
607
|
+
case tp.event
|
608
|
+
when :line
|
609
|
+
"line at #{loc}"
|
610
|
+
when :call, :c_call
|
611
|
+
klass = tp.defined_class
|
612
|
+
"#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} at #{loc}"
|
613
|
+
when :return, :c_return
|
614
|
+
klass = tp.defined_class
|
615
|
+
"#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} => #{tp.return_value.inspect} at #{loc}"
|
616
|
+
when :b_call
|
617
|
+
"b_call at #{loc}"
|
618
|
+
when :b_return
|
619
|
+
"b_return => #{tp.return_value} at #{loc}"
|
620
|
+
when :class
|
621
|
+
"class #{tp.self} at #{loc}"
|
622
|
+
when :end
|
623
|
+
"class #{tp.self} end at #{loc}"
|
624
|
+
else
|
625
|
+
"#{tp.event} at #{loc}"
|
626
|
+
end
|
627
|
+
|
628
|
+
case tp.event
|
629
|
+
when :call, :b_call, :return, :b_return, :class, :end
|
630
|
+
level -= 1
|
631
|
+
end
|
632
|
+
|
633
|
+
"Tracing:#{' ' * level} #{info}"
|
634
|
+
rescue => e
|
635
|
+
p e
|
636
|
+
pp e.backtrace
|
637
|
+
exit!
|
638
|
+
end
|
639
|
+
|
640
|
+
def iterate_bps
|
641
|
+
deleted_bps = []
|
642
|
+
i = 0
|
643
|
+
@bps.each{|key, bp|
|
644
|
+
if !bp.deleted?
|
645
|
+
yield key, bp, i
|
646
|
+
i += 1
|
647
|
+
else
|
648
|
+
deleted_bps << bp
|
649
|
+
end
|
650
|
+
}
|
651
|
+
ensure
|
652
|
+
deleted_bps.each{|bp| @bps.delete bp}
|
653
|
+
end
|
654
|
+
|
655
|
+
def show_bps specific_bp = nil
|
656
|
+
iterate_bps do |key, bp, i|
|
657
|
+
@ui.puts "#%d %s" % [i, bp.to_s] if !specific_bp || bp == specific_bp
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
def bp_index specific_bp_key
|
662
|
+
iterate_bps do |key, bp, i|
|
663
|
+
if key == specific_bp_key
|
664
|
+
return [bp, i]
|
665
|
+
end
|
666
|
+
end
|
667
|
+
nil
|
668
|
+
end
|
669
|
+
|
670
|
+
def delete_breakpoint arg = nil
|
671
|
+
case arg
|
672
|
+
when nil
|
673
|
+
@bps.each{|key, bp| bp.delete}
|
674
|
+
@bps.clear
|
675
|
+
else
|
676
|
+
del_bp = nil
|
677
|
+
iterate_bps{|key, bp, i| del_bp = bp if i == arg}
|
678
|
+
if del_bp
|
679
|
+
del_bp.delete
|
680
|
+
@bps.delete del_bp.key
|
681
|
+
return [arg, del_bp]
|
682
|
+
end
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
def repl_add_breakpoint arg
|
687
|
+
arg.strip!
|
688
|
+
|
689
|
+
case arg
|
690
|
+
when /\Aif\s+(.+)\z/
|
691
|
+
cond = $1
|
692
|
+
when /(.+?)\s+if\s+(.+)\z/
|
693
|
+
sig = $1
|
694
|
+
cond = $2
|
695
|
+
else
|
696
|
+
sig = arg
|
697
|
+
end
|
698
|
+
|
699
|
+
case sig
|
700
|
+
when /\A(\d+)\z/
|
701
|
+
add_line_breakpoint @tc.location.path, $1.to_i, cond: cond
|
702
|
+
when /\A(.+)[:\s+](\d+)\z/
|
703
|
+
add_line_breakpoint $1, $2.to_i, cond: cond
|
704
|
+
when /\A(.+)([\.\#])(.+)\z/
|
705
|
+
@tc << [:breakpoint, :method, $1, $2, $3, cond]
|
706
|
+
return :noretry
|
707
|
+
when nil
|
708
|
+
add_check_breakpoint cond
|
709
|
+
else
|
710
|
+
@ui.puts "Unknown breakpoint format: #{arg}"
|
711
|
+
@ui.puts
|
712
|
+
show_help 'b'
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
# threads
|
717
|
+
|
718
|
+
def update_thread_list
|
719
|
+
list = Thread.list
|
720
|
+
thcs = []
|
721
|
+
unmanaged = []
|
722
|
+
|
723
|
+
list.each{|th|
|
724
|
+
case
|
725
|
+
when th == Thread.current
|
726
|
+
# ignore
|
727
|
+
when @th_clients.has_key?(th)
|
728
|
+
thcs << @th_clients[th]
|
729
|
+
else
|
730
|
+
unmanaged << th
|
731
|
+
end
|
732
|
+
}
|
733
|
+
return thcs.sort_by{|thc| thc.id}, unmanaged
|
734
|
+
end
|
735
|
+
|
736
|
+
def thread_list
|
737
|
+
thcs, unmanaged_ths = update_thread_list
|
738
|
+
thcs.each_with_index{|thc, i|
|
739
|
+
@ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
|
740
|
+
}
|
741
|
+
|
742
|
+
if !unmanaged_ths.empty?
|
743
|
+
@ui.puts "The following threads are not managed yet by the debugger:"
|
744
|
+
unmanaged_ths.each{|th|
|
745
|
+
@ui.puts " " + th.to_s
|
746
|
+
}
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
def thread_switch n
|
751
|
+
thcs, unmanaged_ths = update_thread_list
|
752
|
+
|
753
|
+
if tc = thcs[n]
|
754
|
+
if tc.mode
|
755
|
+
@tc = tc
|
756
|
+
else
|
757
|
+
@ui.puts "#{tc.thread} is not controllable yet."
|
758
|
+
end
|
759
|
+
end
|
760
|
+
thread_list
|
761
|
+
end
|
762
|
+
|
763
|
+
def thread_client_create th
|
764
|
+
@th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
|
765
|
+
end
|
766
|
+
|
767
|
+
def setup_threads
|
768
|
+
stop_all_threads do
|
769
|
+
Thread.list.each{|th|
|
770
|
+
thread_client_create(th)
|
771
|
+
}
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
def on_thread_begin th
|
776
|
+
if @th_clients.has_key? th
|
777
|
+
# OK
|
778
|
+
else
|
779
|
+
# TODO: NG?
|
780
|
+
thread_client_create th
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
def thread_client
|
785
|
+
thr = Thread.current
|
786
|
+
if @th_clients.has_key? thr
|
787
|
+
@th_clients[thr]
|
788
|
+
else
|
789
|
+
@th_clients[thr] = thread_client_create(thr)
|
790
|
+
end
|
791
|
+
end
|
792
|
+
|
793
|
+
def stop_all_threads
|
794
|
+
current = Thread.current
|
795
|
+
|
796
|
+
if Thread.list.size > 1
|
797
|
+
TracePoint.new(:line) do
|
798
|
+
th = Thread.current
|
799
|
+
if current == th || @management_threads.include?(th)
|
800
|
+
next
|
801
|
+
else
|
802
|
+
tc = ThreadClient.current
|
803
|
+
tc.on_pause
|
804
|
+
end
|
805
|
+
end.enable do
|
806
|
+
yield
|
807
|
+
ensure
|
808
|
+
@th_clients.each{|thr, tc|
|
809
|
+
case thr
|
810
|
+
when current, (@tc && @tc.thread)
|
811
|
+
next
|
812
|
+
else
|
813
|
+
tc << :continue if thr != Thread.current
|
814
|
+
end
|
815
|
+
}
|
816
|
+
end
|
817
|
+
else
|
818
|
+
yield
|
819
|
+
end
|
820
|
+
end
|
821
|
+
|
822
|
+
## event
|
823
|
+
|
824
|
+
def on_load iseq, src
|
825
|
+
@sr.add iseq, src
|
826
|
+
|
827
|
+
pending_line_breakpoints do |bp|
|
828
|
+
if bp.path == (iseq.absolute_path || iseq.path)
|
829
|
+
bp.try_activate
|
830
|
+
end
|
831
|
+
end
|
832
|
+
end
|
833
|
+
|
834
|
+
# breakpoint management
|
835
|
+
|
836
|
+
def add_breakpoint bp
|
837
|
+
if @bps.has_key? bp.key
|
838
|
+
@ui.puts "duplicated breakpoint: #{bp}"
|
839
|
+
else
|
840
|
+
@bps[bp.key] = bp
|
841
|
+
end
|
842
|
+
end
|
843
|
+
|
844
|
+
def rehash_bps
|
845
|
+
bps = @bps.values
|
846
|
+
@bps.clear
|
847
|
+
bps.each{|bp|
|
848
|
+
add_breakpoint bp
|
849
|
+
}
|
850
|
+
end
|
851
|
+
|
852
|
+
def break? file, line
|
853
|
+
@bps.has_key? [file, line]
|
854
|
+
end
|
855
|
+
|
856
|
+
def add_catch_breakpoint arg
|
857
|
+
bp = CatchBreakpoint.new(arg)
|
858
|
+
add_braekpoint bp
|
859
|
+
end
|
860
|
+
|
861
|
+
def add_check_breakpoint expr
|
862
|
+
bp = CheckBreakpoint.new(expr)
|
863
|
+
add_breakpoint bp
|
864
|
+
end
|
865
|
+
|
866
|
+
def resolve_path file
|
867
|
+
File.realpath(File.expand_path(file))
|
868
|
+
rescue Errno::ENOENT
|
869
|
+
return file if file == '-e'
|
870
|
+
$LOAD_PATH.each do |lp|
|
871
|
+
libpath = File.join(lp, file)
|
872
|
+
return File.realpath(libpath)
|
873
|
+
rescue Errno::ENOENT
|
874
|
+
# next
|
875
|
+
end
|
876
|
+
|
877
|
+
raise
|
878
|
+
end
|
879
|
+
|
880
|
+
def add_line_breakpoint file, line, **kw
|
881
|
+
file = resolve_path(file)
|
882
|
+
bp = LineBreakpoint.new(file, line, **kw)
|
883
|
+
add_breakpoint bp
|
884
|
+
rescue Errno::ENOENT => e
|
885
|
+
@ui.puts e.message
|
886
|
+
end
|
887
|
+
|
888
|
+
def pending_line_breakpoints
|
889
|
+
@bps.find_all do |key, bp|
|
890
|
+
LineBreakpoint === bp && !bp.iseq
|
891
|
+
end.each do |key, bp|
|
892
|
+
yield bp
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
def method_added tp
|
897
|
+
b = tp.binding
|
898
|
+
if var_name = b.local_variables.first
|
899
|
+
mid = b.local_variable_get(var_name)
|
900
|
+
found = false
|
901
|
+
|
902
|
+
@bps.each{|k, bp|
|
903
|
+
case bp
|
904
|
+
when MethodBreakpoint
|
905
|
+
if bp.method.nil?
|
906
|
+
found = true
|
907
|
+
if bp.sig_method_name == mid.to_s
|
908
|
+
bp.enable(quiet: true)
|
909
|
+
end
|
910
|
+
end
|
911
|
+
end
|
912
|
+
}
|
913
|
+
unless found
|
914
|
+
METHOD_ADDED_TRACKER.disable
|
915
|
+
end
|
916
|
+
end
|
917
|
+
end
|
918
|
+
end
|
919
|
+
|
920
|
+
# String for requring location
|
921
|
+
# nil for -r
|
922
|
+
def self.require_location
|
923
|
+
locs = caller_locations
|
924
|
+
dir_prefix = /#{__dir__}/
|
925
|
+
|
926
|
+
locs.each do |loc|
|
927
|
+
case loc.absolute_path
|
928
|
+
when dir_prefix
|
929
|
+
when %r{rubygems/core_ext/kernel_require\.rb}
|
930
|
+
else
|
931
|
+
return loc
|
932
|
+
end
|
933
|
+
end
|
934
|
+
nil
|
935
|
+
end
|
936
|
+
|
937
|
+
def self.console
|
938
|
+
initialize_session UI_Console.new
|
939
|
+
|
940
|
+
@prev_handler = trap(:SIGINT){
|
941
|
+
ThreadClient.current.on_trap :SIGINT
|
942
|
+
}
|
943
|
+
end
|
944
|
+
|
945
|
+
def self.add_line_breakpoint file, line, **kw
|
946
|
+
::DEBUGGER__::SESSION.add_line_breakpoint file, line, **kw
|
947
|
+
end
|
948
|
+
|
949
|
+
def self.add_catch_breakpoint pat
|
950
|
+
::DEBUGGER__::SESSION.add_catch_breakpoint pat
|
951
|
+
end
|
952
|
+
|
953
|
+
class << self
|
954
|
+
define_method :initialize_session do |ui|
|
955
|
+
::DEBUGGER__.const_set(:SESSION, Session.new(ui))
|
956
|
+
|
957
|
+
# default breakpoints
|
958
|
+
|
959
|
+
# ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
|
960
|
+
|
961
|
+
Binding.module_eval do
|
962
|
+
::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1
|
963
|
+
def bp; nil; end
|
964
|
+
end
|
965
|
+
|
966
|
+
if ::DEBUGGER__::CONFIG[:nonstop] != '1'
|
967
|
+
if loc = ::DEBUGGER__.require_location
|
968
|
+
# require 'debug/console' or 'debug'
|
969
|
+
add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
|
970
|
+
else
|
971
|
+
# -r
|
972
|
+
add_line_breakpoint $0, 1, oneshot: true, hook_call: false
|
973
|
+
end
|
974
|
+
end
|
975
|
+
|
976
|
+
load_rc
|
977
|
+
end
|
978
|
+
end
|
979
|
+
|
980
|
+
def self.load_rc
|
981
|
+
['./rdbgrc.rb', File.expand_path('~/.rdbgrc.rb')].each{|path|
|
982
|
+
if File.file? path
|
983
|
+
load path
|
984
|
+
end
|
985
|
+
}
|
986
|
+
|
987
|
+
# debug commands file
|
988
|
+
[::DEBUGGER__::CONFIG[:init_script],
|
989
|
+
'./.rdbgrc',
|
990
|
+
File.expand_path('~/.rdbgrc')].each{|path|
|
991
|
+
next unless path
|
992
|
+
|
993
|
+
if File.file? path
|
994
|
+
::DEBUGGER__::SESSION.add_initial_commands File.readlines(path)
|
995
|
+
end
|
996
|
+
}
|
997
|
+
|
998
|
+
# given debug commands
|
999
|
+
if ::DEBUGGER__::CONFIG[:commands]
|
1000
|
+
cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
|
1001
|
+
::DEBUGGER__::SESSION.add_initial_commands cmds
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def self.parse_help
|
1006
|
+
helps = Hash.new{|h, k| h[k] = []}
|
1007
|
+
desc = cat = nil
|
1008
|
+
cmds = []
|
1009
|
+
|
1010
|
+
File.read(__FILE__).each_line do |line|
|
1011
|
+
case line
|
1012
|
+
when /\A\s*### (.+)/
|
1013
|
+
cat = $1
|
1014
|
+
break if $1 == 'END'
|
1015
|
+
when /\A when (.+)/
|
1016
|
+
next unless cat
|
1017
|
+
next unless desc
|
1018
|
+
ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
|
1019
|
+
helps[cat] << [ws, desc]
|
1020
|
+
desc = nil
|
1021
|
+
cmds.concat ws
|
1022
|
+
when /\A\s+# (\s*\*.+)/
|
1023
|
+
if desc
|
1024
|
+
desc << "\n" + $1
|
1025
|
+
else
|
1026
|
+
desc = $1
|
1027
|
+
end
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
@commands = cmds
|
1031
|
+
@helps = helps
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
def self.helps
|
1035
|
+
(defined?(@helps) && @helps) || parse_help
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
def self.commands
|
1039
|
+
(defined?(@commands) && @commands) || (parse_help; @commands)
|
1040
|
+
end
|
1041
|
+
|
1042
|
+
def self.help
|
1043
|
+
r = []
|
1044
|
+
self.helps.each{|cat, cmds|
|
1045
|
+
r << "### #{cat}"
|
1046
|
+
r << ''
|
1047
|
+
cmds.each{|ws, desc|
|
1048
|
+
r << desc
|
1049
|
+
}
|
1050
|
+
r << ''
|
1051
|
+
}
|
1052
|
+
r.join("\n")
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
CONFIG = ::DEBUGGER__.parse_argv(ENV['RUBY_DEBUG_OPT'])
|
1056
|
+
|
1057
|
+
class ::Module
|
1058
|
+
def method_added mid; end
|
1059
|
+
def singleton_method_added mid; end
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
def self.method_added tp
|
1063
|
+
begin
|
1064
|
+
SESSION.method_added tp
|
1065
|
+
rescue Exception => e
|
1066
|
+
p e
|
1067
|
+
end
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
METHOD_ADDED_TRACKER = self.create_method_added_tracker
|
1071
|
+
end
|