debug 1.0.0.beta4 → 1.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
data/lib/debug/session.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  
2
+ # skip to load debugger for bundle exec
3
+ return if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
4
+
5
+ require_relative 'config'
2
6
  require_relative 'thread_client'
3
7
  require_relative 'source_repository'
4
8
  require_relative 'breakpoint'
5
- require_relative 'config'
9
+
10
+ require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
6
11
 
7
12
  class RubyVM::InstructionSequence
8
13
  def traceable_lines_norec lines
@@ -40,6 +45,10 @@ class RubyVM::InstructionSequence
40
45
  def last_line
41
46
  self.to_a[4][:code_location][2]
42
47
  end
48
+
49
+ def first_line
50
+ self.to_a[4][:code_location][0]
51
+ end
43
52
  end
44
53
 
45
54
  module DEBUGGER__
@@ -51,7 +60,7 @@ module DEBUGGER__
51
60
  # [file, line] => LineBreakpoint
52
61
  # "Error" => CatchBreakpoint
53
62
  # "Foo#bar" => MethodBreakpoint
54
- # [:watch, expr] => WatchExprBreakpoint
63
+ # [:watch, ivar] => WatchIVarBreakpoint
55
64
  # [:check, expr] => CheckBreakpoint
56
65
  @th_clients = {} # {Thread => ThreadClient}
57
66
  @q_evt = Queue.new
@@ -60,76 +69,20 @@ module DEBUGGER__
60
69
  @tc_id = 0
61
70
  @initial_commands = []
62
71
 
72
+ @frame_map = {} # {id => [threadId, frame_depth]} for DAP
73
+ @var_map = {1 => [:globals], } # {id => ...} for DAP
74
+ @src_map = {} # {id => src}
75
+
63
76
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
64
- ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
77
+ unless @management_threads.include? Thread.current
78
+ ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
79
+ end
65
80
  }
66
81
  @tp_load_script.enable
67
82
 
68
83
  @session_server = Thread.new do
69
84
  Thread.current.abort_on_exception = true
70
-
71
- while evt = @q_evt.pop
72
- tc, output, ev, *ev_args = evt
73
- output.each{|str| @ui.puts str}
74
-
75
- case ev
76
- when :load
77
- iseq, src = ev_args
78
- on_load iseq, src
79
- tc << :continue
80
- when :thread_begin
81
- th = ev_args.shift
82
- on_thread_begin th
83
- tc << :continue
84
- when :suspend
85
- case ev_args.first
86
- when :breakpoint
87
- bp, i = bp_index ev_args[1]
88
- if bp
89
- @ui.puts "\nStop by \##{i} #{bp}"
90
- end
91
- when :trap
92
- @ui.puts ''
93
- @ui.puts "\nStop by #{ev_args[1]}"
94
- end
95
-
96
- if @displays.empty?
97
- wait_command_loop tc
98
- else
99
- tc << [:eval, :display, @displays]
100
- end
101
- when :result
102
- case ev_args.first
103
- when :watch
104
- bp = ev_args[1]
105
- @bps[bp.key] = bp
106
- show_bps bp
107
- when :try_display
108
- failed_results = ev_args[1]
109
- if failed_results.size > 0
110
- i, msg = failed_results.last
111
- if i+1 == @displays.size
112
- @ui.puts "canceled: #{@displays.pop}"
113
- end
114
- end
115
- when :method_breakpoint
116
- bp = ev_args[1]
117
- if bp
118
- @bps[bp.key] = bp
119
- show_bps bp
120
- else
121
- # can't make a bp
122
- end
123
- else
124
- # ignore
125
- end
126
-
127
- wait_command_loop tc
128
- end
129
- end
130
- ensure
131
- @bps.each{|k, bp| bp.disable}
132
- @th_clients.each{|th, thc| thc.close}
85
+ session_server_main
133
86
  end
134
87
 
135
88
  @management_threads = [@session_server]
@@ -138,11 +91,84 @@ module DEBUGGER__
138
91
  setup_threads
139
92
 
140
93
  @tp_thread_begin = TracePoint.new(:thread_begin){|tp|
141
- ThreadClient.current.on_thread_begin Thread.current
94
+ unless @management_threads.include?(th = Thread.current)
95
+ ThreadClient.current.on_thread_begin th
96
+ end
142
97
  }
143
98
  @tp_thread_begin.enable
144
99
  end
145
100
 
