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.9.1/lib/debug/server_cdp.rb, line 177
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.9.1/lib/debug/server_cdp.rb, line 188
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.9.1/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
    # 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.9.1/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&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.9.1/lib/debug/server_cdp.rb, line 659
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.9.1/lib/debug/server_cdp.rb, line 624
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.9.1/lib/debug/server_cdp.rb, line 676
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.9.1/lib/debug/server_cdp.rb, line 671
def deactivate_bp
  @q_msg << 'del'
  @q_ans << 'y'
end
del_bp(bps, k) click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 642
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.9.1/lib/debug/server_cdp.rb, line 651
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.9.1/lib/debug/server_cdp.rb, line 465
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.9.1/lib/debug/server_cdp.rb, line 692
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.9.1/lib/debug/server_cdp.rb, line 205
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.9.1/lib/debug/server_cdp.rb, line 459
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.9.1/lib/debug/server_cdp.rb, line 455
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.9.1/lib/debug/server_cdp.rb, line 242
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.9.1/lib/debug/server_cdp.rb, line 451
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.9.1/lib/debug/server_cdp.rb, line 688
def sock skip: false
  yield $stderr
end