Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 17743ef

Browse files
committed
Bug 1571426 - Read geckodriver port number in fixture, r=webdriver-reviewers,jdescottes
Instead of passing in a port selected in the fixture, pass in 0 as the port, allowing geckodriver to select the port, and then read the port number from the utput in stdout. This should prevent us trying to reuse an already bound port in a race-free way. Differential Revision: https://phabricator.services.mozilla.com/D189926
1 parent 1164415 commit 17743ef

File tree

2 files changed

+50
-35
lines changed

2 files changed

+50
-35
lines changed

testing/web-platform/mozilla/tests/webdriver/support/fixtures.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import argparse
22
import json
33
import os
4+
import re
45
import socket
56
import subprocess
7+
import threading
68
import time
79
from contextlib import suppress
810
from urllib.parse import urlparse
@@ -12,8 +14,6 @@
1214
from mozprofile import Preferences, Profile
1315
from mozrunner import FirefoxRunner
1416

15-
from support.network import get_free_port
16-
1717

1818
def get_arg_value(arg_names, args):
1919
"""Get an argument value from a list of arguments
@@ -232,6 +232,8 @@ def wait(self):
232232

233233

234234
class Geckodriver:
235+
PORT_RE = re.compile(b".*Listening on [^ :]*:(\d+)")
236+
235237
def __init__(self, configuration, hostname=None, extra_args=None):
236238
self.config = configuration["webdriver"]
237239
self.requested_capabilities = configuration["capabilities"]
@@ -241,12 +243,11 @@ def __init__(self, configuration, hostname=None, extra_args=None):
241243

242244
self.command = None
243245
self.proc = None
244-
self.port = get_free_port()
246+
self.port = None
247+
self.reader_thread = None
245248

246-
capabilities = {"alwaysMatch": self.requested_capabilities}
247-
self.session = webdriver.Session(
248-
self.hostname, self.port, capabilities=capabilities
249-
)
249+
self.capabilities = {"alwaysMatch": self.requested_capabilities}
250+
self.session = None
250251

251252
@property
252253
def remote_agent_port(self):
@@ -257,14 +258,20 @@ def remote_agent_port(self):
257258

258259
def start(self):
259260
self.command = (
260-
[self.config["binary"], "--port", str(self.port)]
261+
[self.config["binary"], "--port", "0"]
261262
+ self.config["args"]
262263
+ self.extra_args
263264
)
264265

265266
print(f"Running command: {' '.join(self.command)}")
266-
self.proc = subprocess.Popen(self.command, env=self.env)
267+
self.proc = subprocess.Popen(self.command, env=self.env, stdout=subprocess.PIPE)
267268

269+
self.reader_thread = threading.Thread(
270+
target=readOutputLine,
271+
args=(self.proc.stdout, self.processOutputLine),
272+
daemon=True,
273+
)
274+
self.reader_thread.start()
268275
# Wait for the port to become ready
269276
end_time = time.time() + 10
270277
while time.time() < end_time:
@@ -273,24 +280,53 @@ def start(self):
273280
raise ChildProcessError(
274281
f"geckodriver terminated with code {returncode}"
275282
)
276-
with socket.socket() as sock:
277-
if sock.connect_ex((self.hostname, self.port)) == 0:
278-
break
283+
if self.port is not None:
284+
with socket.socket() as sock:
285+
if sock.connect_ex((self.hostname, self.port)) == 0:
286+
break
287+
else:
288+
time.sleep(0.1)
279289
else:
290+
if self.port is None:
291+
raise OSError(
292+
f"Failed to read geckodriver port started on {self.hostname}"
293+
)
280294
raise ConnectionRefusedError(
281295
f"Failed to connect to geckodriver on {self.hostname}:{self.port}"
282296
)
283297

298+
self.session = webdriver.Session(
299+
self.hostname, self.port, capabilities=self.capabilities
300+
)
301+
284302
return self
285303

286-
def stop(self):
287-
self.delete_session()
304+
def processOutputLine(self, line):
305+
if self.port is None:
306+
m = self.PORT_RE.match(line)
307+
if m is not None:
308+
self.port = int(m.groups()[0])
288309

310+
def stop(self):
311+
if self.session is not None:
312+
self.delete_session()
289313
if self.proc:
290314
self.proc.kill()
315+
self.port = None
316+
if self.reader_thread is not None:
317+
self.reader_thread.join()
291318

292319
def new_session(self):
293320
self.session.start()
294321

295322
def delete_session(self):
296323
self.session.end()
324+
325+
326+
def readOutputLine(stream, callback):
327+
while True:
328+
line = stream.readline()
329+
if not line:
330+
break
331+
332+
callback(line)

testing/web-platform/mozilla/tests/webdriver/support/network.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import socket
21
from http.client import HTTPConnection
32

43

@@ -47,26 +46,6 @@ def http_request(server_host, server_port, path="/status", host=None, origin=Non
4746
return conn.getresponse()
4847

4948

50-
def get_free_port():
51-
"""Get a random unbound port"""
52-
max_attempts = 10
53-
err = None
54-
for _ in range(max_attempts):
55-
s = socket.socket()
56-
try:
57-
s.bind(("127.0.0.1", 0))
58-
except OSError as e:
59-
err = e
60-
continue
61-
else:
62-
return s.getsockname()[1]
63-
finally:
64-
s.close()
65-
if err is None:
66-
err = Exception("Failed to get a free port")
67-
raise err
68-
69-
7049
def get_host(port_type, hostname, server_port):
7150
if port_type == "default_port":
7251
return hostname

0 commit comments

Comments
 (0)