diff --git a/packages/cli/src/repowise/cli/commands/serve_cmd.py b/packages/cli/src/repowise/cli/commands/serve_cmd.py index 786a0a7c8..687c8422f 100644 --- a/packages/cli/src/repowise/cli/commands/serve_cmd.py +++ b/packages/cli/src/repowise/cli/commands/serve_cmd.py @@ -253,6 +253,8 @@ def _load_local_provider_config() -> None: def _is_port_free(host: str, port: int) -> bool: """Return True if *port* can be bound on *host* right now.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + if os.name != "nt": + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((host, port)) except OSError: diff --git a/tests/unit/cli/test_serve_port_fallback.py b/tests/unit/cli/test_serve_port_fallback.py index a5ecbec95..bbf43b019 100644 --- a/tests/unit/cli/test_serve_port_fallback.py +++ b/tests/unit/cli/test_serve_port_fallback.py @@ -7,6 +7,7 @@ from __future__ import annotations +import os import socket import pytest @@ -68,3 +69,27 @@ def test_find_free_port_handles_contiguous_busy_block( chosen = serve_cmd._find_free_port("127.0.0.1", 3000, "web UI", max_attempts=5) assert isinstance(chosen, int) assert 1024 <= chosen <= 65535 + + +@pytest.mark.skipif(os.name == "nt", reason="SO_REUSEADDR probe fix is POSIX-only (issue #840)") +def test_is_port_free_ignores_time_wait() -> None: + """Regression test for issue #840: TIME_WAIT sockets shouldn't block the probe.""" + host = "127.0.0.1" + lis = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if os.name != "nt": + lis.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + lis.bind((host, 0)) + port = lis.getsockname()[1] + lis.listen(1) + + cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + cli.connect((host, port)) + conn, _ = lis.accept() + + # Active close from the server side pushes the listener's port into TIME_WAIT + lis.close() + conn.close() + cli.close() + + # The probe should report it as free because it uses SO_REUSEADDR too + assert serve_cmd._is_port_free(host, port) is True