Skip to content

Commit 29c2ef5

Browse files
authored
add message to why port is skipped (#5584)
* add message to why port is skipped * set SO_REUSEADDR to 1 * change tests * windows is weird * sock close * not both * use weird guy to not use list * != windows maybe * test 0.0.0.0 for CI * empty string ig * remove :: * maybe * maybe
1 parent 3154987 commit 29c2ef5

2 files changed

Lines changed: 48 additions & 70 deletions

File tree

reflex/utils/processes.py

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import signal
99
import socket
1010
import subprocess
11+
import sys
1112
from collections.abc import Callable, Generator, Sequence
1213
from concurrent import futures
1314
from contextlib import closing
@@ -68,12 +69,11 @@ def _can_bind_at_port(
6869
"""
6970
try:
7071
with closing(socket.socket(address_family, socket.SOCK_STREAM)) as sock:
72+
if sys.platform != "win32":
73+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
7174
sock.bind((address, port))
72-
except OverflowError:
73-
return False
74-
except PermissionError:
75-
return False
76-
except OSError:
75+
except (OverflowError, PermissionError, OSError) as e:
76+
console.warn(f"Unable to bind to {address}:{port} due to: {e}.")
7777
return False
7878
return True
7979

@@ -87,38 +87,13 @@ def is_process_on_port(port: int) -> bool:
8787
Returns:
8888
Whether a process is running on the given port.
8989
"""
90-
return not _can_bind_at_port( # Test IPv4 localhost (127.0.0.1)
91-
socket.AF_INET, "127.0.0.1", port
92-
) or not _can_bind_at_port(
93-
socket.AF_INET6, "::1", port
94-
) # Test IPv6 localhost (::1)
90+
return (
91+
not _can_bind_at_port(socket.AF_INET, "", port) # Test IPv4 local network
92+
or not _can_bind_at_port(socket.AF_INET6, "", port) # Test IPv6 local network
93+
)
9594

9695

97-
def change_port(port: int, _type: str) -> int:
98-
"""Change the port.
99-
100-
Args:
101-
port: The port.
102-
_type: The type of the port.
103-
104-
Returns:
105-
The new port.
106-
107-
Raises:
108-
Exit: If the port is invalid or if the new port is occupied.
109-
"""
110-
new_port = port + 1
111-
if new_port < 0 or new_port > 65535:
112-
console.error(
113-
f"The {_type} port: {port} is invalid. It must be between 0 and 65535."
114-
)
115-
raise click.exceptions.Exit(1)
116-
if is_process_on_port(new_port):
117-
return change_port(new_port, _type)
118-
console.info(
119-
f"The {_type} will run on port [bold underline]{new_port}[/bold underline]."
120-
)
121-
return new_port
96+
MAXIMUM_PORT = 2**16 - 1
12297

12398

12499
def handle_port(service_name: str, port: int, auto_increment: bool) -> int:
@@ -137,13 +112,28 @@ def handle_port(service_name: str, port: int, auto_increment: bool) -> int:
137112
Exit:when the port is in use.
138113
"""
139114
console.debug(f"Checking if {service_name.capitalize()} port: {port} is in use.")
115+
140116
if not is_process_on_port(port):
141117
console.debug(f"{service_name.capitalize()} port: {port} is not in use.")
142118
return port
119+
143120
if auto_increment:
144-
return change_port(port, service_name)
145-
console.error(f"{service_name.capitalize()} port: {port} is already in use.")
146-
raise click.exceptions.Exit
121+
for new_port in range(port + 1, MAXIMUM_PORT + 1):
122+
if not is_process_on_port(new_port):
123+
console.info(
124+
f"The {service_name} will run on port [bold underline]{new_port}[/bold underline]."
125+
)
126+
return new_port
127+
console.debug(
128+
f"{service_name.capitalize()} port: {new_port} is already in use."
129+
)
130+
131+
# If we reach here, it means we couldn't find an available port.
132+
console.error(f"Unable to find an available port for {service_name}")
133+
else:
134+
console.error(f"{service_name.capitalize()} port: {port} is already in use.")
135+
136+
raise click.exceptions.Exit(1)
147137

148138

149139
@overload

tests/units/utils/test_processes.py

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def test_is_process_on_port_free_port():
1515
"""Test is_process_on_port returns False when port is free."""
1616
# Find a free port
1717
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
18-
sock.bind(("127.0.0.1", 0))
18+
sock.bind(("", 0))
1919
free_port = sock.getsockname()[1]
2020

2121
# Port should be free after socket is closed
@@ -26,8 +26,7 @@ def test_is_process_on_port_occupied_port():
2626
"""Test is_process_on_port returns True when port is occupied."""
2727
# Create a server socket to occupy a port
2828
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
29-
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
30-
server_socket.bind(("127.0.0.1", 0))
29+
server_socket.bind(("", 0))
3130
server_socket.listen(1)
3231

3332
occupied_port = server_socket.getsockname()[1]
@@ -44,8 +43,7 @@ def test_is_process_on_port_ipv6():
4443
# Test with IPv6 socket
4544
try:
4645
server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
47-
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
48-
server_socket.bind(("::1", 0))
46+
server_socket.bind(("", 0))
4947
server_socket.listen(1)
5048

5149
occupied_port = server_socket.getsockname()[1]
@@ -64,8 +62,7 @@ def test_is_process_on_port_both_protocols():
6462
"""Test is_process_on_port detects occupation on either IPv4 or IPv6."""
6563
# Create IPv4 server
6664
ipv4_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
67-
ipv4_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
68-
ipv4_socket.bind(("127.0.0.1", 0))
65+
ipv4_socket.bind(("", 0))
6966
ipv4_socket.listen(1)
7067

7168
port = ipv4_socket.getsockname()[1]
@@ -116,46 +113,37 @@ def test_is_process_on_port_permission_error():
116113
assert result is True
117114

118115

119-
@pytest.mark.parametrize("should_listen", [True, False])
120-
def test_is_process_on_port_concurrent_access(should_listen):
121-
"""Test is_process_on_port works correctly with concurrent access.
116+
def test_is_process_on_port_concurrent_access():
117+
"""Test is_process_on_port works correctly with concurrent access."""
118+
shared = None
122119

123-
Args:
124-
should_listen: Whether the server socket should call listen() or just bind().
125-
"""
126-
127-
def create_server_and_test(port_holder, listen):
120+
def create_server_and_test():
121+
nonlocal shared
128122
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
129-
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
130-
server.bind(("127.0.0.1", 0))
123+
server.bind(("", 0))
131124

132-
if listen:
133-
server.listen(1)
125+
server.listen(1)
134126

135127
port = server.getsockname()[1]
136-
port_holder[0] = port
128+
shared = port
137129

138130
# Small delay to ensure the test runs while server is active
139131
time.sleep(0.1)
140132
server.close()
141133

142-
port_holder = [None]
143-
thread = threading.Thread(
144-
target=create_server_and_test, args=(port_holder, should_listen)
145-
)
134+
thread = threading.Thread(target=create_server_and_test)
146135
thread.start()
147136

148137
# Wait a bit for the server to start
149138
time.sleep(0.05)
150139

151-
if port_holder[0] is not None:
152-
# Port should be occupied while server is running (both bound-only and listening)
153-
assert is_process_on_port(port_holder[0])
140+
assert shared is not None
141+
142+
# Port should be occupied while server is running (both bound-only and listening)
143+
assert is_process_on_port(shared)
154144

155145
thread.join()
156146

157-
# After thread ends and server closes, port should be free
158-
if port_holder[0] is not None:
159-
# Give it a moment for the socket to be fully released
160-
time.sleep(0.1)
161-
assert not is_process_on_port(port_holder[0])
147+
# Give it a moment for the socket to be fully released
148+
time.sleep(0.1)
149+
assert not is_process_on_port(shared)

0 commit comments

Comments
 (0)