Skip to content

Commit 8ce6078

Browse files
author
xianglongfei_uniontech
committed
Fixed the issue #5550.Supports specifying port ranges, with automatic matching between listen and URI.
Signed-off-by: xianglongfei <xianglongfei@uniontech.com>
1 parent 74b7379 commit 8ce6078

File tree

4 files changed

+149
-19
lines changed

4 files changed

+149
-19
lines changed

avocado/core/nrunner/task.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,51 @@ def _create_connection(self):
5353
Creates connection with `self.uri` based on `socket.create_connection`
5454
"""
5555
if ":" in self.uri:
56-
host, port = self.uri.split(":")
57-
port = int(port)
58-
for _ in range(600):
56+
host, port_spec = self.uri.rsplit(":", 1)
57+
if "-" in port_spec:
58+
start_s, end_s = port_spec.split("-", 1)
5959
try:
60-
self._connection = socket.create_connection((host, port))
61-
break
62-
except ConnectionRefusedError as error:
63-
LOG.warning(error)
64-
time.sleep(1)
60+
start = int(start_s)
61+
end = int(end_s)
62+
except ValueError as exc:
63+
raise ValueError(
64+
f"Invalid port range in status server URI: {self.uri}"
65+
) from exc
66+
if start > end:
67+
raise ValueError(
68+
f"Invalid port range (start > end) in status server URI: {self.uri}"
69+
)
70+
last_error = None
71+
for port in range(start, end + 1):
72+
for _ in range(600):
73+
try:
74+
self._connection = socket.create_connection((host, port))
75+
break
76+
except ConnectionRefusedError as error:
77+
last_error = error
78+
LOG.warning(error)
79+
time.sleep(1)
80+
if self._connection is not None:
81+
break
82+
else:
83+
# If we could not connect to any port in the range even
84+
# after retries, raise the last connection error (if any)
85+
# or attempt one final connection to the last port so that
86+
# the underlying exception is surfaced.
87+
if last_error is not None:
88+
raise last_error
89+
self._connection = socket.create_connection((host, end))
6590
else:
66-
self._connection = socket.create_connection((host, port))
91+
port = int(port_spec)
92+
for _ in range(600):
93+
try:
94+
self._connection = socket.create_connection((host, port))
95+
break
96+
except ConnectionRefusedError as error:
97+
LOG.warning(error)
98+
time.sleep(1)
99+
else:
100+
self._connection = socket.create_connection((host, port))
67101
else:
68102
self._connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
69103
self._connection.connect(self.uri)

avocado/core/status/server.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33

44
from avocado.core.settings import settings
5+
from avocado.core.output import LOG_JOB
56

67

78
class StatusServer:
@@ -27,15 +28,49 @@ def uri(self):
2728
async def create_server(self):
2829
limit = settings.as_dict().get("run.status_server_buffer_size")
2930
if ":" in self._uri:
30-
host, port = self._uri.split(":")
31-
port = int(port)
32-
self._server_task = await asyncio.start_server(
33-
self.cb, host=host, port=port, limit=limit
34-
)
31+
host, port_spec = self._uri.rsplit(":", 1)
32+
if "-" in port_spec:
33+
start_s, end_s = port_spec.split("-", 1)
34+
try:
35+
start = int(start_s)
36+
end = int(end_s)
37+
except ValueError as exc:
38+
raise ValueError(
39+
f"Invalid port range in status server URI: {self._uri}"
40+
) from exc
41+
if start > end:
42+
raise ValueError(
43+
f"Invalid port range (start > end) in status server URI: {self._uri}"
44+
)
45+
last_exc = None
46+
for port in range(start, end + 1):
47+
try:
48+
self._server_task = await asyncio.start_server(
49+
self.cb, host=host, port=port, limit=limit
50+
)
51+
# Update URI to the actual chosen port so that
52+
# other components can rely on the concrete value.
53+
self._uri = f"{host}:{port}"
54+
LOG_JOB.info("Status server listening on %s", self._uri)
55+
break
56+
except OSError as exc:
57+
last_exc = exc
58+
if self._server_task is None:
59+
# Re-raise the last failure or a generic error if none.
60+
raise last_exc or OSError(
61+
f"Could not bind status server to any port in range {start}-{end} on {host}"
62+
)
63+
else:
64+
port = int(port_spec)
65+
self._server_task = await asyncio.start_server(
66+
self.cb, host=host, port=port, limit=limit
67+
)
68+
LOG_JOB.info("Status server listening on %s", self._uri)
3569
else:
3670
self._server_task = await asyncio.start_unix_server(
3771
self.cb, path=self._uri, limit=limit
3872
)
73+
LOG_JOB.info("Status server listening on %s", self._uri)
3974

4075
async def serve_forever(self):
4176
if self._server_task is None:

avocado/plugins/runner_nrunner.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import os
2222
import platform
2323
import random
24+
import socket
2425
import tempfile
2526

2627
from avocado.core.dispatcher import SpawnerDispatcher
@@ -211,6 +212,53 @@ def __init__(self):
211212
super().__init__()
212213
self.status_server_dir = None
213214

215+
@staticmethod
216+
def _resolve_listen_address(listen):
217+
"""
218+
Resolves a listen URI that may contain a port range into a concrete
219+
host:port endpoint.
220+
221+
This is used so that both the status server and the tasks share the
222+
same, concrete port instead of independently trying to guess a free
223+
port within a range.
224+
"""
225+
if ":" not in listen:
226+
return listen
227+
host, port_spec = listen.rsplit(":", 1)
228+
if "-" not in port_spec:
229+
return listen
230+
231+
start_s, end_s = port_spec.split("-", 1)
232+
try:
233+
start = int(start_s)
234+
end = int(end_s)
235+
except ValueError as exc:
236+
raise JobError(
237+
f"Invalid port range in status server URI: {listen}"
238+
) from exc
239+
if start > end:
240+
raise JobError(
241+
f"Invalid port range (start > end) in status server URI: {listen}"
242+
)
243+
244+
last_exc = None
245+
for port in range(start, end + 1):
246+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
247+
try:
248+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
249+
sock.bind((host, port))
250+
# If we can bind here, the real status server should also be
251+
# able to bind to the same port later.
252+
return f"{host}:{port}"
253+
except OSError as exc:
254+
last_exc = exc
255+
finally:
256+
sock.close()
257+
258+
raise JobError(
259+
f"Could not bind status server to any port in range {start}-{end} on {host}: {last_exc}"
260+
)
261+
214262
def _determine_status_server(self, test_suite, config_key):
215263
if test_suite.config.get("run.status_server_auto"):
216264
# no UNIX domain sockets on Windows
@@ -240,10 +288,18 @@ def _sync_status_server_urls(self, config):
240288
def _create_status_server(self, test_suite, job):
241289
self._sync_status_server_urls(test_suite.config)
242290
listen = self._determine_status_server(test_suite, "run.status_server_listen")
291+
resolved_listen = self._resolve_listen_address(listen)
292+
if resolved_listen != listen:
293+
# Keep configuration consistent so that tasks will connect to the
294+
# exact same endpoint chosen here.
295+
test_suite.config["run.status_server_listen"] = resolved_listen
296+
server_uri = test_suite.config.get("run.status_server_uri")
297+
if server_uri in (listen, DEFAULT_SERVER_URI):
298+
test_suite.config["run.status_server_uri"] = resolved_listen
243299
# pylint: disable=W0201
244300
self.status_repo = StatusRepo(job.unique_id)
245301
# pylint: disable=W0201
246-
self.status_server = StatusServer(listen, self.status_repo)
302+
self.status_server = StatusServer(resolved_listen, self.status_repo)
247303

248304
async def _update_status(self, job):
249305
message_handler = MessageHandler()

man/avocado.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,20 @@ Options for subcommand `run` (`avocado run --help`)::
185185
server.
186186
--status-server-listen HOST_PORT
187187
URI where status server will listen on. Usually a
188-
"HOST:PORT" string. This is only effective if
188+
"HOST:PORT" string, or a "HOST:START-END" port
189+
range (for example, "127.0.0.1:8888-9000") in which
190+
case an available port within the range will be
191+
chosen. This is only effective if
189192
"status_server_auto" is disabled. If
190193
"status_server_uri" is not set, the value from
191194
"status_server_listen " will be used.
192195
--status-server-uri HOST_PORT
193196
URI for connecting to the status server, usually a
194-
"HOST:PORT" string. Use this if your status server is
195-
in another host, or different port. This is only
196-
effective if "status_server_auto" is disabled. If
197+
"HOST:PORT" string, or a "HOST:START-END" port range
198+
where an available port within the range will be
199+
chosen. Use this if your status server is in another
200+
host, or different port. This is only effective if
201+
"status_server_auto" is disabled. If
197202
"status_server_listen" is not set, the value from
198203
"status_server_uri" will be used.
199204
--max-parallel-tasks NUMBER_OF_TASKS

0 commit comments

Comments
 (0)