123456789_123456789_123456789_123456789_123456789_

Module: DEBUGGER__::UI_CDP

Relationships & Source Files
Namespace Children
Modules:
Classes:
Exceptions:
Defined in: lib/debug/server_cdp.rb

Constant Summary

Class Method Summary

Instance Method Summary

Class Method Details

.get_chrome_path(candidates)

Raises:

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 178

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

.get_devtools_endpoint(tf)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 189

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

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 87

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
    uuid = SecureRandom.uuid
    # 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)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 21

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&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)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 651

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

#cleanup_reader

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 668

def cleanup_reader
  super
  Process.kill :KILL, @chrome_pid if @chrome_pid
end

#deactivate_bp

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 663

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

#del_bp(bps, k)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 634

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(event, **result)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 683

def fire_event event, **result
  if result.empty?
    send_event event
  else
    send_event event, **result
  end
end

#get_source_code(path)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 643

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

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

#process

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 474

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
      @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

    # breakpoint
    when 'Debugger.getPossibleBreakpoints'
      @q_msg << req
    when 'Debugger.setBreakpointByUrl'
      line = req.dig('params', 'lineNumber')
      if regexp = req.dig('params', 'urlRegex')
        path = regexp.match(/(.*)\|/)[1].gsub("\\", "")
        cond = req.dig('params', 'condition')
        src = get_source_code path
        end_line = src.lines.count
        line = end_line  if line > end_line
        b_id = "1:#{line}:#{regexp}"
        if cond != ''
          SESSION.add_line_breakpoint(path, line + 1, cond: cond)
        else
          SESSION.add_line_breakpoint(path, line + 1)
        end
        bps[b_id] = bps.size
        # 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
      elsif url = req.dig('params', 'url')
        b_id = "#{line}:#{url}"
        send_response req,
                      breakpointId: b_id,
                      locations: []
      elsif hash = req.dig('params', 'scriptHash')
        b_id = "#{line}:#{hash}"
        send_response req,
                      breakpointId: b_id,
                      locations: []
      else
        raise 'Unsupported'
      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)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 695

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

#respond(req, **result)

Called by the SESSION thread

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 675

def respond req, **result
  send_response req, **result
end

#respond_fail(req, **result)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 679

def respond_fail req, **result
  send_fail_response req, **result
end

#send_chrome_response(req)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 206

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)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 464

def send_event method, **params
  if params.empty?
    @ws_server.send method: method, params: {}
  else
    @ws_server.send method: method, params: params
  end
end

#send_fail_response(req, **res)

[ GitHub ]

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

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

#send_http_res(body)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 243

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)

[ GitHub ]

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

def send_response req, **res
  if res.empty?
    @ws_server.send id: req['id'], result: {}
  else
    @ws_server.send id: req['id'], result: res
  end
end

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

Yields:

  • ($stderr)
[ GitHub ]

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

def sock skip: false
  yield $stderr
end