← All incidents

minortool-misuseverifiedfirsthand

Pressing Stop on a dashboard made the home-built launcher shoot itself

In a home-built app launcher, pressing Stop on a registered dashboard stopped the launcher itself instead of the dashboard.

Observed
Severity score
3/10
Blast radius
uptime
Tags
#lsof#process-management#port-forwarding#misdiagnosis#ai-coding

Cause

The stop logic found 'whoever holds this port' with lsof and killed it. After a forwarding feature was added so the apps could be used from another device, the launcher's own forwarding socket held the same port, and the launcher landed on its own kill list.

Consequence

The dashboard couldn't be stopped and the launcher UI reported that it couldn't reach its server. The AI coding agent first misdiagnosed it as fallout from cleaning up its own tool session.

Fix

The port-lookup function now always excludes the launcher's own PID (os.getpid()), locked in with a test. Whenever you add a listener, revisit the code that stops things. When a process disappears, check for Terminated: 15 first.

What happened

The project was a personal app launcher: one page to start and stop a set of home-built dashboards. One day it gained a forwarding feature — a plain TCP relay so apps that only listened locally could also be used from another device, a tablet.

Right after that, pressing Stop on a dashboard stopped the launcher instead.

The chaos on the ground

The symptoms were “the dashboard won’t stop” and “can’t reach the launcher’s server.” The button did nothing to its target, and the page you pressed it on went dark.

This is where the AI coding agent got it wrong. Its first diagnosis was that the launcher had died as a side effect of cleaning up its own tool session. Since the launcher had been started from the agent’s own shell, that sounded plausible. But the answer had been sitting in the terminal all along: Terminated: 15. That line means someone deliberately sent SIGTERM. The someone was the launcher.

Root cause

This launcher stops apps by port, not by PID. It starts child processes in a new session so that closing the launcher doesn’t take them down with it — but that also means that after a restart it can no longer tell which processes it started. So stopping was designed as: use lsof to find whoever holds the port, and kill that.

The forwarder listens on the same port number as the app it relays. The moment forwarding was turned on, the launcher itself became “someone holding that port.” The Stop button did exactly what it was designed to do, and by design it shot the launcher.

A change that added a new listener went in without anyone revisiting the assumption behind “kill whoever holds the port.”

The fix

  • The function that finds processes on a port (_pids_on_port) now always excludes the launcher’s own PID (os.getpid()). That behavior is pinned down by a test.
  • Whenever you add a listener, revisit the code that stops things. Ask again, every time: is the thing holding this port only the thing I want to stop?
  • When a process vanishes, look for Terminated: 15 before building theories. If it’s there, something stopped it on purpose; it didn’t crash.

Like its sibling entry agent-kill-by-port-overreach, this one shows the trap in “decide what to kill by port number”: someone else may be holding the same port.

Sources

  • Firsthand account from the site operator. There is no public write-up to link to.