debug 1.2.4 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/debug/server.rb CHANGED
@@ -15,6 +15,8 @@ module DEBUGGER__
15
15
  @q_ans = nil
16
16
  @unsent_messages = []
17
17
  @width = 80
18
+ @repl = true
19
+ @session = nil
18
20
  end
19
21
 
20
22
  class Terminate < StandardError
@@ -22,6 +24,7 @@ module DEBUGGER__
22
24
 
23
25
  def deactivate
24
26
  @reader_thread.raise Terminate
27
+ @reader_thread.join
25
28
  end
26
29
 
27
30
  def accept
@@ -36,9 +39,11 @@ module DEBUGGER__
36
39
  end
37
40
 
38
41
  def activate session, on_fork: false
42
+ @session = session
39
43
  @reader_thread = Thread.new do
40
44
  # An error on this thread should break the system.
41
45
  Thread.current.abort_on_exception = true
46
+ Thread.current.name = 'DEBUGGER__::Server::reader'
42
47
 
43
48
  accept do |server, already_connected: false|
44
49
  DEBUGGER__.warn "Connected."
@@ -52,7 +57,7 @@ module DEBUGGER__
52
57
  # flush unsent messages
53
58
  @unsent_messages.each{|m|
54
59
  @sock.puts m
55
- }
60
+ } if @repl
56
61
  @unsent_messages.clear
57
62
 
58
63
  @q_msg = Queue.new
@@ -68,6 +73,7 @@ module DEBUGGER__
68
73
  raise # should catch at outer scope
69
74
  rescue => e
70
75
  DEBUGGER__.warn "ReaderThreadError: #{e}"
76
+ pp e.backtrace
71
77
  ensure
72
78
  DEBUGGER__.warn "Disconnected."
73
79
  @sock = nil
@@ -103,23 +109,58 @@ module DEBUGGER__
103
109
 
104
110
  raise unless @sock.read(2) == "\r\n"
105
111
  self.extend(UI_DAP)
112
+ @repl = false
106
113
  dap_setup @sock.read($1.to_i)
114
+ when /^GET \/ HTTP\/1.1/
115
+ require_relative 'server_cdp'
116
+
117
+ self.extend(UI_CDP)
118
+ @repl = false
119
+ @web_sock = UI_CDP::WebSocket.new(@sock)
120
+ @web_sock.handshake
107
121
  else
108
122
  raise "Greeting message error: #{g}"
109
123
  end
110
124
  end
111
125
 
112
126
  def process
113
- while line = @sock.gets
127
+ while true
128
+ DEBUGGER__.info "sleep IO.select"
129
+ r = IO.select([@sock])
130
+ DEBUGGER__.info "wakeup IO.select"
131
+
132
+ line = @session.process_group.sync do
133
+ unless IO.select([@sock], nil, nil, 0)
134
+ DEBUGGER__.info "UI_Server can not read"
135
+ break :can_not_read
136
+ end
137
+ @sock.gets&.chomp.tap{|line|
138
+ DEBUGGER__.info "UI_Server received: #{line}"
139
+ }
140
+ end
141
+
142
+ next if line == :can_not_read
143
+
114
144
  case line
115
145
  when /\Apause/
116
146
  pause
117
- when /\Acommand ?(.+)/
118
- @q_msg << $1
119
- when /\Aanswer (.*)/
120
- @q_ans << $1
121
- when /\Awidth (.+)/
122
- @width = $1.to_i
147
+ when /\Acommand (\d+) (\d+) ?(.+)/
148
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
149
+
150
+ if $1.to_i == Process.pid
151
+ @width = $2.to_i
152
+ @q_msg << $3
153
+ else
154
+ raise "pid:#{Process.pid} but get #{line}"
155
+ end
156
+ when /\Aanswer (\d+) (.*)/
157
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
158
+
159
+ if $1.to_i == Process.pid
160
+ @q_ans << $2
161
+ else
162
+ raise "pid:#{Process.pid} but get #{line}"
163
+ end
123
164
  else
