debug 1.0.0.beta4 → 1.0.0.beta5
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.
- 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
|