5

I am using Capybara Selenium to run headless Chrome, which works great, except I cannot figure out how to use remote debugging. When I add --remote-debugging-port=4444 or --remote-debugging-port=9222 or --remote-debugging-port=9521, Selenium no longer connects to the browser to run the test.

How do I get remote debugging to work? Here is my code for reference:

Capybara.register_driver :selenium do |app|
  # from https://github.com/SeleniumHQ/selenium/issues/3738
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(loggingPrefs: {browser: 'ALL'})
  options = Selenium::WebDriver::Chrome::Options.new
  options.add_argument '--disable-infobars' # hide info bar about chrome automating test
  # if we don't use this flag, every selenium test will die with the error:
  # "unknown error: Chrome failed to start: exited abnormally"
  options.add_argument '--no-sandbox'
  # BREAKS THINGS if uncommented
  # options.add_argument '--remote-debugging-port=4444'
  options.add_argument '--headless'
  options.add_argument '--window-size=1600,2400'
  options.add_preference('profile.default_content_settings.popups', 0)
  options.add_preference('download.default_directory', DownloadHelpers::PATH.to_s)
  Capybara::Selenium::Driver.new(
    app,
    clear_local_storage: true,
    clear_session_storage: true,
    browser: :chrome,
    options: options,
    desired_capabilities: capabilities,
  )
end
rogersillito
  • 869
  • 1
  • 10
  • 27
Julie
  • 1,941
  • 3
  • 17
  • 30

3 Answers3

6

Since chrome 67 and chromedriver 2.39, chromedriver now correctly uses the port you specify with --remote-debugging-port. This removes quite a bit of complexity from my answer above. The steps I now take, which work for my use case of needing to configure download settings using chrome_remote, are as follows:

It makes uses of a nodejs library, crmux - which allows multiple clients to connect to the remote debug port of chrome at the same time.

  1. Get nodejs installed first: Nodejs v9.7.0 works fine
  2. Install crmux by running npm install crmux -g
  3. Before you start chromedriver (Capybara::Selenium::Driver.new), you need to spawn a separate thread that will fire up crmux, which will let both you and chromedriver communicate with chrome itself via the port you specified in Capybara (4444):

    crmux --port=4444 --listen=4444

  4. You may want to add a sleep 3 after the spawn command in the main script/thread to give time for crmux to start before you continue with your test startup.

You can then use chrome_remote (for example) to access chrome using port 4444, while capybara is doing its thing.

rogersillito
  • 869
  • 1
  • 10
  • 27
  • It doesn't seem to work when `--port` and `--listen` are set to the same port. Is that a typo? (`chrome` gives ` bind() returned an error, errno=98: Address already in use` if `crmux` is already listening to the port.) How did you get this to work? I'm not clear yet which things to point to which ports. Could you edit your answer with example source code? – Tyler Rick Dec 20 '18 at 22:13
  • 1
    Oops, part of my problem that I was using chromedriver 2.37, not 2.39+. Upgrading to 2.45 fixed the first of my problems (that `Selenium::WebDriver::Chrome` hung when trying to handshake). Now to figure out the ports... – Tyler Rick Dec 20 '18 at 22:32
  • 2
    I finally figured it out: You can set the Chrome `--remote-debugging-port` to whatever you want using `browser_options.args << "--remote-debugging-port=9222"` (default is 0, which makes it choose random port). (Setting the port like this works regardless of whether using crmux.) Then you set up `crmux` to listen on a *different* port, like 9223 (the default), but you have to pass `--port 9222` with the *same* port as Chrome's "--remote-debugging-port" because that it is the port it proxies traffic *to*. Now you can connect to crmux's *listen* port from `chrome_remote`, another browser, etc. – Tyler Rick Dec 21 '18 at 00:46
  • 2
    In case you're interested, I wrote a gem to help integrate crmux and chrome_remote with Capybara similar to how you did it: https://github.com/TylerRick/capybara-chrome_dev_tools. Please let me know or contribute if you have any suggestions or improvements. I also added https://github.com/TylerRick/capybara-chrome_response_headers which uses `chrome_remote` to give you `response_headers` and `status_code` in your tests. – Tyler Rick Dec 21 '18 at 06:10
  • @TylerRick - `--port` and `--listen` were intentionally set to the same port. _I agree completely that this shouldn't work_ - however it does in my case. I'm also setting `--remote-debugging-port` as you describe when starting chrome itself. I'm working in a Windows environment... Great work on the gem BTW. – rogersillito Jan 04 '19 at 10:38