124
165
  STDERR.puts "unsupported: #{line}"
125
166
  exit!
@@ -135,6 +176,21 @@ module DEBUGGER__
135
176
  @width
136
177
  end
137
178
 
179
+ def sigurg_overridden? prev_handler
180
+ case prev_handler
181
+ when "SYSTEM_DEFAULT"
182
+ false
183
+ when Proc
184
+ if prev_handler.source_location[0] == __FILE__
185
+ false
186
+ else
187
+ true
188
+ end
189
+ else
190
+ true
191
+ end
192
+ end
193
+
138
194
  def setup_interrupt
139
195
  prev_handler = trap(:SIGURG) do
140
196
  # $stderr.puts "trapped SIGINT"
@@ -148,8 +204,8 @@ module DEBUGGER__
148
204
  end
149
205
  end
150
206
 
151
- if prev_handler != "SYSTEM_DEFAULT"
152
- DEBUGGER__.warn "SIGURG handler is overriddend by the debugger."
207
+ if sigurg_overridden?(prev_handler)
208
+ DEBUGGER__.warn "SIGURG handler is overridden by the debugger."
153
209
  end
154
210
  yield
155
211
  ensure
@@ -191,7 +247,7 @@ module DEBUGGER__
191
247
 
192
248
  def ask prompt
193
249
  sock do |s|
194
- s.puts "ask #{prompt}"
250
+ s.puts "ask #{Process.pid} #{prompt}"
195
251
  @q_ans.pop
196
252
  end
197
253
  end
@@ -220,13 +276,23 @@ module DEBUGGER__
220
276
 
221
277
  def readline prompt
222
278
  input = (sock do |s|
223
- s.puts "input"
279
+ if @repl
280
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
281
+ line = "input #{Process.pid}"
282
+ DEBUGGER__.info "send: #{line}"
283
+ s.puts line
284
+ end
224
285
  sleep 0.01 until @q_msg
286
+ @q_msg.pop.tap{|msg|
287
+ DEBUGGER__.info "readline: #{msg.inspect}"
288
+ }
289
+ end || 'continue')
225
290
 
226
- @q_msg.pop
227
- end || 'continue').strip
228
-
229
- input
291
+ if input.is_a?(String)
292
+ input.strip
293
+ else
294
+ input
295
+ end
230
296
  end
231
297
 
232
298
  def pause
@@ -244,6 +310,7 @@ module DEBUGGER__
244
310
 
245
311
  class UI_TcpServer < UI_ServerBase
246
312
  def initialize host: nil, port: nil
313
+ @addr = nil
247
314
  @host = host || CONFIG[:host] || '127.0.0.1'
248
315
  @port = port || begin
249
316
  port_str = CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
@@ -263,7 +330,24 @@ module DEBUGGER__
263
330
 
264
331
  begin
265
332
  Socket.tcp_server_sockets @host, @port do |socks|
266
- ::DEBUGGER__.warn "Debugger can attach via TCP/IP (#{socks.map{|e| e.local_address.inspect}})"
333
+ addr = socks[0].local_address.inspect_sockaddr # Change this part if `socks` are multiple.
334
+ rdbg = File.expand_path('../../exe/rdbg', __dir__)
335
+
336
+ DEBUGGER__.warn "Debugger can attach via TCP/IP (#{addr})"
337
+ DEBUGGER__.info <<~EOS
338
+ With rdbg, use the following command line:
339
+ #
340
+ # #{rdbg} --attach #{addr.split(':').join(' ')}
341
+ #
342
+ EOS
343
+
344
+ DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome'
345
+ With Chrome browser, type the following URL in the address-bar:
346
+
347
+ devtools://devtools/bundled/inspector.html?ws=#{addr}
348
+
349
+ EOS
350
+
267
351
  Socket.accept_loop(socks) do |sock, client|
