Skip to content

[fix](cli): detect bound ports before launch#2071

Merged
yyj6666667 merged 2 commits into
kvcache-ai:mainfrom
VectorPeak:fix
Jul 6, 2026
Merged

[fix](cli): detect bound ports before launch#2071
yyj6666667 merged 2 commits into
kvcache-ai:mainfrom
VectorPeak:fix

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the kt-kernel CLI port availability check so it tests whether the server can actually bind the requested host/port, instead of probing whether a client can connect to it.

Fixes #2072

What Problem This Solves

kt run uses the interactive setup flow to choose and persist server launch settings. The host/port validation happens before the configuration is saved or reused, so the helper needs to answer the same question the later server launch will ask: can this process bind the requested local address?

The affected path is:

kt run interactive setup
  -> kt_kernel.cli.utils.run_interactive.configure_host_and_port(...)
    -> kt_kernel.cli.utils.port_checker.is_port_available(...)
    -> kt_kernel.cli.utils.port_checker.find_available_port(...)

There are two user-visible entry points into the same helper:

new interactive config
  -> configure_host_and_port()
  -> check user-entered host/port
  -> print "Port <port> is available" or suggest another port

saved run config
  -> reuse saved_host / saved_port
  -> check saved port before launch
  -> keep it or prompt for a replacement

Before this change, is_port_available() used socket.connect_ex() to decide whether the port was available. That checks client reachability: whether a client can connect to a server that is already listening at the target address. The CLI needs server bindability: whether the launch process can reserve that local host/port with bind().

Those two checks differ in an important edge case. Another process can already hold a port by calling bind() without calling listen() yet. In that state:

other process: bind("127.0.0.1", port) succeeds and keeps the port reserved
client probe : connect_ex("127.0.0.1", port) fails because nothing is listening
old helper   : treats the failed connection as "available"
server launch: later bind("127.0.0.1", port) fails because the port is already reserved

That means the CLI can print that a port is available, save or suggest that port, and only fail later when the actual server launch tries to bind it. The validation step is therefore checking the wrong side of the socket lifecycle.

Change

  • Replace the connect_ex() reachability probe with a short-lived bind() check.
  • Keep the existing 0.0.0.0 handling by binding through the wildcard host.
  • Set SO_REUSEADDR only on non-Windows platforms so Linux/macOS checks match typical server reuse behavior without introducing Windows false positives.
  • Add focused CPU tests for the bound-but-not-listening false positive and the non-Windows SO_REUSEADDR branch.

Evidence

The regression test covers the exact false-positive shape:

holder = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
holder.bind(("127.0.0.1", 0))
port = holder.getsockname()[1]

self.assertFalse(port_checker.is_port_available("127.0.0.1", port))
self.assertEqual(
    port_checker.find_available_port("127.0.0.1", port, max_attempts=1),
    (False, port),
)

The reviewer-requested platform branch is also covered with a mocked Linux platform check:

with patch.object(port_checker.sys, "platform", "linux"):
    with patch.object(port_checker.socket, "socket", return_value=sock):
        self.assertTrue(port_checker.is_port_available("127.0.0.1", 12345))

sock.setsockopt.assert_called_once_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

Post-fix local behavior proof:

{'case': 'bound_without_listen', 'available': False, 'suggested_same': (False, 57452)}

Possible call chain / impact

Affected interactive paths:

kt-kernel/python/cli/utils/run_interactive.py
  -> configure_host_and_port()
    -> is_port_available(host, port)
    -> find_available_port(host, port + 1, max_attempts=100)

saved run config path
  -> saved_host / saved_port
  -> is_port_available(saved_host, saved_port)
  -> find_available_port(saved_host, saved_port + 1, max_attempts=100)

This PR only changes the CLI helper used to decide whether a TCP server port can be bound. It does not change model loading, SGLang launch arguments, network serving behavior after a port is selected, or non-CLI kernel code.

Validation

  • mamba run -n prw-ktransformers-py311 python -m unittest per_commit.test_port_checker -v from kt-kernel/test - passed, 2 tests
  • Local post-fix behavior proof for a bound-but-not-listening socket - passed
  • git diff --check - passed

I also attempted mamba run -n prw-ktransformers-py311 python run_suite.py --hw cpu --suite default from kt-kernel/test; on this Windows environment it stops before the suite because the runner invokes python3, which is not available here (test_basic_cpu.py exits 9009). The focused regression tests above run successfully with the same Python 3.11 conda environment.

Before submitting

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the port checker utility to determine port availability by attempting to bind to the port, and adds a corresponding unit test. The reviewer noted that on non-Windows platforms, attempting to bind to a port in the TIME_WAIT state without setting SO_REUSEADDR will fail, leading to false negatives. They provided a code suggestion to conditionally set SO_REUSEADDR on non-Windows platforms to avoid this issue while preventing false positives on Windows.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +20 to +23
bind_host = "" if host == "0.0.0.0" else host
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind((bind_host, port))
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

On non-Windows platforms (like Linux and macOS), if a port is in the TIME_WAIT state (for example, right after a server process is stopped), attempting to bind to it without setting the SO_REUSEADDR socket option will fail with an OSError (Address already in use). Since standard server implementations typically set SO_REUSEADDR to allow binding to ports in TIME_WAIT, this port checker will incorrectly report such ports as unavailable (false negatives).

To prevent this, we should set SO_REUSEADDR on non-Windows platforms. We must avoid setting it on Windows, because on Windows, SO_REUSEADDR allows multiple sockets to bind to the exact same port even if one is actively listening, which would cause false positives (reporting an active port as available).

Suggested change
bind_host = "" if host == "0.0.0.0" else host
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind((bind_host, port))
return True
import sys
bind_host = "" if host == "0.0.0.0" else host
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
if sys.platform != "win32":
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((bind_host, port))
return True

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a good catch. I updated the helper to set SO_REUSEADDR only when sys.platform != win32, so Linux/macOS bind checks better match normal server reuse behavior while avoiding Windows false positives. I also added a focused test that mocks the non-Windows branch and verifies setsockopt(SO_REUSEADDR) is applied before bind().

Validation rerun:

  • mamba run -n prw-ktransformers-py311 python -m unittest per_commit.test_port_checker -v - passed, 2 tests
  • local bound-but-not-listening behavior proof - passed
  • git diff --check - passed

@yyj6666667

Copy link
Copy Markdown
Collaborator

Yes, the problem is real.

The old is_port_available() uses connect_ex(), which checks whether a client can connect to a listening server. It does not prove the process can later bind() the address. I confirmed the issue locally

@yyj6666667
yyj6666667 merged commit cb9f47d into kvcache-ai:main Jul 6, 2026
callmegaga pushed a commit to callmegaga/ktransformers that referenced this pull request Jul 10, 2026
* [fix](cli): detect bound ports before launch

* [fix](cli): align port reuse check by platform
callmegaga pushed a commit to callmegaga/ktransformers that referenced this pull request Jul 10, 2026
* [fix](cli): detect bound ports before launch

* [fix](cli): align port reuse check by platform
callmegaga pushed a commit to callmegaga/ktransformers that referenced this pull request Jul 10, 2026
* [fix](cli): detect bound ports before launch

* [fix](cli): align port reuse check by platform
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug](cli): port availability check can accept already-bound ports

2 participants