fix: step past occupied ports and open a connectable URL - #168
Open
q0821 wants to merge 1 commit into
Open
Conversation
The dashboard could start successfully and still be unreachable. Two independent causes, both invisible from the error the browser shows. A reverse proxy holding the port. OrbStack and Docker Desktop bind *:8080 as a wildcard. TCPServer sets allow_reuse_address, so on macOS a later bind to 127.0.0.1:8080 succeeds and coexists with it rather than raising EADDRINUSE. The proxy still wins connections on the family it holds and answers them with nothing, which the browser reports as ERR_EMPTY_RESPONSE. Because bind() never fails, any port fallback keyed on EADDRINUSE would never trigger; create_server now probes each candidate by connecting to it, on both address families, before binding. Handing the browser a bind address. serve() printed and opened http://localhost:PORT, but getaddrinfo returns ::1 ahead of 127.0.0.1 on macOS while the server listens on IPv4 only, so the browser could miss the server entirely and land on whatever holds the IPv6 wildcard. browser_url() maps bind addresses to somewhere actually connectable, and both the printed line and the opened URL now go through it. Fallback is limited to the interactive path. Under --no-browser the VS Code extension has already picked the port, polls that exact port for readiness, and keys the webview's localStorage on that origin, so drifting elsewhere would read as a failed start. That path binds the requested port or exits, as before. Also removes the sleep(1.0) the browser thread used to guess at server readiness: serve() now reports the bound port through an on_ready callback once the socket is listening.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
python cli.py dashboardcan start successfully and still be unreachable, showingERR_EMPTY_RESPONSEin the browser. Two independent causes, neither visible from that error.A reverse proxy holding the port. OrbStack and Docker Desktop bind
*:8080as a wildcard.TCPServersetsallow_reuse_address, so on macOS/BSD a later bind to127.0.0.1:8080succeeds and coexists with it rather than raisingEADDRINUSE. The proxy still wins connections on the address family it holds and answers them with nothing, which the browser reports asERR_EMPTY_RESPONSE. Worth stressing: becausebind()never fails here, a port fallback keyed onEADDRINUSEwould never fire.Handing the browser a bind address.
serve()printed and openedhttp://localhost:PORT, butgetaddrinforeturns::1ahead of127.0.0.1on macOS whileThreadingHTTPServerdefaults toAF_INETand listens on IPv4 only. The browser follows::1, misses the server, and lands on whatever holds the IPv6 wildcard, or on nothing.Measured on macOS 15 with OrbStack running:
Change
port_is_taken()probes a port by connecting to it, on both address families, sincebind()cannot answer the question.create_server()steps up to the next free port (10 candidates) and returns the port it actually bound.browser_url()maps a bind address to something connectable. Both the printed line and the opened URL go through it, so copy-pasting from the terminal works too.--no-browserthe port is pinned and the server binds it or exits.sleep(1.0)the browser thread used to guess at readiness:serve()reports the bound port through anon_readycallback once the socket is listening.VS Code extension is unaffected
extension.ts:121picks the port viaresolveStablePort,:126polls that exact port for readiness, and the webview'slocalStorageis keyed on that origin, so silent drift would read as a failed start. The extension always passes--no-browser, which pins the port. No TypeScript changes;server-manager.ts:75already documents that callers handle port-collision recovery at a higher level.Verified by simulating the extension's spawn contract:
--no-browser --port <free>/api/datareturns 200--no-browser --port <taken>Port N is already in use., never movesTests
167 pass. The headline regression test reproduces the bug exactly: with an IPv6-only squatter, a bare
ThreadingHTTPServerbind succeeds, so any fallback keyed on bind failure keeps the port and leaves the browser talking to the squatter.End-to-end with OrbStack on 8080:
Note: the suite emits a pre-existing
ResourceWarning: unclosed socket. It reproduces on unmodifiedmain, so it is untouched here.