debug 1.3.1 → 1.4.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,22 +4,178 @@ 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
+ 4.times 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
+ sleep 0.1
43
+ ws_client.send sessionId: s_id, id: 1,
44
+ method: 'Page.navigate',
45
+ params: {
46
+ url: "devtools://devtools/bundled/inspector.html?ws=#{addr}"
47
+ }
48
+ end
49
+ end
50
+ pid
51
+ rescue Errno::ENOENT
52
+ nil
53
+ end
54
+
55
+ def get_chrome_path
56
+ return CONFIG[:chrome_path] if CONFIG[:chrome_path]
57
+
58
+ # The process to check OS is based on `selenium` project.
59
+ case RbConfig::CONFIG['host_os']
60
+ when /mswin|msys|mingw|cygwin|emc/
61
+ 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
62
+ when /darwin|mac os/
63
+ '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
64
+ when /linux/
65
+ 'google-chrome'
66
+ else
67
+ raise "Unsupported OS"
68
+ end
69
+ end
70
+
71
+ def run_new_chrome
72
+ dir = Dir.mktmpdir
73
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting
74
+ 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}")
75
+ stdin.close
76
+ stdout.close
77
+
78
+ data = stderr.readpartial 4096
79
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
80
+ port = $1
81
+ path = $2
82
+ end
83
+ stderr.close
84
+
85
+ at_exit{
86
+ CONFIG[:skip_path] = [//] # skip all
87
+ FileUtils.rm_rf dir
88
+ }
89
+
90
+ [port, path, wait_thr.pid]
91
+ end
92
+ end
93
+
94
+ class WebSocketClient
95
+ def initialize s
96
+ @sock = s
97
+ end
98
+
99
+ def handshake port, path
100
+ key = SecureRandom.hex(11)
101
+ @sock.print "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"
102
+ res = @sock.readpartial 4092
103
+ $stderr.puts '[>]' + res if SHOW_PROTOCOL
104
+
105
+ if res.match /^Sec-WebSocket-Accept: (.*)\r\n/
106
+ correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
107
+ raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
108
+ else
109
+ raise "Unknown response: #{res}"
110
+ end
111
+ end
112
+
113
+ def send **msg
114
+ msg = JSON.generate(msg)
115
+ frame = []
116
+ fin = 0b10000000
117
+ opcode = 0b00000001
118
+ frame << fin + opcode
119
+
120
+ mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
121
+ bytesize = msg.bytesize
122
+ if bytesize < 126
123
+ payload_len = bytesize
124
+ elsif bytesize < 2 ** 16
125
+ payload_len = 0b01111110
126
+ ex_payload_len = [bytesize].pack('n*').bytes
127
+ else
128
+ payload_len = 0b01111111
129
+ ex_payload_len = [bytesize].pack('Q>').bytes
130
+ end
131
+
132
+ frame << mask + payload_len
133
+ frame.push *ex_payload_len if ex_payload_len
134
+
135
+ frame.push *masking_key = 4.times.map{rand(1..255)}
136
+ masked = []
137
+ msg.bytes.each_with_index do |b, i|
138
+ masked << (b ^ masking_key[i % 4])
139
+ end
140
+
141
+ frame.push *masked
142
+ @sock.print frame.pack 'c*'
143
+ end
144
+
145
+ def extract_data
146
+ first_group = @sock.getbyte
147
+ fin = first_group & 0b10000000 != 128
148
+ raise 'Unsupported' if fin
149
+ opcode = first_group & 0b00001111
150
+ raise "Unsupported: #{opcode}" unless opcode == 1
151
+
152
+ second_group = @sock.getbyte
153
+ mask = second_group & 0b10000000 == 128
154
+ raise 'The server must not mask any frames' if mask
155
+ payload_len = second_group & 0b01111111
156
+ # TODO: Support other payload_lengths
157
+ if payload_len == 126
158
+ payload_len = @sock.read(2).unpack('n*')[0]
159
+ end
160
+
161
+ data = JSON.parse @sock.read payload_len
162
+ $stderr.puts '[>]' + data.inspect if SHOW_PROTOCOL
163
+ data
164
+ end
165
+ end
166
+
167
+ class Detach < StandardError
168
+ end
169
+
170
+ class WebSocketServer
13
171
  def initialize s
14
172
  @sock = s
15
173
  end
16
174
 
17
175
  def handshake
18
- CONFIG.set_config no_color: true
19
-
20
176
  req = @sock.readpartial 4096
21
177
  $stderr.puts '[>]' + req if SHOW_PROTOCOL
22
-
178
+
23
179
  if req.match /^Sec-WebSocket-Key: (.*)\r\n/
24
180
  accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
25
181
  @sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
@@ -34,7 +190,7 @@ module DEBUGGER__
34
190
  fin = 0b10000000
35
191
  opcode = 0b00000001
36
192
  frame << fin + opcode
37
-
193
+
38
194
  mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
39
195
  bytesize = msg.bytesize
40
196
  if bytesize < 126
@@ -46,7 +202,7 @@ module DEBUGGER__
46
202
  payload_len = 0b01111111
47
203
  ex_payload_len = [bytesize].pack('Q>').bytes
48
204
  end
49
-
205
+
50
206
  frame << mask + payload_len
51
207
  frame.push *ex_payload_len if ex_payload_len
52
208
  frame.push *msg.bytes
@@ -57,7 +213,9 @@ module DEBUGGER__
57
213
  first_group = @sock.getbyte
58
214
  fin = first_group & 0b10000000 != 128
59
215
  raise 'Unsupported' if fin
216
+
60
217
  opcode = first_group & 0b00001111
218
+ raise Detach if opcode == 8
61
219
  raise "Unsupported: #{opcode}" unless opcode == 1
62
220
 
63
221
  second_group = @sock.getbyte
@@ -82,32 +240,40 @@ module DEBUGGER__
82
240
 
83
241
  def send_response req, **res
84
242
  if res.empty?
85
- @web_sock.send id: req['id'], result: {}
243
+ @ws_server.send id: req['id'], result: {}
86
244
  else
87
- @web_sock.send id: req['id'], result: res
245
+ @ws_server.send id: req['id'], result: res
88
246
  end
89
247
  end
90
248
 
249
+ def send_fail_response req, **res
250
+ @ws_server.send id: req['id'], error: res
251
+ end
252
+
91
253
  def send_event method, **params
92
254
  if params.empty?
93
- @web_sock.send method: method, params: {}
255
+ @ws_server.send method: method, params: {}
94
256
  else
95
- @web_sock.send method: method, params: params
257
+ @ws_server.send method: method, params: params
96
258
  end
97
259
  end
98
260
 
261
+ INVALID_REQUEST = -32600
262
+
99
263
  def process
264
+ bps = {}
265
+ @src_map = {}
100
266
  loop do
101
- req = @web_sock.extract_data
267
+ req = @ws_server.extract_data
102
268
  $stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
103
- bps = []
104
269
 
105
270
  case req['method']
106
271
 
107
272
  ## boot/configuration
108
273
  when 'Page.getResourceTree'
109
- abs = File.absolute_path($0)
110
- src = File.read(abs)
274
+ path = File.absolute_path($0)
275
+ src = File.read(path)
276
+ @src_map[path] = src
111
277
  send_response req,
112
278
  frameTree: {
113
279
  frame: {
@@ -120,11 +286,11 @@ module DEBUGGER__
120
286
  ]
121
287
  }
122
288
  send_event 'Debugger.scriptParsed',
123
- scriptId: abs,
124
- url: "http://debuggee#{abs}",
289
+ scriptId: path,
290
+ url: "http://debuggee#{path}",
125
291
  startLine: 0,
126
292
  startColumn: 0,
127
- endLine: src.count('\n'),
293
+ endLine: src.count("\n"),
128
294
  endColumn: 0,
129
295
  executionContextId: 1,
130
296
  hash: src.hash
@@ -136,12 +302,12 @@ module DEBUGGER__
136
302
  }
