Keep browser tabs across daemon upgrades, and run as a user service #5

Merged
natsukium merged 1 commit from feat/spawn-local-daemon into main 2026-10-14 13:23:43 +09:00
Owner

felis 0.2.0's felis daemon upgrade keeps every session but closes every client connection, and the gateway turned that into {"ev":"closed"} for every open tab. It also needed a daemon already running on the local socket.

  • Rejoin after a lost daemon connection. A stream whose daemon connection drops is re-dialed with felis's redial_session (live-only attach by id) and continues under the same sid. closed is sent only when the session is gone or exited, or the host stays unreachable. A detach during the rejoin aborts it; input in the gap is dropped, as the browser drops it without a sid.
  • Start the local daemon like a felis window. Dials go through Reconnector::dial_launch, so a cold local socket starts felis-daemon (systemd user-manager hand-off on Linux, fork elsewhere).
  • --socket defaults to felis's own socket, so the gateway shares the daemon felis windows use. --help prints usage.
  • Nix modules: homeManagerModules.default (systemd user unit / launchd agent with AbandonProcessGroup) and nixosModules.default (user unit for one user, with lingering). Always a user service: from a system unit, a forked daemon would live in the gateway's cgroup.

Verification

  • cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test (30, three new for rejoin), cargo deny check.
  • Live, against a felis 0.2.0 daemon: felis daemon upgrade under an attached broker stream left the same sid streaming with no closed, and input typed afterwards reached the shell; exit in that shell then produced closed. A cold socket was started by the gateway and the daemon outlived the gateway.
  • Modules evaluated on NixOS, home-manager on x86_64-linux and aarch64-darwin. Not yet exercised on a live host: the systemd hand-off and launchd AbandonProcessGroup.
felis 0.2.0's `felis daemon upgrade` keeps every session but closes every client connection, and the gateway turned that into `{"ev":"closed"}` for every open tab. It also needed a daemon already running on the local socket. - **Rejoin after a lost daemon connection.** A stream whose daemon connection drops is re-dialed with felis's `redial_session` (live-only attach by id) and continues under the same `sid`. `closed` is sent only when the session is gone or exited, or the host stays unreachable. A detach during the rejoin aborts it; input in the gap is dropped, as the browser drops it without a `sid`. - **Start the local daemon like a felis window.** Dials go through `Reconnector::dial_launch`, so a cold local socket starts `felis-daemon` (systemd user-manager hand-off on Linux, fork elsewhere). - **`--socket` defaults to felis's own socket**, so the gateway shares the daemon felis windows use. `--help` prints usage. - **Nix modules**: `homeManagerModules.default` (systemd user unit / launchd agent with `AbandonProcessGroup`) and `nixosModules.default` (user unit for one `user`, with lingering). Always a user service: from a system unit, a forked daemon would live in the gateway's cgroup. ## Verification - `cargo fmt --check`, `cargo clippy --all-targets -D warnings`, `cargo test` (30, three new for rejoin), `cargo deny check`. - Live, against a felis 0.2.0 daemon: `felis daemon upgrade` under an attached broker stream left the same `sid` streaming with no `closed`, and input typed afterwards reached the shell; `exit` in that shell then produced `closed`. A cold socket was started by the gateway and the daemon outlived the gateway. - Modules evaluated on NixOS, home-manager on x86_64-linux and aarch64-darwin. Not yet exercised on a live host: the systemd hand-off and launchd `AbandonProcessGroup`.
feat: keep browser tabs across daemon upgrades, and run as a user service
All checks were successful
ci / fmt / clippy / test / deny (pull_request) Successful in 1m16s
0068932b66
felis 0.2.0's `felis daemon upgrade` replaces the daemon in place and
keeps every session, but closes every client connection on the way.
The gateway reported any lost daemon connection to the browser as the
stream's death, so each upgrade turned every open tab into a dead one
that had to be re-attached by hand. A lost connection is now re-dialed
with felis's own `redial_session`, the ladder a felis window uses after
a transport loss, and the stream carries on under the same `sid`; only
a session that is gone or has exited still ends in `closed`. This lives
here rather than in the browser because the browser's `felis-json`
frames carry no exit signal, so only the gateway can tell an upgrade
from a shell that ended. The same ladder also rides out a dropped ssh
relay.

The gateway also refused to work without a daemon already running on
the local socket, while it let a remote relay start one. Local dials now
go through felis's `Reconnector::dial_launch`, so a cold socket starts
the daemon exactly as a felis window would: handed to the systemd user
manager as a unit of its own where there is one, forked otherwise.
`--socket` defaults to felis's own socket for the same reason, so the
gateway lands on the daemon the user's windows already share instead of
a second one.

That makes the gateway the only process to keep running, so the flake
now exports home-manager (systemd user unit / launchd agent) and NixOS
modules for it. Both run it as the user it serves, never as a system
service: a daemon forked into a system unit's cgroup would be stopped
with the gateway. The launchd agent sets AbandonProcessGroup for the
same reason, and the NixOS module turns on lingering so the gateway
runs while nobody is logged in.

Assisted-by: Claude Code
natsukium deleted branch feat/spawn-local-daemon 2026-10-14 13:23:43 +09:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
natsukium/felis-web-gateway!5
No description provided.