use std::sync::{Arc, RwLock};
use crate::{BandwidthPolicy, BwCaps, PeerTier};
#[derive(Default, Debug, Clone)]
struct GovernorState {
on_battery: bool,
metered: bool,
}
#[derive(Clone)]
pub struct BandwidthGovernor {
policy: BandwidthPolicy,
state: Arc<RwLock<GovernorState>>,
}
impl BandwidthGovernor {
pub fn new(policy: BandwidthPolicy) -> Self {
Self { policy, state: Arc::new(RwLock::new(GovernorState::default())) }
}
pub fn effective_caps(&self, tier: PeerTier) -> BwCaps {
let s = self.state.read().unwrap();
if s.on_battery || s.metered {
return BwCaps::passive();
}
self.policy.caps_for(tier)
}
pub fn is_passive(&self) -> bool {
let s = self.state.read().unwrap();
s.on_battery || s.metered
}
pub fn set_battery(&self, on_battery: bool) {
self.state.write().unwrap().on_battery = on_battery;
}
pub fn set_metered(&self, metered: bool) {
self.state.write().unwrap().metered = metered;
}
pub fn probe_os(&self) {
if let Some(on_battery) = detect_battery() {
self.set_battery(on_battery);
}
}
pub fn has_policy(&self) -> bool {
!matches!(
self.policy.caps_for(PeerTier::Workstation),
BwCaps { up_mbit: 0, .. }
)
}
}
impl std::fmt::Debug for BandwidthGovernor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self.state.read().unwrap();
f.debug_struct("BandwidthGovernor")
.field("on_battery", &s.on_battery)
.field("metered", &s.metered)
.field("is_passive", &(s.on_battery || s.metered))
.finish()
}
}
#[cfg(target_os = "macos")]
fn detect_battery() -> Option<bool> {
let out = std::process::Command::new("pmset")
.args(["-g", "ps"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
if text.contains("Battery Power") {
Some(true)
} else if text.contains("AC Power") {
Some(false)
} else {
None
}
}
#[cfg(target_os = "linux")]
fn detect_battery() -> Option<bool> {
let base = std::path::Path::new("/sys/class/power_supply");
if !base.exists() {
return None;
}
for entry in std::fs::read_dir(base).ok()? {
let entry = entry.ok()?;
let status_path = entry.path().join("status");
if let Ok(status) = std::fs::read_to_string(&status_path) {
let s = status.trim();
if s == "Discharging" {
return Some(true);
}
if s == "Charging" || s == "Full" {
return Some(false);
}
}
}
None
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn detect_battery() -> Option<bool> {
None
}
#[cfg(test)]
mod tests {
use super::*;
fn policy() -> BandwidthPolicy {
BandwidthPolicy::default()
.role(PeerTier::Cloud, BwCaps { up_mbit: 1000, down_mbit: 10_000 })
.role(PeerTier::Rig, BwCaps { up_mbit: 10, down_mbit: 100 })
.role(PeerTier::Workstation, BwCaps { up_mbit: 5, down_mbit: 50 })
.role(PeerTier::Mobile, BwCaps::passive())
}
#[test]
fn caps_by_tier_on_ac() {
let g = BandwidthGovernor::new(policy());
assert_eq!(g.effective_caps(PeerTier::Cloud).up_mbit, 1000);
assert_eq!(g.effective_caps(PeerTier::Rig).up_mbit, 10);
assert_eq!(g.effective_caps(PeerTier::Workstation).up_mbit, 5);
assert_eq!(g.effective_caps(PeerTier::Mobile).up_mbit, 0);
}
#[test]
fn battery_forces_passive() {
let g = BandwidthGovernor::new(policy());
assert!(!g.is_passive());
g.set_battery(true);
assert!(g.is_passive());
let caps = g.effective_caps(PeerTier::Cloud);
assert_eq!(caps.up_mbit, 0, "battery must drop Cloud to passive");
assert_eq!(caps.down_mbit, 50, "down_mbit matches BwCaps::passive()");
}
#[test]
fn metered_forces_passive() {
let g = BandwidthGovernor::new(policy());
g.set_metered(true);
assert!(g.is_passive());
assert_eq!(g.effective_caps(PeerTier::Rig).up_mbit, 0);
}
#[test]
fn battery_override_is_togglable() {
let g = BandwidthGovernor::new(policy());
g.set_battery(true);
assert_eq!(g.effective_caps(PeerTier::Workstation).up_mbit, 0);
g.set_battery(false);
assert_eq!(g.effective_caps(PeerTier::Workstation).up_mbit, 5);
}
#[test]
fn metered_and_battery_independent() {
let g = BandwidthGovernor::new(policy());
g.set_battery(true);
g.set_metered(true);
assert!(g.is_passive());
g.set_battery(false);
assert!(g.is_passive(), "metered alone still passive");
g.set_metered(false);
assert!(!g.is_passive(), "both off → active");
}
#[test]
fn unconfigured_tier_defaults_to_passive() {
let g = BandwidthGovernor::new(BandwidthPolicy::default());
assert_eq!(g.effective_caps(PeerTier::Workstation).up_mbit, 0);
}
#[test]
fn clone_shares_state() {
let g1 = BandwidthGovernor::new(policy());
let g2 = g1.clone();
g1.set_battery(true);
assert!(g2.is_passive(), "clone must share Arc<RwLock<GovernorState>>");
}
#[test]
fn probe_os_does_not_panic() {
let g = BandwidthGovernor::new(policy());
g.probe_os();
}
}