137
303
  when 'Debugger.getScriptSource'
138
304
  s_id = req.dig('params', 'scriptId')
139
- src = File.read(s_id)
305
+ src = get_source_code s_id
140
306
  send_response req, scriptSource: src
141
307
  @q_msg << req
142
308
  when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
143
309
  'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
144
- 'Debugger.setBreakpointsActive', 'Runtime.releaseObjectGroup'
310
+ 'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear'
145
311
  send_response req
146
312
 
147
313
  ## control
@@ -151,25 +317,60 @@ module DEBUGGER__
151
317
  send_response req
152
318
  send_event 'Debugger.resumed'
153
319
  when 'Debugger.stepOver'
154
- @q_msg << 'n'
155
- @q_msg << req
156
- send_response req
157
- send_event 'Debugger.resumed'
320
+ begin
321
+ @session.check_postmortem
322
+ @q_msg << 'n'
323
+ send_response req
324
+ send_event 'Debugger.resumed'
325
+ rescue PostmortemError
326
+ send_fail_response req,
327
+ code: INVALID_REQUEST,
328
+ message: "'stepOver' is not supported while postmortem mode"
329
+ ensure
330
+ @q_msg << req
331
+ end
158
332
  when 'Debugger.stepInto'
159
- @q_msg << 's'
160
- @q_msg << req
161
- send_response req
162
- send_event 'Debugger.resumed'
333
+ begin
334
+ @session.check_postmortem
335
+ @q_msg << 's'
336
+ send_response req
337
+ send_event 'Debugger.resumed'
338
+ rescue PostmortemError
339
+ send_fail_response req,
340
+ code: INVALID_REQUEST,
341
+ message: "'stepInto' is not supported while postmortem mode"
342
+ ensure
343
+ @q_msg << req
344
+ end
163
345
  when 'Debugger.stepOut'
