Socket-activate the agent with a systemd user unit #18

Open
opened 2026-08-19 23:58:47 +00:00 by dusan · 0 comments
Owner

Context

The agent is started on demand by a window and outlives it
(crates/terminalko-agent/src/client.rs, start_agent): a single setsid, never
waited on, so it is reparented rather than killed when the window exits. It takes
an exclusive flock so a second one steps aside, unlinks a stale socket, and
exits by itself once it has no windows and no sessions
(docs/spec/session-agent.md).

The plan called socket activation an optional improvement, "not v1".

What it would add

A terminalko-agent.socket user unit, with the agent as its service:

  • systemd owns the socket, so there is no stale-socket case to handle and no
    race between two windows starting an agent — the flock and the unlink logic
    become belt-and-braces rather than load-bearing;
  • the agent's lifetime is managed where the rest of the user session is managed,
    so systemctl --user status terminalko-agent says something useful, and its
    log goes to the journal instead of a window's stderr;
  • it composes with loginctl enable-linger, which is the documented way to make
    sessions survive logout — today that only half works, because nothing restarts
    the agent after the session scope is torn down.

What to be careful about

  • The socket path must stay the same as today's
    ($XDG_RUNTIME_DIR/terminalko/agent.sock, overridable with TK_RUNTIME_DIR),
    or every window and terminalkoctl invocation has to learn a second path;
  • the double start must stay harmless: a window may still spawn an agent directly
    on a system without systemd, and both paths have to end with exactly one agent
    serving. Keep the flock;
  • the idle exit must not fight the unit. With Restart= set wrongly, an agent
    that exits because it has nothing to do gets restarted immediately;
  • Accept=no (one agent, many connections), not Accept=yes;
  • passing the listening descriptor: the agent currently creates its own listener
    with gio::SocketService::add_address. With socket activation it must instead
    adopt the descriptor systemd passes ($LISTEN_FDS, fd 3) — gio::Socket::from_fd
    plus SocketListener::add_socket — and fall back to creating its own when
    nothing was passed. That fallback is what keeps the non-systemd path working.

Acceptance criteria

  • With the unit installed, connecting to the socket starts the agent, and a window
    never spawns one directly.
  • Without the unit — or on a system without systemd — everything works exactly as
    it does today, including the tests, which start the agent as a plain process.
  • systemctl --user stop terminalko-agent stops sessions cleanly and a window
    reconnecting starts a fresh agent.
  • The idle timeout still ends the agent, and the unit does not resurrect it.
  • docs/spec/session-agent.md documents both paths, and the unit is installed by
    meson and listed in the rpm spec.

Notes

## Context The agent is started **on demand** by a window and outlives it (`crates/terminalko-agent/src/client.rs`, `start_agent`): a single `setsid`, never waited on, so it is reparented rather than killed when the window exits. It takes an exclusive `flock` so a second one steps aside, unlinks a stale socket, and exits by itself once it has no windows and no sessions (`docs/spec/session-agent.md`). The plan called socket activation an optional improvement, "not v1". ## What it would add A `terminalko-agent.socket` user unit, with the agent as its service: * **systemd owns the socket**, so there is no stale-socket case to handle and no race between two windows starting an agent — the flock and the unlink logic become belt-and-braces rather than load-bearing; * the agent's lifetime is managed where the rest of the user session is managed, so `systemctl --user status terminalko-agent` says something useful, and its log goes to the journal instead of a window's stderr; * it composes with `loginctl enable-linger`, which is the documented way to make sessions survive logout — today that only half works, because nothing restarts the agent after the session scope is torn down. ## What to be careful about * **The socket path must stay the same** as today's (`$XDG_RUNTIME_DIR/terminalko/agent.sock`, overridable with `TK_RUNTIME_DIR`), or every window and `terminalkoctl` invocation has to learn a second path; * the double start must stay harmless: a window may still spawn an agent directly on a system without systemd, and both paths have to end with exactly one agent serving. Keep the flock; * the idle exit must not fight the unit. With `Restart=` set wrongly, an agent that exits because it has nothing to do gets restarted immediately; * `Accept=no` (one agent, many connections), not `Accept=yes`; * passing the listening descriptor: the agent currently creates its own listener with `gio::SocketService::add_address`. With socket activation it must instead adopt the descriptor systemd passes (`$LISTEN_FDS`, fd 3) — `gio::Socket::from_fd` plus `SocketListener::add_socket` — and fall back to creating its own when nothing was passed. That fallback is what keeps the non-systemd path working. ## Acceptance criteria - With the unit installed, connecting to the socket starts the agent, and a window never spawns one directly. - Without the unit — or on a system without systemd — everything works exactly as it does today, including the tests, which start the agent as a plain process. - `systemctl --user stop terminalko-agent` stops sessions cleanly and a window reconnecting starts a fresh agent. - The idle timeout still ends the agent, and the unit does not resurrect it. - `docs/spec/session-agent.md` documents both paths, and the unit is installed by meson and listed in the rpm spec. ## Notes - <https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html> - `sd_listen_fds` semantics matter (`LISTEN_PID` must match); the `libsystemd` crate exists, but reading two environment variables directly avoids a dependency.
Sign in to join this conversation.
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
dusan/terminalko#18
No description provided.