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.
@@ -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
@@ -0,0 +1,4 @@
1
+ require_relative 'console'
2
+ return unless defined?(DEBUGGER__)
3
+
4
+ DEBUGGER__.console
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 UI_Server
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
- @sock = server
15
- @q_msg = Queue.new
16
- @q_ans = Queue.new
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
- pause
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
- yield @sock if @sock
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
- s.puts "out #{line.chomp}"
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