164
- @q_msg << 'fin'
165
- @q_msg << req
346
+ begin
347
+ @session.check_postmortem
348
+ @q_msg << 'fin'
349
+ send_response req
350
+ send_event 'Debugger.resumed'
351
+ rescue PostmortemError
352
+ send_fail_response req,
353
+ code: INVALID_REQUEST,
354
+ message: "'stepOut' is not supported while postmortem mode"
355
+ ensure
356
+ @q_msg << req
357
+ end
358
+ when 'Debugger.setSkipAllPauses'
359
+ skip = req.dig('params', 'skip')
360
+ if skip
361
+ deactivate_bp
362
+ else
363
+ activate_bp bps
364
+ end
166
365
  send_response req
167
- send_event 'Debugger.resumed'
168
366
 
169
367
  # breakpoint
170
368
  when 'Debugger.getPossibleBreakpoints'
171
369
  s_id = req.dig('params', 'start', 'scriptId')
172
- line = req.dig('params', 'lineNumber')
370
+ line = req.dig('params', 'start', 'lineNumber')
371
+ src = get_source_code s_id
372
+ end_line = src.count("\n")
373
+ line = end_line if line > end_line
173
374
  send_response req,
174
375
  locations: [
175
376
  { scriptId: s_id,
@@ -178,33 +379,108 @@ module DEBUGGER__
178
379
  ]
179
380
  when 'Debugger.setBreakpointByUrl'
180
381
  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)
382
+ url = req.dig('params', 'url')
383
+ locations = []
384
+ if url.match /http:\/\/debuggee(.*)/
385
+ path = $1
386
+ cond = req.dig('params', 'condition')
387
+ src = get_source_code path
388
+ end_line = src.count("\n")
389
+ line = end_line if line > end_line
390
+ b_id = "1:#{line}:#{path}"
391
+ if cond != ''
392
+ SESSION.add_line_breakpoint(path, line + 1, cond: cond)
393
+ else
394
+ SESSION.add_line_breakpoint(path, line + 1)
395
+ end
396
+ bps[b_id] = bps.size
397
+ locations << {scriptId: path, lineNumber: line}
185
398
  else
186
- bps << SESSION.add_line_breakpoint(path, line + 1)
399
+ b_id = "1:#{line}:#{url}"
187
400
  end
188
401
  send_response req,
189
- breakpointId: (bps.size - 1).to_s,
190
- locations: [
191
- scriptId: path,
192
- lineNumber: line
193
- ]
402
+ breakpointId: b_id,
403
+ locations: locations
194
404
  when 'Debugger.removeBreakpoint'
195
405
  b_id = req.dig('params', 'breakpointId')
