Skip to content

Commit

Permalink
Generalize SocketLock to account for Chrome port race condition
Browse files Browse the repository at this point in the history
When the Chrome browser boots up via the Ruby library, it attempts
to find a free port and launch the bridge on that port. However, it's
possible that, if you're running tests in parallel, two bridges will
try to bind to the same port at the same time. This will cause a very
obtuse "end of file" error as documented [here][1].

This patch is based on one initially proposed in the original
[bug report][2]. However, it goes a step further, taking the technical
comments into account, and generalizing the `SocketLock` class used
in the Firefox launcher to assist.

[1]: grosser/parallel_tests#322
[2]: https://code.google.com/p/selenium/issues/detail?id=8241

Signed-off-by: Alexei Barantsev <barancev@gmail.com>
  • Loading branch information
diurnalist authored and barancev committed Jun 2, 2015
1 parent d6b0a91 commit 437725c
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 100 deletions.
68 changes: 47 additions & 21 deletions rb/lib/selenium/webdriver/chrome/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ module Chrome
#
# @api private
#

class Service
START_TIMEOUT = 20
STOP_TIMEOUT = 5
DEFAULT_PORT = 9515
MISSING_TEXT = "Unable to find the chromedriver executable. Please download the server from http://chromedriver.storage.googleapis.com/index.html and place it somewhere on your PATH. More info at https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver."

attr_reader :uri
START_TIMEOUT = 20
SOCKET_LOCK_TIMEOUT = 45
STOP_TIMEOUT = 5
DEFAULT_PORT = 9515
MISSING_TEXT = "Unable to find the chromedriver executable. Please download the server from http://chromedriver.storage.googleapis.com/index.html and place it somewhere on your PATH. More info at https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver."

def self.executable_path
@executable_path ||= (
Expand All @@ -47,33 +45,33 @@ def self.executable_path=(path)
end

def self.default_service(*extra_args)
new executable_path, PortProber.above(DEFAULT_PORT), *extra_args
new executable_path, DEFAULT_PORT, *extra_args
end

def initialize(executable_path, port, *extra_args)
@uri = URI.parse "http://#{Platform.localhost}:#{port}"
server_command = [executable_path, "--port=#{port}", *extra_args]
@executable_path = executable_path
@host = Platform.localhost
@port = Integer(port)

@process = ChildProcess.build(*server_command)
@socket_poller = SocketPoller.new Platform.localhost, port, START_TIMEOUT
raise Error::WebDriverError, "invalid port: #{@port}" if @port < 1

@process.io.inherit! if $DEBUG == true
@extra_args = extra_args
end

def start
@process.start
Platform.exit_hook { stop } # make sure we don't leave the server running

unless @socket_poller.connected?
raise Error::WebDriverError, "unable to connect to chromedriver #{@uri}"
socket_lock.locked do
find_free_port
start_process
connect_until_stable
end

Platform.exit_hook { stop } # make sure we don't leave the server running
end

def stop
return if @process.nil? || @process.exited?

Net::HTTP.start(uri.host, uri.port) do |http|
Net::HTTP.start(@host, @port) do |http|
http.open_timeout = STOP_TIMEOUT / 2
http.read_timeout = STOP_TIMEOUT / 2

Expand All @@ -85,8 +83,36 @@ def stop
# ok, force quit
@process.stop STOP_TIMEOUT
end
end # Service

def uri
URI.parse "http://#{@host}:#{@port}"
end

def find_free_port
@port = PortProber.above @port
end

def start_process
server_command = [@executable_path, "--port=#{@port}", *@extra_args]
@process = ChildProcess.build(*server_command)

@process.io.inherit! if $DEBUG == true
@process.start
end

def connect_until_stable
@socket_poller = SocketPoller.new @host, @port, START_TIMEOUT

unless @socket_poller.connected?
raise Error::WebDriverError, "unable to connect to chromedriver #{@host}:#{@port}"
end
end

def socket_lock
@socket_lock ||= SocketLock.new(@port - 1, SOCKET_LOCK_TIMEOUT)
end

end # Service
end # Chrome
end # WebDriver
end # Service
end # Service
1 change: 1 addition & 0 deletions rb/lib/selenium/webdriver/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
require 'selenium/webdriver/common/proxy'
require 'selenium/webdriver/common/log_entry'
require 'selenium/webdriver/common/file_reaper'
require 'selenium/webdriver/common/socket_lock'
require 'selenium/webdriver/common/socket_poller'
require 'selenium/webdriver/common/port_prober'
require 'selenium/webdriver/common/zipper'
Expand Down
76 changes: 76 additions & 0 deletions rb/lib/selenium/webdriver/common/socket_lock.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

module Selenium
module WebDriver
class SocketLock

def initialize(port, timeout)
@port = port
@timeout = timeout
end

#
# Attempt to acquire a lock on the given port. Control is yielded to an
# execution block if the lock could be successfully obtained.
#

def locked(&blk)
lock

begin
yield
ensure
release
end
end

private

def lock
max_time = Time.now + @timeout

until can_lock? || Time.now >= max_time
sleep 0.1
end

unless did_lock?
raise Error::WebDriverError, "unable to bind to locking port #{@port} within #{@timeout} seconds"
end
end

def release
@server && @server.close
end

def can_lock?
@server = TCPServer.new(Platform.localhost, @port)
ChildProcess.close_on_exec @server

true
rescue SocketError, Errno::EADDRINUSE, Errno::EBADF => ex
$stderr.puts "#{self}: #{ex.message}" if $DEBUG
false
end

def did_lock?
!!@server
end

end # SocketLock
end # WebDriver
end # Selenium
1 change: 0 additions & 1 deletion rb/lib/selenium/webdriver/firefox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

require 'selenium/webdriver/firefox/util'
require 'selenium/webdriver/firefox/extension'
require 'selenium/webdriver/firefox/socket_lock'
require 'selenium/webdriver/firefox/binary'
require 'selenium/webdriver/firefox/profiles_ini'
require 'selenium/webdriver/firefox/profile'
Expand Down
78 changes: 0 additions & 78 deletions rb/lib/selenium/webdriver/firefox/socket_lock.rb

This file was deleted.

0 comments on commit 437725c

Please sign in to comment.