module DEBUGGER__::UI_CDP - debug: Ruby Standard Library Documentation

# module DEBUGGER__::UI_CDP

### Constants

INVALID_REQUEST
ITERATIONS
SHOW_PROTOCOL
TIMEOUT_SEC

### Public Class Methods

get_chrome_path(candidates) click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/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_chrome() click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/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

### Public Instance Methods

activate_bp(bps) click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/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_reader() click to toggle source
Calls superclass method

    # File debug-1.10.0/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_bp() click to toggle source

    # File debug-1.10.0/lib/debug/server_cdp.rb, line 670
    def deactivate_bp
      @q_msg << 'del'
      @q_ans << 'y'
    end

del_bp(bps, k) click to toggle source

    # File debug-1.10.0/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

fire_event(method, **params)
Alias for: send_event
get_source_code(path) click to toggle source

    # File debug-1.10.0/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

process() click to toggle source

    # File debug-1.10.0/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 = "") click to toggle source

    # File debug-1.10.0/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

respond(req, **res)

Called by the SESSION thread

Alias for: send_response
respond_fail(req, **res)
Alias for: send_fail_response
send_chrome_response(req) click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/lib/debug/server_cdp.rb, line 458
    def send_event method, **params
      @ws_server.send method: method, params: params
    end

Also aliased as: fire_event
send_fail_response(req, **res) click to toggle source

    # File debug-1.10.0/lib/debug/server_cdp.rb, line 454
    def send_fail_response req, **res
      @ws_server.send id: req['id'], error: res
    end

Also aliased as: respond_fail
send_http_res(body) click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/lib/debug/server_cdp.rb, line 450
    def send_response req, **res
      @ws_server.send id: req['id'], result: res
    end

Also aliased as: respond
sock(skip: false) { |$stderr| ... } click to toggle source

    # File debug-1.10.0/lib/debug/server_cdp.rb, line 687
    def sock skip: false
      yield $stderr
    end