196
- @q_msg << "del #{b_id}"
406
+ bps = del_bp bps, b_id
407
+ send_response req
408
+ when 'Debugger.setBreakpointsActive'
409
+ active = req.dig('params', 'active')
410
+ if active
411
+ activate_bp bps
412
+ else
413
+ deactivate_bp # TODO: Change this part because catch breakpoints should not be deactivated.
414
+ end
415
+ send_response req
416
+ when 'Debugger.setPauseOnExceptions'
417
+ state = req.dig('params', 'state')
418
+ ex = 'Exception'
419
+ case state
420
+ when 'none'
421
+ @q_msg << 'config postmortem = false'
422
+ bps = del_bp bps, ex
423
+ when 'uncaught'
424
+ @q_msg << 'config postmortem = true'
425
+ bps = del_bp bps, ex
426
+ when 'all'
427
+ @q_msg << 'config postmortem = false'
428
+ SESSION.add_catch_breakpoint ex
429
+ bps[ex] = bps.size
430
+ end
197
431
  send_response req
198
432
 
199
433
  when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
200
434
  @q_msg << req
201
435
  end
202
436
  end
437
+ rescue Detach
438
+ @q_msg << 'continue'
439
+ end
440
+
441
+ def del_bp bps, k
442
+ return bps unless idx = bps[k]
443
+
444
+ bps.delete k
445
+ bps.each_key{|i| bps[i] -= 1 if bps[i] > idx}
446
+ @q_msg << "del #{idx}"
447
+ bps
448
+ end
449
+
450
+ def get_source_code path
451
+ return @src_map[path] if @src_map[path]
452
+
453
+ src = File.read(path)
454
+ @src_map[path] = src
455
+ src
456
+ end
457
+
458
+ def activate_bp bps
459
+ bps.each_key{|k|
460
+ if k.match /^\d+:(\d+):(.*)/
461
+ line = $1
462
+ path = $2
463
+ SESSION.add_line_breakpoint(path, line.to_i + 1)
464
+ else
465
+ SESSION.add_catch_breakpoint 'Exception'
466
+ end
467
+ }
468
+ end
469
+
470
+ def deactivate_bp
471
+ @q_msg << 'del'
472
+ @q_ans << 'y'
473
+ end
474
+
475
+ def cleanup_reader
476
+ Process.kill :KILL, @chrome_pid if @chrome_pid
203
477
  end
204
478
 
205
479
  ## Called by the SESSION thread
206
480
 
207
481
  def readline prompt
482
+ return 'c' unless @q_msg
483
+
208
484
  @q_msg.pop || 'kill!'
209
485
  end
210
486
 
@@ -212,6 +488,10 @@ module DEBUGGER__
212
488
  send_response req, **result
213
489
  end
214
490
 
491
+ def respond_fail req, **result
492
+ send_fail_response req, **result
493
+ end
494
+
215
495
  def fire_event event, **result
216
496
  if result.empty?
217
497
  send_event event
@@ -231,26 +511,48 @@ module DEBUGGER__
231
511
  end
232
512
 
233
513
  class Session
514
+ def fail_response req, **result
515
+ @ui.respond_fail req, result
516
+ return :retry
517
+ end
518
+
519
+ INVALID_PARAMS = -32602
520
+
234
521
  def process_protocol_request req
235
522
  case req['method']
236
523
  when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
237
524
  @tc << [:cdp, :backtrace, req]
238
525
  when 'Debugger.evaluateOnCallFrame'
239
- expr = req.dig('params', 'expression')
240
- @tc << [:cdp, :evaluate, req, expr]
526
+ frame_id = req.dig('params', 'callFrameId')
527
+ if fid = @frame_map[frame_id]
528
+ expr = req.dig('params', 'expression')
529
+ @tc << [:cdp, :evaluate, req, fid, expr]
530
+ else
531
+ fail_response req,
532
+ code: INVALID_PARAMS,
533
+ message: "'callFrameId' is an invalid"
534
+ end
241
535
  when 'Runtime.getProperties'
242
536
  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
537
+ if ref = @obj_map[oid]
538
+ case ref[0]
539
+ when 'local'
540
+ frame_id = ref[1]
541
+ fid = @frame_map[frame_id]
542
+ @tc << [:cdp, :scope, req, fid]
543
+ when 'properties'
544
+ @tc << [:cdp, :properties, req, oid]
545
+ when 'script', 'global'
546
+ # TODO: Support script and global types
547
+ @ui.respond req
548
+ return :retry
549
+ else
550
+ raise "Unknown type: #{ref.inspect}"
551
+ end
552
+ else
553
+ fail_response req,
554
+ code: INVALID_PARAMS,
555
+ message: "'objectId' is an invalid"
254
556
  end
