Module: DEBUGGER__::UI_CDP

Defined in:
lib/debug/server_cdp.rb

Defined Under Namespace

Modules: WebSocketUtils Classes: Detach, NotFoundChromeEndpointError, UnsupportedError, WebSocketClient, WebSocketServer

Constant Summary collapse

SHOW_PROTOCOL =
ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
TIMEOUT_SEC =
5
ITERATIONS =
50
INVALID_REQUEST =
-32600

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.get_chrome_path(candidates) ⇒ Object

Raises:



176
177
178
179
180
181
182
183
# File 'lib/debug/server_cdp.rb', line 176

def get_chrome_path candidates
  candidates.each{|c|
    if File.exist? c
      return c
    end
  }
  raise UnsupportedError
end

.get_devtools_endpoint(tf) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/debug/server_cdp.rb', line 187

def get_devtools_endpoint tf
  i = 1
  while i < ITERATIONS
    i += 1
    if File.exist?(tf) && data = File.read(tf)
      if data.match(/DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/)
        port = $1
        path = $2
        return [port, path]
      end
    end
    sleep 0.1
  end
  raise NotFoundChromeEndpointError
end

.run_new_chromeObject



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/debug/server_cdp.rb', line 86

def run_new_chrome
  path = CONFIG[:chrome_path]

  data = nil
  port = nil
  wait_thr = nil

  # The process to check OS is based on `selenium` project.
  case RbConfig::CONFIG['host_os']
  when /mswin|msys|mingw|cygwin|emc/
    if path.nil?
      candidates = ['C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe']
      path = get_chrome_path candidates
    end
    # The path is based on https://github.com/sindresorhus/open/blob/v8.4.0/index.js#L128.
    stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
    tf = Tempfile.create(['debug-', '.txt'])

    stdin.puts("Start-process '#{path}' -Argumentlist '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', '--user-data-dir=C:\\temp' -Wait -RedirectStandardError #{tf.path}")
    stdin.close
    stdout.close
    stderr.close
    port, path = get_devtools_endpoint(tf.path)

    at_exit{
      DEBUGGER__.skip_all

      stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
      stdin.puts("Stop-process -Name chrome")
      stdin.close
      stdout.close
      stderr.close
      tf.close
      begin
        File.unlink(tf)
      rescue Errno::EACCES
      end
    }
  when /darwin|mac os/
    path = path || '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
    dir = Dir.mktmpdir
    # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
    stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
    stdin.close
    stdout.close
    data = stderr.readpartial 4096
    stderr.close
    if data.match(/DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/)
      port = $1
      path = $2
    end

    at_exit{
      DEBUGGER__.skip_all
      FileUtils.rm_rf dir
    }
  when /linux/
    path = path || 'google-chrome'
    dir = Dir.mktmpdir
    # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
    stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
    stdin.close
    stdout.close
    data = ''
    begin
      Timeout.timeout(TIMEOUT_SEC) do
        until data.match?(/DevTools listening on ws:\/\/127.0.0.1:\d+.*/)
          data = stderr.readpartial 4096
        end
      end
    rescue Exception
      raise NotFoundChromeEndpointError
    end
    stderr.close
    if data.match(/DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/)
      port = $1
      path = $2
    end

    at_exit{
      DEBUGGER__.skip_all
      FileUtils.rm_rf dir
    }
  else
    raise UnsupportedError
  end

  [port, path, wait_thr.pid]
end

.setup_chrome(addr, uuid) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/debug/server_cdp.rb', line 20

def setup_chrome addr, uuid
  return if CONFIG[:chrome_path] == ''

  port, path, pid = run_new_chrome
  begin
    s = Socket.tcp '127.0.0.1', port
  rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
    return
  end

  ws_client = WebSocketClient.new(s)
  ws_client.handshake port, path
  ws_client.send id: 1, method: 'Target.getTargets'

  loop do
    res = ws_client.extract_data
    case res['id']
    when 1
      target_info = res.dig('result', 'targetInfos')
      page = target_info.find{|t| t['type'] == 'page'}
      ws_client.send id: 2, method: 'Target.attachToTarget',
                    params: {
                      targetId: page['targetId'],
                      flatten: true
                    }
    when 2
      s_id = res.dig('result', 'sessionId')
      # TODO: change id
      ws_client.send sessionId: s_id, id: 100, method: 'Network.enable'
      ws_client.send sessionId: s_id, id: 3,
                    method: 'Page.enable'
    when 3
      s_id = res['sessionId']
      ws_client.send sessionId: s_id, id: 4,
                    method: 'Page.getFrameTree'
    when 4
      s_id = res['sessionId']
      f_id = res.dig('result', 'frameTree', 'frame', 'id')
      ws_client.send sessionId: s_id, id: 5,
                    method: 'Page.navigate',
                    params: {
                      url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&noJavaScriptCompletion=true&ws=#{addr}/#{uuid}",
                      frameId: f_id
                    }
    when 101
      break
    else
      if res['method'] == 'Network.webSocketWillSendHandshakeRequest'
        s_id = res['sessionId']
        # Display the console by entering ESC key
        ws_client.send sessionId: s_id, id: 101,  # TODO: change id
                      method:"Input.dispatchKeyEvent",
                      params: {
                        type:"keyDown",
                        windowsVirtualKeyCode:27 # ESC key
                      }
      end
    end
  end
  pid
