SIGN IN SIGN UP

feat: Rust CLI — log subcommand, byte-compatible port of the Python logger (LEO-334) (#23)

* feat: Rust CLI — log subcommand, byte-compatible port of the Python logger (LEO-334)

Port the Python logger loop to `sensorwatch log` (visible alias `run`) on the
existing CLI crate: same config schema with the same lenient warn-and-fall-back
coercions, warn-once-and-retry when HWiNFO shared memory is unavailable, daily
rotation on local-day rollover with retention pruning on startup and rollover,
and graceful shutdown on console signals (ctrlc termination set; condvar sleep
wakes instantly). Output is byte-compatible with the Python logger — Python's
JSON separators and key order, pendulum's local-offset ISO 8601 rendering, and
CRLF line endings on Windows — locked in by a golden fixture generated once by
the frozen Python logger and byte-compared in tests. Three documented
divergences: bare "unknown" type labels, always-six fractional digits, and
NaN as null.

No new runtime dependencies (toml/jiff/ctrlc were pre-declared in LEO-333);
Cargo.lock is untouched and the 1.82 MSRV locked check passes. The config and
logger unit suites are ported from the Python side with an injectable clock
and temp dirs, plus new coverage for retention-on-rollover and the strict
cutoff boundary. Docs updated in lockstep: SKILL.md Recipe 2 is Rust-first,
README usage/output/config, the CLI README gains a `log` section, and the
ROADMAP marks Phase-1 step 2 shipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: cfg-gate the stderr test helper used only off-Windows

The Windows clippy job (-D warnings) flags it as dead code, since its only
caller is the cfg(not(windows)) fast-exit test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: degrade an absurd interval_seconds to sleep-until-shutdown, not a panic

interval_seconds = i64::MAX is a valid TOML integer the config floor accepts,
and Instant::now() + interval panicked on the first tick. checked_add now
yields an open-ended condvar wait instead (woken only by the shutdown signal),
matching how the retention path already degrades absurd values to a no-op —
and how the Python loop survives the same config. Tests cover the timeout,
already-flagged-overflow, and no-deadline-wake paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: make an explicit --verbose take precedence over RUST_LOG

--verbose only set the env_logger default, so RUST_LOG=error suppressed the
debug output the user explicitly asked for. The verbose branch now pins the
filter to debug without consulting the environment; without the flag, RUST_LOG
still overrides the info (log) / error (snapshot) defaults as before. Help
text and the CLI README document the precedence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: name the default config lookup in the missing --config warning

The fallback chain can still load ./config.toml, so claiming "falling back to
defaults" could misstate which config was used.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: recover from a poisoned shutdown lock instead of panicking

The ctrlc handler, the loop-top check, and wait_for_shutdown now use
PoisonError::into_inner — the flag is a plain bool, always safe to read, and a
shutdown request must stay deliverable even if a panic ever occurs while the
lock is held (a panic inside the signal-handler thread being the worst case).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: make the README snapshot snippet's working directory explicit

The snippet silently depended on the cd from the logger build block above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: include signal-handler-install failure in the skill's exit-1 causes

Keeps the skill's exit-code summary consistent with the CLI README's table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf: lowercase sensor filter patterns once, outside the sampling loop

matches_sensor re-lowercased every pattern per reading per tick. Config now
builds a SensorFilter (patterns normalized once) that the loop constructs
before sampling; the config keeps the patterns as configured so the startup
log still prints them verbatim, like Python. Per-reading matching now
allocates only the sensor name's lowercase form. Raised by two reviewers
(lcj-kimi-coder round 1, Copilot round 2) — the display-copy shape answers
the original objection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Agent for Leonard Janke <leonard.janke.agent@proton.me>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
L
lcj-claude-coder committed
de15106582868de4f6bc42cbc0a8612556a8fc89
Parent: c35f16f
Committed by GitHub <noreply@github.com> on 7/3/2026, 12:52:02 AM