debug 1.3.3 → 1.5.0

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.
@@ -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
349
  origin: "http://#{@addr}",
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'
574
+ when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.enable'
251
575
  @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
+ @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
+ @tc << [:cdp, :scope, req, fid]
595
+ when 'properties'
596
+ @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