268
352
  @client_addr = client
269
353
  yield @sock_for_fork = sock
@@ -298,6 +382,64 @@ module DEBUGGER__
298
382
  super()
299
383
  end
300
384
 
385
+ def vscode_setup
386
+ require 'tmpdir'
387
+ require 'json'
388
+ require 'fileutils'
389
+
390
+ dir = Dir.mktmpdir("ruby-debug-vscode-")
391
+ at_exit{
392
+ FileUtils.rm_rf dir
393
+ }
394
+ Dir.chdir(dir) do
395
+ Dir.mkdir('.vscode')
396
+ open('README.rb', 'w'){|f|
397
+ f.puts <<~MSG
398
+ # Wait for starting the attaching to the Ruby process
399
+ # This file will be removed at the end of the debuggee process.
400
+ #
401
+ # Note that vscode-rdbg extension is needed. Please install if you don't have.
402
+ MSG
403
+ }
404
+ open('.vscode/launch.json', 'w'){|f|
405
+ f.puts JSON.pretty_generate({
406
+ version: '0.2.0',
407
+ configurations: [
408
+ {
409
+ type: "rdbg",
410
+ name: "Attach with rdbg",
411
+ request: "attach",
412
+ rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
413
+ debugPort: @sock_path,
414
+ autoAttach: true,
415
+ }
416
+ ]
417
+ })
418
+ }
419
+ end
420
+
421
+ cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
422
+ cmdline = cmds.join(' ')
423
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"
424
+
425
+ STDERR.puts "Launching: #{cmdline}"
426
+ env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
427
+
428
+ unless system(env, *cmds)
429
+ DEBUGGER__.warn <<~MESSAGE
430
+ Can not invoke the command.
431
+ Use the command-line on your terminal (with modification if you need).
432
+
433
+ #{cmdline}
434
+
435
+ If your application is running on a SSH remote host, please try:
436
+
437
+ #{ssh_cmdline}
438
+
439
+ MESSAGE
440
+ end
441
+ end
442
+
301
443
  def accept
302
444
  super # for fork
303
445
 
@@ -310,6 +452,8 @@ module DEBUGGER__
310
452
  end
311
453
 
312
454
  ::DEBUGGER__.warn "Debugger can attach via UNIX domain socket (#{@sock_path})"
455
+ vscode_setup if CONFIG[:open_frontend] == 'vscode'
456
+
313
457
  Socket.unix_server_loop @sock_path do |sock, client|
314
458
  @sock_for_fork = sock
315
459
  @client_addr = client
