site serve and view: pick a free port when the registry gives none, and report a held port as taken #249

Closed
opened 2026-09-29 01:08:31 +01:00 by vnprc-agent · 2 comments
Contributor

Let allod site serve start a preview for a site that has no port in the registry, on a free port it picks, and let allod site view find that port by itself, so serving and viewing a site for the first time needs no registry edit and no invented port number. The same change makes a start report success only when the port is held by the preview itself, which a picked port depends on.

Primary goals:

  • No port, still served — with no preview_port and no --port, serve picks a free port on 127.0.0.1, starts the preview there and prints the address.
  • The port is stable while the preview runs — a second serve for a running preview prints the same address, and --stop needs no port.
  • view asks the serving machine — with no port of its own, view forwards the port the remote start reports.
  • Success means the preview holds the port — a port that another process holds is reported as taken, not as a running preview.
  • A registry port still wins — a site with a preview_port is served there, unchanged, and --port still overrides both.
  • The app's contract is unchanged — the app is handed one port and must fail if it is taken.

Current state: no port

serve refuses a site with no port (cmd/allod/site_serve.go:108, at 3f917eb):

$ allod site serve
allod: no preview port for <checkout>: give its entry in <inventory>/scripts/repositories.json a preview_port, or pass --port

The way past it is --port <n> with a number the reader makes up, without knowing which ports are free or in use by another site's preview. view refuses the same way (cmd/allod/site_view.go:35) and discards what the remote start prints (site_view.go:48), so it cannot learn a port from the machine that serves.

The refusal is reached in two situations that look the same to the reader: the site is new and has no registry entry yet, and the machine reads a registry that does not list the site. The second is tracked in allod/archetypes#119.

A fixed port per site exists so that the two machines agree on it without asking each other. view already runs a command on the serving machine before it forwards, so it can ask.

Current state: a port held by something else

Observed on a dev VM, with another process listening on the site's port before the start, for three apps (zola, live-server, Vite with --strictPort):

$ allod site serve
http://127.0.0.1:<port>
$ echo $?
0
$ systemctl --user show allod-preview-<slug>.service -p ActiveState -p Result -p NRestarts -p Restart
ActiveState=inactive
Result=success
Restart=no
NRestarts=0

The journal of the unit for the same start, live-server's case:

ERROR live_server::http_layer::listener] Address 127.0.0.1:<port> is already in use
allod-preview-<slug>.service: Main process exited, code=exited, status=101/n/a
allod-preview-<slug>.service: Failed with result 'exit-code'.

Each app failed as its contract requires and did not move to another port. serve had already returned: sitePreviewWait (site_serve.go:201) takes a connection to the port as good news while the unit is up, and the unit is still up while nix run is evaluating, before the app has tried to bind. The comment above that function names the case. The printed address is the other process's.

systemctl show a moment after the failure reads Result=success and ExecMainStatus=0, so the failure is in the journal and not in the unit's properties.

Default

Before starting a unit that is not running, serve connects to the port once. An answer means the port is taken: exit 1, naming the port, and nothing is started. This check is what a registry port and --port get too.

For a site with no port, serve asks the kernel for a free one by binding 127.0.0.1 port 0, releases it, and hands that number to the unit as it hands a registry port today. The port is recorded with the unit, so a later serve for the same site reads it back while the unit is up and picks a new one only when it is not. The message says the port was picked, and that a preview_port in the registry makes it permanent.

Between releasing the port and the app binding it another process can take it. The app then fails and serve reports a stopped unit with its log lines. One retry with a new port is enough; more hides a real fault.

view, with no port from its registry or --port, reads the address the remote start prints and forwards that port. With a port of its own it passes it along as today.

Alternative for the held port: accept a connection only when the listening socket belongs to a process of the unit. It closes the window between the check and the bind as well, and needs the socket's owner read from the kernel. Left as a follow-up unless the check before the start proves not enough.

Alternative for the missing port: a fixed default port for every site without one. Rejected because two such sites collide on the second start.

Validation

  • A site with no registry port and no --port: exit 0, an address on 127.0.0.1, the page answers there.
  • A second serve while running: the same address, still one unit.
  • --stop, then serve: a port is picked again; it need not be the same one.
  • view for that site with no --port: forwards the port the serving machine reported.
  • A port held by another process, from the registry, from --port, and taken between pick and bind: exit 1 each time, no address printed, no unit left running.
  • A site with a registry port, and one given --port, on a free port: served where they are today.
  • Nothing listens on any address but 127.0.0.1.