101
+ def session_server_main
102
+ while evt = @q_evt.pop
103
+ # varible `@internal_info` is only used for test
104
+ tc, output, ev, @internal_info, *ev_args = evt
105
+ output.each{|str| @ui.puts str}
106
+
107
+ case ev
108
+ when :load
109
+ iseq, src = ev_args
110
+ on_load iseq, src
111
+ @ui.event :load
112
+ tc << :continue
113
+ when :thread_begin
114
+ th = ev_args.shift
115
+ on_thread_begin th
116
+ @ui.event :thread_begin, th
117
+ tc << :continue
118
+ when :suspend
119
+ case ev_args.first
120
+ when :breakpoint
121
+ bp, i = bp_index ev_args[1]
122
+ @ui.event :suspend_bp, i, bp
123
+ when :trap
124
+ @ui.event :suspend_trap, ev_args[1]
125
+ else
126
+ @ui.event :suspended
127
+ end
128
+
129
+ if @displays.empty?
130
+ wait_command_loop tc
131
+ else
132
+ tc << [:eval, :display, @displays]
133
+ end
134
+ when :result
135
+ case ev_args.first
136
+ when :watch
137
+ bp = ev_args[1]
138
+ @bps[bp.key] = bp
139
+ show_bps bp
140
+ when :try_display
141
+ failed_results = ev_args[1]
142
+ if failed_results.size > 0
143
+ i, _msg = failed_results.last
144
+ if i+1 == @displays.size
145
+ @ui.puts "canceled: #{@displays.pop}"
146
+ end
147
+ end
148
+ when :method_breakpoint
149
+ bp = ev_args[1]
150
+ if bp
151
+ @bps[bp.key] = bp
152
+ show_bps bp
153
+ else
154
+ # can't make a bp
155
+ end
156
+ else
157
+ # ignore
158
+ end
159
+
160
+ wait_command_loop tc
161
+
162
+ when :dap_result
163
+ dap_event ev_args # server.rb
164
+ wait_command_loop tc
165
+ end
166
+ end
167
+ ensure
168
+ @bps.each{|k, bp| bp.disable}
169
+ @th_clients.each{|th, thc| thc.close}
170
+ end
171
+
146
172
  def add_initial_commands cmds
147
173
  cmds.each{|c|
148
174
  c.gsub('#.*', '').strip!
@@ -150,11 +176,11 @@ module DEBUGGER__
150
176
  }
151
177
  end
152
178
 
153
- def source path
179
+ def source iseq
154
180
  if CONFIG[:use_colorize]
155
- @sr.get_colored(path)
181
+ @sr.get_colored(iseq)
156
182
  else
157
- @sr.get(path)
183
+ @sr.get(iseq)
158
184
  end
159
185
  end
160
186
 
@@ -182,12 +208,24 @@ module DEBUGGER__
182
208
 
183
209
  def wait_command
184
210
  if @initial_commands.empty?
211
+ @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
185
212
  line = @ui.readline
186
213
  else
187
214
  line = @initial_commands.shift.strip
188
215
  @ui.puts "(rdbg:init) #{line}"
189
216
  end
190
217
 
218
+ case line
219
+ when String
220
+ process_command line
221
+ when Hash
222
+ process_dap_request line # defined in server.rb
223
+ else
224
+ raise "unexpected input: #{line.inspect}"
225
+ end
226
+ end
227
+
228
+ def process_command line
191
229
  if line.empty?
192
230
  if @repl_prev_line
193
231
  line = @repl_prev_line
@@ -324,8 +362,8 @@ module DEBUGGER__
324
362
  end
325
363
  return :retry
326
364
 
327
- # * `watch <expr>`
328
- # * Stop the execution when the result of `<expr>` is changed.
365
+ # * `watch @ivar`
366
+ # * Stop the execution when the result of current scope's `@ivar` is changed.
329
367
  # * Note that this feature is super slow.
330
368
  when 'wat', 'watch'
331
369
  if arg
@@ -407,7 +445,7 @@ module DEBUGGER__
407
445
  # * `i[nfo]`, `i[nfo] l[ocal[s]]`
408
446
  # * Show information about the current frame (local variables)
409
447
  # * It includes `self` as `%self` and a return value as `%return`.
410
- # * `i[nfo] th[read[s]]
448
+ # * `i[nfo] th[read[s]]`
411
449
  # * Show all threads (same as `th[read]`).
412
450
  when 'i', 'info'
413
451
  case arg
@@ -461,9 +499,11 @@ module DEBUGGER__
461
499
  case arg
