debug 1.0.0.alpha1 → 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 +4 -0
- data/CONTRIBUTING.md +145 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +20 -19
- data/README.md +398 -54
- data/Rakefile +21 -1
- data/TODO.md +33 -0
- data/debug.gemspec +7 -10
- data/exe/rdbg +39 -1
- data/ext/debug/debug.c +119 -0
- data/ext/debug/extconf.rb +2 -0
- data/ext/debug/iseq_collector.c +91 -0
- data/lib/debug.rb +1 -1
- data/lib/debug/breakpoint.rb +362 -34
- data/lib/debug/client.rb +53 -17
- data/lib/debug/color.rb +70 -0
- data/lib/debug/config.rb +169 -7
- data/lib/debug/console.rb +96 -0
- data/lib/debug/frame_info.rb +144 -0
- data/lib/debug/open.rb +12 -0
- data/lib/debug/run.rb +4 -0
- data/lib/debug/server.rb +178 -27
- data/lib/debug/server_dap.rb +605 -0
- data/lib/debug/session.rb +779 -193
- data/lib/debug/source_repository.rb +77 -20
- data/lib/debug/test_console.rb +0 -0
- data/lib/debug/thread_client.rb +390 -179
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +350 -0
- metadata +29 -28
- data/lib/debug/repl.rb +0 -69
- data/lib/debug/tcpserver.rb +0 -22
- data/lib/debug/unixserver.rb +0 -18
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
|
|
2
|
+
module DEBUGGER__
|
|
3
|
+
FrameInfo = Struct.new(:location, :self, :binding, :iseq, :class, :frame_depth,
|
|
4
|
+
:has_return_value, :return_value,
|
|
5
|
+
:has_raised_exception, :raised_exception,
|
|
6
|
+
:show_line)
|
|
7
|
+
|
|
8
|
+
# extend FrameInfo with debug.so
|
|
9
|
+
if File.exist? File.join(__dir__, 'debug.so')
|
|
10
|
+
require_relative 'debug.so'
|
|
11
|
+
else
|
|
12
|
+
require "debug/debug"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class FrameInfo
|
|
16
|
+
HOME = ENV['HOME'] ? (ENV['HOME'] + '/') : nil
|
|
17
|
+
|
|
18
|
+
def path
|
|
19
|
+
location.path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def realpath
|
|
23
|
+
location.absolute_path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pretty_path
|
|
27
|
+
use_short_path = ::DEBUGGER__::CONFIG[:use_short_path]
|
|
28
|
+
|
|
29
|
+
case
|
|
30
|
+
when use_short_path && path.start_with?(dir = ::DEBUGGER__::CONFIG["rubylibdir"] + '/')
|
|
31
|
+
path.sub(dir, '$(rubylibdir)/')
|
|
32
|
+
when use_short_path && Gem.path.any? do |gp|
|
|
33
|
+
path.start_with?(dir = gp + '/gems/')
|
|
34
|
+
end
|
|
35
|
+
path.sub(dir, '$(Gem)/')
|
|
36
|
+
when HOME && path.start_with?(HOME)
|
|
37
|
+
path.sub(HOME, '~/')
|
|
38
|
+
else
|
|
39
|
+
path
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def name
|
|
44
|
+
# p frame_type: frame_type, self: self
|
|
45
|
+
case frame_type
|
|
46
|
+
when :block
|
|
47
|
+
level, block_loc, _args = block_identifier
|
|
48
|
+
"block in #{block_loc}#{level}"
|
|
49
|
+
when :method
|
|
50
|
+
ci, _args = method_identifier
|
|
51
|
+
"#{ci}"
|
|
52
|
+
when :c
|
|
53
|
+
c_identifier
|
|
54
|
+
when :other
|
|
55
|
+
other_identifier
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def file_lines
|
|
60
|
+
SESSION.source(self.iseq)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def frame_type
|
|
64
|
+
if binding && iseq
|
|
65
|
+
if iseq.type == :block
|
|
66
|
+
:block
|
|
67
|
+
elsif callee
|
|
68
|
+
:method
|
|
69
|
+
else
|
|
70
|
+
:other
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
:c
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
BLOCK_LABL_REGEXP = /\Ablock( \(\d+ levels\))* in (.+)\z/
|
|
78
|
+
|
|
79
|
+
def block_identifier
|
|
80
|
+
return unless frame_type == :block
|
|
81
|
+
args = parameters_info(iseq.argc)
|
|
82
|
+
_, level, block_loc = location.label.match(BLOCK_LABL_REGEXP).to_a
|
|
83
|
+
[level || "", block_loc, args]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def method_identifier
|
|
87
|
+
return unless frame_type == :method
|
|
88
|
+
args = parameters_info(iseq.argc)
|
|
89
|
+
ci = "#{klass_sig}#{callee}"
|
|
90
|
+
[ci, args]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def c_identifier
|
|
94
|
+
return unless frame_type == :c
|
|
95
|
+
"[C] #{klass_sig}#{location.base_label}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def other_identifier
|
|
99
|
+
return unless frame_type == :other
|
|
100
|
+
location.label
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def callee
|
|
104
|
+
@callee ||= binding&.eval('__callee__', __FILE__, __LINE__)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def return_str
|
|
108
|
+
if binding && iseq && has_return_value
|
|
109
|
+
DEBUGGER__.short_inspect(return_value)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def location_str
|
|
114
|
+
"#{pretty_path}:#{location.lineno}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def get_singleton_class obj
|
|
120
|
+
obj.singleton_class # TODO: don't use it
|
|
121
|
+
rescue TypeError
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parameters_info(argc)
|
|
126
|
+
vars = iseq.locals[0...argc]
|
|
127
|
+
vars.map{|var|
|
|
128
|
+
begin
|
|
129
|
+
{ name: var, value: DEBUGGER__.short_inspect(binding.local_variable_get(var)) }
|
|
130
|
+
rescue NameError, TypeError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
}.compact
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def klass_sig
|
|
137
|
+
if self.class == get_singleton_class(self.self)
|
|
138
|
+
"#{self.self}."
|
|
139
|
+
else
|
|
140
|
+
"#{self.class}#"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/debug/open.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Open the door for the debugger to connect.
|
|
3
|
+
# Users can connect to debuggee program with "rdbg --attach" option.
|
|
4
|
+
#
|
|
5
|
+
# If RUBY_DEBUG_PORT envval is provided (digits), open TCP/IP port.
|
|
6
|
+
# Otherwise, UNIX domain socket is used.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
require_relative 'server'
|
|
10
|
+
return unless defined?(DEBUGGER__)
|
|
11
|
+
|
|
12
|
+
DEBUGGER__.open
|
data/lib/debug/run.rb
ADDED
data/lib/debug/server.rb
CHANGED
|
@@ -1,40 +1,51 @@
|
|
|
1
1
|
require 'socket'
|
|
2
|
+
|
|
2
3
|
require_relative 'session'
|
|
4
|
+
return unless defined?(DEBUGGER__)
|
|
5
|
+
|
|
6
|
+
require_relative 'config'
|
|
7
|
+
require_relative 'version'
|
|
3
8
|
|
|
4
9
|
module DEBUGGER__
|
|
5
|
-
class
|
|
10
|
+
class UI_ServerBase < UI_Base
|
|
6
11
|
def initialize
|
|
7
12
|
@sock = nil
|
|
13
|
+
@accept_m = Mutex.new
|
|
14
|
+
@accept_cv = ConditionVariable.new
|
|
8
15
|
@client_addr = nil
|
|
9
16
|
@q_msg = Queue.new
|
|
10
17
|
@q_ans = Queue.new
|
|
18
|
+
@unsent_messages = []
|
|
19
|
+
@width = 80
|
|
11
20
|
|
|
12
21
|
@reader_thread = Thread.new do
|
|
22
|
+
# An error on this thread should break the system.
|
|
23
|
+
Thread.current.abort_on_exception = true
|
|
24
|
+
|
|
13
25
|
accept do |server|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@
|
|
26
|
+
DEBUGGER__.message "Connected."
|
|
27
|
+
|
|
28
|
+
@accept_m.synchronize{
|
|
29
|
+
@sock = server
|
|
30
|
+
greeting
|
|
31
|
+
|
|
32
|
+
@accept_cv.signal
|
|
33
|
+
|
|
34
|
+
# flush unsent messages
|
|
35
|
+
@unsent_messages.each{|m|
|
|
36
|
+
@sock.puts m
|
|
37
|
+
}
|
|
38
|
+
@unsent_messages.clear
|
|
39
|
+
}
|
|
17
40
|
|
|
18
41
|
setup_interrupt do
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
begin
|
|
22
|
-
while line = @sock.gets
|
|
23
|
-
case line
|
|
24
|
-
when /\Apause/
|
|
25
|
-
pause
|
|
26
|
-
when /\Acommand ?(.+)/
|
|
27
|
-
@q_msg << $1
|
|
28
|
-
when /\Aanswer (.*)/
|
|
29
|
-
@q_ans << $1
|
|
30
|
-
else
|
|
31
|
-
STDERR.puts "unsupported: #{line}"
|
|
32
|
-
exit!
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
42
|
+
process
|
|
36
43
|
end
|
|
44
|
+
|
|
45
|
+
rescue => e
|
|
46
|
+
DEBUGGER__.message "ReaderThreadError: #{e}"
|
|
37
47
|
ensure
|
|
48
|
+
DEBUGGER__.message "Disconnected."
|
|
38
49
|
@sock = nil
|
|
39
50
|
@q_msg.close
|
|
40
51
|
@q_ans.close
|
|
@@ -42,11 +53,68 @@ module DEBUGGER__
|
|
|
42
53
|
end
|
|
43
54
|
end
|
|
44
55
|
|
|
56
|
+
def greeting
|
|
57
|
+
case g = @sock.gets
|
|
58
|
+
when /^version:\s+(.+)\s+width: (\d+) cookie:\s+(.*)$/
|
|
59
|
+
v, w, c = $1, $2, $3
|
|
60
|
+
# TODO: protocol version
|
|
61
|
+
if v != VERSION
|
|
62
|
+
raise "Incompatible version (#{VERSION} client:#{$1})"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
cookie = CONFIG[:cookie]
|
|
66
|
+
if cookie && cookie != c
|
|
67
|
+
raise "Cookie mismatch (#{$2.inspect} was sent)"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@width = w.to_i
|
|
71
|
+
|
|
72
|
+
when /^Content-Length: (\d+)/
|
|
73
|
+
require_relative 'server_dap'
|
|
74
|
+
|
|
75
|
+
raise unless @sock.read(2) == "\r\n"
|
|
76
|
+
self.extend(UI_DAP)
|
|
77
|
+
dap_setup @sock.read($1.to_i)
|
|
78
|
+
else
|
|
79
|
+
raise "Greeting message error: #{g}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def process
|
|
84
|
+
@q_msg = Queue.new
|
|
85
|
+
@q_ans = Queue.new
|
|
86
|
+
|
|
87
|
+
pause
|
|
88
|
+
|
|
89
|
+
while line = @sock.gets
|
|
90
|
+
case line
|
|
91
|
+
when /\Apause/
|
|
92
|
+
pause
|
|
93
|
+
when /\Acommand ?(.+)/
|
|
94
|
+
@q_msg << $1
|
|
95
|
+
when /\Aanswer (.*)/
|
|
96
|
+
@q_ans << $1
|
|
97
|
+
when /\Awidth (.+)/
|
|
98
|
+
@width = $1.to_i
|
|
99
|
+
else
|
|
100
|
+
STDERR.puts "unsupported: #{line}"
|
|
101
|
+
exit!
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def remote?
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def width
|
|
111
|
+
@width
|
|
112
|
+
end
|
|
45
113
|
|
|
46
114
|
def setup_interrupt
|
|
47
115
|
prev_handler = trap(:SIGINT) do
|
|
48
116
|
# $stderr.puts "trapped SIGINT"
|
|
49
|
-
ThreadClient.current.on_trap
|
|
117
|
+
ThreadClient.current.on_trap :SIGINT
|
|
50
118
|
|
|
51
119
|
case prev_handler
|
|
52
120
|
when Proc
|
|
@@ -69,8 +137,23 @@ module DEBUGGER__
|
|
|
69
137
|
|
|
70
138
|
class NoRemoteError < Exception; end
|
|
71
139
|
|
|
72
|
-
def sock
|
|
73
|
-
|
|
140
|
+
def sock skip: false
|
|
141
|
+
if s = @sock # already connection
|
|
142
|
+
# ok
|
|
143
|
+
elsif skip == true # skip process
|
|
144
|
+
return yield nil
|
|
145
|
+
else # wait for connection
|
|
146
|
+
until s = @sock
|
|
147
|
+
@accept_m.synchronize{
|
|
148
|
+
unless @sock
|
|
149
|
+
DEBUGGER__.message "wait for debuger connection..."
|
|
150
|
+
@accept_cv.wait(@accept_m)
|
|
151
|
+
end
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
yield s
|
|
74
157
|
rescue Errno::EPIPE
|
|
75
158
|
# ignore
|
|
76
159
|
end
|
|
@@ -82,17 +165,24 @@ module DEBUGGER__
|
|
|
82
165
|
end
|
|
83
166
|
end
|
|
84
167
|
|
|
85
|
-
def puts str
|
|
168
|
+
def puts str = nil
|
|
86
169
|
case str
|
|
87
170
|
when Array
|
|
88
171
|
enum = str.each
|
|
89
172
|
when String
|
|
90
173
|
enum = str.each_line
|
|
174
|
+
when nil
|
|
175
|
+
enum = [''].each
|
|
91
176
|
end
|
|
92
177
|
|
|
93
|
-
sock do |s|
|
|
178
|
+
sock skip: true do |s|
|
|
94
179
|
enum.each do |line|
|
|
95
|
-
|
|
180
|
+
msg = "out #{line.chomp}"
|
|
181
|
+
if s
|
|
182
|
+
s.puts msg
|
|
183
|
+
else
|
|
184
|
+
@unsent_messages << msg
|
|
185
|
+
end
|
|
96
186
|
end
|
|
97
187
|
end
|
|
98
188
|
end
|
|
@@ -116,4 +206,65 @@ module DEBUGGER__
|
|
|
116
206
|
end
|
|
117
207
|
end
|
|
118
208
|
end
|
|
209
|
+
|
|
210
|
+
class UI_TcpServer < UI_ServerBase
|
|
211
|
+
def initialize host: nil, port: nil
|
|
212
|
+
@host = host || ::DEBUGGER__::CONFIG[:host] || '127.0.0.1'
|
|
213
|
+
@port = port || begin
|
|
214
|
+
port_str = ::DEBUGGER__::CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
|
|
215
|
+
if /\A\d+\z/ !~ port_str
|
|
216
|
+
raise "Specify digits for port number"
|
|
217
|
+
else
|
|
218
|
+
port_str.to_i
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
super()
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def accept
|
|
226
|
+
Socket.tcp_server_sockets @host, @port do |socks|
|
|
227
|
+
::DEBUGGER__.message "Debugger can attach via TCP/IP (#{socks.map{|e| e.local_address.inspect}})"
|
|
228
|
+
Socket.accept_loop(socks) do |sock, client|
|
|
229
|
+
@client_addr = client
|
|
230
|
+
yield sock
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
rescue => e
|
|
234
|
+
$stderr.puts e.message
|
|
235
|
+
pp e.backtrace
|
|
236
|
+
exit
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
class UI_UnixDomainServer < UI_ServerBase
|
|
241
|
+
def initialize sock_dir: nil, sock_path: nil
|
|
242
|
+
@sock_path = sock_path
|
|
243
|
+
@sock_dir = sock_dir || DEBUGGER__.unix_domain_socket_dir
|
|
244
|
+
|
|
245
|
+
super()
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def accept
|
|
249
|
+
case
|
|
250
|
+
when @sock_path
|
|
251
|
+
when sp = ::DEBUGGER__::CONFIG[:sock_path]
|
|
252
|
+
@sock_path = sp
|
|
253
|
+
else
|
|
254
|
+
@sock_path = DEBUGGER__.create_unix_domain_socket_name(@sock_dir)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
::DEBUGGER__.message "Debugger can attach via UNIX domain socket (#{@sock_path})"
|
|
258
|
+
Socket.unix_server_loop @sock_path do |sock, client|
|
|
259
|
+
@client_addr = client
|
|
260
|
+
yield sock
|
|
261
|
+
ensure
|
|
262
|
+
sock.close
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def self.message msg
|
|
268
|
+
$stderr.puts "DEBUGGER: #{msg}"
|
|
269
|
+
end
|
|
119
270
|
end
|
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module DEBUGGER__
|
|
4
|
+
module UI_DAP
|
|
5
|
+
SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
|
|
6
|
+
|
|
7
|
+
def dap_setup bytes
|
|
8
|
+
DEBUGGER__.set_config(use_colorize: false)
|
|
9
|
+
@seq = 0
|
|
10
|
+
|
|
11
|
+
$stderr.puts '[>]' + bytes if SHOW_PROTOCOL
|
|
12
|
+
req = JSON.load(bytes)
|
|
13
|
+
|
|
14
|
+
# capability
|
|
15
|
+
send_response(req,
|
|
16
|
+
## Supported
|
|
17
|
+
supportsConfigurationDoneRequest: true,
|
|
18
|
+
supportsFunctionBreakpoints: true,
|
|
19
|
+
supportsConditionalBreakpoints: true,
|
|
20
|
+
supportTerminateDebuggee: true,
|
|
21
|
+
supportsTerminateRequest: true,
|
|
22
|
+
exceptionBreakpointFilters: [
|
|
23
|
+
{
|
|
24
|
+
filter: 'any',
|
|
25
|
+
label: 'rescue any exception',
|
|
26
|
+
#supportsCondition: true,
|
|
27
|
+
#conditionDescription: '',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
filter: 'RuntimeError',
|
|
31
|
+
label: 'rescue RuntimeError',
|
|
32
|
+
default: true,
|
|
33
|
+
#supportsCondition: true,
|
|
34
|
+
#conditionDescription: '',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
supportsExceptionFilterOptions: true,
|
|
38
|
+
|
|
39
|
+
## Will be supported
|
|
40
|
+
# supportsExceptionOptions: true,
|
|
41
|
+
# supportsHitConditionalBreakpoints:
|
|
42
|
+
# supportsEvaluateForHovers:
|
|
43
|
+
# supportsSetVariable: true,
|
|
44
|
+
# supportSuspendDebuggee:
|
|
45
|
+
# supportsLogPoints:
|
|
46
|
+
# supportsLoadedSourcesRequest:
|
|
47
|
+
# supportsDataBreakpoints:
|
|
48
|
+
# supportsBreakpointLocationsRequest:
|
|
49
|
+
|
|
50
|
+
## Possible?
|
|
51
|
+
# supportsStepBack:
|
|
52
|
+
# supportsRestartFrame:
|
|
53
|
+
# supportsCompletionsRequest:
|
|
54
|
+
# completionTriggerCharacters:
|
|
55
|
+
# supportsModulesRequest:
|
|
56
|
+
# additionalModuleColumns:
|
|
57
|
+
# supportedChecksumAlgorithms:
|
|
58
|
+
# supportsRestartRequest:
|
|
59
|
+
# supportsValueFormattingOptions:
|
|
60
|
+
# supportsExceptionInfoRequest:
|
|
61
|
+
# supportsDelayedStackTraceLoading:
|
|
62
|
+
# supportsTerminateThreadsRequest:
|
|
63
|
+
# supportsSetExpression:
|
|
64
|
+
# supportsClipboardContext:
|
|
65
|
+
|
|
66
|
+
## Never
|
|
67
|
+
# supportsGotoTargetsRequest:
|
|
68
|
+
# supportsStepInTargetsRequest:
|
|
69
|
+
# supportsReadMemoryRequest:
|
|
70
|
+
# supportsDisassembleRequest:
|
|
71
|
+
# supportsCancelRequest:
|
|
72
|
+
# supportsSteppingGranularity:
|
|
73
|
+
# supportsInstructionBreakpoints:
|
|
74
|
+
)
|
|
75
|
+
send_event 'initialized'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def send **kw
|
|
79
|
+
kw[:seq] = @seq += 1
|
|
80
|
+
str = JSON.dump(kw)
|
|
81
|
+
$stderr.puts "[<] #{str}" if SHOW_PROTOCOL
|
|
82
|
+
# STDERR.puts "[STDERR] [<] #{str}"
|
|
83
|
+
@sock.print header = "Content-Length: #{str.size}\r\n\r\n"
|
|
84
|
+
@sock.write str
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def send_response req, success: true, **kw
|
|
88
|
+
if kw.empty?
|
|
89
|
+
send type: 'response',
|
|
90
|
+
command: req['command'],
|
|
91
|
+
request_seq: req['seq'],
|
|
92
|
+
success: success,
|
|
93
|
+
message: success ? 'Success' : 'Failed'
|
|
94
|
+
else
|
|
95
|
+
send type: 'response',
|
|
96
|
+
command: req['command'],
|
|
97
|
+
request_seq: req['seq'],
|
|
98
|
+
success: success,
|
|
99
|
+
message: success ? 'Success' : 'Failed',
|
|
100
|
+
body: kw
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def send_event name, **kw
|
|
105
|
+
if kw.empty?
|
|
106
|
+
send type: 'event', event: name
|
|
107
|
+
else
|
|
108
|
+
send type: 'event', event: name, body: kw
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def recv_request
|
|
113
|
+
case header = @sock.gets
|
|
114
|
+
when /Content-Length: (\d+)/
|
|
115
|
+
b = @sock.read(2)
|
|
116
|
+
raise b.inspect unless b == "\r\n"
|
|
117
|
+
|
|
118
|
+
l = @sock.read(s = $1.to_i)
|
|
119
|
+
$stderr.puts "[>] #{l}" if SHOW_PROTOCOL
|
|
120
|
+
JSON.load(l)
|
|
121
|
+
when nil
|
|
122
|
+
nil
|
|
123
|
+
else
|
|
124
|
+
raise "unrecognized line: #{l} (#{l.size} bytes)"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def process
|
|
129
|
+
while req = recv_request
|
|
130
|
+
raise "not a request: #{req.inpsect}" unless req['type'] == 'request'
|
|
131
|
+
args = req.dig('arguments')
|
|
132
|
+
|
|
133
|
+
case req['command']
|
|
134
|
+
|
|
135
|
+
## boot/configuration
|
|
136
|
+
when 'launch'
|
|
137
|
+
send_response req
|
|
138
|
+
when 'setBreakpoints'
|
|
139
|
+
path = args.dig('source', 'path')
|
|
140
|
+
bp_args = args['breakpoints']
|
|
141
|
+
bps = []
|
|
142
|
+
bp_args.each{|bp|
|
|
143
|
+
line = bp['line']
|
|
144
|
+
if cond = bp['condition']
|
|
145
|
+
bps << SESSION.add_line_breakpoint(path, line, cond: cond)
|
|
146
|
+
else
|
|
147
|
+
bps << SESSION.add_line_breakpoint(path, line)
|
|
148
|
+
end
|
|
149
|
+
}
|
|
150
|
+
send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
|
|
151
|
+
when 'setFunctionBreakpoints'
|
|
152
|
+
send_response req
|
|
153
|
+
when 'setExceptionBreakpoints'
|
|
154
|
+
filters = args.dig('filterOptions').map{|bp_info|
|
|
155
|
+
case bp_info.dig('filterId')
|
|
156
|
+
when 'any'
|
|
157
|
+
bp = SESSION.add_catch_breakpoint 'Exception'
|
|
158
|
+
when 'RuntimeError'
|
|
159
|
+
bp = SESSION.add_catch_breakpoint 'RuntimeError'
|
|
160
|
+
else
|
|
161
|
+
bp = nil
|
|
162
|
+
end
|
|
163
|
+
{
|
|
164
|
+
verifiled: bp ? true : false,
|
|
165
|
+
message: bp.inspect,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
send_response req, breakpoints: filters
|
|
169
|
+
when 'configurationDone'
|
|
170
|
+
send_response req
|
|
171
|
+
@q_msg << 'continue'
|
|
172
|
+
when 'attach'
|
|
173
|
+
send_response req
|
|
174
|
+
Process.kill(:SIGINT, Process.pid)
|
|
175
|
+
when 'disconnect'
|
|
176
|
+
send_response req
|
|
177
|
+
@q_msg << 'continue'
|
|
178
|
+
|
|
179
|
+
## control
|
|
180
|
+
when 'continue'
|
|
181
|
+
@q_msg << 'c'
|
|
182
|
+
send_response req, allThreadsContinued: true
|
|
183
|
+
when 'next'
|
|
184
|
+
@q_msg << 'n'
|
|
185
|
+
send_response req
|
|
186
|
+
when 'stepIn'
|
|
187
|
+
@q_msg << 's'
|
|
188
|
+
send_response req
|
|
189
|
+
when 'stepOut'
|
|
190
|
+
@q_msg << 'fin'
|
|
191
|
+
send_response req
|
|
192
|
+
when 'terminate'
|
|
193
|
+
send_response req
|
|
194
|
+
exit
|
|
195
|
+
when 'pause'
|
|
196
|
+
send_response req
|
|
197
|
+
Process.kill(:SIGINT, Process.pid)
|
|
198
|
+
|
|
199
|
+
## query
|
|
200
|
+
when 'threads'
|
|
201
|
+
send_response req, threads: SESSION.managed_thread_clients.map{|tc|
|
|
202
|
+
{ id: tc.id,
|
|
203
|
+
name: tc.name,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
when 'stackTrace',
|
|
208
|
+
'scopes',
|
|
209
|
+
'variables',
|
|
210
|
+
'evaluate',
|
|
211
|
+
'source'
|
|
212
|
+
@q_msg << req
|
|
213
|
+
else
|
|
214
|
+
raise "Unknown request: #{req.inspect}"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
## called by the SESSION thread
|
|
220
|
+
|
|
221
|
+
def readline
|
|
222
|
+
@q_msg.pop || 'kill!'
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def sock skip: false
|
|
226
|
+
yield $stderr
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def respond req, res
|
|
230
|
+
send_response(req, **res)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def puts result
|
|
234
|
+
# STDERR.puts "puts: #{result}"
|
|
235
|
+
# send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def event type, *args
|
|
239
|
+
case type
|
|
240
|
+
when :suspend_bp
|
|
241
|
+
_i, bp = *args
|
|
242
|
+
if bp.kind_of?(CatchBreakpoint)
|
|
243
|
+
reason = 'exception'
|
|
244
|
+
text = bp.description
|
|
245
|
+
else
|
|
246
|
+
reason = 'breakpoint'
|
|
247
|
+
text = bp ? bp.description : 'temporary bp'
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
send_event 'stopped', reason: reason,
|
|
251
|
+
description: text,
|
|
252
|
+
text: text,
|
|
253
|
+
threadId: 1,
|
|
254
|
+
allThreadsStopped: true
|
|
255
|
+
when :suspend_trap
|
|
256
|
+
send_event 'stopped', reason: 'pause',
|
|
257
|
+
threadId: 1,
|
|
258
|
+
allThreadsStopped: true
|
|
259
|
+
when :suspended
|
|
260
|
+
send_event 'stopped', reason: 'step',
|
|
261
|
+
threadId: 1,
|
|
262
|
+
allThreadsStopped: true
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
class Session
|
|
268
|
+
def find_tc id
|
|
269
|
+
@th_clients.each{|th, tc|
|
|
270
|
+
return tc if tc.id == id
|
|
271
|
+
}
|
|
272
|
+
return nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def fail_response req, **kw
|
|
276
|
+
@ui.respond req, success: false, **kw
|
|
277
|
+
return :retry
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def process_dap_request req
|
|
281
|
+
case req['command']
|
|
282
|
+
when 'stackTrace'
|
|
283
|
+
tid = req.dig('arguments', 'threadId')
|
|
284
|
+
if tc = find_tc(tid)
|
|
285
|
+
tc << [:dap, :backtrace, req]
|
|
286
|
+
else
|
|
287
|
+
fail_response req
|
|
288
|
+
end
|
|
289
|
+
when 'scopes'
|
|
290
|
+
frame_id = req.dig('arguments', 'frameId')
|
|
291
|
+
if @frame_map[frame_id]
|
|
292
|
+
tid, fid = @frame_map[frame_id]
|
|
293
|
+
if tc = find_tc(tid)
|
|
294
|
+
tc << [:dap, :scopes, req, fid]
|
|
295
|
+
else
|
|
296
|
+
fail_response req
|
|
297
|
+
end
|
|
298
|
+
else
|
|
299
|
+
fail_response req
|
|
300
|
+
end
|
|
301
|
+
when 'variables'
|
|
302
|
+
varid = req.dig('arguments', 'variablesReference')
|
|
303
|
+
if ref = @var_map[varid]
|
|
304
|
+
case ref[0]
|
|
305
|
+
when :globals
|
|
306
|
+
vars = global_variables.map do |name|
|
|
307
|
+
File.write('/tmp/x', "#{name}\n")
|
|
308
|
+
gv = 'Not implemented yet...'
|
|
309
|
+
{
|
|
310
|
+
name: name,
|
|
311
|
+
value: gv.inspect,
|
|
312
|
+
type: (gv.class.name || gv.class.to_s),
|
|
313
|
+
variablesReference: 0,
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
@ui.respond req, {
|
|
318
|
+
variables: vars,
|
|
319
|
+
}
|
|
320
|
+
return :retry
|
|
321
|
+
|
|
322
|
+
when :scope
|
|
323
|
+
frame_id = ref[1]
|
|
324
|
+
tid, fid = @frame_map[frame_id]
|
|
325
|
+
|
|
326
|
+
if tc = find_tc(tid)
|
|
327
|
+
tc << [:dap, :scope, req, fid]
|
|
328
|
+
else
|
|
329
|
+
fail_response req
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
when :variable
|
|
333
|
+
tid, vid = ref[1], ref[2]
|
|
334
|
+
|
|
335
|
+
if tc = find_tc(tid)
|
|
336
|
+
tc << [:dap, :variable, req, vid]
|
|
337
|
+
else
|
|
338
|
+
fail_response req
|
|
339
|
+
end
|
|
340
|
+
else
|
|
341
|
+
raise "Uknown type: #{ref.inspect}"
|
|
342
|
+
end
|
|
343
|
+
else
|
|
344
|
+
fail_response req
|
|
345
|
+
end
|
|
346
|
+
when 'evaluate'
|
|
347
|
+
frame_id = req.dig('arguments', 'frameId')
|
|
348
|
+
if @frame_map[frame_id]
|
|
349
|
+
tid, fid = @frame_map[frame_id]
|
|
350
|
+
expr = req.dig('arguments', 'expression')
|
|
351
|
+
if tc = find_tc(tid)
|
|
352
|
+
tc << [:dap, :evaluate, req, fid, expr]
|
|
353
|
+
else
|
|
354
|
+
fail_response req
|
|
355
|
+
end
|
|
356
|
+
else
|
|
357
|
+
fail_response req, result: "can't evaluate"
|
|
358
|
+
end
|
|
359
|
+
when 'source'
|
|
360
|
+
ref = req.dig('arguments', 'sourceReference')
|
|
361
|
+
if src = @src_map[ref]
|
|
362
|
+
@ui.respond req, content: src.join
|
|
363
|
+
else
|
|
364
|
+
fail_response req, message: 'not found...'
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
return :retry
|
|
368
|
+
else
|
|
369
|
+
raise "Unknown DAP request: #{req.inspect}"
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def dap_event args
|
|
374
|
+
# puts({dap_event: args}.inspect)
|
|
375
|
+
type, req, result = args
|
|
376
|
+
|
|
377
|
+
case type
|
|
378
|
+
when :backtrace
|
|
379
|
+
result[:stackFrames].each.with_index{|fi, i|
|
|
380
|
+
fi[:id] = id = @frame_map.size + 1
|
|
381
|
+
@frame_map[id] = [req.dig('arguments', 'threadId'), i]
|
|
382
|
+
if fi[:source] && src = fi[:source][:sourceReference]
|
|
383
|
+
src_id = @src_map.size + 1
|
|
384
|
+
@src_map[src_id] = src
|
|
385
|
+
fi[:source][:sourceReference] = src_id
|
|
386
|
+
end
|
|
387
|
+
}
|
|
388
|
+
@ui.respond req, result
|
|
389
|
+
when :scopes
|
|
390
|
+
frame_id = req.dig('arguments', 'frameId')
|
|
391
|
+
local_scope = result[:scopes].first
|
|
392
|
+
local_scope[:variablesReference] = id = @var_map.size + 1
|
|
393
|
+
|
|
394
|
+
@var_map[id] = [:scope, frame_id]
|
|
395
|
+
@ui.respond req, result
|
|
396
|
+
when :scope
|
|
397
|
+
tid = result.delete :tid
|
|
398
|
+
register_vars result[:variables], tid
|
|
399
|
+
@ui.respond req, result
|
|
400
|
+
when :variable
|
|
401
|
+
tid = result.delete :tid
|
|
402
|
+
register_vars result[:variables], tid
|
|
403
|
+
@ui.respond req, result
|
|
404
|
+
when :evaluate
|
|
405
|
+
tid = result.delete :tid
|
|
406
|
+
register_var result, tid
|
|
407
|
+
@ui.respond req, result
|
|
408
|
+
else
|
|
409
|
+
raise "unsupported: #{args.inspect}"
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def register_var v, tid
|
|
414
|
+
if (tl_vid = v[:variablesReference]) > 0
|
|
415
|
+
vid = @var_map.size + 1
|
|
416
|
+
@var_map[vid] = [:variable, tid, tl_vid]
|
|
417
|
+
v[:variablesReference] = vid
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def register_vars vars, tid
|
|
422
|
+
raise tid.inspect unless tid.kind_of?(Integer)
|
|
423
|
+
vars.each{|v|
|
|
424
|
+
register_var v, tid
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
class ThreadClient
|
|
430
|
+
def process_dap args
|
|
431
|
+
# pp tc: self, args: args
|
|
432
|
+
type = args.shift
|
|
433
|
+
req = args.shift
|
|
434
|
+
|
|
435
|
+
case type
|
|
436
|
+
when :backtrace
|
|
437
|
+
event! :dap_result, :backtrace, req, {
|
|
438
|
+
stackFrames: @target_frames.map.with_index{|frame, i|
|
|
439
|
+
path = frame.path
|
|
440
|
+
ref = frame.file_lines unless File.exist?(path)
|
|
441
|
+
|
|
442
|
+
{
|
|
443
|
+
# id: ??? # filled by SESSION
|
|
444
|
+
name: frame.name,
|
|
445
|
+
line: frame.location.lineno,
|
|
446
|
+
column: 1,
|
|
447
|
+
source: {
|
|
448
|
+
name: File.basename(frame.path),
|
|
449
|
+
path: path,
|
|
450
|
+
sourceReference: ref,
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
when :scopes
|
|
456
|
+
fid = args.shift
|
|
457
|
+
frame = @target_frames[fid]
|
|
458
|
+
lnum = frame.binding ? frame.binding.local_variables.size : 0
|
|
459
|
+
|
|
460
|
+
event! :dap_result, :scopes, req, scopes: [{
|
|
461
|
+
name: 'Local variables',
|
|
462
|
+
presentationHint: 'locals',
|
|
463
|
+
# variablesReference: N, # filled by SESSION
|
|
464
|
+
namedVariables: lnum,
|
|
465
|
+
indexedVariables: 0,
|
|
466
|
+
expensive: false,
|
|
467
|
+
}, {
|
|
468
|
+
name: 'Global variables',
|
|
469
|
+
presentationHint: 'globals',
|
|
470
|
+
variablesReference: 1, # GLOBAL
|
|
471
|
+
namedVariables: global_variables.size,
|
|
472
|
+
indexedVariables: 0,
|
|
473
|
+
expensive: false,
|
|
474
|
+
}]
|
|
475
|
+
when :scope
|
|
476
|
+
fid = args.shift
|
|
477
|
+
frame = @target_frames[fid]
|
|
478
|
+
if b = frame.binding
|
|
479
|
+
vars = b.local_variables.map{|name|
|
|
480
|
+
v = b.local_variable_get(name)
|
|
481
|
+
variable(name, v)
|
|
482
|
+
}
|
|
483
|
+
vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
|
|
484
|
+
vars.unshift variable('%return', frame.return_value) if frame.has_return_value
|
|
485
|
+
vars.unshift variable('%self', b.receiver)
|
|
486
|
+
else
|
|
487
|
+
vars = [variable('%self', frame.self)]
|
|
488
|
+
vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
|
|
489
|
+
vars.push variable('%return', frame.return_value) if frame.has_return_value
|
|
490
|
+
end
|
|
491
|
+
event! :dap_result, :scope, req, variables: vars, tid: self.id
|
|
492
|
+
|
|
493
|
+
when :variable
|
|
494
|
+
vid = args.shift
|
|
495
|
+
obj = @var_map[vid]
|
|
496
|
+
if obj
|
|
497
|
+
case req.dig('arguments', 'filter')
|
|
498
|
+
when 'indexed'
|
|
499
|
+
start = req.dig('arguments', 'start') || 0
|
|
500
|
+
count = req.dig('arguments', 'count') || obj.size
|
|
501
|
+
vars = (start ... (start + count)).map{|i|
|
|
502
|
+
variable(i.to_s, obj[i])
|
|
503
|
+
}
|
|
504
|
+
else
|
|
505
|
+
vars = []
|
|
506
|
+
|
|
507
|
+
case obj
|
|
508
|
+
when Hash
|
|
509
|
+
vars = obj.map{|k, v|
|
|
510
|
+
variable(DEBUGGER__.short_inspect(k), v)
|
|
511
|
+
}
|
|
512
|
+
when Struct
|
|
513
|
+
vars = obj.members.map{|m|
|
|
514
|
+
variable(m, obj[m])
|
|
515
|
+
}
|
|
516
|
+
when String
|
|
517
|
+
vars = [
|
|
518
|
+
variable('#length', obj.length),
|
|
519
|
+
variable('#encoding', obj.encoding)
|
|
520
|
+
]
|
|
521
|
+
when Class, Module
|
|
522
|
+
vars = obj.instance_variables.map{|iv|
|
|
523
|
+
variable(iv, obj.instance_variable_get(iv))
|
|
524
|
+
}
|
|
525
|
+
vars.unshift variable('%ancestors', obj.ancestors[1..])
|
|
526
|
+
when Range
|
|
527
|
+
vars = [
|
|
528
|
+
variable('#begin', obj.begin),
|
|
529
|
+
variable('#end', obj.end),
|
|
530
|
+
]
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
vars += obj.instance_variables.map{|iv|
|
|
534
|
+
variable(iv, obj.instance_variable_get(iv))
|
|
535
|
+
}
|
|
536
|
+
vars.unshift variable('#class', obj.class)
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
|
|
540
|
+
|
|
541
|
+
when :evaluate
|
|
542
|
+
fid, expr = args
|
|
543
|
+
frame = @target_frames[fid]
|
|
544
|
+
|
|
545
|
+
if frame && (b = frame.binding)
|
|
546
|
+
begin
|
|
547
|
+
result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
|
|
548
|
+
rescue Exception => e
|
|
549
|
+
result = e
|
|
550
|
+
end
|
|
551
|
+
else
|
|
552
|
+
result = 'can not evaluate on this frame...'
|
|
553
|
+
end
|
|
554
|
+
event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
|
|
555
|
+
else
|
|
556
|
+
raise "Unkown req: #{args.inspect}"
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def evaluate_result r
|
|
561
|
+
v = variable nil, r
|
|
562
|
+
v.delete(:name)
|
|
563
|
+
v[:result] = DEBUGGER__.short_inspect(r)
|
|
564
|
+
v
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def variable_ name, obj, indexedVariables: 0, namedVariables: 0, use_short: true
|
|
568
|
+
if indexedVariables > 0 || namedVariables > 0
|
|
569
|
+
vid = @var_map.size + 1
|
|
570
|
+
@var_map[vid] = obj
|
|
571
|
+
else
|
|
572
|
+
vid = 0
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
ivnum = obj.instance_variables.size
|
|
576
|
+
|
|
577
|
+
{ name: name,
|
|
578
|
+
value: DEBUGGER__.short_inspect(obj, use_short),
|
|
579
|
+
type: obj.class.name || obj.class.to_s,
|
|
580
|
+
variablesReference: vid,
|
|
581
|
+
indexedVariables: indexedVariables,
|
|
582
|
+
namedVariables: namedVariables + ivnum,
|
|
583
|
+
}
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def variable name, obj
|
|
587
|
+
case obj
|
|
588
|
+
when Array
|
|
589
|
+
variable_ name, obj, indexedVariables: obj.size
|
|
590
|
+
when Hash
|
|
591
|
+
variable_ name, obj, namedVariables: obj.size
|
|
592
|
+
when String
|
|
593
|
+
variable_ name, obj, use_short: false, namedVariables: 3 # #to_str, #length, #encoding
|
|
594
|
+
when Struct
|
|
595
|
+
variable_ name, obj, namedVariables: obj.size
|
|
596
|
+
when Class, Module
|
|
597
|
+
variable_ name, obj, namedVariables: 1 # %ancestors (#ancestors without self)
|
|
598
|
+
when Range
|
|
599
|
+
variable_ name, obj, namedVariables: 2 # #begin, #end
|
|
600
|
+
else
|
|
601
|
+
variable_ name, obj, namedVariables: 1 # #class
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|