255
557
  end
256
558
  end
@@ -260,7 +562,9 @@ module DEBUGGER__
260
562
 
261
563
  case type
262
564
  when :backtrace
263
- result[:callFrames].each do |frame|
565
+ result[:callFrames].each.with_index do |frame, i|
566
+ frame_id = frame[:callFrameId]
567
+ @frame_map[frame_id] = i
264
568
  s_id = frame.dig(:location, :scriptId)
265
569
  if File.exist?(s_id) && !@script_paths.include?(s_id)
266
570
  src = File.read(s_id)
@@ -269,19 +573,66 @@ module DEBUGGER__
269
573
  url: frame[:url],
270
574
  startLine: 0,
271
575
  startColumn: 0,
272
- endLine: src.count('\n'),
576
+ endLine: src.count("\n"),
273
577
  endColumn: 0,
274
578
  executionContextId: @script_paths.size + 1,
275
579
  hash: src.hash
276
580
  @script_paths << s_id
277
581
  end
582
+
583
+ frame[:scopeChain].each {|s|
584
+ oid = s.dig(:object, :objectId)
585
+ @obj_map[oid] = [s[:type], frame_id]
586
+ }
587
+ end
588
+
589
+ if oid = result.dig(:data, :objectId)
590
+ @obj_map[oid] = ['properties']
278
591
  end
279
- result[:reason] = 'other'
280
592
  @ui.fire_event 'Debugger.paused', **result
281
593
  when :evaluate
594
+ message = result.delete :message
595
+ if message
596
+ fail_response req,
597
+ code: INVALID_PARAMS,
598
+ message: message
599
+ else
600
+ rs = result.dig(:response, :result)
601
+ [rs].each{|obj|
602
+ if oid = obj[:objectId]
603
+ @obj_map[oid] = ['properties']
604
+ end
605
+ }
606
+ @ui.respond req, **result[:response]
607
+
608
+ out = result[:output]
609
+ if out && !out.empty?
610
+ @ui.fire_event 'Runtime.consoleAPICalled',
611
+ type: 'log',
612
+ args: [
613
+ type: out.class,
614
+ value: out
615
+ ],
616
+ executionContextId: 1, # Change this number if something goes wrong.
617
+ timestamp: Time.now.to_f
618
+ end
619
+ end
620
+ when :scope
621
+ result.each{|obj|
622
+ if oid = obj.dig(:value, :objectId)
623
+ @obj_map[oid] = ['properties']
624
+ end
625
+ }
282
626
  @ui.respond req, result: result
283
627
  when :properties
284
- @ui.respond req, result: result
628
+ result.each_value{|v|
629
+ v.each{|obj|
630
+ if oid = obj.dig(:value, :objectId)
631
+ @obj_map[oid] = ['properties']
632
+ end
633
+ }
634
+ }
635
+ @ui.respond req, **result
285
636
  end
286
637
  end
287
638
  end
@@ -293,43 +644,49 @@ module DEBUGGER__
293
644
 
294
645
  case type
295
646
  when :backtrace