3

Updating my ChromeDriver fixed it for me. I didn't have to do anything else. Before it would hang when attempting to start the test.

Specifically I was on ChromeDriver 2.36 and I upgraded to ChromeDriver 2.40. I don't think the Chrome version was a problem, since I was on Chrome 67 both before and after.

Here's how I'm registering the driver:

Capybara.register_driver :headless_chrome do |app|
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
    chromeOptions: { args: %w[headless window-size=1280,960 remote-debugging-port=9222] }
  )
  Capybara::Selenium::Driver.new(app, browser: :chrome, desired_capabilities: capabilities)
end

After that I ran my test with a debugger (binding.pry) placed where I wanted to inspect. Then when I hit the debugger I navigated to http://localhost:9222/ in a normal instance of Chrome and was able to follow a link to view what was happening in the headless Chrome instance, including the browser console output I needed.

Calaway
  • 714
  • 6
  • 12
0

Update: if using versions since Chrome 67/chromedriver 2.39, my alternative answer above provides a simpler solution


The core issue here is Chromedriver also uses the remote debugging port connection to communicate with Chrome. This uses the websocket protocol, which only supports a single-client connected at a time. Normally, when chromedriver starts the chromedriver process, it will choose a random free TCP port number and use this to access the remote debug port. If you specify --remote-debuggging-port=9222 however, Chrome will be opened with the debug port you have requested, but chromedriver will silently continue to try and open a connection using this random port number.

The solution I ended up with was heavily inspired by comment 20 in this chromedriver issue. It required quite a bit of code to get it working, but works solidly. It makes uses of a nodejs library, crmux - which allows multiple clients to connect to the remote debug port of chrome at the same time.

  1. Get nodejs installed first: Nodejs v9.7.0 works fine
  2. Install crmux by running npm install crmux -g
  3. Before you start chromedriver (Capybara::Selenium::Driver.new), you need to spawn a separate thread that will do a few things: look for the remote debug port chromedriver is trying to use to connect to chrome, and then use this to fire up crmux. Once this has happened Capybara etc will work as normal.
  4. My separate thread runs a ruby script that first executes a netstat command repeatedly until it finds the relevant entry for chromedriver (TCP status is SYN_SENT). This separate thread must continue to run in the background while chrome is up and running.

The code for this is:

$chrdrv_wait_timeout = 60
$chrdrv_exe = "chromedriver.exe"

def get_netstat_output
  stdout = `netstat -a -b -n`
  stat_lines = stdout.split("\n")
  stat_lines
end

def try_get_requested_port
  socket_state = "SYN_SENT" # i.e. sent with no reply
  statout = get_netstat_output
  n = statout.length
  i = 0
  loop do
    i += 1
    # find lines relating to chromedriver
    exe_match = /^ +\[#{$chrdrv_exe}\]$/.match statout[i]
    if exe_match != nil
      # check preceeding lines which should contain port info
      port_match = /TCP.*:([0-9]+)\W+#{socket_state}/.match statout[i-1]
      if port_match != nil
        return port_match[1].to_i
      end
    end
    break unless i < n
  end
  return nil
end

def get_tcp_port_requested_by_chromedriver
  i = 1
  loop do
    puts "Waiting for #{$chrdrv_exe}: #{i}"
    port = try_get_requested_port
    if port != nil
      return port
    end
    break unless i < $chrdrv_wait_timeout
    sleep 1
    i += 1
  end
  raise Exception, "Failed to get TCP port requested by #{$chrdrv_exe} (gave up after #{$chrdrv_wait_timeout} seconds)"
end

(I'm working in Windows: for Mac/Linux the netstat syntax/output is probably different so the code will need adjustment; the key thing is you need it to output the executable owner of each connection entry - and parse the bit relating to chromedriver to get the port in question).

  1. Once this has found the random port (I'll use 12225 as an example), the background ruby script can then execute a crmux process, which will reunite chromedriver with chrome itself via the port you specified in Capybara (4444):

    crmux --port=4444 --listen=12225

  2. Finally, this separate script saves the discovered listen port to a text file. This allows the main script/thread that is running capybara to know the port number it can use to get access to chrome (via crmux's multiplexed connection) by reading in the port from that file. So you can then use chrome_remote to access chrome using port 12225, for example, while capybara is doing its thing.

Tyler Rick
  • 9,191
  • 6
  • 60
  • 60
rogersillito
  • 869
  • 1
  • 10
  • 27