rescue Errno::ENOENT, UnsupportedError, NotFoundChromeEndpointError
  nil
end

Instance Method Details

#activate_bp(bps) ⇒ Object



658
659
660
661
662
663
664
665
666
667
668
# File 'lib/debug/server_cdp.rb', line 658

def activate_bp bps
  bps.each_key{|k|
    if k.match(/^\d+:(\d+):(.*)/)
      line = $1
      path = $2
      SESSION.add_line_breakpoint(path, line.to_i + 1)
    else
      SESSION.add_catch_breakpoint 'Exception'
    end
  }
end

#add_line_breakpoint(req, b_id, path) ⇒ Object



623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# File 'lib/debug/server_cdp.rb', line 623

def add_line_breakpoint req, b_id, path
  cond = req.dig('params', 'condition')
  line = req.dig('params', 'lineNumber')
  src = get_source_code path
  end_line = src.lines.count
  line = end_line  if line > end_line
  if cond != ''
    SESSION.add_line_breakpoint(path, line + 1, cond: cond)
  else
    SESSION.add_line_breakpoint(path, line + 1)
  end
  # Because we need to return scriptId, responses are returned in SESSION thread.
  req['params']['scriptId'] = path
  req['params']['lineNumber'] = line
  req['params']['breakpointId'] = b_id
  @q_msg << req
end

#cleanup_readerObject



675
676
677
678
679
# File 'lib/debug/server_cdp.rb', line 675

def cleanup_reader
  super
  Process.kill :KILL, @chrome_pid if @chrome_pid
rescue Errno::ESRCH # continue if @chrome_pid process is not found
end

#deactivate_bpObject



670
671
672
673
# File 'lib/debug/server_cdp.rb', line 670

def deactivate_bp
  @q_msg << 'del'
  @q_ans << 'y'
end

#del_bp(bps, k) ⇒ Object



641
642
643
644
645
646
647
648
# File 'lib/debug/server_cdp.rb', line 641

def del_bp bps, k
  return bps unless idx = bps[k]

  bps.delete k
  bps.each_key{|i| bps[i] -= 1 if bps[i] > idx}
  @q_msg << "del #{idx}"
  bps
end

#get_source_code(path) ⇒ Object



650
651
652
653
654
655
656
# File 'lib/debug/server_cdp.rb', line 650

def get_source_code path
  return @src_map[path] if @src_map[path]

  src = File.read(path)
  @src_map[path] = src
  src
end

#processObject



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
# File 'lib/debug/server_cdp.rb', line 464

