Name FISHE_WEB_PORT when a web port is busy or misspelled - #177
Conversation
Both web entry points reported the two ordinary startup failures as a raw traceback. web/serve.py - the one the Dockerfile runs, and so the one most likely to be handed a value from outside - converted FISHE_WEB_PORT with a bare int(), so a typo died on "invalid literal for int() with base 10", naming neither the variable nor what it should hold. Neither entry point handled a port that was simply taken: both bound inside a constructor with nothing around it, so a second copy of the game surfaced as an errno raised from inside http.server. allow_reuse_address does not cover that; it only reopens a socket left in TIME_WAIT. serve.py now gets the integer check UserInterfaceFactory's WEB branch already had, and both entry points turn a refused bind into a sentence naming the address, the reason, and FISHE_WEB_PORT. The failure happens before the front-end exists, so there is no showDialogue to say it through - it is raised, the way the factory already does. serve.py also binds before announcing the URL, so a failure is no longer preceded by an address that was never served. The two defaults stay apart (8080 here, 8000 for the server-backed front-end) since they are separate programs that can run at once; a comment now says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The helper was only exercised through its failure branch, so a version that always raised would have passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-reviewThe full diff was read and checked for logic errors, missed edge cases, front-end-parity gaps, schema/reader-writer mismatches, missing tests on public methods, and doc drift. No blocking issue was found. Three notes are recorded below, along with one gap that has already been closed on this branch. Front-end parity — verified as not applicable rather than skipped. Only Save-file contract — no field on Socket cleanup on a failed bind — checked, because a helper that swallows and re-raises is where a half-open socket would be leaked. Notes (non-blocking)
Addressed during review
This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). |
examples/web_app.py printed the address before anything was listening on it, and built that address from its own copy of FISHE_WEB_HOST/FISHE_WEB_PORT rather than from the server. Both ordinary startup failures - a misspelled port and a busy one - were therefore preceded by a confident sentence naming an address that would never answer, since the value is not converted there at all: UserInterfaceFactory's WEB branch does that, and WebUserInterface binds, both after the print. web/serve.py was given the opposite ordering in #177, so the two web entry points disagreed about it. The ordering cannot simply be swapped there: FishE.__init__ builds the front-end and then blocks in the save-file manager, so control never comes back to main() to say what was bound. The announcement moves to WebUserInterface instead, immediately after _bindServer returns, and names the socket's own address - so a caller that asked for an ephemeral port is told the one it actually got. The entry point now announces nothing of its own. The "8000" default and the two variables that move it move to webUserInterface as DEFAULT_HOST/DEFAULT_PORT and resolveAddressFromEnvironment(), which the factory branch and the constructor now share; the example no longer reads the environment at all. web/serve.py keeps its own 8080 default, deliberately, as its comment already explains. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
web/serve.py— the entry point theDockerfileruns, and therefore the one most likely to be handed a value from outside — convertedFISHE_WEB_PORTwith a bareint(). A typo was reported asValueError: invalid literal for int() with base 10: '80801x', which names neither the variable nor what it should hold. The integer checkUserInterfaceFactory's WEB branch already had has been given to it as well.OSError: [Errno 98] Address already in usetraced throughhttp.server.allow_reuse_addressdoes not cover this; only a socket left inTIME_WAITis reopened by it, while a live listener still refuses the bind. A refused bind is now turned into a sentence naming the address, the reason (already listening / not permitted), andFISHE_WEB_PORT.showDialogueis available to say them through — they are raised, the way the factory already does.web/serve.pynow binds before the URL is announced, so a failure is no longer preceded by an address that was never served.examples/web_app.py), since those are separate programs that can be run at the same time; a comment now records why. Only the check on the value is shared in spirit.README.mdnow mentionsFISHE_WEB_PORT/FISHE_WEB_HOST, which the new messages instruct the player to set.Front-ends touched
Only the two web entry points bind a socket, so this failure path has no console, pygame, or Pyodide equivalent: the console and pygame front-ends open no server, and the Pyodide front-end runs inside the browser tab. No
BaseUserInterfaceprimitive was changed, so front-end parity is unaffected.Test plan
python3 -m compileall -q src tests webpython3 -m pytest— 820 passed, including four new tests intests/web/test_serve.py(default port, port from the environment, a misspelled port naming the variable, a taken port explained rather than traced) and one intests/ui/test_webUserInterface.py(a taken port explained rather than traced)blackrun over the changed filesPlayer/Stats/TimeServicefield changed, soschemas/*.jsonand the*JsonReaderWriterclasses are untouchedCloses #176
This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).