debug 0.2.1 → 1.0.0.alpha0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,598 @@
1
+ require 'json'
2
+ require 'pp'
3
+ require 'debug_inspector'
4
+ require 'iseq_collector'
5
+
6
+ require_relative 'source_repository'
7
+ require_relative 'breakpoint'
8
+ require_relative 'thread_client'
9
+
10
+ class RubyVM::InstructionSequence
11
+ def traceable_lines_norec lines
12
+ code = self.to_a[13]
13
+ line = 0
14
+ code.each{|e|
15
+ case e
16
+ when Integer
17
+ line = e
18
+ when Symbol
19
+ if /\ARUBY_EVENT_/ =~ e.to_s
20
+ lines[line] = [e, *lines[line]]
21
+ end
22
+ end
23
+ }
24
+ end
25
+
26
+ def traceable_lines_rec lines
27
+ self.each_child{|ci| ci.traceable_lines_rec(lines)}
28
+ traceable_lines_norec lines
29
+ end
30
+
31
+ def type
32
+ self.to_a[9]
33
+ end
34
+
35
+ def argc
36
+ self.to_a[4][:arg_size]
37
+ end
38
+
39
+ def locals
40
+ self.to_a[10]
41
+ end
42
+ end
43
+
44
+ module DEBUGGER__
45
+ class Session
46
+ def initialize ui
47
+ @ui = ui
48
+ @sr = SourceRepository.new
49
+ @reserved_bps = []
50
+ @bps = {} # [file, line] => LineBreakpoint || "Error" => CatchBreakpoint
51
+ @th_clients = {} # {Thread => ThreadClient}
52
+ @q_evt = Queue.new
53
+ @displays = []
54
+ @tc = nil
55
+
56
+ @tp_load_script = TracePoint.new(:script_compiled){|tp|
57
+ ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
58
+ }.enable
59
+
60
+ @session_server = Thread.new do
61
+ Thread.current.abort_on_exception = true
62
+
63
+ while evt = @q_evt.pop
64
+ tc, output, ev, *ev_args = evt
65
+ output.each{|str| @ui.puts str}
66
+
67
+ case ev
68
+ when :load
69
+ iseq, src = ev_args
70
+ on_load iseq, src
71
+ tc << :continue
72
+ when :suspend
73
+ if @displays.empty?
74
+ wait_command_loop tc
75
+ else
76
+ tc << [:eval, :display, @displays]
77
+ end
78
+ when :result
79
+ wait_command_loop tc
80
+ end
81
+ end
82
+ end
83
+
84
+ @management_threads = [@session_server]
85
+
86
+ setup_threads
87
+ end
88
+
89
+ attr_reader :management_threads
90
+
91
+ def source path
92
+ @sr.get(path)
93
+ end
94
+
95
+ def inspect
96
+ "DEBUGGER__::SESSION"
97
+ end
98
+
99
+ def wait_command_loop tc
100
+ @tc = tc
101
+ stop_all_threads do
102
+ loop do
103
+ case wait_command
104
+ when :retry
105
+ # nothing
106
+ else
107
+ break
108
+ end
109
+ rescue Interrupt
110
+ retry
111
+ end
112
+ end
113
+ end
114
+
115
+ def wait_command
116
+ line = @ui.readline
117
+
118
+ if line.empty?
119
+ if @repl_prev_line
120
+ line = @repl_prev_line
121
+ else
122
+ return :retry
123
+ end
124
+ else
125
+ @repl_prev_line = line
126
+ end
127
+
128
+ /([^\s]+)(?:\s+(.+))?/ =~ line
129
+ cmd, arg = $1, $2
130
+
131
+ # p cmd: [cmd, *arg]
132
+
133
+ case cmd
134
+
135
+ # control
136
+ when 's', 'step'
137
+ @tc << [:step, :in]
138
+ when 'n', 'next'
139
+ @tc << [:step, :next]
140
+ when 'fin', 'finish'
141
+ @tc << [:step, :finish]
142
+ when 'c', 'continue'
143
+ @tc << :continue
144
+ when 'q', 'quit'
145
+ if ask 'Really quit?'
146
+ @ui.quit arg.to_i
147
+ @tc << :continue
148
+ else
149
+ return :retry
150
+ end
151
+ when 'kill'
152
+ if ask 'Really quit?'
153
+ exit! (arg || 1).to_i
154
+ else
155
+ return :retry
156
+ end
157
+
158
+ # breakpoints
159
+ when 'b', 'break'
160
+ if arg == nil
161
+ show_bps
162
+ else
163
+ bp = repl_add_breakpoint arg
164
+ show_bps bp if bp
165
+ end
166
+ return :retry
167
+ when 'bv'
168
+ h = Hash.new{|h, k| h[k] = []}
169
+ @bps.each{|key, bp|
170
+ if LineBreakpoint === bp
171
+ h[bp.path] << {lnum: bp.line}
172
+ end
173
+ }
174
+ if h.empty?
175
+ # TODO: clean?
176
+ else
177
+ open(".rdb_breakpoints.json", 'w'){|f| JSON.dump(h, f)}
178
+ end
179
+
180
+ vimsrc = File.join(__dir__, 'bp.vim')
181
+ system("vim -R -S #{vimsrc} #{@tc.location.path}")
182
+
183
+ if File.exist?(".rdb_breakpoints.json")
184
+ pp JSON.load(File.read(".rdb_breakpoints.json"))
185
+ end
186
+
187
+ return :retry
188
+ when 'catch'
189
+ if arg
190
+ bp = add_catch_breakpoint arg
191
+ show_bps bp if bp
192
+ end
193
+ return :retry
194
+ when 'del', 'delete'
195
+ bp =
196
+ case arg
197
+ when nil
198
+ show_bps
199
+ if ask "Remove all breakpoints?", 'N'
200
+ delete_breakpoint
201
+ end
202
+ when /\d+/
203
+ delete_breakpoint arg.to_i
204
+ else
205
+ nil
206
+ end
207
+ @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
208
+ return :retry
209
+
210
+ # evaluate
211
+ when 'p'
212
+ @tc << [:eval, :p, arg.to_s]
213
+ when 'pp'
214
+ @tc << [:eval, :pp, arg.to_s]
215
+ when 'e', 'eval', 'call'
216
+ @tc << [:eval, :call, arg]
217
+ when 'irb'
218
+ @tc << [:eval, :call, 'binding.irb']
219
+
220
+ # evaluate/frame selector
221
+ when 'up'
222
+ @tc << [:frame, :up]
223
+ when 'down'
224
+ @tc << [:frame, :down]
225
+ when 'frame', 'f'
226
+ @tc << [:frame, :set, arg]
227
+
228
+ # information
229
+ when 'bt', 'backtrace'
230
+ @tc << [:show, :backtrace]
231
+ when 'list'
232
+ @tc << [:show, :list]
233
+ when 'info'
234
+ case arg
235
+ when 'l', 'local', 'locals'
236
+ @tc << [:show, :locals]
237
+ when 'i', 'instance', 'ivars'
238
+ @tc << [:show, :ivars]
239
+ else
240
+ @ui.puts "unknown info argument: #{arg}"
241
+ return :retry
242
+ end
243
+ when 'display'
244
+ @displays << arg if arg && !arg.empty?
245
+ @tc << [:eval, :display, @displays]
246
+ when 'undisplay'
247
+ case arg
248
+ when /(\d+)/
249
+ if @displays[n = $1.to_i]
250
+ if ask "clear \##{n} #{@displays[n]}?"
251
+ @displays.delete_at n
252
+ end
253
+ end
254
+ @tc << [:eval, :display, @displays]
255
+ when nil
256
+ if ask "clear all?", 'N'
257
+ @displays.clear
258
+ end
259
+ end
260
+ return :retry
261
+
262
+ # trace
263
+ when 'trace'
264
+ case arg
265
+ when 'on'
266
+ @tracer ||= TracePoint.new(){|tp|
267
+ next if tp.path == __FILE__
268
+ next if tp.path == '<internal:trace_point>'
269
+ # next if tp.event != :line
270
+ @ui.puts pretty_tp(tp)
271
+ }
272
+ @tracer.enable
273
+ when 'off'
274
+ @tracer && @tracer.disable
275
+ else
276
+ enabled = (@tracer && @tracer.enabled?) ? true : false
277
+ @ui.puts "Trace #{enabled ? 'on' : 'off'}"
278
+ end
279
+ return :retry
280
+
281
+ # threads
282
+ when 'th', 'thread'
283
+ case arg
284
+ when nil, 'list', 'l'
285
+ thread_list
286
+ when /(\d+)/
287
+ thread_switch $1.to_i
288
+ else
289
+ @ui.puts "unknown thread command: #{arg}"
290
+ end
291
+ return :retry
292
+
293
+ else
294
+ @ui.puts "unknown command: #{line}"
295
+ @repl_prev_line = nil
296
+ return :retry
297
+ end
298
+
299
+ rescue Interrupt
300
+ return :retry
301
+ rescue SystemExit
302
+ raise
303
+ rescue Exception => e
304
+ @ui.puts "[REPL ERROR] #{e.inspect}"
305
+ @ui.puts e.backtrace.map{|e| ' ' + e}
306
+ return :retry
307
+ end
308
+
309
+ def ask msg, default = 'Y'
310
+ opts = '[y/n]'.tr(default.downcase, default)
311
+ input = @ui.ask("#{msg} #{opts} ")
312
+ input = default if input.empty?
313
+ case input
314
+ when 'y', 'Y'
315
+ true
316
+ else
317
+ false
318
+ end
319
+ end
320
+
321
+ def msig klass, receiver
322
+ if klass.singleton_class?
323
+ "#{receiver}."
324
+ else
325
+ "#{klass}#"
326
+ end
327
+ end
328
+
329
+ def pretty_tp tp
330
+ loc = "#{tp.path}:#{tp.lineno}"
331
+ level = caller.size
332
+
333
+ info =
334
+ case tp.event
335
+ when :line
336
+ "line at #{loc}"
337
+ when :call, :c_call
338
+ klass = tp.defined_class
339
+ "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} at #{loc}"
340
+ when :return, :c_return
341
+ klass = tp.defined_class
342
+ "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} => #{tp.return_value.inspect} at #{loc}"
343
+ when :b_call
344
+ "b_call at #{loc}"
345
+ when :b_return
346
+ "b_return => #{tp.return_value} at #{loc}"
347
+ when :class
348
+ "class #{tp.self} at #{loc}"
349
+ when :end
350
+ "class #{tp.self} end at #{loc}"
351
+ else
352
+ "#{tp.event} at #{loc}"
353
+ end
354
+
355
+ case tp.event
356
+ when :call, :b_call, :return, :b_return, :class, :end
357
+ level -= 1
358
+ end
359
+
360
+ "Tracing:#{' ' * level} #{info}"
361
+ rescue => e
362
+ p e
363
+ pp e.backtrace
364
+ exit!
365
+ end
366
+
367
+ def show_bps specified_bp = nil
368
+ @bps.each_with_index{|(key, bp), i|
369
+ if !specified_bp || bp == specified_bp
370
+ @ui.puts "#%d %s" % [i, bp.to_s]
371
+ end
372
+ }
373
+ end
374
+
375
+ def thread_list
376
+ thcs, unmanaged_ths = update_thread_list
377
+ thcs.each_with_index{|thc, i|
378
+ @ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
379
+ }
380
+
381
+ if !unmanaged_ths.empty?
382
+ @ui.puts "The following threads are not managed yet by the debugger:"
383
+ unmanaged_ths.each{|th|
384
+ @ui.puts " " + th.to_s
385
+ }
386
+ end
387
+ end
388
+
389
+ def thread_switch n
390
+ if th = @th_clients.keys[n]
391
+ @tc = @th_clients[th]
392
+ end
393
+ thread_list
394
+ end
395
+
396
+ def update_thread_list
397
+ list = Thread.list
398
+ thcs = []
399
+ unmanaged = []
400
+
401
+ list.each{|th|
402
+ case
403
+ when th == Thread.current
404
+ # ignore
405
+ when @th_clients.has_key?(th)
406
+ thcs << @th_clients[th]
407
+ else
408
+ unmanaged << th
409
+ end
410
+ }
411
+ return thcs, unmanaged
412
+ end
413
+
414
+ def delete_breakpoint arg = nil
415
+ case arg
416
+ when nil
417
+ @bps.each{|key, bp| bp.disable}
418
+ @bps.clear
419
+ else
420
+ if bp = @bps[key = @bps.keys[arg]]
421
+ bp.disable
422
+ @bps.delete key
423
+ return [arg, bp]
424
+ end
425
+ end
426
+ end
427
+
428
+ def repl_add_breakpoint arg
429
+ arg.strip!
430
+
431
+ if /(.+?)\s+if\s+(.+)\z/ =~ arg
432
+ sig = $1
433
+ cond = $2
434
+ else
435
+ sig = arg
436
+ end
437
+
438
+ case sig
439
+ when /\A(\d+)\z/
440
+ add_line_breakpoint @tc.location.path, $1.to_i, cond
441
+ when /\A(.+):(\d+)\z/
442
+ add_line_breakpoint $1, $2.to_i, cond
443
+ when /\A(.+)[\.\#](.+)\z/
444
+ add_method_breakpoint arg, cond
445
+ else
446
+ raise "unknown breakpoint format: #{arg}"
447
+ end
448
+ end
449
+
450
+ def break? file, line
451
+ @bps.has_key? [file, line]
452
+ end
453
+
454
+ def setup_threads
455
+ stop_all_threads do
456
+ Thread.list.each{|th|
457
+ @th_clients[th] = ThreadClient.new(@q_evt, Queue.new, th)
458
+ }
459
+ end
460
+ end
461
+
462
+ def thread_client
463
+ thr = Thread.current
464
+ @th_clients[thr] ||= ThreadClient.new(@q_evt, Queue.new)
465
+ end
466
+
467
+ def stop_all_threads
468
+ current = Thread.current
469
+
470
+ if Thread.list.size > 1
471
+ TracePoint.new(:line) do
472
+ th = Thread.current
473
+ if current == th || @management_threads.include?(th)
474
+ next
475
+ else
476
+ tc = ThreadClient.current
477
+ tc.on_pause
478
+ end
479
+ end.enable do
480
+ yield
481
+ ensure
482
+ @th_clients.each{|thr, tc|
483
+ case thr
484
+ when current, (@tc && @tc.thread)
485
+ next
486
+ else
487
+ tc << :continue if thr != Thread.current
488
+ end
489
+ }
490
+ end
491
+ else
492
+ yield
493
+ end
494
+ end
495
+
496
+ ## event
497
+
498
+ def on_load iseq, src
499
+ @sr.add iseq, src
500
+ @reserved_bps.each{|(path, line, cond)|
501
+ if path == iseq.absolute_path
502
+ add_line_breakpoint(path, line, cond)
503
+ end
504
+ }
505
+ end
506
+
507
+ # configuration
508
+
509
+ def add_catch_breakpoint arg
510
+ bp = CatchBreakpoint.new(arg)
511
+ @bps[bp.key] = bp
512
+ bp
513
+ end
514
+
515
+ def add_line_breakpoint_exact iseq, events, file, line, cond
516
+ if @bps[[file, line]]
517
+ return nil # duplicated
518
+ end
519
+
520
+ bp = case
521
+ when events.include?(:RUBY_EVENT_CALL)
522
+ # "def foo" line set bp on the beggining of method foo
523
+ LineBreakpoint.new(:call, iseq, line, cond)
524
+ when events.include?(:RUBY_EVENT_LINE)
525
+ LineBreakpoint.new(:line, iseq, line, cond)
526
+ when events.include?(:RUBY_EVENT_RETURN)
527
+ LineBreakpoint.new(:return, iseq, line, cond)
528
+ when events.include?(:RUBY_EVENT_B_RETURN)
529
+ LineBreakpoint.new(:b_return, iseq, line, cond)
530
+ when events.include?(:RUBY_EVENT_END)
531
+ LineBreakpoint.new(:end, iseq, line, cond)
532
+ else
533
+ nil
534
+ end
535
+ @bps[bp.key] = bp if bp
536
+ end
537
+
538
+ NearestISeq = Struct.new(:iseq, :line, :events)
539
+
540
+ def add_line_breakpoint_nearest file, line, cond
541
+ nearest = nil # NearestISeq
542
+
543
+ ObjectSpace.each_iseq{|iseq|
544
+ if iseq.absolute_path == file && iseq.first_lineno <= line
545
+ iseq.traceable_lines_norec(line_events = {})
546
+ lines = line_events.keys.sort
547
+
548
+ if !lines.empty? && lines.last >= line
549
+ nline = lines.bsearch{|l| line <= l}
550
+ events = line_events[nline]
551
+
552
+ if !nearest
553
+ nearest = NearestISeq.new(iseq, nline, events)
554
+ else
555
+ if nearest.iseq.first_lineno <= iseq.first_lineno
556
+ if (nearest.line > line && !nearest.events.include?(:RUBY_EVENT_CALL)) ||
557
+ events.include?(:RUBY_EVENT_CALL)
558
+ nearest = NearestISeq.new(iseq, nline, events)
559
+ end
560
+ end
561
+ end
562
+ end
563
+ end
564
+ }
565
+
566
+ if nearest
567
+ add_line_breakpoint_exact nearest.iseq, nearest.events, file, nearest.line, cond
568
+ else
569
+ return nil
570
+ end
571
+ end
572
+
573
+ def resolve_path file
574
+ File.realpath(File.expand_path(file))
575
+ rescue Errno::ENOENT
576
+ file
577
+ end
578
+
579
+ def add_line_breakpoint file, line, cond = nil
580
+ file = resolve_path(file)
581
+ bp = add_line_breakpoint_nearest file, line, cond
582
+ @reserved_bps << [file, line, cond] unless bp
583
+ bp
584
+ end
585
+
586
+ def add_method_breakpoint signature
587
+ raise
588
+ end
589
+ end
590
+
591
+ def self.add_line_breakpoint file, line, if: if_not_given = true
592
+ ::DEBUGGER__::SESSION.add_line_breakpoint file, line, if_not_given ? nil : binding.local_variable_get(:if)
593
+ end
594
+
595
+ def self.add_catch_breakpoint pat
596
+ ::DEBUGGER__::SESSION.add_catch_breakpoint pat
597
+ end
598
+ end
@@ -0,0 +1,20 @@
1
+ module DEBUGGER__
2
+ class SourceRepository
3
+ def initialize
4
+ @files = {} # filename => [src, iseq]
5
+ end
6
+
7
+ def add iseq, src
8
+ begin
9
+ src = File.read(iseq.path)
10
+ rescue
11
+ src = nil
12
+ end unless src
13
+ @files[iseq.path] = [src, iseq]
14
+ end
15
+
16
+ def get path
17
+ @files[path]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ require 'socket'
2
+ require_relative 'server'
3
+
4
+ module DEBUGGER__
5
+ class UI_TcpServer < UI_Server
6
+ def accept
7
+ host = ENV[''] || 'localhost'
8
+ port = ENV['RUBY_DEBUG_PORT'] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
9
+ port = port.to_i.tap{|i| i != 0 || raise("Specify valid port number (#{port} is specified)")}
10
+
11
+ $stderr.puts "Debugger can attach via TCP/IP (#{host}:#{port})"
12
+ Socket.tcp_server_loop(host, port) do |sock, client|
13
+ yield sock
14
+ end
15
+ rescue => e
16
+ $stderr.puts e.message
17
+ exit
18
+ end
19
+ end
20
+
21
+ SESSION = Session.new(s = UI_TcpServer.new)
22
+ SESSION.management_threads << s.reader_thread
23
+ end