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.
@@ -0,0 +1,27 @@
1
+ module DEBUGGER__
2
+ class SourceRepository
3
+ def initialize
4
+ @files = {} # filename => [src, iseq]
5
+ end
6
+
7
+ def add iseq, src
8
+ path = iseq.absolute_path
9
+ path = '-e' if iseq.path == '-e'
10
+
11
+ case
12
+ when path = iseq.absolute_path
13
+ src = File.read(path)
14
+ when iseq.path == '-e'
15
+ path = '-e'
16
+ else
17
+ src = nil
18
+ end
19
+
20
+ @files[path] = src.lines if src
21
+ end
22
+
23
+ def get path
24
+ @files[path]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,618 @@
1
+ require 'objspace'
2
+ require 'pp'
3
+
4
+ module DEBUGGER__
5
+ class ThreadClient
6
+ def self.current
7
+ Thread.current[:DEBUGGER__ThreadClient] || begin
8
+ tc = ::DEBUGGER__::SESSION.thread_client
9
+ Thread.current[:DEBUGGER__ThreadClient] = tc
10
+ end
11
+ end
12
+
13
+ attr_reader :location, :thread, :mode, :id
14
+
15
+ def initialize id, q_evt, q_cmd, thr = Thread.current
16
+ @id = id
17
+ @thread = thr
18
+ @q_evt = q_evt
19
+ @q_cmd = q_cmd
20
+ @step_tp = nil
21
+ @output = []
22
+ @src_lines_on_stop = (DEBUGGER__::CONFIG[:show_src_lines] || 10).to_i
23
+ @show_frames_on_stop = (DEBUGGER__::CONFIG[:show_frames] || 2).to_i
24
+ set_mode nil
25
+ end
26
+
27
+ def close
28
+ @q_cmd.close
29
+ end
30
+
31
+ def inspect
32
+ "#<DBG:TC #{self.id}:#{self.mode}@#{@thread.backtrace[-1]}>"
33
+ end
34
+
35
+ def puts str = ''
36
+ case str
37
+ when nil
38
+ @output << "\n"
39
+ when Array
40
+ str.each{|s| puts s}
41
+ else
42
+ @output << str.chomp + "\n"
43
+ end
44
+ end
45
+
46
+ def << req
47
+ @q_cmd << req
48
+ end
49
+
50
+ def event! ev, *args
51
+ @q_evt << [self, @output, ev, *args]
52
+ @output = []
53
+ end
54
+
55
+ ## events
56
+
57
+ def on_trap sig
58
+ if self.mode == :wait_next_action
59
+ # raise Interrupt
60
+ else
61
+ on_suspend :trap, sig: sig
62
+ end
63
+ end
64
+
65
+ def on_pause
66
+ on_suspend :pause
67
+ end
68
+
69
+ def on_thread_begin th
70
+ event! :thread_begin, th
71
+ wait_next_action
72
+ end
73
+
74
+ def on_load iseq, eval_src
75
+ event! :load, iseq, eval_src
76
+ wait_next_action
77
+ end
78
+
79
+ def on_breakpoint tp, bp
80
+ on_suspend tp.event, tp, bp: bp
81
+ end
82
+
83
+ def on_suspend event, tp = nil, bp: nil, sig: nil
84
+ @current_frame_index = 0
85
+ @target_frames = DEBUGGER__.capture_frames __dir__
86
+
87
+ cf = @target_frames.first
88
+ if cf
89
+ @location = cf.location
90
+ case event
91
+ when :return, :b_return, :c_return
92
+ cf.has_return_value = true
93
+ cf.return_value = tp.return_value
94
+ end
95
+ end
96
+
97
+ if event != :pause
98
+ show_src max_lines: @src_lines_on_stop
99
+ show_frames @show_frames_on_stop
100
+
101
+ if bp
102
+ event! :suspend, :breakpoint, bp.key
103
+ elsif sig
104
+ event! :suspend, :trap, sig
105
+ else
106
+ event! :suspend, event
107
+ end
108
+ end
109
+
110
+ wait_next_action
111
+ end
112
+
113
+ ## control all
114
+
115
+ begin
116
+ TracePoint.new(:raise){}.enable(target_thread: Thread.current)
117
+ SUPPORT_TARGET_THREAD = true
118
+ rescue ArgumentError
119
+ SUPPORT_TARGET_THREAD = false
120
+ end
121
+
122
+ def step_tp
123
+ @step_tp.disable if @step_tp
124
+
125
+ thread = Thread.current
126
+
127
+ if SUPPORT_TARGET_THREAD
128
+ @step_tp = TracePoint.new(:line, :b_return, :return){|tp|
129
+ next if SESSION.break? tp.path, tp.lineno
130
+ next if !yield
131
+ next if tp.path.start_with?(__dir__)
132
+ next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
133
+
134
+ tp.disable
135
+ on_suspend tp.event, tp
136
+ }
137
+ @step_tp.enable(target_thread: thread)
138
+ else
139
+ @step_tp = TracePoint.new(:line, :b_return, :return){|tp|
140
+ next if thread != Thread.current
141
+ next if SESSION.break? tp.path, tp.lineno
142
+ next if !yield
143
+ next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
144
+
145
+ tp.disable
146
+ on_suspend tp.event, tp
147
+ }
148
+ @step_tp.enable
149
+ end
150
+ end
151
+
152
+ def current_frame
153
+ if @target_frames
154
+ @target_frames[@current_frame_index]
155
+ else
156
+ nil
157
+ end
158
+ end
159
+
160
+ def file_lines path
161
+ if (src_lines = SESSION.source(path))
162
+ src_lines
163
+ elsif File.exist?(path)
164
+ File.readlines(path)
165
+ end
166
+ end
167
+
168
+ def show_src(frame_index: @current_frame_index,
169
+ update_line: false,
170
+ max_lines: 10,
171
+ start_line: nil,
172
+ end_line: nil,
173
+ dir: +1)
174
+ #
175
+ if @target_frames && frame = @target_frames[frame_index]
176
+ if file_lines = file_lines(path = frame.location.path)
177
+ frame_line = frame.location.lineno - 1
178
+
179
+ lines = file_lines.map.with_index do |e, i|
180
+ if i == frame_line
181
+ "=> #{'%4d' % (i+1)}| #{e}"
182
+ else
183
+ " #{'%4d' % (i+1)}| #{e}"
184
+ end
185
+ end
186
+
187
+ unless start_line
188
+ if frame.show_line
189
+ if dir > 0
190
+ start_line = frame.show_line
191
+ else
192
+ end_line = frame.show_line - max_lines
193
+ start_line = [end_line - max_lines, 0].max
194
+ end
195
+ else
196
+ start_line = [frame_line - max_lines/2, 0].max
197
+ end
198
+ end
199
+
200
+ unless end_line
201
+ end_line = [start_line + max_lines, lines.size].min
202
+ end
203
+
204
+ if update_line
205
+ frame.show_line = end_line
206
+ end
207
+
208
+ if start_line != end_line && max_lines
209
+ puts "[#{start_line+1}, #{end_line}] in #{pretty_path(path)}" if !update_line && max_lines != 1
210
+ puts lines[start_line ... end_line]
211
+ end
212
+ else # no file lines
213
+ puts "# No sourcefile available for #{path}"
214
+ end
215
+ end
216
+ end
217
+
218
+ def show_by_editor path = nil
219
+ unless path
220
+ if @target_frames && frame = @target_frames[@current_frame_index]
221
+ path = frame.location.path
222
+ else
223
+ return # can't get path
224
+ end
225
+ end
226
+
227
+ if File.exist?(path)
228
+ if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR'])
229
+ puts "command: #{editor}"
230
+ puts " path: #{path}"
231
+ system(editor, path)
232
+ else
233
+ puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']"
234
+ end
235
+ else
236
+ puts "Can not find file: #{path}"
237
+ end
238
+ end
239
+
240
+ def show_locals
241
+ if s = current_frame&.self
242
+ puts " %self => #{s}"
243
+ end
244
+ if current_frame&.has_return_value
245
+ puts " %return => #{current_frame.return_value}"
246
+ end
247
+ if b = current_frame&.binding
248
+ b.local_variables.each{|loc|
249
+ puts " #{loc} => #{b.local_variable_get(loc).inspect}"
250
+ }
251
+ end
252
+ end
253
+
254
+ def show_ivars
255
+ if s = current_frame&.self
256
+ s.instance_variables.each{|iv|
257
+ puts " #{iv} => #{s.instance_variable_get(iv)}"
258
+ }
259
+ end
260
+ end
261
+
262
+ def frame_eval src, re_raise: false
263
+ begin
264
+ @success_last_eval = false
265
+
266
+ b = current_frame.binding
267
+ result = if b
268
+ f, l = b.source_location
269
+ b.eval(src, "(rdbg)/#{f}")
270
+ else
271
+ frame_self = current_frame.self
272
+ frame_self.instance_eval(src)
273
+ end
274
+ @success_last_eval = true
275
+ result
276
+
277
+ rescue Exception => e
278
+ return yield(e) if block_given?
279
+
280
+ puts "eval error: #{e}"
281
+
282
+ e.backtrace_locations.each do |loc|
283
+ break if loc.path == __FILE__
284
+ puts " #{loc}"
285
+ end
286
+ raise if re_raise
287
+ end
288
+ end
289
+
290
+ def parameters_info b, vars
291
+ vars.map{|var|
292
+ begin
293
+ "#{var}=#{short_inspect(b.local_variable_get(var))}"
294
+ rescue NameError, TypeError
295
+ nil
296
+ end
297
+ }.compact.join(', ')
298
+ end
299
+
300
+ def get_singleton_class obj
301
+ obj.singleton_class # TODO: don't use it
302
+ rescue TypeError
303
+ nil
304
+ end
305
+
306
+ def klass_sig frame
307
+ klass = frame.class
308
+ if klass == get_singleton_class(frame.self)
309
+ "#{frame.self}."
310
+ else
311
+ "#{frame.class}#"
312
+ end
313
+ end
314
+
315
+ SHORT_INSPECT_LENGTH = 40
316
+
317
+ def short_inspect obj
318
+ str = obj.inspect
319
+ if str.length > SHORT_INSPECT_LENGTH
320
+ str[0...SHORT_INSPECT_LENGTH] + '...'
321
+ else
322
+ str
323
+ end
324
+ end
325
+
326
+ HOME = ENV['HOME'] ? (ENV['HOME'] + '/') : nil
327
+
328
+ def pretty_path path
329
+ use_short_path = CONFIG[:use_short_path]
330
+
331
+ case
332
+ when use_short_path && path.start_with?(dir = RbConfig::CONFIG["rubylibdir"] + '/')
333
+ path.sub(dir, '$(rubylibdir)/')
334
+ when use_short_path && Gem.path.any? do |gp|
335
+ path.start_with?(dir = gp + '/gems/')
336
+ end
337
+ path.sub(dir, '$(Gem)/')
338
+ when HOME && path.start_with?(HOME)
339
+ path.sub(HOME, '~/')
340
+ else
341
+ path
342
+ end
343
+ end
344
+
345
+ def pretty_location loc
346
+ " at #{pretty_path(loc.path)}:#{loc.lineno}"
347
+ end
348
+
349
+ def frame_str i
350
+ frame = @target_frames[i]
351
+ b = frame.binding
352
+
353
+ cur_str = (@current_frame_index == i ? '=>' : ' ')
354
+
355
+ if b && (iseq = frame.iseq)
356
+ if iseq.type == :block
357
+ if (argc = iseq.argc) > 0
358
+ args = parameters_info b, iseq.locals[0...argc]
359
+ args_str = "{|#{args}|}"
360
+ end
361
+
362
+ label_prefix = frame.location.label.sub('block'){ "block#{args_str}" }
363
+ ci_str = label_prefix
364
+ elsif (callee = b.eval('__callee__', __FILE__, __LINE__)) && (argc = iseq.argc) > 0
365
+ args = parameters_info b, iseq.locals[0...argc]
366
+ ksig = klass_sig frame
367
+ ci_str = "#{ksig}#{callee}(#{args})"
368
+ else
369
+ ci_str = frame.location.label
370
+ end
371
+
372
+ loc_str = "#{pretty_location(frame.location)}"
373
+
374
+ if frame.has_return_value
375
+ return_str = " #=> #{short_inspect(frame.return_value)}"
376
+ end
377
+ else
378
+ ksig = klass_sig frame
379
+ callee = frame.location.base_label
380
+ ci_str = "[C] #{ksig}#{callee}"
381
+ loc_str = "#{pretty_location(frame.location)}"
382
+ end
383
+
384
+ "#{cur_str}##{i}\t#{ci_str}#{loc_str}#{return_str}"
385
+ end
386
+
387
+ def show_frames max = (@target_frames || []).size
388
+ if max > 0 && frames = @target_frames
389
+ size = @target_frames.size
390
+ max += 1 if size == max + 1
391
+ max.times{|i|
392
+ break if i >= size
393
+ puts frame_str(i)
394
+ }
395
+ puts " # and #{size - max} frames (use `bt' command for all frames)" if max < size
396
+ end
397
+ end
398
+
399
+ def show_frame i=0
400
+ puts frame_str(i)
401
+ end
402
+
403
+ def show_object_info expr
404
+ begin
405
+ result = frame_eval(expr, re_raise: true)
406
+ rescue Exception
407
+ # ignore
408
+ else
409
+ klass = ObjectSpace.internal_class_of(result)
410
+ exists = []
411
+ klass.ancestors.each{|k|
412
+ puts "= #{k}"
413
+ if (ms = (k.instance_methods(false) - exists)).size > 0
414
+ puts ms.sort.join("\t")
415
+ exists |= ms
416
+ end
417
+ }
418
+ end
419
+ end
420
+
421
+ def add_breakpoint args
422
+ case args.first
423
+ when :method
424
+ klass_name, op, method_name, cond = args[1..]
425
+ bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond)
426
+ begin
427
+ bp.enable
428
+ rescue Exception => e
429
+ puts e.message
430
+ ::DEBUGGER__::METHOD_ADDED_TRACKER.enable
431
+ end
432
+ event! :result, :method_breakpoint, bp
433
+ else
434
+ raise "unknown breakpoint: #{args}"
435
+ end
436
+ end
437
+
438
+ def set_mode mode
439
+ @mode = mode
440
+ end
441
+
442
+ def wait_next_action
443
+ set_mode :wait_next_action
444
+
445
+ while cmds = @q_cmd.pop
446
+ # pp [self, cmds: cmds]
447
+
448
+ cmd, *args = *cmds
449
+
450
+ case cmd
451
+ when :continue
452
+ break
453
+ when :step
454
+ step_type = args[0]
455
+ case step_type
456
+ when :in
457
+ step_tp{true}
458
+ when :next
459
+ frame = @target_frames.first
460
+ path = frame.location.absolute_path || "!eval:#{frame.location.path}"
461
+ line = frame.location.lineno
462
+ frame.iseq.traceable_lines_norec(lines = {})
463
+ next_line = lines.keys.bsearch{|e| e > line}
464
+ if !next_line && (last_line = frame.iseq.last_line) > line
465
+ next_line = last_line
466
+ end
467
+ depth = @target_frames.first.frame_depth
468
+
469
+ step_tp{
470
+ loc = caller_locations(2, 1).first
471
+ loc_path = loc.absolute_path || "!eval:#{loc.path}"
472
+
473
+ (next_line && loc_path == path &&
474
+ (loc_lineno = loc.lineno) > line && loc_lineno <= next_line) ||
475
+ (DEBUGGER__.frame_depth - 3 < depth)
476
+ }
477
+ when :finish
478
+ depth = @target_frames.first.frame_depth
479
+ step_tp{
480
+ # 3 is debugger's frame count
481
+ DEBUGGER__.frame_depth - 3 < depth
482
+ }
483
+ else
484
+ raise
485
+ end
486
+ break
487
+ when :eval
488
+ eval_type, eval_src = *args
489
+
490
+ case eval_type
491
+ when :display, :try_display
492
+ else
493
+ result = frame_eval(eval_src)
494
+ end
495
+ result_type = nil
496
+
497
+ case eval_type
498
+ when :p
499
+ puts "=> " + result.inspect
500
+ when :pp
501
+ puts "=> "
502
+ PP.pp(result, out = ''.dup)
503
+ puts out
504
+ when :call
505
+ result = frame_eval(eval_src)
506
+ when :display, :try_display
507
+ failed_results = []
508
+ eval_src.each_with_index{|src, i|
509
+ result = frame_eval(src){|e|
510
+ failed_results << [i, e.message]
511
+ "<error: #{e.message}>"
512
+ }
513
+ puts "#{i}: #{src} = #{result}"
514
+ }
515
+
516
+ result_type = eval_type
517
+ result = failed_results
518
+ when :watch
519
+ if @success_last_eval
520
+ puts "#{eval_src} = #{result}"
521
+ result = WatchExprBreakpoint.new(eval_src, result)
522
+ result_type = :watch
523
+ else
524
+ result = nil
525
+ end
526
+ else
527
+ raise "unknown error option: #{args.inspect}"
528
+ end
529
+
530
+ event! :result, result_type, result
531
+ when :frame
532
+ type, arg = *args
533
+ case type
534
+ when :up
535
+ if @current_frame_index + 1 < @target_frames.size
536
+ @current_frame_index += 1
537
+ show_src max_lines: 1
538
+ show_frame(@current_frame_index)
539
+ end
540
+ when :down
541
+ if @current_frame_index > 0
542
+ @current_frame_index -= 1
543
+ show_src max_lines: 1
544
+ show_frame(@current_frame_index)
545
+ end
546
+ when :set
547
+ if arg
548
+ index = arg.to_i
549
+ if index >= 0 && index < @target_frames.size
550
+ @current_frame_index = index
551
+ else
552
+ puts "out of frame index: #{index}"
553
+ end
554
+ end
555
+ show_src max_lines: 1
556
+ show_frame(@current_frame_index)
557
+ else
558
+ raise "unsupported frame operation: #{arg.inspect}"
559
+ end
560
+ event! :result, nil
561
+ when :show
562
+ type = args.shift
563
+
564
+ case type
565
+ when :backtrace
566
+ show_frames
567
+
568
+ when :list
569
+ show_src(update_line: true, **(args.first || {}))
570
+
571
+ when :edit
572
+ show_by_editor(args.first)
573
+
574
+ when :local
575
+ show_frame
576
+ show_locals
577
+ show_ivars
578
+
579
+ when :object_info
580
+ expr = args.shift
581
+ show_object_info expr
582
+
583
+ else
584
+ raise "unknown show param: " + [type, *args].inspect
585
+ end
586
+
587
+ event! :result, nil
588
+
589
+ when :breakpoint
590
+ add_breakpoint args
591
+ else
592
+ raise [cmd, *args].inspect
593
+ end
594
+ end
595
+
596
+ rescue SystemExit
597
+ raise
598
+ rescue Exception => e
599
+ pp [__FILE__, __LINE__, e, e.backtrace]
600
+ raise
601
+ ensure
602
+ set_mode nil
603
+ end
604
+
605
+ def to_s
606
+ loc = current_frame&.location
607
+
608
+ if loc
609
+ str = "(#{@thread.name || @thread.status})@#{loc}"
610
+ else
611
+ str = "(#{@thread.name || @thread.status})@#{@thread.to_s}"
612
+ end
613
+
614
+ str += " (not under control)" unless self.mode
615
+ str
616
+ end
617
+ end
618
+ end