296
- event! :cdp_result, :backtrace, req, {
647
+ exception = nil
648
+ result = {
649
+ reason: 'other',
297
650
  callFrames: @target_frames.map.with_index{|frame, i|
651
+ exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception
652
+
298
653
  path = frame.realpath || frame.path
299
654
  if path.match /<internal:(.*)>/
300
- abs = $1
301
- else
302
- abs = path
655
+ path = $1
303
656
  end
304
657
 
305
- local_scope = {
658
+ {
306
659
  callFrameId: SecureRandom.hex(16),
307
660
  functionName: frame.name,
661
+ functionLocation: {
662
+ scriptId: path,
663
+ lineNumber: 0
664
+ },
308
665
  location: {
309
- scriptId: abs,
666
+ scriptId: path,
310
667
  lineNumber: frame.location.lineno - 1 # The line number is 0-based.
311
668
  },
312
- url: "http://debuggee#{abs}",
669
+ url: "http://debuggee#{path}",
313
670
  scopeChain: [
314
671
  {
315
672
  type: 'local',
316
673
  object: {
317
674
  type: 'object',
318
- objectId: "#{i}:local"
675
+ objectId: rand.to_s
319
676
  }
320
677
  },
321
678
  {
322
679
  type: 'script',
323
680
  object: {
324
681
  type: 'object',
325
- objectId: "#{i}:script"
682
+ objectId: rand.to_s
326
683
  }
327
684
  },
328
685
  {
329
686
  type: 'global',
330
687
  object: {
331
688
  type: 'object',
332
- objectId: "#{i}:global"
689
+ objectId: rand.to_s
333
690
  }
334
691
  }
335
692
  ],
@@ -339,15 +696,83 @@ module DEBUGGER__
339
696
  }
340
697
  }
341
698
  }
699
+
700
+ if exception
701
+ result[:data] = evaluate_result exception
702
+ result[:reason] = 'exception'
703
+ end
704
+ event! :cdp_result, :backtrace, req, result
342
705
  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
706
+ res = {}
707
+ fid, expr = args
708
+ frame = @target_frames[fid]
709
+ message = nil
710
+
711
+ if frame && (b = frame.binding)
712
+ b = b.dup
713
+ special_local_variables current_frame do |name, var|
714
+ b.local_variable_set(name, var) if /\%/ !~name
715
+ end
716
+
717
+ result = nil
718
+
719
+ case req.dig('params', 'objectGroup')
720
+ when 'popover'
721
+ case expr
722
+ # Chrome doesn't read instance variables
723
+ when /\A\$\S/
724
+ global_variables.each{|gvar|
725
+ if gvar.to_s == expr
726
+ result = eval(gvar.to_s)
727
+ break false
728
+ end
729
+ } and (message = "Error: Not defined global variable: #{expr.inspect}")
730
+ when /(\A[A-Z][a-zA-Z]*)/
731
+ unless result = search_const(b, $1)
732
+ message = "Error: Not defined constant: #{expr.inspect}"
733
+ end
734
+ else
735
+ begin
736
+ # try to check local variables
737
+ b.local_variable_defined?(expr) or raise NameError
738
+ result = b.local_variable_get(expr)
739
+ rescue NameError
740
+ # try to check method
741
+ if b.receiver.respond_to? expr, include_all: true
742
+ result = b.receiver.method(expr)
743
+ else
744
+ message = "Error: Can not evaluate: #{expr.inspect}"
745
+ end
746
+ end
747
+ end
748
+ else
749
+ begin
750
+ orig_stdout = $stdout
751
+ $stdout = StringIO.new
752
+ result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
753
+ rescue Exception => e
754
+ result = e
755
+ b = result.backtrace.map{|e| " #{e}\n"}
756
+ line = b.first.match('.*:(\d+):in .*')[1].to_i
757
+ res[:exceptionDetails] = {
758
+ exceptionId: 1,
759
+ text: 'Uncaught',
760
+ lineNumber: line - 1,
761
+ columnNumber: 0,
762
+ exception: evaluate_result(result),
763
+ }
764
+ ensure
765
+ output = $stdout.string
766
+ $stdout = orig_stdout
767
+ end
768
+ end
769
+ else
770
+ result = Exception.new("Error: Can not evaluate on this frame")
348
771
  end
349
- event! :cdp_result, :evaluate, req, evaluate_result(result)
350
- when :properties
772
+
773
+ res[:result] = evaluate_result(result)
774
+ event! :cdp_result, :evaluate, req, message: message, response: res, output: output
775
+ when :scope
351
776
  fid = args.shift
352
777
  frame = @target_frames[fid]
353
778
  if b = frame.binding
@@ -355,8 +780,9 @@ module DEBUGGER__
355
780
  v = b.local_variable_get(name)
356
781
  variable(name, v)
357
782
  }
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
783
+ special_local_variables frame do |name, val|
784
+ vars.unshift variable(name, val)
785
+ end
360
786
  vars.unshift variable('%self', b.receiver)
361
787
  elsif lvars = frame.local_variables
362
788
  vars = lvars.map{|var, val|
@@ -364,46 +790,131 @@ module DEBUGGER__
364
790
  }
365
791
  else
366
792
  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
793
+ special_local_variables frame do |name, val|
794
+ vars.unshift variable(name, val)
795
+ end
796
+ end
797
+ event! :cdp_result, :scope, req, vars
798
+ when :properties
799
+ oid = args.shift
800
+ result = []
801
+ prop = []
802
+
803
+ if obj = @obj_map[oid]
804
+ case obj
805
+ when Array
806
+ result = obj.map.with_index{|o, i|
807
+ variable i.to_s, o
808
+ }
809
+ when Hash
810
+ result = obj.map{|k, v|
811
+ variable(k, v)
812
+ }
813
+ when Struct
814
+ result = obj.members.map{|m|
815
+ variable(m, obj[m])
816
+ }
817
+ when String
818
+ prop = [
819
+ property('#length', obj.length),
820
+ property('#encoding', obj.encoding)
821
+ ]
822
+ when Class, Module
823
+ result = obj.instance_variables.map{|iv|
824
+ variable(iv, obj.instance_variable_get(iv))
825
+ }
826
+ prop = [property('%ancestors', obj.ancestors[1..])]
827
+ when Range
828
+ prop = [
829
+ property('#begin', obj.begin),
830
+ property('#end', obj.end),
831
+ ]
832
+ end
833
+
834
+ result += obj.instance_variables.map{|iv|
835
+ variable(iv, obj.instance_variable_get(iv))
836
+ }
837
+ prop += [property('#class', obj.class)]
369
838
  end
370
- event! :cdp_result, :properties, req, vars
839
+ event! :cdp_result, :properties, req, result: result, internalProperties: prop
371
840
  end
372
841
  end
373
842
 
843
+ def search_const b, expr
844
+ cs = expr.split('::')
845
+ [Object, *b.eval('Module.nesting')].reverse_each{|mod|
846
+ if cs.all?{|c|
847
+ if mod.const_defined?(c)
848
+ mod = mod.const_get(c)
849
+ else
850
+ false
851
+ end
852
+ }
853
+ # if-body
854
+ return mod
855
+ end
856
+ }
857
+ false
858
+ end
859
+
374
860
  def evaluate_result r
375
861
  v = variable nil, r
376
862
  v[:value]
377
863
  end
378
864
 
379
- def variable_ name, obj, type, use_short: true
380
- {
865
+ def property name, obj
866
+ v = variable name, obj
867
+ v.delete :configurable
868
+ v.delete :enumerable
869
+ v
870
+ end
871
+
872
+ def variable_ name, obj, type, description: obj.inspect, subtype: nil
873
+ oid = rand.to_s
874
+ @obj_map[oid] = obj
875
+ prop = {
381
876
  name: name,
382
877
  value: {
383
878
  type: type,
384
- value: DEBUGGER__.short_inspect(obj, use_short)
879
+ description: description,
880
+ value: obj,
881
+ objectId: oid
385
882
  },
386
- configurable: true,
387
- enumerable: true
883
+ configurable: true, # TODO: Change these parts because
884
+ enumerable: true # they are not necessarily `true`.
388
885
  }
886
+
887
+ if type == 'object'
888
+ v = prop[:value]
889
+ v.delete :value
890
+ v[:subtype] = subtype if subtype
891
+ v[:className] = obj.class
892
+ end
893
+ prop
389
894
  end
390
895
 
391
896
  def variable name, obj
392
897
  case obj
393
- when Array, Hash, Range, NilClass, Time
394
- variable_ name, obj, 'object'
898
+ when Array
899
+ variable_ name, obj, 'object', description: "Array(#{obj.size})", subtype: 'array'
900
+ when Hash
901
+ variable_ name, obj, 'object', description: "Hash(#{obj.size})", subtype: 'map'
395
902
  when String
396
- variable_ name, obj, 'string', use_short: false
397
- when Class, Module, Struct
398
- variable_ name, obj, 'function'
903
+ variable_ name, obj, 'string', description: obj
904
+ when Class, Module, Struct, Range, Time, Method
905
+ variable_ name, obj, 'object'
399
906
  when TrueClass, FalseClass
400
907
  variable_ name, obj, 'boolean'
401
908
  when Symbol
402
909
  variable_ name, obj, 'symbol'
403
- when Float
404
- variable_ name, obj, 'number'
405
- when Integer
910
+ when Integer, Float
406
911
  variable_ name, obj, 'number'
912
+ when Exception
913
+ bt = nil
914
+ if log = obj.backtrace
915
+ bt = log.map{|e| " #{e}\n"}.join
916
+ end
917
+ variable_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
407
918
  else
408
919
  variable_ name, obj, 'undefined'
409
920
  end