feat(daemon): route commands and the TUI through a running daemon

Session::open attaches to a daemon when one is reachable and logs in
otherwise, so every existing command works unchanged against either. A local
socket is found automatically, which is what makes an agent's invocation need
no flags at all; --no-daemon opts out.

Commands now read identity, shares, server, and download directory from the
session rather than from this run's config: routed to a daemon those differ,
and the session is the one actually on the network. Without that, whoami
named the wrong server and shares reindex would have imposed the client
machine's folders on the daemon.
This commit is contained in:
Michel de Graaf
2026-07-27 10:33:41 +02:00
parent f54f8f9b82
commit a0354f66a8
13 changed files with 1622 additions and 28 deletions
+33 -2
View File
@@ -106,6 +106,18 @@ the interval the server sets and prints `search`-shaped objects for what it
finds, so one long-running process covers both halves of being a good
participant.
**`daemon`** — stay logged in as a service other commands share. Prints
progress on stderr and nothing on stdout; it does not return until stopped.
While it runs, every other command on this machine uses its session
automatically — no flags, no credentials, no second login. `--bind ADDR` also
accepts connections from other machines, which need the token.
**`daemon status`** — one object: `user`, `server`, `version`, `protocol`,
`listening`, `listen_port`, `shared_folders`, `shared_files`, `download_dir`,
`session_loss`, `clients`, `uptime_secs`. Exit 3 when nothing is running.
**`daemon stop`** — ask it to shut down; prints nothing.
**`daemon token`** — the secret a client on another machine needs. Local
clients never need it.
**`shares list`** — one object per configured folder: `directory`, `usable`.
**`shares add <directory>`** and **`shares remove <directory>`** — change the
config file; they print nothing.
@@ -152,8 +164,27 @@ is the normal way to be selective.
## Running several at once
The server allows one session per account, and the later login wins: two runs
sharing a username silently kill each other's session. Give every concurrent
run its own name.
sharing a username silently kill each other's session.
The daemon is the answer to this. Start one, and every command afterwards
borrows its session instead of opening one:
```bash
soulseek-rs daemon & # once
soulseek-rs search 'gary beck fold' --json # no login, no flags
soulseek-rs search 'marcal steady' --json # concurrent, same session
```
Commands find it on their own: it listens on a Unix socket only you can open,
so there is nothing to configure and no token to pass. `daemon status` says
whether one is running. This also makes downloads outlive the command that
started them — the transfer belongs to the daemon, so the queue survives.
Two things change when a daemon is in play: files land in the *daemon's*
download directory, and `--no-daemon` is how one run opts out and logs in for
itself.
Without a daemon, give every concurrent run its own name instead:
```bash
for q in 'gary beck fold' 'marcal steady'; do