462
500
  when 'on'
463
501
  dir = __dir__
464
- @tracer ||= TracePoint.new(){|tp|
502
+ @tracer ||= TracePoint.new(:call, :return, :b_call, :b_return, :line, :class, :end){|tp|
465
503
  next if File.dirname(tp.path) == dir
466
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']
467
507
  # next if tp.event != :line
468
508
  @ui.puts pretty_tp(tp)
469
509
  }
@@ -725,6 +765,8 @@ module DEBUGGER__
725
765
  case
726
766
  when th == Thread.current
727
767
  # ignore
768
+ when @management_threads.include?(th)
769
+ # ignore
728
770
  when @th_clients.has_key?(th)
729
771
  thcs << @th_clients[th]
730
772
  else
@@ -748,8 +790,13 @@ module DEBUGGER__
748
790
  end
749
791
  end
750
792
 
793
+ def managed_thread_clients
794
+ thcs, _unmanaged_ths = update_thread_list
795
+ thcs
796
+ end
797
+
751
798
  def thread_switch n
752
- thcs, unmanaged_ths = update_thread_list
799
+ thcs, _unmanaged_ths = update_thread_list
753
800
 
754
801
  if tc = thcs[n]
755
802
  if tc.mode
@@ -867,12 +914,16 @@ module DEBUGGER__
867
914
  def resolve_path file
868
915
  File.realpath(File.expand_path(file))
869
916
  rescue Errno::ENOENT
870
- return file if file == '-e'
871
- $LOAD_PATH.each do |lp|
872
- libpath = File.join(lp, file)
873
- return File.realpath(libpath)
874
- rescue Errno::ENOENT
875
- # next
917
+ case file
918
+ when '-e', '-'
919
+ return file
920
+ else
921
+ $LOAD_PATH.each do |lp|
922
+ libpath = File.join(lp, file)
923
+ return File.realpath(libpath)
924
+ rescue Errno::ENOENT
925
+ # next
926
+ end
876
927
  end
877
928
 
878
929
  raise
@@ -917,6 +968,29 @@ module DEBUGGER__
917
968
  end
918
969
  end
919
970
  end
971
+
972
+ def width
973
+ @ui.width
974
+ end
975
+
976
+ def check_forked
977
+ unless @session_server.status
978
+ # TODO: Support it
979
+ raise 'DEBUGGER: stop at forked process is not supported yet.'
980
+ end
981
+ end
982
+ end
983
+
984
+ class UI_Base
985
+ def event type, *args
986
+ case type
987
+ when :suspend_bp
988
+ i, bp = *args
989
+ puts "\nStop by \##{i} #{bp}" if bp
990
+ when :suspend_trap
991
+ puts "\nStop by #{args.first}"
992
+ end
993
+ end
920
994
  end
921
995
 
922
996
  # manual configuration methods
@@ -940,7 +1014,7 @@ module DEBUGGER__
940
1014
  when dir_prefix
941
1015
  when %r{rubygems/core_ext/kernel_require\.rb}
942
1016
  else
943
- return loc
1017
+ return loc if loc.absolute_path
944
1018
  end
945
1019
  end
946
1020
  nil
@@ -952,6 +1026,7 @@ module DEBUGGER__
952
1026
  set_config(kw)
953
1027
 
954
1028
  require_relative 'console'
1029
+
955
1030
  initialize_session UI_Console.new
956
1031
 
957
1032
  @prev_handler = trap(:SIGINT){
@@ -992,8 +1067,15 @@ module DEBUGGER__
992
1067
  # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
993
1068
 
994
1069
  Binding.module_eval do
995
- ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1
996
- def bp; nil; end
1070
+ def bp command: nil
1071
+ if command
1072
+ cmds = command.split(";;")
1073
+ SESSION.add_initial_commands cmds
1074
+ end
1075
+
1076
+ ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true
1077
+ true
1078
+ end
997
1079
  end
998
1080
 
999
1081
  if !::DEBUGGER__::CONFIG[:nonstop]
@@ -1018,13 +1100,15 @@ module DEBUGGER__
1018
1100
  }
1019
1101
 
1020
1102
  # debug commands file
1021
- [::DEBUGGER__::CONFIG[:init_script],
1103
+ [init_script = ::DEBUGGER__::CONFIG[:init_script],
1022
1104
  './.rdbgrc',
1023
1105
  File.expand_path('~/.rdbgrc')].each{|path|
1024
- next unless path
1106
+ next unless path
1025
1107
 
1026
1108
  if File.file? path
1027
1109
  ::DEBUGGER__::SESSION.add_initial_commands File.readlines(path)
1110
+ elsif path == init_script
1111
+ warn "Not found: #{path}"
1028
1112
  end
1029
1113
  }
1030
1114
 
@@ -1086,6 +1170,7 @@ module DEBUGGER__
1086
1170
  end
1087
1171
 
1088
1172
  class ::Module
1173
+ undef method_added
1089
1174
  def method_added mid; end
1090
1175
  def singleton_method_added mid; end
1091
1176
  end
@@ -1099,4 +1184,14 @@ module DEBUGGER__
1099
1184
  end
1100
1185
 
1101
1186
  METHOD_ADDED_TRACKER = self.create_method_added_tracker
1187
+
1188
+ SHORT_INSPECT_LENGTH = 40
1189
+ def self.short_inspect obj, use_short = true
1190
+ str = obj.inspect
1191
+ if use_short && str.length > SHORT_INSPECT_LENGTH
1192
+ str[0...SHORT_INSPECT_LENGTH] + '...'
1193
+ else
1194
+ str
1195
+ end
1196
+ end
1102
1197
  end
@@ -1,55 +1,75 @@
1
- require 'irb/color' # IRB::Color.colorize_code
1
+ require_relative 'color'
2
2
 
3
3
  module DEBUGGER__
4
4
  class SourceRepository
5
+ SrcInfo = Struct.new(:src, :colored)
6
+
5
7
  def initialize
6
- @files = {} # filename => [src, iseq]
7
- @color_files = {}
8
+ @files = {} # filename => SrcInfo
8
9
  end
9
10
 
10
11
  def add iseq, src
11
- path = iseq.absolute_path
12
- path = '-e' if iseq.path == '-e'
13
- add_path path, src: src
12
+ if (path = iseq.absolute_path) && File.exist?(path)
13
+ add_path path
14
+ elsif src
15
+ add_iseq iseq, src
16
+ end
14
17
  end
15
18
 
16
- def add_path path, src: nil
17
- case
18
- when src
19
- if File.file?(path)
20
- path = '(eval)' + path
21
- src = nil
22
- end
23
- when path == '-e'
24
- when path
25
- begin
26
- src = File.read(path)
27
- rescue SystemCallError
28
- end
29
- else
30
- src = nil
19
+ def all_iseq iseq, rs = []
20
+ rs << iseq
21
+ iseq.each_child{|ci|
22
+ all_iseq(ci, rs)
23
+ }
24
+ rs
25
+ end
26
+
27
+ private def add_iseq iseq, src
28
+ line = iseq.first_line
29
+ if line > 1
30
+ src = ("\n" * (line - 1)) + src
31
31
  end
32
+ si = SrcInfo.new(src.lines)
32
33
 
33
- if src
34
+ all_iseq(iseq).each{|e|
35
+ e.instance_variable_set(:@debugger_si, si)
36
+ e.freeze
37
+ }
38
+ end
39
+
40
+ private def add_path path
41
+ begin
42
+ src = File.read(path)
34
43
  src = src.gsub("\r\n", "\n") # CRLF -> LF
35
- @files[path] = src.lines
44
+ @files[path] = SrcInfo.new(src.lines)
45
+ rescue SystemCallError
36
46
  end
37
47
  end
38
48
 
39
- def get path
40
- if @files.has_key? path
49
+ private def get_si iseq
50
+ return unless iseq
51
+
52
+ if iseq.instance_variable_defined?(:@debugger_si)
53
+ iseq.instance_variable_get(:@debugger_si)
54
+ elsif @files.has_key?(path = iseq.absolute_path)
41
55
  @files[path]
42
- else
43
- add_path path
56
+ elsif path
57
+ add_path(path)
44
58
  end
45
59
  end
46
60
 
47
- def get_colored path
48
- if src_lines = @color_files[path]
49
- return src_lines
50
- else
51
- if src_lines = get(path)
52
- @color_files[path] = IRB::Color.colorize_code(src_lines.join).lines
61
+ def get iseq
62
+ if si = get_si(iseq)
63
+ si.src
64
+ end
65
+ end
66
+
67
+ include Color
68
+
69
+ def get_colored iseq
70
+ if si = get_si(iseq)
71
+ si.colored || begin
72
+ si.colored = colorize_code(si.src.join).lines
53
73
  end
54
74
  end
55
75
  end