Step 6: control socket and daemon

Unix-socket JSON-lines protocol replacing the printMSG sentinels, with
an Emitter so scan code is agnostic about whether a terminal or a UI is
watching. Daemon serves clients and runs a TTL-aware scheduler; commands
funnel through one worker so scans cannot overlap, and the CLI proxies to
a running daemon rather than competing with it.

The pre-fetch retention sweep was emitting the terminal ReapDone event,
which would have ended a client's read before the scan began.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
This commit is contained in:
2026-09-09 20:49:55 +00:00
parent 21d32104fe
commit b12e0c46dd
4 changed files with 579 additions and 113 deletions

View File

@@ -12,8 +12,7 @@ The full design and step list live in the plan file at
- [x] **3. `feed.rs`** — conditional GET, RSS-then-Atom parse, persist entries.
- [x] **4. `download.rs`** — downloads, filters, dedupe.
- [x] **5. `retention.rs`** — oldest-first quota + age reaper.
- [ ] **6. `ipc.rs` + daemon** broadcast event bus, UDS JSON-lines server, TTL scheduler,
CLI-proxies-to-daemon. *Done when:* smoke 3 passes.
- [x] **6. `ipc.rs` + daemon** — UDS JSON-lines server, TTL scheduler, CLI-proxies-to-daemon.
- [ ] **7. `torrent.rs`** — librqbit, seed to ratio/time, stall abort. *Done when:* smoke 5 passes.
- [ ] **8. OPML + polish** — import/export, add/rm/status, tracing setup, systemd unit, README.
@@ -30,6 +29,44 @@ The full design and step list live in the plan file at
---
## 2026-09-09 — Step 6: ipc.rs + daemon
`src/ipc.rs`: `Event` and `Command` as serde-tagged enums (`{"ev":...}` / `{"cmd":...}`), one JSON
object per line over a Unix socket. `Emitter` is the single output path — it broadcasts to socket
clients, prints the human rendering to a terminal, or both, so the scan code no longer knows how it
is being watched. That replaces `printMSG` and its `;;1;;1;;100.00;;42.31` sentinels.
`main.rs` restructured around a `Ctx` (config, db, client, emitter). `ipx daemon` binds the socket,
serves clients, and runs a 60s ticker that defers to each feed's TTL. Commands from every client
funnel through one mpsc queue into a single worker, which is what stops two scans overlapping.
Any CLI subcommand with a wire form probes the socket first and proxies to a running daemon;
`--local` forces the work to happen in-process. SIGINT/SIGTERM remove the socket on the way out.
**Bug the smoke test caught:** `fetch` runs a retention sweep first, and that sweep was emitting the
terminal `ReapDone`. A UI waiting for its fetch to finish would have stopped reading before the scan
started. Only a standalone `ipx reap` emits it now.
Verified: `cargo test` 24/24. Smoke 3 in full — daemon starts and binds; a raw socket client sending
`{"cmd":"fetch","force":true}` gets `feed_start` → 14 throttled `progress` events → `download_done`
`feed_done``scan_done`; `{"cmd":"status"}` answers `{"ev":"status","feeds":1,...}`. With the
daemon up, `ipx fetch` from the CLI logged `command from a client cmd=Fetch { .. }` in the daemon
and rendered the streamed events, so it proxied rather than downloading in parallel. Socket removed
on SIGTERM; with no daemon the same command runs locally.
Deferred: `{"cmd":"cancel","enclosure":N}` from the plan's protocol is **not implemented**
downloads run sequentially in one worker, so there is nothing to cancel concurrently yet. It wants
a per-download cancellation token, which is worth doing when downloads go parallel. Say if you want
it sooner.
Note: the event stream is a broadcast, so a CLI client seeing a busy daemon also sees that other
work. Fine for a terminal; a UI wanting strict request/response would want per-request ids.
Also note: the daemon reads config once at startup — changing `config.toml` needs a restart.
Next: step 7 — `torrent.rs`.
---
## 2026-09-09 — Step 5: retention.rs
`src/retention.rs`: reconcile pass (rows claiming a file that is gone become `reaped`, fixing the