Close

A launched process is never a serving one.

A project log for Scottina

A pocket-sized front panel for the diagnostic tools you already use

scottskyScottsky 08/14/2026 at 00:200 Comments

A launched process is not a serving one

Starting Kismet or Node-RED is one line: Popen a command, or systemctl start a unit. Neither of those tells you the thing actually works. A process can spawn and then crash three seconds later reading a stale config; a systemd unit can report success while the app inside it is still binding its listener. None of that is "ready" in any sense a person standing at the bench cares about — ready means the browser UI will actually load if they type the address in.

So kilodash/webapp.py doesn't trust the launch call. It trusts a TCP probe against the app's own port. probe() is nothing exotic — socket. create_connection with a 0.4-second timeout — but it's the one signal that can't be faked by a process existing. Only once that connection succeeds does a tile's status card turn green and say "Web UI confirmed."

A small state machine, not a boolean

WebApp tracks four states: STOPPED, STARTING, UP, ERROR. launch() moves it to STARTING and stamps the time. From there, poll() — called once a tick from the screen's own tick() — does the throttled checking: a probe every 0.5 s while STARTING, promoting to UP the moment the port answers; a check of the child process's own exit code in the same window, so a crash reports "Process exited (code …)" instead of just sitting in STARTING forever; and a 30-second ready_timeout past which a slow app just gets marked ERROR with a plain "Timed out waiting for web UI" rather than a tile that hangs on "Launching…" indefinitely. Once UP, the probe backs off to every 2 s — enough to notice the app died mid-session without hammering the port while everything is fine.

Adopt, don't duplicate

launch() probes before it starts anything. If the port already answers — the app autostarted at boot, or was left running from a previous visit to the screen — kilodash adopts it as UP instead of spawning a second copy. That one check is what makes leaving a screen and coming back to it safe, and it's also why Node-RED and Kismet don't stop themselves on on_leave by default: the app keeps serving, and the next time the tile opens, launch() finds it already up and just confirms it.

What every screen gets for free

None of this is app-specific — the app-specific part is one subclass of WebAppScreen setting app_name, port, and a service or start_cmd. In exchange it gets the same compact status card everywhere: border color is the state, body text is the exact IP:port to type into a phone or laptop once the state is UP, and a tap on the card stops the app (with a confirm) or launches it again. The probe itself always dials 127.0.0.1 — the app binds locally on the Pi — but the displayed address comes from a separate lan_ip() lookup, so the thing you'd type from another device is never localhost.

That's the whole contract: launch, wait for a real answer on the wire, show where to point a browser. Kismet and Node-RED both run on it today without either of them knowing the other exists.

Discussions