Keep containers alive as root; notice containers that exit on their own #71

Merged
jknapp merged 2 commits from fix/container-keepalive into main 2026-11-03 00:41:14 +00:00
2 Commits
Author SHA1 Message Date
shadowdaoandClaude Opus 5.5 2eabf6972b Address review of the keepalive / container-watch change
Secret Scan / scan (push) Successful in 9s
Build App (Preview) / compute-version (pull_request) Successful in 9s
Secret Scan / scan (pull_request) Successful in 8s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / test (pull_request) Successful in 4m10s
Build App (Preview) / build-macos (pull_request) Successful in 3m12s
Build App (Preview) / build-linux (pull_request) Successful in 9m31s
Build App (Preview) / build-windows (pull_request) Successful in 6m47s
Build App (Preview) / prune-previews (pull_request) Successful in 3s
Build Container / build-container (pull_request) Successful in 17m21s
- Event renamed to `project-container-exited` and carries a reason:
  `exited` (died while the app was watching) toasts; `found_stopped` (the
  first look of a run, e.g. after a host reboot) updates silently.
  Reconnect sweeps report `exited`, since the app was running then.
- listen() in useProjectStatusEvents gets a .catch, like its sibling.
- Keepalive respawns are throttled to ~1/s with a fork-free pause
  (`read -t` on a FIFO, `sleep 1` fallback), so a failing fork or an
  instantly-dying sleep cannot spin a core. Measured: 10748 respawns in
  3 s unthrottled vs 3.
- Watcher backoff resets only after a working connection (an event
  delivered, or up for 60 s); ping-OK-but-events-fail now backs off to
  60 s instead of re-sweeping every 5 s.
- Every Docker call in the watcher (ping, sweep, inspect, stream) races
  the app's cancellation, so a wedged daemon cannot hold shutdown.
- TOCTOU between the inspect and the flip: ProjectsStore now counts
  writes; running_on() returns a sighting at a revision and mark_exited()
  refuses with Changed if anything was written since (a stop+start of the
  same container id included), and settle() re-confirms.
- The "Running on this container" rule lives once, in the store, with
  the 12-char guard on both ids, matched under the lock without cloning.
- Marketplace readiness fallback also requires a root-owned process.
- TECHNICAL.md no longer documents the `su … sleep infinity` ending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 16:15:31 -08:00
shadowdaoandClaude Opus 5.5 eba0d95780 Keep containers alive as root; notice containers that exit on their own
Secret Scan / scan (push) Successful in 7s
Build App (Preview) / compute-version (pull_request) Successful in 12s
Secret Scan / scan (pull_request) Successful in 6s
Build App (Preview) / test (pull_request) Successful in 4m45s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 3m16s
Build App (Preview) / build-linux (pull_request) Successful in 5m36s
Build App (Preview) / build-windows (pull_request) Failing after 6m38s
Build App (Preview) / prune-previews (pull_request) Skipped
Build Container / build-container (pull_request) Successful in 23m46s
The entrypoint ended in `exec su -s /bin/bash claude -c "exec sleep
infinity"`, so a project container's whole lifetime was a sleep owned by
the unprivileged claude user. Claude Code runs as claude and routinely runs
`pkill sleep`, `pkill -f`, `kill -9 -1` while testing, and any of those
stopped the container: every terminal disconnected and the app kept
showing Running.

Entrypoint: stay resident as root and wait on a root-owned sleep run as
`triple-c-keepalive infinity`, respawned if killed, with a TERM/INT/HUP
trap so `docker stop` returns at once (550 ms, exit 0, vs 2.6 s / 143
before). Unprivileged kills now get EPERM; `sudo pkill sleep` is survived
by the respawn. The drop to claude dated from the initial commit with no
stated purpose; its only reader was the marketplace readiness probe, which
now accepts either generation of entrypoint.

App: container_watch.rs holds one Docker event subscription filtered
daemon-side to container `die`. A die naming a Running project's container
is confirmed with an inspect, then ProjectsStore::mark_exited flips it to
Stopped as a compare-and-set that skips any project a command holds the
project lock on. Each (re)subscription re-checks Running projects and asks
for events since just before it. The frontend applies the new
`project-status-changed` event and toasts why the sessions died.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:57:12 -08:00