Scope

In: serve's port resolution and its check before starting, where the picked port is kept, view reading the port back, the messages, and docs/allod-site-preview.md, whose contract section describes a held port as reported "as serving". Out: which registry a dev VM reads, tracked in allod/archetypes#119; listing the known sites, allod/tools#248.

Let `allod site serve` start a preview for a site that has no port in the registry, on a free port it picks, and let `allod site view` find that port by itself, so serving and viewing a site for the first time needs no registry edit and no invented port number. The same change makes a start report success only when the port is held by the preview itself, which a picked port depends on. Primary goals: - **No port, still served** — with no `preview_port` and no `--port`, `serve` picks a free port on 127.0.0.1, starts the preview there and prints the address. - **The port is stable while the preview runs** — a second `serve` for a running preview prints the same address, and `--stop` needs no port. - **`view` asks the serving machine** — with no port of its own, `view` forwards the port the remote start reports. - **Success means the preview holds the port** — a port that another process holds is reported as taken, not as a running preview. - **A registry port still wins** — a site with a `preview_port` is served there, unchanged, and `--port` still overrides both. - **The app's contract is unchanged** — the app is handed one port and must fail if it is taken. ### Current state: no port `serve` refuses a site with no port (`cmd/allod/site_serve.go:108`, at `3f917eb`): ``` $ allod site serve allod: no preview port for <checkout>: give its entry in <inventory>/scripts/repositories.json a preview_port, or pass --port ``` The way past it is `--port <n>` with a number the reader makes up, without knowing which ports are free or in use by another site's preview. `view` refuses the same way (`cmd/allod/site_view.go:35`) and discards what the remote start prints (`site_view.go:48`), so it cannot learn a port from the machine that serves. The refusal is reached in two situations that look the same to the reader: the site is new and has no registry entry yet, and the machine reads a registry that does not list the site. The second is tracked in allod/archetypes#119. A fixed port per site exists so that the two machines agree on it without asking each other. `view` already runs a command on the serving machine before it forwards, so it can ask. ### Current state: a port held by something else Observed on a dev VM, with another process listening on the site's port before the start, for three apps (zola, live-server, Vite with `--strictPort`): ``` $ allod site serve http://127.0.0.1:<port> $ echo $? 0 $ systemctl --user show allod-preview-<slug>.service -p ActiveState -p Result -p NRestarts -p Restart ActiveState=inactive Result=success Restart=no NRestarts=0 ``` The journal of the unit for the same start, live-server's case: ``` ERROR live_server::http_layer::listener] Address 127.0.0.1:<port> is already in use allod-preview-<slug>.service: Main process exited, code=exited, status=101/n/a allod-preview-<slug>.service: Failed with result 'exit-code'. ``` Each app failed as its contract requires and did not move to another port. `serve` had already returned: `sitePreviewWait` (`site_serve.go:201`) takes a connection to the port as good news while the unit is up, and the unit is still up while `nix run` is evaluating, before the app has tried to bind. The comment above that function names the case. The printed address is the other process's. `systemctl show` a moment after the failure reads `Result=success` and `ExecMainStatus=0`, so the failure is in the journal and not in the unit's properties. ### Default Before starting a unit that is not running, `serve` connects to the port once. An answer means the port is taken: exit 1, naming the port, and nothing is started. This check is what a registry port and `--port` get too. For a site with no port, `serve` asks the kernel for a free one by binding 127.0.0.1 port 0, releases it, and hands that number to the unit as it hands a registry port today. The port is recorded with the unit, so a later `serve` for the same site reads it back while the unit is up and picks a new one only when it is not. The message says the port was picked, and that a `preview_port` in the registry makes it permanent. Between releasing the port and the app binding it another process can take it. The app then fails and `serve` reports a stopped unit with its log lines. One retry with a new port is enough; more hides a real fault. `view`, with no port from its registry or `--port`, reads the address the remote start prints and forwards that port. With a port of its own it passes it along as today. Alternative for the held port: accept a connection only when the listening socket belongs to a process of the unit. It closes the window between the check and the bind as well, and needs the socket's owner read from the kernel. Left as a follow-up unless the check before the start proves not enough. Alternative for the missing port: a fixed default port for every site without one. Rejected because two such sites collide on the second start. ### Validation - A site with no registry port and no `--port`: exit 0, an address on 127.0.0.1, the page answers there. - A second `serve` while running: the same address, still one unit. - `--stop`, then `serve`: a port is picked again; it need not be the same one. - `view` for that site with no `--port`: forwards the port the serving machine reported. - A port held by another process, from the registry, from `--port`, and taken between pick and bind: exit 1 each time, no address printed, no unit left running. - A site with a registry port, and one given `--port`, on a free port: served where they are today. - Nothing listens on any address but 127.0.0.1. ### Scope In: `serve`'s port resolution and its check before starting, where the picked port is kept, `view` reading the port back, the messages, and `docs/allod-site-preview.md`, whose contract section describes a held port as reported "as serving". Out: which registry a dev VM reads, tracked in allod/archetypes#119; listing the known sites, allod/tools#248.
vnprc-agent changed title from site serve: pick a free port when the registry gives none to site serve and view: pick a free port when the registry gives none, and report a held port as taken 2026-09-29 01:13:21 +01:00
Member

