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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +1 -0
- data/CONTRIBUTING.md +48 -0
- data/Gemfile +1 -1
- data/README.md +72 -37
- data/debug.gemspec +2 -0
- data/exe/rdbg +8 -1
- data/ext/debug/debug.c +9 -8
- data/lib/debug/breakpoint.rb +71 -24
- data/lib/debug/client.rb +49 -6
- data/lib/debug/color.rb +70 -0
- data/lib/debug/config.rb +39 -5
- data/lib/debug/console.rb +8 -1
- data/lib/debug/frame_info.rb +61 -30
- data/lib/debug/open.rb +2 -0
- data/lib/debug/run.rb +2 -0
- data/lib/debug/server.rb +72 -19
- data/lib/debug/server_dap.rb +605 -0
- data/lib/debug/session.rb +181 -86
- data/lib/debug/source_repository.rb +53 -33
- data/lib/debug/test_console.rb +0 -0
- data/lib/debug/thread_client.rb +91 -23
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +51 -28
- metadata +21 -3
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
179
|
+
def source iseq
|
154
180
|
if CONFIG[:use_colorize]
|
155
|
-
@sr.get_colored(
|
181
|
+
@sr.get_colored(iseq)
|
156
182
|
else
|
157
|
-
@sr.get(
|
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
|
328
|
-
# * Stop the execution when the result of
|
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,
|
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
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
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
|
-
|
996
|
-
|
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
|
-
|
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
|
-
|
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 =>
|
7
|
-
@color_files = {}
|
8
|
+
@files = {} # filename => SrcInfo
|
8
9
|
end
|
9
10
|
|
10
11
|
def add iseq, src
|
11
|
-
path = iseq.absolute_path
|
12
|
-
|
13
|
-
|
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
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
40
|
-
|
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
|
-
|
43
|
-
add_path
|
56
|
+
elsif path
|
57
|
+
add_path(path)
|
44
58
|
end
|
45
59
|
end
|
46
60
|
|
47
|
-
def
|
48
|
-
if
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|