debug 1.3.4 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,23 +4,242 @@ require 'json'
4
4
  require 'digest/sha1'
5
5
  require 'base64'
6
6
  require 'securerandom'
7
+ require 'stringio'
8
+ require 'open3'
9
+ require 'tmpdir'
7
10
 
8
11
  module DEBUGGER__
9
12
  module UI_CDP
10
13
  SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
11
14
 
12
- class WebSocket
15
+ class << self
16
+ def setup_chrome addr
17
+ return if CONFIG[:chrome_path] == ''
18
+
19
+ port, path, pid = run_new_chrome
20
+ begin
21
+ s = Socket.tcp '127.0.0.1', port
22
+ rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
23
+ return
24
+ end
25
+
26
+ ws_client = WebSocketClient.new(s)
27
+ ws_client.handshake port, path
28
+ ws_client.send id: 1, method: 'Target.getTargets'
29
+
30
+ loop do
31
+ res = ws_client.extract_data
32
+ case
33
+ when res['id'] == 1 && target_info = res.dig('result', 'targetInfos')
34
+ page = target_info.find{|t| t['type'] == 'page'}
35
+ ws_client.send id: 2, method: 'Target.attachToTarget',
36
+ params: {
37
+ targetId: page['targetId'],
38
+ flatten: true
39
+ }
40
+ when res['id'] == 2
41
+ s_id = res.dig('result', 'sessionId')
42
+ ws_client.send sessionId: s_id, id: 3,
43
+ method: 'Page.enable'
44
+ when res['id'] == 3
45
+ s_id = res['sessionId']
46
+ ws_client.send sessionId: s_id, id: 4,
47
+ method: 'Page.getFrameTree'
48
+ when res['id'] == 4
49
+ s_id = res['sessionId']
50
+ f_id = res.dig('result', 'frameTree', 'frame', 'id')
51
+ ws_client.send sessionId: s_id, id: 5,
52
+ method: 'Page.navigate',
53
+ params: {
54
+ url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=#{addr}/#{SecureRandom.uuid}",
55
+ frameId: f_id
56
+ }
57
+ when res['method'] == 'Page.loadEventFired'
58
+ break
59
+ end
60
+ end
61
+ pid
62
+ rescue Errno::ENOENT
63
+ nil
64
+ end
65
+
66
+ def get_chrome_path
67
+ return CONFIG[:chrome_path] if CONFIG[:chrome_path]
68
+
69
+ # The process to check OS is based on `selenium` project.
70
+ case RbConfig::CONFIG['host_os']
71
+ when /mswin|msys|mingw|cygwin|emc/
72
+ 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
73
+ when /darwin|mac os/
74
+ '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
75
+ when /linux/
76
+ 'google-chrome'
77
+ else
78
+ raise "Unsupported OS"
79
+ end
80
+ end
81
+
82
+ def run_new_chrome
83
+ dir = Dir.mktmpdir
84
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting
85
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{get_chrome_path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
86
+ stdin.close
87
+ stdout.close
88
+
89
+ data = stderr.readpartial 4096
90
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
91
+ port = $1
92
+ path = $2
93
+ end
94
+ stderr.close
95
+
96
+ at_exit{
97
+ CONFIG[:skip_path] = [//] # skip all
98
+ FileUtils.rm_rf dir
99
+ }
100
+
101
+ [port, path, wait_thr.pid]
102
+ end
103
+ end
104
+
105
+ module WebSocketUtils
106
+ class Frame
107
+ attr_reader :b
108
+
109
+ def initialize
110
+ @b = ''.b
111
+ end
112
+
113
+ def << obj
114
+ case obj
115
+ when String
116
+ @b << obj.b
117
+ when Enumerable
118
+ obj.each{|e| self << e}
119
+ end
120
+ end
121
+
122
+ def char bytes
123
+ @b << bytes
124
+ end
125
+
126
+ def ulonglong bytes
127
+ @b << [bytes].pack('Q>')
128
+ end
129
+
130
+ def uint16 bytes
131
+ @b << [bytes].pack('n*')
132
+ end
133
+ end
134
+
135
+ def show_protocol dir, msg
136
+ if DEBUGGER__::UI_CDP::SHOW_PROTOCOL
137
+ $stderr.puts "\#[#{dir}] #{msg}"
138
+ end
139
+ end
140
+ end
141
+
142
+ class WebSocketClient
143
+ include WebSocketUtils
144
+
145
+ def initialize s
146
+ @sock = s
147
+ end
148
+
149
+ def handshake port, path
150
+ key = SecureRandom.hex(11)
151
+ req = "GET #{path} HTTP/1.1\r\nHost: 127.0.0.1:#{port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: #{key}==\r\n\r\n"
152
+ show_protocol :>, req
153
+ @sock.print req
154
+ res = @sock.readpartial 4092
155
+ show_protocol :<, res
156
+
157
+ if res.match /^Sec-WebSocket-Accept: (.*)\r\n/
158
+ correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
159
+ raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
160
+ else
161
+ raise "Unknown response: #{res}"
162
+ end
163
+ end
164
+
165
+ def send **msg
166
+ msg = JSON.generate(msg)
167
+ show_protocol :>, msg
168
+ frame = Frame.new
169
+ fin = 0b10000000
170
+ opcode = 0b00000001
171
+ frame.char fin + opcode
172
+
173
+ mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
174
+ bytesize = msg.bytesize
175
+ if bytesize < 126
176
+ payload_len = bytesize
177
+ frame.char mask + payload_len
178
+ elsif bytesize < 2 ** 16
179
+ payload_len = 0b01111110
180
+ frame.char mask + payload_len
181
+ frame.uint16 bytesize
182
+ elsif bytesize < 2 ** 64
183
+ payload_len = 0b01111111
184
+ frame.char mask + payload_len
185
+ frame.ulonglong bytesize
186
+ else
187
+ raise 'Bytesize is too big.'
188
+ end
189
+
190
+ masking_key = 4.times.map{
191
+ key = rand(1..255)
192
+ frame.char key
193
+ key
194
+ }
195
+ msg.bytes.each_with_index do |b, i|
196
+ frame.char(b ^ masking_key[i % 4])
197
+ end
198
+
199
+ @sock.print frame.b
200
+ end
201
+
202
+ def extract_data
203
+ first_group = @sock.getbyte
204
+ fin = first_group & 0b10000000 != 128
205
+ raise 'Unsupported' if fin
206
+ opcode = first_group & 0b00001111
207
+ raise "Unsupported: #{opcode}" unless opcode == 1
208
+
209
+ second_group = @sock.getbyte
210
+ mask = second_group & 0b10000000 == 128
211
+ raise 'The server must not mask any frames' if mask
212
+ payload_len = second_group & 0b01111111
213
+ # TODO: Support other payload_lengths
214
+ if payload_len == 126
215
+ payload_len = @sock.read(2).unpack('n*')[0]
216
+ end
217
+
218
+ msg = @sock.read payload_len
219
+ show_protocol :<, msg
220
+ JSON.parse msg
221
+ end
222
+ end
223
+
224
+ class Detach < StandardError
225
+ end
226
+
227
+ class WebSocketServer
228
+ include WebSocketUtils
229
+
13
230
  def initialize s
14
231
  @sock = s
15
232
  end
16
233
 
17
- def handshake
234
+ def handshake
18
235
  req = @sock.readpartial 4096
19
- $stderr.puts '[>]' + req if SHOW_PROTOCOL
20
-
236
+ show_protocol '>', req
237
+
21
238
  if req.match /^Sec-WebSocket-Key: (.*)\r\n/
22
239
  accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
23
- @sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
240
+ res = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
241
+ @sock.print res
242
+ show_protocol :<, res
24
243
  else
25
244
  "Unknown request: #{req}"
26
245
  end
@@ -28,34 +247,40 @@ module DEBUGGER__
28
247
 
29
248
  def send **msg
30
249
  msg = JSON.generate(msg)
31
- frame = []
250
+ show_protocol :<, msg
251
+ frame = Frame.new
32
252
  fin = 0b10000000
33
253
  opcode = 0b00000001
34
- frame << fin + opcode
35
-
254
+ frame.char fin + opcode
255
+
36
256
  mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
37
257
  bytesize = msg.bytesize
38
258
  if bytesize < 126
39
259
  payload_len = bytesize
260
+ frame.char mask + payload_len
40
261
  elsif bytesize < 2 ** 16
41
262
  payload_len = 0b01111110
42
- ex_payload_len = [bytesize].pack('n*').bytes
43
- else
263
+ frame.char mask + payload_len
264
+ frame.uint16 bytesize
265
+ elsif bytesize < 2 ** 64
44
266
  payload_len = 0b01111111
45
- ex_payload_len = [bytesize].pack('Q>').bytes
267
+ frame.char mask + payload_len
268
+ frame.ulonglong bytesize
269
+ else
270
+ raise 'Bytesize is too big.'
46
271
  end
47
-
48
- frame << mask + payload_len
49
- frame.push *ex_payload_len if ex_payload_len
50
- frame.push *msg.bytes
51
- @sock.print frame.pack 'c*'
272
+
273
+ frame << msg
274
+ @sock.print frame.b
52
275
  end
53
276
 
54
277
  def extract_data
55
278
  first_group = @sock.getbyte
56
279
  fin = first_group & 0b10000000 != 128
57
280
  raise 'Unsupported' if fin
281
+
58
282
  opcode = first_group & 0b00001111
283
+ raise Detach if opcode == 8
59
284
  raise "Unsupported: #{opcode}" unless opcode == 1
60
285
 
61
286
  second_group = @sock.getbyte
@@ -74,138 +299,201 @@ module DEBUGGER__
74
299
  masked = @sock.getbyte
75
300
  unmasked << (masked ^ masking_key[n % 4])
76
301
  end
77
- JSON.parse unmasked.pack 'c*'
302
+ msg = unmasked.pack 'c*'
303
+ show_protocol :>, msg
304
+ JSON.parse msg
78
305
  end
79
306
  end
80
307
 
81
308
  def send_response req, **res
82
309
  if res.empty?
83
- @web_sock.send id: req['id'], result: {}
310
+ @ws_server.send id: req['id'], result: {}
84
311
  else
85
- @web_sock.send id: req['id'], result: res
312
+ @ws_server.send id: req['id'], result: res
86
313
  end
87
314
  end
88
315
 
316
+ def send_fail_response req, **res
317
+ @ws_server.send id: req['id'], error: res
318
+ end
319
+
89
320
  def send_event method, **params
90
321
  if params.empty?
91
- @web_sock.send method: method, params: {}
322
+ @ws_server.send method: method, params: {}
92
323
  else
93
- @web_sock.send method: method, params: params
324
+ @ws_server.send method: method, params: params
94
325
  end
95
326
  end
96
327
 
328
+ INVALID_REQUEST = -32600
329
+
97
330
  def process
98
- bps = []
331
+ bps = {}
99
332
  @src_map = {}
100
333
  loop do
101
- req = @web_sock.extract_data
102
- $stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
334
+ req = @ws_server.extract_data
103
335
 
104
336
  case req['method']
105
337
 
106
338
  ## boot/configuration
107
- when 'Page.getResourceTree'
108
- abs = File.absolute_path($0)
109
- src = File.read(abs)
110
- @src_map[abs] = src
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
339
+ when 'Debugger.getScriptSource'
340
+ @q_msg << req
341
+ when 'Debugger.enable'
342
+ send_response req
343
+ @q_msg << req
344
+ when 'Runtime.enable'
345
+ send_response req
131
346
  send_event 'Runtime.executionContextCreated',
132
347
  context: {
133
348
  id: SecureRandom.hex(16),
134
- origin: "http://#{@addr}",
349
+ origin: "http://#{@local_addr.inspect_sockaddr}",
135
350
  name: ''
136
351
  }
137
- when 'Debugger.getScriptSource'
138
- s_id = req.dig('params', 'scriptId')
139
- src = get_source_code s_id
140
- send_response req, scriptSource: src
141
- @q_msg << req
352
+ when 'Runtime.getIsolateId'
353
+ send_response req,
354
+ id: SecureRandom.hex
355
+ when 'Runtime.terminateExecution'
356
+ send_response req
357
+ exit
142
358
  when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
143
359
  'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
144
- 'Debugger.setBreakpointsActive', 'Runtime.releaseObjectGroup'
360
+ 'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear', 'Runtime.runIfWaitingForDebugger'
145
361
  send_response req
146
362
 
147
363
  ## control
148
364
  when 'Debugger.resume'
149
- @q_msg << 'c'
150
- @q_msg << req
151
365
  send_response req
152
366
  send_event 'Debugger.resumed'
153
- when 'Debugger.stepOver'
154
- @q_msg << 'n'
367
+ @q_msg << 'c'
155
368
  @q_msg << req
156
- send_response req
157
- send_event 'Debugger.resumed'
369
+ when 'Debugger.stepOver'
370
+ begin
371
+ @session.check_postmortem
372
+ send_response req
373
+ send_event 'Debugger.resumed'
374
+ @q_msg << 'n'
375
+ rescue PostmortemError
376
+ send_fail_response req,
377
+ code: INVALID_REQUEST,
378
+ message: "'stepOver' is not supported while postmortem mode"
379
+ ensure
380
+ @q_msg << req
381
+ end
158
382
  when 'Debugger.stepInto'
159
- @q_msg << 's'
160
- @q_msg << req
161
- send_response req
162
- send_event 'Debugger.resumed'
383
+ begin
384
+ @session.check_postmortem
385
+ send_response req
386
+ send_event 'Debugger.resumed'
387
+ @q_msg << 's'
388
+ rescue PostmortemError
389
+ send_fail_response req,
390
+ code: INVALID_REQUEST,
391
+ message: "'stepInto' is not supported while postmortem mode"
392
+ ensure
393
+ @q_msg << req
394
+ end
163
395
  when 'Debugger.stepOut'
164
- @q_msg << 'fin'
165
- @q_msg << req
396
+ begin
397
+ @session.check_postmortem
398
+ send_response req
399
+ send_event 'Debugger.resumed'
400
+ @q_msg << 'fin'
401
+ rescue PostmortemError
402
+ send_fail_response req,
403
+ code: INVALID_REQUEST,
404
+ message: "'stepOut' is not supported while postmortem mode"
405
+ ensure
406
+ @q_msg << req
407
+ end
408
+ when 'Debugger.setSkipAllPauses'
409
+ skip = req.dig('params', 'skip')
410
+ if skip
411
+ deactivate_bp
412
+ else
413
+ activate_bp bps
414
+ end
166
415
  send_response req
167
- send_event 'Debugger.resumed'
168
416
 
169
417
  # breakpoint
170
418
  when 'Debugger.getPossibleBreakpoints'
171
- s_id = req.dig('params', 'start', 'scriptId')
172
- line = req.dig('params', 'start', 'lineNumber')
173
- src = get_source_code s_id
174
- end_line = src.count("\n")
175
- line = end_line if line > end_line
176
- send_response req,
177
- locations: [
178
- { scriptId: s_id,
179
- lineNumber: line,
180
- }
181
- ]
419
+ @q_msg << req
182
420
  when 'Debugger.setBreakpointByUrl'
183
421
  line = req.dig('params', 'lineNumber')
184
- path = req.dig('params', 'url').match('http://debuggee(.*)')[1]
185
- cond = req.dig('params', 'condition')
186
- src = get_source_code path
187
- end_line = src.count("\n")
188
- line = end_line if line > end_line
189
- if cond != ''
190
- bps << SESSION.add_line_breakpoint(path, line + 1, cond: cond)
422
+ if regexp = req.dig('params', 'urlRegex')
423
+ path = regexp.match(/(.*)\|/)[1].gsub("\\", "")
424
+ cond = req.dig('params', 'condition')
425
+ src = get_source_code path
426
+ end_line = src.lines.count
427
+ line = end_line if line > end_line
428
+ b_id = "1:#{line}:#{regexp}"
429
+ if cond != ''
430
+ SESSION.add_line_breakpoint(path, line + 1, cond: cond)
431
+ else
432
+ SESSION.add_line_breakpoint(path, line + 1)
433
+ end
434
+ bps[b_id] = bps.size
435
+ # Because we need to return scriptId, responses are returned in SESSION thread.
436
+ req['params']['scriptId'] = path
437
+ req['params']['lineNumber'] = line
438
+ req['params']['breakpointId'] = b_id
439
+ @q_msg << req
440
+ elsif url = req.dig('params', 'url')
441
+ b_id = "#{line}:#{url}"
442
+ send_response req,
443
+ breakpointId: b_id,
444
+ locations: []
445
+ elsif hash = req.dig('params', 'scriptHash')
446
+ b_id = "#{line}:#{hash}"
447
+ send_response req,
448
+ breakpointId: b_id,
449
+ locations: []
191
450
  else
192
- bps << SESSION.add_line_breakpoint(path, line + 1)
451
+ raise 'Unsupported'
193
452
  end
194
- send_response req,
195
- breakpointId: (bps.size - 1).to_s,
196
- locations: [
197
- scriptId: path,
198
- lineNumber: line
199
- ]
200
453
  when 'Debugger.removeBreakpoint'
201
454
  b_id = req.dig('params', 'breakpointId')
202
- @q_msg << "del #{b_id}"
455
+ bps = del_bp bps, b_id
456
+ send_response req
457
+ when 'Debugger.setBreakpointsActive'
458
+ active = req.dig('params', 'active')
459
+ if active
460
+ activate_bp bps
461
+ else
462
+ deactivate_bp # TODO: Change this part because catch breakpoints should not be deactivated.
463
+ end
464
+ send_response req
465
+ when 'Debugger.setPauseOnExceptions'
466
+ state = req.dig('params', 'state')
467
+ ex = 'Exception'
468
+ case state
469
+ when 'none'
470
+ @q_msg << 'config postmortem = false'
471
+ bps = del_bp bps, ex
472
+ when 'uncaught'
473
+ @q_msg << 'config postmortem = true'
474
+ bps = del_bp bps, ex
475
+ when 'all'
476
+ @q_msg << 'config postmortem = false'
477
+ SESSION.add_catch_breakpoint ex
478
+ bps[ex] = bps.size
479
+ end
203
480
  send_response req
204
481
 
205
482
  when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
206
483
  @q_msg << req
207
484
  end
208
485
  end
486
+ rescue Detach
487
+ @q_msg << 'continue'
488
+ end
489
+
490
+ def del_bp bps, k
491
+ return bps unless idx = bps[k]
492
+
493
+ bps.delete k
494
+ bps.each_key{|i| bps[i] -= 1 if bps[i] > idx}
495
+ @q_msg << "del #{idx}"
496
+ bps
209
497
  end
210
498
 
211
499
  def get_source_code path
@@ -216,9 +504,33 @@ module DEBUGGER__
216
504
  src
217
505
  end
218
506
 
507
+ def activate_bp bps
508
+ bps.each_key{|k|
509
+ if k.match /^\d+:(\d+):(.*)/
510
+ line = $1
511
+ path = $2
512
+ SESSION.add_line_breakpoint(path, line.to_i + 1)
513
+ else
514
+ SESSION.add_catch_breakpoint 'Exception'
515
+ end
516
+ }
517
+ end
518
+
519
+ def deactivate_bp
520
+ @q_msg << 'del'
521
+ @q_ans << 'y'
522
+ end
523
+
524
+ def cleanup_reader
525
+ super
526
+ Process.kill :KILL, @chrome_pid if @chrome_pid
527
+ end
528
+
219
529
  ## Called by the SESSION thread
220
530
 
221
531
  def readline prompt
532
+ return 'c' unless @q_msg
533
+
222
534
  @q_msg.pop || 'kill!'
223
535
  end
224
536
 
@@ -226,6 +538,10 @@ module DEBUGGER__
226
538
  send_response req, **result
227
539
  end
228
540
 
541
+ def respond_fail req, **result
542
+ send_fail_response req, **result
543
+ end
544
+
229
545
  def fire_event event, **result
230
546
  if result.empty?
231
547
  send_event event
@@ -245,27 +561,95 @@ module DEBUGGER__
245
561
  end
246
562
 
247
563
  class Session
564
+ def fail_response req, **result
565
+ @ui.respond_fail req, **result
566
+ return :retry
567
+ end
568
+
569
+ INVALID_PARAMS = -32602
570
+ INTERNAL_ERROR = -32603
571
+
248
572
  def process_protocol_request req
249
573
  case req['method']
250
- when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
251
- @tc << [:cdp, :backtrace, req]
574
+ when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.enable'
575
+ request_tc [:cdp, :backtrace, req]
252
576
  when 'Debugger.evaluateOnCallFrame'
253
- expr = req.dig('params', 'expression')
254
- @tc << [:cdp, :evaluate, req, expr]
577
+ frame_id = req.dig('params', 'callFrameId')
578
+ group = req.dig('params', 'objectGroup')
579
+ if fid = @frame_map[frame_id]
580
+ expr = req.dig('params', 'expression')
581
+ request_tc [:cdp, :evaluate, req, fid, expr, group]
582
+ else
583
+ fail_response req,
584
+ code: INVALID_PARAMS,
585
+ message: "'callFrameId' is an invalid"
586
+ end
255
587
  when 'Runtime.getProperties'
256
588
  oid = req.dig('params', 'objectId')
257
- case oid
258
- when /(\d?):local/
259
- @tc << [:cdp, :properties, req, $1.to_i]
260
- when /\d?:script/
261
- # TODO: Support a script type
262
- @ui.respond req
263
- return :retry
264
- when /\d?:global/
265
- # TODO: Support a global type
266
- @ui.respond req
267
- return :retry
589
+ if ref = @obj_map[oid]
590
+ case ref[0]
591
+ when 'local'
592
+ frame_id = ref[1]
593
+ fid = @frame_map[frame_id]
594
+ request_tc [:cdp, :scope, req, fid]
595
+ when 'properties'
596
+ request_tc [:cdp, :properties, req, oid]
597
+ when 'script', 'global'
598
+ # TODO: Support script and global types
599
+ @ui.respond req, result: []
600
+ return :retry
601
+ else
602
+ raise "Unknown type: #{ref.inspect}"
603
+ end
604
+ else
605
+ fail_response req,
606
+ code: INVALID_PARAMS,
607
+ message: "'objectId' is an invalid"
608
+ end
609
+ when 'Debugger.getScriptSource'
610
+ s_id = req.dig('params', 'scriptId')
611
+ if src = @src_map[s_id]
612
+ @ui.respond req, scriptSource: src
613
+ else
614
+ fail_response req,
615
+ code: INVALID_PARAMS,
616
+ message: "'scriptId' is an invalid"
268
617
  end
618
+ return :retry
619
+ when 'Debugger.getPossibleBreakpoints'
620
+ s_id = req.dig('params', 'start', 'scriptId')
621
+ if src = @src_map[s_id]
622
+ lineno = req.dig('params', 'start', 'lineNumber')
623
+ end_line = src.lines.count
624
+ lineno = end_line if lineno > end_line
625
+ @ui.respond req,
626
+ locations: [{
627
+ scriptId: s_id,
628
+ lineNumber: lineno
629
+ }]
630
+ else
631
+ fail_response req,
632
+ code: INVALID_PARAMS,
633
+ message: "'scriptId' is an invalid"
634
+ end
635
+ return :retry
636
+ when 'Debugger.setBreakpointByUrl'
637
+ path = req.dig('params', 'scriptId')
638
+ if s_id = @scr_id_map[path]
639
+ lineno = req.dig('params', 'lineNumber')
640
+ b_id = req.dig('params', 'breakpointId')
641
+ @ui.respond req,
642
+ breakpointId: b_id,
643
+ locations: [{
644
+ scriptId: s_id,
645
+ lineNumber: lineno
646
+ }]
647
+ else
648
+ fail_response req,
649
+ code: INTERNAL_ERROR,
650
+ message: 'The target script is not found...'
651
+ end
652
+ return :retry
269
653
  end
270
654
  end
271
655
 
@@ -274,28 +658,115 @@ module DEBUGGER__
274
658
 
275
659
  case type
276
660
  when :backtrace
277
- result[:callFrames].each do |frame|
278
- s_id = frame.dig(:location, :scriptId)
279
- if File.exist?(s_id) && !@script_paths.include?(s_id)
280
- src = File.read(s_id)
281
- @ui.fire_event 'Debugger.scriptParsed',
661
+ result[:callFrames].each.with_index do |frame, i|
662
+ frame_id = frame[:callFrameId]
663
+ @frame_map[frame_id] = i
664
+ path = frame[:url]
665
+ unless s_id = @scr_id_map[path]
666
+ s_id = (@scr_id_map.size + 1).to_s
667
+ @scr_id_map[path] = s_id
668
+ if path && File.exist?(path)
669
+ src = File.read(path)
670
+ end
671
+ @src_map[s_id] = src
672
+ end
673
+ if src = @src_map[s_id]
674
+ lineno = src.lines.count
675
+ else
676
+ lineno = 0
677
+ end
678
+ frame[:location][:scriptId] = s_id
679
+ frame[:functionLocation][:scriptId] = s_id
680
+ @ui.fire_event 'Debugger.scriptParsed',
681
+ scriptId: s_id,
682
+ url: frame[:url],
683
+ startLine: 0,
684
+ startColumn: 0,
685
+ endLine: lineno,
686
+ endColumn: 0,
687
+ executionContextId: 1,
688
+ hash: src.hash.inspect
689
+
690
+ frame[:scopeChain].each {|s|
691
+ oid = s.dig(:object, :objectId)
692
+ @obj_map[oid] = [s[:type], frame_id]
693
+ }
694
+ end
695
+
696
+ if oid = result.dig(:data, :objectId)
697
+ @obj_map[oid] = ['properties']
698
+ end
699
+ @ui.fire_event 'Debugger.paused', **result
700
+ when :evaluate
701
+ message = result.delete :message
702
+ if message
703
+ fail_response req,
704
+ code: INVALID_PARAMS,
705
+ message: message
706
+ else
707
+ src = req.dig('params', 'expression')
708
+ s_id = (@src_map.size + 1).to_s
709
+ @src_map[s_id] = src
710
+ lineno = src.lines.count
711
+ @ui.fire_event 'Debugger.scriptParsed',
282
712
  scriptId: s_id,
283
- url: frame[:url],
713
+ url: '',
284
714
  startLine: 0,
285
715
  startColumn: 0,
286
- endLine: src.count("\n"),
716
+ endLine: lineno,
287
717
  endColumn: 0,
288
- executionContextId: @script_paths.size + 1,
289
- hash: src.hash
290
- @script_paths << s_id
718
+ executionContextId: 1,
719
+ hash: src.hash.inspect
720
+ if exc = result.dig(:response, :exceptionDetails)
721
+ exc[:stackTrace][:callFrames].each{|frame|
722
+ if frame[:url].empty?
723
+ frame[:scriptId] = s_id
724
+ else
725
+ path = frame[:url]
726
+ unless s_id = @scr_id_map[path]
727
+ s_id = (@scr_id_map.size + 1).to_s
728
+ @scr_id_map[path] = s_id
729
+ end
730
+ frame[:scriptId] = s_id
731
+ end
732
+ }
733
+ end
734
+ rs = result.dig(:response, :result)
735
+ [rs].each{|obj|
736
+ if oid = obj[:objectId]
737
+ @obj_map[oid] = ['properties']
738
+ end
739
+ }
740
+ @ui.respond req, **result[:response]
741
+
742
+ out = result[:output]
743
+ if out && !out.empty?
744
+ @ui.fire_event 'Runtime.consoleAPICalled',
745
+ type: 'log',
746
+ args: [
747
+ type: out.class,
748
+ value: out
749
+ ],
750
+ executionContextId: 1, # Change this number if something goes wrong.
751
+ timestamp: Time.now.to_f
291
752
  end
292
753
  end
293
- result[:reason] = 'other'
294
- @ui.fire_event 'Debugger.paused', **result
295
- when :evaluate
754
+ when :scope
755
+ result.each{|obj|
756
+ if oid = obj.dig(:value, :objectId)
757
+ @obj_map[oid] = ['properties']
758
+ end
759
+ }
296
760
  @ui.respond req, result: result
297
761
  when :properties
298
- @ui.respond req, result: result
762
+ result.each_value{|v|
763
+ v.each{|obj|
764
+ if oid = obj.dig(:value, :objectId)
765
+ @obj_map[oid] = ['properties']
766
+ end
767
+ }
768
+ }
769
+ @ui.respond req, **result
299
770
  end
300
771
  end
301
772
  end
@@ -307,43 +778,52 @@ module DEBUGGER__
307
778
 
308
779
  case type
309
780
  when :backtrace
310
- event! :cdp_result, :backtrace, req, {
781
+ exception = nil
782
+ result = {
783
+ reason: 'other',
311
784
  callFrames: @target_frames.map.with_index{|frame, i|
785
+ exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception
786
+
312
787
  path = frame.realpath || frame.path
313
- if path.match /<internal:(.*)>/
314
- abs = $1
788
+
789
+ if frame.iseq.nil?
790
+ lineno = 0
315
791
  else
316
- abs = path
792
+ lineno = frame.iseq.first_line - 1
317
793
  end
318
794
 
319
- local_scope = {
795
+ {
320
796
  callFrameId: SecureRandom.hex(16),
321
797
  functionName: frame.name,
798
+ functionLocation: {
799
+ # scriptId: N, # filled by SESSION
800
+ lineNumber: lineno
801
+ },
322
802
  location: {
323
- scriptId: abs,
803
+ # scriptId: N, # filled by SESSION
324
804
  lineNumber: frame.location.lineno - 1 # The line number is 0-based.
325
805
  },
326
- url: "http://debuggee#{abs}",
806
+ url: path,
327
807
  scopeChain: [
328
808
  {
329
809
  type: 'local',
330
810
  object: {
331
811
  type: 'object',
332
- objectId: "#{i}:local"
812
+ objectId: rand.to_s
333
813
  }
334
814
  },
335
815
  {
336
816
  type: 'script',
337
817
  object: {
338
818
  type: 'object',
339
- objectId: "#{i}:script"
819
+ objectId: rand.to_s
340
820
  }
341
821
  },
342
822
  {
343
823
  type: 'global',
344
824
  object: {
345
825
  type: 'object',
346
- objectId: "#{i}:global"
826
+ objectId: rand.to_s
347
827
  }
348
828
  }
349
829
  ],
@@ -353,15 +833,102 @@ module DEBUGGER__
353
833
  }
354
834
  }
355
835
  }
836
+
837
+ if exception
838
+ result[:data] = evaluate_result exception
839
+ result[:reason] = 'exception'
840
+ end
841
+ event! :cdp_result, :backtrace, req, result
356
842
  when :evaluate
357
- expr = args.shift
358
- begin
359
- result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
360
- rescue Exception => e
361
- result = e
843
+ res = {}
844
+ fid, expr, group = args
845
+ frame = @target_frames[fid]
846
+ message = nil
847
+
848
+ if frame && (b = frame.eval_binding)
849
+ special_local_variables frame do |name, var|
850
+ b.local_variable_set(name, var) if /\%/ !~name
851
+ end
852
+
853
+ result = nil
854
+
855
+ case group
856
+ when 'popover'
857
+ case expr
858
+ # Chrome doesn't read instance variables
859
+ when /\A\$\S/
860
+ global_variables.each{|gvar|
861
+ if gvar.to_s == expr
862
+ result = eval(gvar.to_s)
863
+ break false
864
+ end
865
+ } and (message = "Error: Not defined global variable: #{expr.inspect}")
866
+ when /(\A((::[A-Z]|[A-Z])\w*)+)/
867
+ unless result = search_const(b, $1)
868
+ message = "Error: Not defined constant: #{expr.inspect}"
869
+ end
870
+ else
871
+ begin
872
+ result = b.local_variable_get(expr)
873
+ rescue NameError
874
+ # try to check method
875
+ if b.receiver.respond_to? expr, include_all: true
876
+ result = b.receiver.method(expr)
877
+ else
878
+ message = "Error: Can not evaluate: #{expr.inspect}"
879
+ end
880
+ end
881
+ end
882
+ when 'console', 'watch-group'
883
+ begin
884
+ orig_stdout = $stdout
885
+ $stdout = StringIO.new
886
+ result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
887
+ rescue Exception => e
888
+ result = e
889
+ b = result.backtrace.map{|e| " #{e}\n"}
890
+ frames = [
891
+ {
892
+ columnNumber: 0,
893
+ functionName: 'eval',
894
+ lineNumber: 0,
895
+ url: ''
896
+ }
897
+ ]
898
+ e.backtrace_locations&.each do |loc|
899
+ break if loc.path == __FILE__
900
+ path = loc.absolute_path || loc.path
901
+ frames << {
902
+ columnNumber: 0,
903
+ functionName: loc.base_label,
904
+ lineNumber: loc.lineno - 1,
905
+ url: path
906
+ }
907
+ end
908
+ res[:exceptionDetails] = {
909
+ exceptionId: 1,
910
+ text: 'Uncaught',
911
+ lineNumber: 0,
912
+ columnNumber: 0,
913
+ exception: evaluate_result(result),
914
+ stackTrace: {
915
+ callFrames: frames
916
+ }
917
+ }
918
+ ensure
919
+ output = $stdout.string
920
+ $stdout = orig_stdout
921
+ end
922
+ else
923
+ message = "Error: unknown objectGroup: #{group}"
924
+ end
925
+ else
926
+ result = Exception.new("Error: Can not evaluate on this frame")
362
927
  end
363
- event! :cdp_result, :evaluate, req, evaluate_result(result)
364
- when :properties
928
+
929
+ res[:result] = evaluate_result(result)
930
+ event! :cdp_result, :evaluate, req, message: message, response: res, output: output
931
+ when :scope
365
932
  fid = args.shift
366
933
  frame = @target_frames[fid]
367
934
  if b = frame.binding
@@ -369,8 +936,9 @@ module DEBUGGER__
369
936
  v = b.local_variable_get(name)
370
937
  variable(name, v)
371
938
  }
372
- vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
373
- vars.unshift variable('%return', frame.return_value) if frame.has_return_value
939
+ special_local_variables frame do |name, val|
940
+ vars.unshift variable(name, val)
941
+ end
374
942
  vars.unshift variable('%self', b.receiver)
375
943
  elsif lvars = frame.local_variables
376
944
  vars = lvars.map{|var, val|
@@ -378,48 +946,198 @@ module DEBUGGER__
378
946
  }
379
947
  else
380
948
  vars = [variable('%self', frame.self)]
381
- vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
382
- vars.push variable('%return', frame.return_value) if frame.has_return_value
949
+ special_local_variables frame do |name, val|
950
+ vars.unshift variable(name, val)
951
+ end
952
+ end
953
+ event! :cdp_result, :scope, req, vars
954
+ when :properties
955
+ oid = args.shift
956
+ result = []
957
+ prop = []
958
+
959
+ if obj = @obj_map[oid]
960
+ case obj
961
+ when Array
962
+ result = obj.map.with_index{|o, i|
963
+ variable i.to_s, o
964
+ }
965
+ when Hash
966
+ result = obj.map{|k, v|
967
+ variable(k, v)
968
+ }
969
+ when Struct
970
+ result = obj.members.map{|m|
971
+ variable(m, obj[m])
972
+ }
973
+ when String
974
+ prop = [
975
+ internalProperty('#length', obj.length),
976
+ internalProperty('#encoding', obj.encoding)
977
+ ]
978
+ when Class, Module
979
+ result = obj.instance_variables.map{|iv|
980
+ variable(iv, obj.instance_variable_get(iv))
981
+ }
982
+ prop = [internalProperty('%ancestors', obj.ancestors[1..])]
983
+ when Range
984
+ prop = [
985
+ internalProperty('#begin', obj.begin),
986
+ internalProperty('#end', obj.end),
987
+ ]
988
+ end
989
+
990
+ result += obj.instance_variables.map{|iv|
991
+ variable(iv, obj.instance_variable_get(iv))
992
+ }
993
+ prop += [internalProperty('#class', obj.class)]
383
994
  end
384
- event! :cdp_result, :properties, req, vars
995
+ event! :cdp_result, :properties, req, result: result, internalProperties: prop
385
996
  end
386
997
  end
387
998
 
999
+ def search_const b, expr
1000
+ cs = expr.delete_prefix('::').split('::')
1001
+ [Object, *b.eval('Module.nesting')].reverse_each{|mod|
1002
+ if cs.all?{|c|
1003
+ if mod.const_defined?(c)
1004
+ mod = mod.const_get(c)
1005
+ else
1006
+ false
1007
+ end
1008
+ }
1009
+ # if-body
1010
+ return mod
1011
+ end
1012
+ }
1013
+ false
1014
+ end
1015
+
388
1016
  def evaluate_result r
389
1017
  v = variable nil, r
390
1018
  v[:value]
391
1019
  end
392
1020
 
393
- def variable_ name, obj, type, use_short: true
394
- {
1021
+ def internalProperty name, obj
1022
+ v = variable name, obj
1023
+ v.delete :configurable
1024
+ v.delete :enumerable
1025
+ v
1026
+ end
1027
+
1028
+ def propertyDescriptor_ name, obj, type, description: nil, subtype: nil
1029
+ description = DEBUGGER__.safe_inspect(obj, short: true) if description.nil?
1030
+ oid = rand.to_s
1031
+ @obj_map[oid] = obj
1032
+ prop = {
395
1033
  name: name,
396
1034
  value: {
397
1035
  type: type,
398
- value: DEBUGGER__.short_inspect(obj, use_short)
1036
+ description: description,
1037
+ value: obj,
1038
+ objectId: oid
399
1039
  },
400
- configurable: true,
401
- enumerable: true
1040
+ configurable: true, # TODO: Change these parts because
1041
+ enumerable: true # they are not necessarily `true`.
1042
+ }
1043
+
1044
+ if type == 'object'
1045
+ v = prop[:value]
1046
+ v.delete :value
1047
+ v[:subtype] = subtype if subtype
1048
+ v[:className] = obj.class
1049
+ end
1050
+ prop
1051
+ end
1052
+
1053
+ def preview_ value, hash, overflow
1054
+ {
1055
+ type: value[:type],
1056
+ subtype: value[:subtype],
1057
+ description: value[:description],
1058
+ overflow: overflow,
1059
+ properties: hash.map{|k, v|
1060
+ pd = propertyDescriptor k, v
1061
+ {
1062
+ name: pd[:name],
1063
+ type: pd[:value][:type],
1064
+ value: pd[:value][:description]
1065
+ }
1066
+ }
402
1067
  }
403
1068
  end
404
1069
 
405
1070
  def variable name, obj
1071
+ pd = propertyDescriptor name, obj
1072
+ case obj
1073
+ when Array
1074
+ pd[:value][:preview] = preview name, obj
1075
+ obj.each_with_index{|item, idx|
1076
+ if valuePreview = preview(idx.to_s, item)
1077
+ pd[:value][:preview][:properties][idx][:valuePreview] = valuePreview
1078
+ end
1079
+ }
1080
+ when Hash
1081
+ pd[:value][:preview] = preview name, obj
1082
+ obj.each_with_index{|item, idx|
1083
+ key, val = item
1084
+ if valuePreview = preview(key, val)
1085
+ pd[:value][:preview][:properties][idx][:valuePreview] = valuePreview
1086
+ end
1087
+ }
1088
+ end
1089
+ pd
1090
+ end
1091
+
1092
+ def preview name, obj
1093
+ case obj
1094
+ when Array
1095
+ pd = propertyDescriptor name, obj
1096
+ overflow = false
1097
+ if obj.size > 100
1098
+ obj = obj[0..99]
1099
+ overflow = true
1100
+ end
1101
+ hash = obj.each_with_index.to_h{|o, i| [i.to_s, o]}
1102
+ preview_ pd[:value], hash, overflow
1103
+ when Hash
1104
+ pd = propertyDescriptor name, obj
1105
+ overflow = false
1106
+ if obj.size > 100
1107
+ obj = obj.to_a[0..99].to_h
1108
+ overflow = true
1109
+ end
1110
+ preview_ pd[:value], obj, overflow
1111
+ else
1112
+ nil
1113
+ end
1114
+ end
1115
+
1116
+ def propertyDescriptor name, obj
406
1117
  case obj
407
- when Array, Hash, Range, NilClass, Time
408
- variable_ name, obj, 'object'
1118
+ when Array
1119
+ propertyDescriptor_ name, obj, 'object', subtype: 'array'
1120
+ when Hash
1121
+ propertyDescriptor_ name, obj, 'object', subtype: 'map'
409
1122
  when String
410
- variable_ name, obj, 'string', use_short: false
411
- when Class, Module, Struct
412
- variable_ name, obj, 'function'
1123
+ propertyDescriptor_ name, obj, 'string', description: obj
413
1124
  when TrueClass, FalseClass
414
- variable_ name, obj, 'boolean'
1125
+ propertyDescriptor_ name, obj, 'boolean'
415
1126
  when Symbol
416
- variable_ name, obj, 'symbol'
417
- when Float
418
- variable_ name, obj, 'number'
419
- when Integer
420
- variable_ name, obj, 'number'
1127
+ propertyDescriptor_ name, obj, 'symbol'
1128
+ when Integer, Float
1129
+ propertyDescriptor_ name, obj, 'number'
1130
+ when Exception
1131
+ bt = ''
1132
+ if log = obj.backtrace_locations
1133
+ log.each do |loc|
1134
+ break if loc.path == __FILE__
1135
+ bt += " #{loc}\n"
1136
+ end
1137
+ end
1138
+ propertyDescriptor_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
421
1139
  else
422
- variable_ name, obj, 'undefined'
1140
+ propertyDescriptor_ name, obj, 'object'
423
1141
  end
424
1142
  end
425
1143
  end