Commit Graph
2 Commits
Author SHA1 Message Date
wesandClaude Opus 5 32d7db1668 Arm the watchdog for every mode, not just snapshot
The watchdog was spawned inside snapshot.run, so legacy highlights mode ran
without it. That is the mode NickelMenu invokes, against the endpoint that
stopped responding and hung a run for 16 days — so the exact failure being
fixed was still reachable from the device's menu.

Moves the spawn into main, ahead of mode dispatch, so both paths are covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:22:23 -04:00
wesandClaude Opus 5 f55e9d0253 Add whole-database snapshot mode
Adds `kb_exfiltrator snapshot <db> [endpoint] [timeout-secs]`, which uploads a
consistent copy of the whole database instead of just extracted highlights. The
server can then parse whatever it wants, so adding a new statistic never means
touching the device again.

Uses VACUUM INTO rather than copying the file. Nickel keeps the database in WAL
mode and recent commits live in KoboReader.sqlite-wal until a checkpoint, so
copying the bare .sqlite silently loses reading progress — measured at 19 hours
of drift on the live device. Copying all three files instead is non-atomic: a
checkpoint landing mid-copy yields a pre-checkpoint main file plus a post-reset
WAL, losing data with no error. VACUUM INTO runs in a read transaction, so it
sees WAL-resident commits and emits one compacted journal_mode=delete file.

Also fixes the failure mode that left a process hung for 16 days: the server
stopped responding and std.http.Client has no timeout, so the process blocked
forever while holding the SQLite handle open. Snapshot mode closes the database
before any network I/O, and a watchdog thread hard-exits after a deadline
covering the whole run (snapshotting can stall on a locked database too).

Two portability constraints, both specific to the Kobo Elipsa:

  - Linux 4.9.77 predates statx (4.11). Zig's File.stat() and getEndPos() both
    issue it, returning ENOSYS as a bare error.Unexpected — after the snapshot
    is already written, so it presents as a post-write failure. readToEndAlloc
    with no size hint uses plain read() calls instead.
  - usize is 32-bit on armv7, so a u64 stat size will not coerce. Only the ARM
    build catches this.

Legacy invocations are unchanged, so the existing NickelMenu item keeps working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:10:33 -04:00