Skip to content

Commit 437725c

Browse files
diurnalistbarancev
authored andcommitted
Generalize SocketLock to account for Chrome port race condition
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 <[email protected]>
1 parent d6b0a91 commit 437725c

File tree

5 files changed

+124
-100
lines changed

5 files changed

+124
-100
lines changed

rb/lib/selenium/webdriver/chrome/service.rb

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ module Chrome
2222
#
2323
# @api private
2424
#
25-
2625
class Service
27-
START_TIMEOUT = 20
28-
STOP_TIMEOUT = 5
29-
DEFAULT_PORT = 9515
30-
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."
31-
32-
attr_reader :uri
26+
START_TIMEOUT = 20
27+
SOCKET_LOCK_TIMEOUT = 45
28+
STOP_TIMEOUT = 5
29+
DEFAULT_PORT = 9515
30+
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."
3331

3432
def self.executable_path
3533
@executable_path ||= (
@@ -47,33 +45,33 @@ def self.executable_path=(path)
4745
end
4846

4947
def self.default_service(*extra_args)
50-
new executable_path, PortProber.above(DEFAULT_PORT), *extra_args
48+
new executable_path, DEFAULT_PORT, *extra_args
5149
end
5250

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

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

60-
@process.io.inherit! if $DEBUG == true
58+
@extra_args = extra_args
6159
end
6260

6361
def start
64-
@process.start
62+
Platform.exit_hook { stop } # make sure we don't leave the server running
6563

66-
unless @socket_poller.connected?
67-
raise Error::WebDriverError, "unable to connect to chromedriver #{@uri}"
64+
socket_lock.locked do
65+
find_free_port
66+
start_process
67+
connect_until_stable
6868
end
69-
70-
Platform.exit_hook { stop } # make sure we don't leave the server running
7169
end
7270

7371
def stop
7472
return if @process.nil? || @process.exited?
7573

76-
Net::HTTP.start(uri.host, uri.port) do |http|
74+
Net::HTTP.start(@host, @port) do |http|
7775
http.open_timeout = STOP_TIMEOUT / 2
7876
http.read_timeout = STOP_TIMEOUT / 2
7977

@@ -85,8 +83,36 @@ def stop
8583
# ok, force quit
8684
@process.stop STOP_TIMEOUT
8785
end
88-
end # Service
8986

87+
def uri
88+
URI.parse "http://#{@host}:#{@port}"
89+
end
90+
91+
def find_free_port
92+
@port = PortProber.above @port
93+
end
94+
95+
def start_process
96+
server_command = [@executable_path, "--port=#{@port}", *@extra_args]
97+
@process = ChildProcess.build(*server_command)
98+
99+
@process.io.inherit! if $DEBUG == true
100+
@process.start
101+
end
102+
103+
def connect_until_stable
104+
@socket_poller = SocketPoller.new @host, @port, START_TIMEOUT
105+
106+
unless @socket_poller.connected?
107+
raise Error::WebDriverError, "unable to connect to chromedriver #{@host}:#{@port}"
108+
end
109+
end
110+
111+
def socket_lock
112+
@socket_lock ||= SocketLock.new(@port - 1, SOCKET_LOCK_TIMEOUT)
113+
end
114+
115+
end # Service
90116
end # Chrome
91117
end # WebDriver
92-
end # Service
118+
end # Service

rb/lib/selenium/webdriver/common.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
require 'selenium/webdriver/common/proxy'
2424
require 'selenium/webdriver/common/log_entry'
2525
require 'selenium/webdriver/common/file_reaper'
26+
require 'selenium/webdriver/common/socket_lock'
2627
require 'selenium/webdriver/common/socket_poller'
2728
require 'selenium/webdriver/common/port_prober'
2829
require 'selenium/webdriver/common/zipper'
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
module Selenium
19+
module WebDriver
20+
class SocketLock
21+
22+
def initialize(port, timeout)
23+
@port = port
24+
@timeout = timeout
25+
end
26+
27+
#
28+
# Attempt to acquire a lock on the given port. Control is yielded to an
29+
# execution block if the lock could be successfully obtained.
30+
#
31+
32+
def locked(&blk)
33+
lock
34+
35+
begin
36+
yield
37+
ensure
38+
release
39+
end
40+
end
41+
42+
private
43+
44+
def lock
45+
max_time = Time.now + @timeout
46+
47+
until can_lock? || Time.now >= max_time
48+
sleep 0.1
49+
end
50+
51+
unless did_lock?
52+
raise Error::WebDriverError, "unable to bind to locking port #{@port} within #{@timeout} seconds"
53+
end
54+
end
55+
56+
def release
57+
@server && @server.close
58+
end
59+
60+
def can_lock?
61+
@server = TCPServer.new(Platform.localhost, @port)
62+
ChildProcess.close_on_exec @server
63+
64+
true
65+
rescue SocketError, Errno::EADDRINUSE, Errno::EBADF => ex
66+
$stderr.puts "#{self}: #{ex.message}" if $DEBUG
67+
false
68+
end
69+
70+
def did_lock?
71+
!!@server
72+
end
73+
74+
end # SocketLock
75+
end # WebDriver
76+
end # Selenium

rb/lib/selenium/webdriver/firefox.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
require 'selenium/webdriver/firefox/util'
2323
require 'selenium/webdriver/firefox/extension'
24-
require 'selenium/webdriver/firefox/socket_lock'
2524
require 'selenium/webdriver/firefox/binary'
2625
require 'selenium/webdriver/firefox/profiles_ini'
2726
require 'selenium/webdriver/firefox/profile'

rb/lib/selenium/webdriver/firefox/socket_lock.rb

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)