Skip to content

feat(power): real metered-network detection on macOS and Linux#95

Open
pmaxhogan wants to merge 1 commit into
mainfrom
feat/metered-network-mac-linux
Open

feat(power): real metered-network detection on macOS and Linux#95
pmaxhogan wants to merge 1 commit into
mainfrom
feat/metered-network-mac-linux

Conversation

@pmaxhogan

Copy link
Copy Markdown
Owner

What

Metered-network detection was real on Windows only (INetworkCostManager::GetCost); macOS and Linux returned a conservative unmetered stub, so skip_on_metered was inert on those platforms (issue #32). This wires both.

  • Linux - reads NetworkManager's aggregate Metered property (org.freedesktop.NetworkManager) over the system bus via zbus's blocking API, dispatched through tokio::task::spawn_blocking (zbus's blocking API must not be driven from inside an async runtime) and bounded by a 5 s timeout so a wedged bus can never stall the AC/battery poll. NMMetered YES/GUESS_YES -> metered, NO/GUESS_NO -> not metered, UNKNOWN or a failed read (no NetworkManager / no system bus) -> Unknown -> not metered.
  • macOS - there is no literal "metered" bit. A long-lived NWPathMonitor (Network.framework C API; its update handler is an Objective-C block via block2, delivered on a libdispatch global queue) caches nw_path_is_expensive (cellular / personal hotspot) || nw_path_is_constrained (Low Data Mode) - the documented metered proxies - into an AtomicU8 that the 30 s power poll reads cheaply.

Design

The per-OS raw read is separated from the decision logic. Three pure classify_* functions (classify_windows_cost, classify_nm_metered, classify_nw_path) plus MeteredStatus/on_metered turn each OS's raw value into the on_metered_network bool. They are #[cfg(any(test, target_os = ...))], so all three are compiled and unit-tested on every OS - the classification is CI-covered on Windows, macOS, and Linux alike; only the thin OS-call adapters (COM / D-Bus / NWPathMonitor) are compile-checked-only off their native OS.

Every read collapses ambiguity to MeteredStatus::Unknown, which maps to false (not metered) - the safe direction, since a wrong "not metered" only fails to skip a rare metered link whereas a wrong "metered" would stall ALL sync.

No stored-format or public-API changes: PowerState shape and the PowerSource trait are unchanged; this is purely additive.

Tested vs compile-only

Development was on Windows, so the non-Windows adapters are compile-verified via the 3-OS CI matrix, not locally:

  • Unit-tested on all three CI OSes (run on Windows locally too): the three classify_* functions, MeteredStatus::on_metered, from_u8, reachable_hint - the full decision logic.
  • Runtime-tested on the Windows CI runner (and locally): detect_metered() real COM read is total (windows_detect_metered_is_total); the existing RealPowerSource construction tests exercise the read path.
  • Runtime-tested on the macOS CI runner: macos_monitor_start_and_read_is_total starts the real NWPathMonitor and reads its cache - the only runtime exercise of the Network.framework / block2 / libdispatch FFI (a wrong signature or bad link fails here).
  • Compile-only (CI matrix): the macOS FFI adapter body and the Linux zbus D-Bus adapter body. There is no metered hardware in CI, and NetworkManager may be absent on the Linux runner, so the Linux read's happy path is not asserted in CI - the D-Bus/classification split keeps the classification (the part with real logic) fully covered, and the adapter is a thin, safe-defaulting wrapper.

APIs were taken from documented sources (zbus proxy/blocking docs; Apple Network.framework; objc2/block2 encoding), not guessed.

Nothing deferred

macOS isExpensive/isConstrained is the documented, feasible proxy (macOS exposes no literal metered bit), so #32 is fully addressed rather than scope-reduced.

Closes #32.

🤖 Generated with Claude Code

Windows metered detection was already real (INetworkCostManager::GetCost);
macOS and Linux returned a conservative unmetered stub, so skip_on_metered
was inert there. Wire both:

- Linux: read NetworkManager's aggregate Metered property over the system
  bus (zbus blocking API dispatched via spawn_blocking off the async
  executor, bounded by a 5s timeout so a wedged bus never stalls the poll).
- macOS: a long-lived NWPathMonitor (Network.framework C API, block2
  handler, libdispatch global queue) caches isExpensive || isConstrained -
  the documented metered proxy, since macOS has no literal metered bit.

Refactor the classification into three pure classify_* functions plus
MeteredStatus, all compiled and unit-tested on every OS so the decision
logic is CI-covered on Windows, macOS, and Linux alike; only the thin
OS-call adapters are compile-checked-only off their native OS. Every read
collapses ambiguity to Unknown -> not metered (safe direction).

Closes #32.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fLmkjpFkiuky72eLBijL4
@github-project-automation github-project-automation Bot moved this to Todo in Driven Jul 3, 2026
@pmaxhogan pmaxhogan added this to the v2.0.0 milestone Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Coverage

Area main this PR delta
Rust (lib crates) 76.76% 76.74% -0.02 (OK)
UI (vue/ts) 88.17% 88.17% +0.00 (OK)

Gate: passed - no coverage regression (epsilon 0.1 pp).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

macOS + Linux metered-network detection (currently inert)

1 participant