Skip to content

viewer: probe port availability by binding, not connecting - #336

Merged
earthtojake merged 1 commit into
developfrom
claude/port-probe-bind
Aug 26, 2026
Merged

viewer: probe port availability by binding, not connecting#336
earthtojake merged 1 commit into
developfrom
claude/port-probe-bind

Conversation

@earthtojake

Copy link
Copy Markdown
Owner

Problem

The launcher's port_is_free probed by CONNECTING to the port, and counted only ConnectionRefusedError as free. On Windows a connect to a closed port routinely fails some other way: Hyper-V/WSL port exclusions, loopback filtering, refusals arriving as timeouts. Every one of those fell into except OSError: return False, so every port read as occupied and the launcher refused to start with a false "already in use" error.

#335's Windows smoke test found this: four random ports that Python itself had just bound and released all failed the connect probe, including ports inside the range its _free_port helper pre-verified as bindable. The test-side workarounds in that PR ruled out every innocent explanation and left the launcher's probe as the cause.

Fix

Probe by binding, the same operation the server is about to perform, so the probe cannot disagree with reality:

  • EADDRINUSE (and EACCES, Windows's answer for its excluded port ranges) keeps the friendly "rerun with --port <n>" message.
  • Any other error counts as free. The probe exists only for that message; a probe that cannot tell must never block a launch. The server's own bind stays authoritative and reports anything genuinely wrong.

skills/cad-viewer/scripts/viewer is a symlink to this file, so the skill copy is covered by the same edit.

Tests

Four new probe-semantics tests in viewer/server_py/tests/test_start_viewer.py:

  • a listening port reads as occupied
  • a bindable port reads as free
  • ambiguous OSErrors (timeout, reset) read as free — the exact Windows failure shape
  • EADDRINUSE/EACCES still block

Verified locally: 15/15 launcher tests pass, the live launch smoke (scripts/test/test-viewer-launch.sh) passes, and a genuinely occupied port still exits 1 with the friendly message.

Unblocks #335 (its Windows run fails only on this probe).

🤖 Generated with Claude Code

The launcher's port_is_free connected to the port and counted only
ConnectionRefusedError as free. On Windows a connect to a closed port
routinely fails some other way (Hyper-V/WSL port exclusions, loopback
filtering, refusals arriving as timeouts), so every port read as occupied
and the launcher refused to start with a false "already in use" error.
Found by #335's Windows smoke test, where four random ports Python had
just bound and released all failed the connect probe.

Bind instead: it is the same operation the server is about to perform, so
the probe cannot disagree with reality. EADDRINUSE and EACCES (Windows's
answer for excluded port ranges) keep the friendly rerun-with---port
message; any other error counts as free, because the probe exists only
for that message and the server's own bind stays authoritative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@earthtojake
earthtojake merged commit 1f75ced into develop Aug 26, 2026
3 checks passed
earthtojake pushed a commit that referenced this pull request Aug 26, 2026
Source ref: develop
Source commit: cce04de
Target branch: main
Previous target: 8f9a7d7
Release base: 8f9a7d7
Previous source: 96675ba

Included commits since previous source:
cce04de Merge pull request #337 from earthtojake/release/0.4.28
c3f3856 Release 0.4.28
c7e2a7c Merge pull request #305 from warun7/fix/viewer-worker-deadlock-and-timeouts
2b65d4f Merge branch 'develop' into fix/viewer-worker-deadlock-and-timeouts
6f0265d Merge pull request #335 from warun7/fix/skill-remediations-and-coverage
1e4aea1 Merge branch 'develop' into fix/skill-remediations-and-coverage
1f75ced Merge pull request #336 from earthtojake/claude/port-probe-bind
3236a5c viewer: probe port availability by binding, not connecting
99a806f tests: pick viewer-smoke ports outside the ephemeral range
5633b65 tests: call the module-level drain helper directly
788bb5d tests: retire a busy candidate port instead of failing the viewer smoke
7306fbe tests: skip the cadgen probe in the viewer start smoke, surface its output
603e812 tests: resolve npm through PATH for the viewer start smoke on Windows
0b64fa3 skills: point gcode at the real cad export CLI; cover cad-viewer; fix skill deps
24e9d28 viewer: restore run_cadgen_cold's terminal error return
3150457 tests: drive the stderr drainer from a real subprocess pipe
dbeea4f viewer: kill the CAD worker and cold subprocess on idleness, not wall clock
06bf1b3 viewer: add worker and cold process timeouts and stream large assets
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.

1 participant