debug 1.0.0.beta4 → 1.0.0.beta8

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