@@ -0,0 +1,412 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'digest/sha1'
5
+ require 'base64'
6
+ require 'securerandom'
7
+
8
+ module DEBUGGER__
9
+ module UI_CDP
10
+ SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
11
+
12
+ class WebSocket
13
+ def initialize s
14
+ @sock = s
15
+ end
16
+
17
+ def handshake
18
+ CONFIG.set_config no_color: true
19
+
20
+ req = @sock.readpartial 4096
21
+ $stderr.puts '[>]' + req if SHOW_PROTOCOL
22
+
23
+ if req.match /^Sec-WebSocket-Key: (.*)\r\n/
24
+ accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
25
+ @sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
26
+ else
27
+ "Unknown request: #{req}"
28
+ end
29
+ end
30
+
31
+ def send **msg
32
+ msg = JSON.generate(msg)
33
+ frame = []
34
+ fin = 0b10000000
35
+ opcode = 0b00000001
36
+ frame << fin + opcode
37
+
38
+ mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
39
+ bytesize = msg.bytesize
40
+ if bytesize < 126
41
+ payload_len = bytesize
42
+ elsif bytesize < 2 ** 16
43
+ payload_len = 0b01111110
44
+ ex_payload_len = [bytesize].pack('n*').bytes
45
+ else
46
+ payload_len = 0b01111111
47
+ ex_payload_len = [bytesize].pack('Q>').bytes
48
+ end
49
+
50
+ frame << mask + payload_len
51
+ frame.push *ex_payload_len if ex_payload_len
52
+ frame.push *msg.bytes
53
+ @sock.print frame.pack 'c*'
54
+ end
55
+
56
+ def extract_data
57
+ first_group = @sock.getbyte
58
+ fin = first_group & 0b10000000 != 128
59
+ raise 'Unsupported' if fin
60
+ opcode = first_group & 0b00001111
61
+ raise "Unsupported: #{opcode}" unless opcode == 1
62
+
63
+ second_group = @sock.getbyte
64
+ mask = second_group & 0b10000000 == 128
65
+ raise 'The client must mask all frames' unless mask
66
+ payload_len = second_group & 0b01111111
67
+ # TODO: Support other payload_lengths
68
+ if payload_len == 126
69
+ payload_len = @sock.gets(2).unpack('n*')[0]
70
+ end
71
+
72
+ masking_key = []
73
+ 4.times { masking_key << @sock.getbyte }
74
+ unmasked = []
75
+ payload_len.times do |n|
76
+ masked = @sock.getbyte
77
+ unmasked << (masked ^ masking_key[n % 4])
78
+ end
79
+ JSON.parse unmasked.pack 'c*'
80
+ end
81
+ end
82
+
83
+ def send_response req, **res
84
+ if res.empty?
85
+ @web_sock.send id: req['id'], result: {}
86
+ else
87
+ @web_sock.send id: req['id'], result: res
88
+ end
89
+ end
90
+
91
+ def send_event method, **params
92
+ if params.empty?
93
+ @web_sock.send method: method, params: {}
94
+ else
95
+ @web_sock.send method: method, params: params
96
+ end
97
+ end
98
+
99
+ def process
100
+ loop do
101
+ req = @web_sock.extract_data
102
+ $stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
103
+ bps = []
104
+
105
+ case req['method']
106
+
107
+ ## boot/configuration
108
+ when 'Page.getResourceTree'
109
+ abs = File.absolute_path($0)
110
+ src = File.read(abs)
111
+ send_response req,
112
+ frameTree: {
113
+ frame: {
114
+ id: SecureRandom.hex(16),
115
+ loaderId: SecureRandom.hex(16),
116
+ url: 'http://debuggee/',
117
+ securityOrigin: 'http://debuggee',
118
+ mimeType: 'text/plain' },
119
+ resources: [
120
+ ]
121
+ }
122
+ send_event 'Debugger.scriptParsed',
123
+ scriptId: abs,
124
+ url: "http://debuggee#{abs}",
125
+ startLine: 0,
126
+ startColumn: 0,
127
+ endLine: src.count('\n'),
128
+ endColumn: 0,
129
+ executionContextId: 1,
130
+ hash: src.hash
131
+ send_event 'Runtime.executionContextCreated',
132
+ context: {
133
+ id: SecureRandom.hex(16),
134
+ origin: "http://#{@addr}",
135
+ name: ''
136
+ }
137
+ when 'Debugger.getScriptSource'
138
+ s_id = req.dig('params', 'scriptId')
139
+ src = File.read(s_id)
140
+ send_response req, scriptSource: src
141
+ @q_msg << req
142
+ when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
143
+ 'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
144
+ 'Debugger.setBreakpointsActive', 'Runtime.releaseObjectGroup'
145
+ send_response req
146
+
147
+ ## control
148
+ when 'Debugger.resume'
149
+ @q_msg << 'c'
150
+ @q_msg << req
151
+ send_response req
152
+ send_event 'Debugger.resumed'
153
+ when 'Debugger.stepOver'
154
+ @q_msg << 'n'
155
+ @q_msg << req
156
+ send_response req
157
+ send_event 'Debugger.resumed'
158
+ when 'Debugger.stepInto'
159
+ @q_msg << 's'
160
+ @q_msg << req
161
+ send_response req
162
+ send_event 'Debugger.resumed'
163
+ when 'Debugger.stepOut'
164
+ @q_msg << 'fin'
165
+ @q_msg << req
166
+ send_response req
167
+ send_event 'Debugger.resumed'
168
+
169
+ # breakpoint
170
+ when 'Debugger.getPossibleBreakpoints'
171
+ s_id = req.dig('params', 'start', 'scriptId')
172
+ line = req.dig('params', 'lineNumber')
173
+ send_response req,
174
+ locations: [
175
+ { scriptId: s_id,
176
+ lineNumber: line,
177
+ }
178
+ ]
179
+ when 'Debugger.setBreakpointByUrl'
180
+ line = req.dig('params', 'lineNumber')
181
+ path = req.dig('params', 'url').match('http://debuggee(.*)')[1]
182
+ cond = req.dig('params', 'condition')
183
+ if cond != ''
184
+ bps << SESSION.add_line_breakpoint(path, line + 1, cond: cond)
185
+ else
186
+ bps << SESSION.add_line_breakpoint(path, line + 1)
187
+ end
188
+ send_response req,
189
+ breakpointId: (bps.size - 1).to_s,
190
+ locations: [
191
+ scriptId: path,
192
+ lineNumber: line
193
+ ]
194
+ when 'Debugger.removeBreakpoint'
195
+ b_id = req.dig('params', 'breakpointId')
196
+ @q_msg << "del #{b_id}"
197
+ send_response req
198
+
199
+ when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
200
+ @q_msg << req
201
+ end
202
+ end
203
+ end
204
+
205
+ ## Called by the SESSION thread
206
+
207
+ def readline prompt
208
+ @q_msg.pop || 'kill!'
209
+ end
210
+
211
+ def respond req, **result
212
+ send_response req, **result
213
+ end
214
+
215
+ def fire_event event, **result
216
+ if result.empty?
217
+ send_event event
218
+ else
219
+ send_event event, **result
220
+ end
221
+ end
222
+
223
+ def sock skip: false
224
+ yield $stderr
225
+ end
226
+
227
+ def puts result
228
+ # STDERR.puts "puts: #{result}"
229
+ # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
230
+ end
231
+ end
232
+
233
+ class Session
234
+ def process_protocol_request req
235
+ case req['method']
236
+ when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
237
+ @tc << [:cdp, :backtrace, req]
238
+ when 'Debugger.evaluateOnCallFrame'
239
+ expr = req.dig('params', 'expression')
240
+ @tc << [:cdp, :evaluate, req, expr]
241
+ when 'Runtime.getProperties'
242
+ oid = req.dig('params', 'objectId')
243
+ case oid
244
+ when /(\d?):local/
245
+ @tc << [:cdp, :properties, req, $1.to_i]
246
+ when /\d?:script/
247
+ # TODO: Support a script type
248
+ @ui.respond req
249
+ return :retry
250
+ when /\d?:global/
251
+ # TODO: Support a global type
252
+ @ui.respond req
253
+ return :retry
254
+ end
255
+ end
256
+ end
257
+
258
+ def cdp_event args
259
+ type, req, result = args
260
+
261
+ case type
262
+ when :backtrace
263
+ result[:callFrames].each do |frame|
264
+ s_id = frame.dig(:location, :scriptId)
265
+ if File.exist?(s_id) && !@script_paths.include?(s_id)
266
+ src = File.read(s_id)
267
+ @ui.fire_event 'Debugger.scriptParsed',
268
+ scriptId: s_id,
269
+ url: frame[:url],
270
+ startLine: 0,
271
+ startColumn: 0,
272
+ endLine: src.count('\n'),
273
+ endColumn: 0,
274
+ executionContextId: @script_paths.size + 1,
275
+ hash: src.hash
276
+ @script_paths << s_id
277
+ end
278
+ end
279
+ result[:reason] = 'other'
280
+ @ui.fire_event 'Debugger.paused', **result
281
+ when :evaluate
282
+ @ui.respond req, result: result
283
+ when :properties
284
+ @ui.respond req, result: result
285
+ end
286
+ end
287
+ end
288
+
289
+ class ThreadClient
290
+ def process_cdp args
291
+ type = args.shift
292
+ req = args.shift
293
+
294
+ case type
295
+ when :backtrace
296
+ event! :cdp_result, :backtrace, req, {
297
+ callFrames: @target_frames.map.with_index{|frame, i|
298
+ path = frame.realpath || frame.path
299
+ if path.match /<internal:(.*)>/
300
+ abs = $1
301
+ else
302
+ abs = path
303
+ end
304
+
305
+ local_scope = {
306
+ callFrameId: SecureRandom.hex(16),
307
+ functionName: frame.name,
308
+ location: {
309
+ scriptId: abs,
310
+ lineNumber: frame.location.lineno - 1 # The line number is 0-based.
311
+ },
312
+ url: "http://debuggee#{abs}",
313
+ scopeChain: [
314
+ {
315
+ type: 'local',
316
+ object: {
317
+ type: 'object',
318
+ objectId: "#{i}:local"
319
+ }
320
+ },
321
+ {
322
+ type: 'script',
323
+ object: {
324
+ type: 'object',
325
+ objectId: "#{i}:script"
326
+ }
327
+ },
328
+ {
329
+ type: 'global',
330
+ object: {
331
+ type: 'object',
332
+ objectId: "#{i}:global"
333
+ }
334
+ }
335
+ ],
336
+ this: {
337
+ type: 'object'
338
+ }
339
+ }
340
+ }
341
+ }
342
+ when :evaluate
343
+ expr = args.shift
344
+ begin
345
+ result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
346
+ rescue Exception => e
347
+ result = e
348
+ end
349
+ event! :cdp_result, :evaluate, req, evaluate_result(result)
350
+ when :properties
351
+ fid = args.shift
352
+ frame = @target_frames[fid]
353
+ if b = frame.binding
354
+ vars = b.local_variables.map{|name|
355
+ v = b.local_variable_get(name)
356
+ variable(name, v)
357
+ }
358
+ vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
359
+ vars.unshift variable('%return', frame.return_value) if frame.has_return_value
360
+ vars.unshift variable('%self', b.receiver)
361
+ elsif lvars = frame.local_variables
362
+ vars = lvars.map{|var, val|
363
+ variable(var, val)
364
+ }
365
+ else
366
+ vars = [variable('%self', frame.self)]
367
+ vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
368
+ vars.push variable('%return', frame.return_value) if frame.has_return_value
369
+ end
370
+ event! :cdp_result, :properties, req, vars
371
+ end
372
+ end
373
+
374
+ def evaluate_result r
375
+ v = variable nil, r
376
+ v[:value]
377
+ end
378
+
379
+ def variable_ name, obj, type, use_short: true
380
+ {
381
+ name: name,
382
+ value: {
383
+ type: type,
384
+ value: DEBUGGER__.short_inspect(obj, use_short)
385
+ },
386
+ configurable: true,
387
+ enumerable: true
388
+ }
389
+ end
390
+
391
+ def variable name, obj
392
+ case obj
393
+ when Array, Hash, Range, NilClass, Time
394
+ variable_ name, obj, 'object'
395
+ when String
396
+ variable_ name, obj, 'string', use_short: false
397
+ when Class, Module, Struct
398
+ variable_ name, obj, 'function'
399
+ when TrueClass, FalseClass
400
+ variable_ name, obj, 'boolean'
401
+ when Symbol
402
+ variable_ name, obj, 'symbol'
403
+ when Float
404
+ variable_ name, obj, 'number'
405
+ when Integer
406
+ variable_ name, obj, 'number'
407
+ else
408
+ variable_ name, obj, 'undefined'
409
+ end
410
+ end
411
+ end
412
+ end