def process
  bps = {}
  @src_map = {}
  loop do
    req = @ws_server.extract_data

    case req['method']

    ## boot/configuration
    when 'Debugger.getScriptSource'
      @q_msg << req
    when 'Debugger.enable'
      send_response req, debuggerId: rand.to_s
      @q_msg << req
    when 'Runtime.enable'
      send_response req
      send_event 'Runtime.executionContextCreated',
                  context: {
                    id: SecureRandom.hex(16),
                    origin: "http://#{@local_addr.inspect_sockaddr}",
                    name: ''
                  }
    when 'Runtime.getIsolateId'
      send_response req,
                    id: SecureRandom.hex
    when 'Runtime.terminateExecution'
      send_response req
      exit
    when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
      'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
      'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear', 'Runtime.runIfWaitingForDebugger'
      send_response req

    ## control
    when 'Debugger.resume'
      send_response req
      send_event 'Debugger.resumed'
      @q_msg << 'c'
      @q_msg << req
    when 'Debugger.stepOver'
      begin
        @session.check_postmortem
        send_response req
        send_event 'Debugger.resumed'
        @q_msg << 'n'
      rescue PostmortemError
        send_fail_response req,
                          code: INVALID_REQUEST,
                          message: "'stepOver' is not supported while postmortem mode"
      ensure
        @q_msg << req
      end
    when 'Debugger.stepInto'
      begin
        @session.check_postmortem
        send_response req
        send_event 'Debugger.resumed'
        @q_msg << 's'
      rescue PostmortemError
        send_fail_response req,
                          code: INVALID_REQUEST,
                          message: "'stepInto' is not supported while postmortem mode"
      ensure
        @q_msg << req
      end
    when 'Debugger.stepOut'
      begin
        @session.check_postmortem
        send_response req
        send_event 'Debugger.resumed'
        @q_msg << 'fin'
      rescue PostmortemError
        send_fail_response req,
                          code: INVALID_REQUEST,
                          message: "'stepOut' is not supported while postmortem mode"
      ensure
        @q_msg << req
      end
    when 'Debugger.setSkipAllPauses'
      skip = req.dig('params', 'skip')
      if skip
        deactivate_bp
      else
        activate_bp bps
      end
      send_response req
    when 'Debugger.pause'
      send_response req
      Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)

    # breakpoint
    when 'Debugger.getPossibleBreakpoints'
      @q_msg << req
    when 'Debugger.setBreakpointByUrl'
      line = req.dig('params', 'lineNumber')
      if regexp = req.dig('params', 'urlRegex')
        b_id = "1:#{line}:#{regexp}"
        bps[b_id] = bps.size
        path = regexp.match(/(.*)\|/)[1].gsub("\\", "")
        add_line_breakpoint(req, b_id, path)
      elsif url = req.dig('params', 'url')
        b_id = "#{line}:#{url}"
        # When breakpoints are set in Script snippet, non-existent path such as "snippet:///Script%20snippet%20%231" sent.
        # That's why we need to check it here.
        if File.exist? url
          bps[b_id] = bps.size
          add_line_breakpoint(req, b_id, url)
        else
          send_response req,
                        breakpointId: b_id,
                        locations: []
        end            
      else
        if hash = req.dig('params', 'scriptHash')
          b_id = "#{line}:#{hash}"
          send_response req,
                        breakpointId: b_id,
                        locations: []
        else
          raise 'Unsupported'
        end
      end
    when 'Debugger.removeBreakpoint'
      b_id = req.dig('params', 'breakpointId')
      bps = del_bp bps, b_id
      send_response req
    when 'Debugger.setBreakpointsActive'
      active = req.dig('params', 'active')
      if active
        activate_bp bps
      else
        deactivate_bp # TODO: Change this part because catch breakpoints should not be deactivated.
      end
      send_response req
    when 'Debugger.setPauseOnExceptions'
      state = req.dig('params', 'state')
      ex = 'Exception'
      case state
      when 'none'
        @q_msg << 'config postmortem = false'
        bps = del_bp bps, ex
      when 'uncaught'
        @q_msg << 'config postmortem = true'
        bps = del_bp bps, ex
      when 'all'
        @q_msg << 'config postmortem = false'
        SESSION.add_catch_breakpoint ex
        bps[ex] = bps.size
      end
      send_response req

    when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
      @q_msg << req
    end
  end
rescue Detach
  @q_msg << 'continue'
end

#puts(result = '') ⇒ Object



691
692
693
694
# File 'lib/debug/server_cdp.rb', line 691

def puts result=''
  # STDERR.puts "puts: #{result}"
  # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
end

#send_chrome_response(req) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/debug/server_cdp.rb', line 204

def send_chrome_response req
  @repl = false
  case req
  when /^GET\s\/json\/version\sHTTP\/1.1/
    body = {
      Browser: "ruby/v#{RUBY_VERSION}",
      'Protocol-Version': "1.1"
    }
    send_http_res body
    raise UI_ServerBase::RetryConnection

  when /^GET\s\/json\sHTTP\/1.1/
    @uuid = @uuid || SecureRandom.uuid
    addr = @local_addr.inspect_sockaddr
    body = [{
      description: "ruby instance",
      devtoolsFrontendUrl: "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=#{addr}/#{@uuid}",
      id: @uuid,
      title: $0,
      type: "node",
      url: "file://#{File.absolute_path($0)}",
      webSocketDebuggerUrl: "ws://#{addr}/#{@uuid}"
    }]
    send_http_res body
    raise UI_ServerBase::RetryConnection

  when /^GET\s\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\sHTTP\/1.1/
    raise 'Incorrect uuid' unless $1 == @uuid

    @need_pause_at_first = false
    CONFIG.set_config no_color: true

    @ws_server = WebSocketServer.new(@sock)
    @ws_server.handshake
  end
end

#send_event(method, **params) ⇒ Object Also known as: fire_event



458
459
460
# File 'lib/debug/server_cdp.rb', line 458

def send_event method, **params
  @ws_server.send method: method, params: params
end

#send_fail_response(req, **res) ⇒ Object Also known as: respond_fail



454
455
456
# File 'lib/debug/server_cdp.rb', line 454

def send_fail_response req, **res
  @ws_server.send id: req['id'], error: res
end

#send_http_res(body) ⇒ Object



241
242
243
244
245
# File 'lib/debug/server_cdp.rb', line 241

def send_http_res body
  json = JSON.generate body
  header = "HTTP/1.0 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\nCache-Control: no-cache\r\nContent-Length: #{json.bytesize}\r\n\r\n"
  @sock.puts "#{header}#{json}"
end

#send_response(req, **res) ⇒ Object Also known as: respond



450
451
452
# File 'lib/debug/server_cdp.rb', line 450

def send_response req, **res
  @ws_server.send id: req['id'], result: res
end

#sock(skip: false) {|$stderr| ... } ⇒ Object

Yields:

  • ($stderr)


687
688
689
# File 'lib/debug/server_cdp.rb', line 687

def sock skip: false
  yield $stderr
end