Skip to content

Announce the web app's URL only once its server is bound - #179

Merged
dmccoystephenson merged 1 commit into
mainfrom
feature/announce-bound-web-address
Aug 14, 2026
Merged

Announce the web app's URL only once its server is bound#179
dmccoystephenson merged 1 commit into
mainfrom
feature/announce-bound-web-address

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

  • examples/web_app.py printed http://{host}:{port} before anything was listening on it, from its own copy of FISHE_WEB_HOST/FISHE_WEB_PORT. 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 neither the conversion (UserInterfaceFactory's WEB branch) nor the bind (WebUserInterface._bindServer) happens until after that print. web/serve.py was given the opposite ordering in Name FISHE_WEB_PORT when a web port is busy or misspelled #177, so the two web entry points disagreed about it.
  • The ordering could not simply be swapped in the example: FishE.__init__ builds the front-end and then blocks in _selectSaveFile(), so control never returns to main() to say what was bound. The announcement is instead made by WebUserInterface immediately after _bindServer returns, and names the socket's own address — so a caller that asked for an ephemeral port is told the port it actually got. The entry point now announces nothing of its own, and a failed bind is no longer preceded by a URL.
  • The "8000" default and the two variables that move it are now stated once, in webUserInterface (DEFAULT_HOST/DEFAULT_PORT and resolveAddressFromEnvironment()), and shared by the factory branch and the constructor; the example no longer reads the environment at all. web/serve.py keeps its own 8080 default deliberately, as the comment there already explains.
  • No behaviour was changed for the Pyodide front-end: it subclasses WebUserInterface with start_server=False, so it has no address and announces nothing.

Test plan

  • python3 -m pytest (with SDL_VIDEODRIVER/SDL_AUDIODRIVER=dummy) — 829 passed
  • New: the bound address is announced with the port the socket actually got
  • New: nothing is announced when no server was started, or when the port was already taken
  • New: resolveAddressFromEnvironment() covers the default, the override, and the misspelled-port ValueError
  • New tests/examples/test_web_app.py: the entry point builds the WEB front-end and prints no address of its own
  • black --check on the changed files

Deferred this cycle

No other issue was open at triage time, so nothing was deferred.

Closes #178

This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

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>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Self-review rubric (scored against the diff and command output, not judgement):

  • Scope: PASS — four files are touched plus one new test file, all of them named by examples/web_app.py announces a URL it has not yet bound, reading FISHE_WEB_PORT a third time to do it #178: the example that announced too early, the factory branch that held the duplicate default, the front-end that binds, and their tests. No unrelated reformatting is present (black --check reports the five changed files unchanged, so nothing was reformatted into the diff).
  • Tests-new: PASS — the one new public function, resolveAddressFromEnvironment(), is exercised three ways (default, override, misspelled port); the new private _announceAddress() is exercised through the constructor by three tests.
  • Tests-fix: PASS — empirically confirmed. With src/ and examples/ reverted to origin/main and the tests kept, exactly the five new tests fail (5 failed, 37 passed); restored, 42 passed. The failure is observable rather than incidental because the announcement is asserted to name the ephemeral port the socket actually got, which the reverted code could not have printed.
  • Sibling structure: PASStests/examples/test_web_app.py follows the shape of tests/web/test_serve.py (module imported by package path, no __init__.py, # check comments, unittest.mock for collaborators).
  • Sibling renames: PASS — no identifier in a parallel pair or series was renamed; _bindServer/address/cleanup keep their names.
  • Docs: PASSREADME.md:34-36 still describes what happens (python3 examples/web_app.py, then http://127.0.0.1:8000), only now that URL is printed after the bind rather than before it; the example's module docstring was updated to say where the default lives and when the URL is printed. No schema, PLANNING.md goal, or persisted field is affected by this change.
  • Issue resolution: PASS — both halves of examples/web_app.py announces a URL it has not yet bound, reading FISHE_WEB_PORT a third time to do it #178 are addressed: the announcement moved behind the bind, and the "8000" default is stated once.
  • CI: PASStest passed in 39s on the PR head; python3 -m pytest under the dummy SDL drivers reports 829 passed locally.
  • Schema-sync: N/A — no persisted field is added or changed, so no schemas/*.json or *JsonReaderWriter is touched.
  • Money-format: N/A — no money or price is displayed by this diff.
  • Deterministic-tests: PASS — no new test uses random; the two that bind sockets ask for port 0 and read back the port that was assigned, rather than assuming one.
  • Headless-pygame: N/A — no pygame code path is touched; the web front-end's servers bind loopback and are closed in finally.
  • camelCase: PASSresolveAddressFromEnvironment, _announceAddress, boundHost, boundPort, portText follow the surrounding style. (host/port keyword arguments keep the names the constructor already had.)

Two observations that were judged rather than measured, folded in here rather than left inline:

  • src/ui/webUserInterface.py:256 — the announcement is unconditional whenever a server is started, so there is no way for an embedder to silence it. No announce=False knob was added because no caller wants one: the only two constructors are the factory (which serves a player) and the Pyodide subclass (which passes start_server=False and so prints nothing). Should a quiet mode ever be wanted, this is where it would go.
  • examples/web_app.py:17 — the module docstring still states 127.0.0.1:8000 in prose, which is a third mention of the number after DEFAULT_HOST/DEFAULT_PORT. It was kept because a reader of the example benefits from seeing the address without following it into webUserInterface, and it now says where the authoritative copy lives; only the code copies were removed.

This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

@dmccoystephenson
dmccoystephenson merged commit 95474a8 into main Aug 14, 2026
1 check passed
@dmccoystephenson
dmccoystephenson deleted the feature/announce-bound-web-address branch August 14, 2026 03:28
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.

examples/web_app.py announces a URL it has not yet bound, reading FISHE_WEB_PORT a third time to do it

1 participant