debug 0.2.1 → 1.0.0.alpha0

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.
@@ -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