Closing as not planned. The half of this issue that picks a port is dropped; the half that reports a held port as taken continues as allod/tools#252.

What was built

Pull request allod/tools#251 implemented this issue as written: serve picked a free port for a site with none, kept it in the unit's environment, retried once, and view read the port back from the VM. It was tested, measured on a dev VM, and reviewed. It is closed unmerged.

What it cost

Picking a port saves one line in the registry, once per site. To save that line the change had to solve four problems the fixed port does not have:

  • The hypervisor does not know the port, so view had to ask the VM and act on the answer. A VM then chooses which port the hypervisor listens on.
  • The port the VM picked can already be in use on the hypervisor. view prints the address, ssh refuses the forward, and every retry reaches the same port because the unit is still running.
  • Another process can take the port between the pick and the app's bind. Measured on a dev VM, the issue's default printed the other process's address with exit 0. Closing that took about a hundred lines that read kernel files to confirm the unit holds the socket, plus a retry.
  • The port needs a second home besides the registry, and a site served this way is missing from the known-sites list of allod/tools#248, which keys on preview_port.

The decision

A site is served on the port the registry gives it, or on --port. A site with neither keeps today's refusal, which already says to add a preview_port. The inventory's own check refuses a registry where two sites share a port or one is out of range, so keeping ports unique is enforced, not left to memory. With a fixed port the hypervisor takes no value from the VM.

What went wrong in the process

The issue was implemented before anyone asked what the pick was for. Of the two situations it names, a machine reading the wrong registry is fixed by allod/archetypes#119, which leaves only a new site with no port yet. That question belonged before the first line of code, not after the pull request.

What continues

allod/tools#252: before starting a unit, serve connects to the port once, and refuses if anything answers.

Closing as not planned. The half of this issue that picks a port is dropped; the half that reports a held port as taken continues as allod/tools#252. ### What was built Pull request allod/tools#251 implemented this issue as written: `serve` picked a free port for a site with none, kept it in the unit's environment, retried once, and `view` read the port back from the VM. It was tested, measured on a dev VM, and reviewed. It is closed unmerged. ### What it cost Picking a port saves one line in the registry, once per site. To save that line the change had to solve four problems the fixed port does not have: - The hypervisor does not know the port, so `view` had to ask the VM and act on the answer. A VM then chooses which port the hypervisor listens on. - The port the VM picked can already be in use on the hypervisor. `view` prints the address, `ssh` refuses the forward, and every retry reaches the same port because the unit is still running. - Another process can take the port between the pick and the app's bind. Measured on a dev VM, the issue's default printed the other process's address with exit 0. Closing that took about a hundred lines that read kernel files to confirm the unit holds the socket, plus a retry. - The port needs a second home besides the registry, and a site served this way is missing from the known-sites list of allod/tools#248, which keys on `preview_port`. ### The decision A site is served on the port the registry gives it, or on `--port`. A site with neither keeps today's refusal, which already says to add a `preview_port`. The inventory's own check refuses a registry where two sites share a port or one is out of range, so keeping ports unique is enforced, not left to memory. With a fixed port the hypervisor takes no value from the VM. ### What went wrong in the process The issue was implemented before anyone asked what the pick was for. Of the two situations it names, a machine reading the wrong registry is fixed by allod/archetypes#119, which leaves only a new site with no port yet. That question belonged before the first line of code, not after the pull request. ### What continues allod/tools#252: before starting a unit, `serve` connects to the port once, and refuses if anything answers.
Member

Closed as not planned.

Closed as not planned.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
allod/tools#249
No description provided.