use futures_util::future::{Either, select};
use futures_util::stream::{BoxStream, StreamExt, select_all};
use log::warn;
use zbus::zvariant::OwnedObjectPath;
use zbus::{Connection, proxy};
use crate::widget::{Msg, Network, NetworkState};
use crate::producer::{MsgSender, Producer, ProducerFuture, ProducerResult};
pub fn network_from_nm(nm_state: u32, conn_type: Option<&str>, ssid: Option<&str>) -> Network {
let state = match nm_state {
10 | 20 | 30 => NetworkState::Disconnected,
40 | 50 | 60 | 70 => connection_state(conn_type),
_ => NetworkState::Unknown,
};
Network::new(state, ssid)
}
fn connection_state(conn_type: Option<&str>) -> NetworkState {
match conn_type {
Some("802-3-ethernet") => NetworkState::Wired,
Some("802-11-wireless") => NetworkState::Wireless,
_ => NetworkState::Unknown,
}
}
#[proxy(
interface = "org.freedesktop.NetworkManager",
default_service = "org.freedesktop.NetworkManager",
default_path = "/org/freedesktop/NetworkManager"
)]
trait NetworkManager {
#[zbus(property)]
fn state(&self) -> zbus::Result<u32>;
#[zbus(property)]
fn primary_connection(&self) -> zbus::Result<OwnedObjectPath>;
}
#[proxy(
interface = "org.freedesktop.NetworkManager.Connection.Active",
default_service = "org.freedesktop.NetworkManager"
)]
trait ActiveConnection {
#[zbus(property, name = "Type")]
fn type_(&self) -> zbus::Result<String>;
#[zbus(property)]
fn devices(&self) -> zbus::Result<Vec<OwnedObjectPath>>;
}
#[proxy(
interface = "org.freedesktop.NetworkManager.Device.Wireless",
default_service = "org.freedesktop.NetworkManager"
)]
trait WirelessDevice {
#[zbus(property)]
fn active_access_point(&self) -> zbus::Result<OwnedObjectPath>;
}
#[proxy(
interface = "org.freedesktop.NetworkManager.AccessPoint",
default_service = "org.freedesktop.NetworkManager"
)]
trait AccessPoint {
#[zbus(property)]
fn ssid(&self) -> zbus::Result<Vec<u8>>;
#[zbus(property)]
fn strength(&self) -> zbus::Result<u8>;
}
struct ActiveAp {
path: OwnedObjectPath,
proxy: AccessPointProxy<'static>,
}
#[hotpath::measure]
async fn read_snapshot(
conn: &Connection,
nm: &NetworkManagerProxy<'_>,
) -> (Option<Network>, Option<ActiveAp>) {
match try_read(conn, nm).await {
Ok((network, ap)) => (Some(network), ap),
Err(e) => {
warn!("networkmanager: reading network state failed: {e}");
(None, None)
}
}
}
#[hotpath::measure]
async fn try_read(
conn: &Connection,
nm: &NetworkManagerProxy<'_>,
) -> zbus::Result<(Network, Option<ActiveAp>)> {
let state = nm.state().await?;
let primary = nm.primary_connection().await?;
if primary.as_str() == "/" {
return Ok((network_from_nm(state, None, None), None));
}
let active = ActiveConnectionProxy::builder(conn)
.path(primary)?
.build()
.await?;
let conn_type = active.type_().await?;
let ap = if conn_type == "802-11-wireless" {
read_access_point(conn, &active).await
} else {
None
};
let (ssid, signal) = match &ap {
Some(ap) => {
let ssid = ap
.proxy
.ssid()
.await
.ok()
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned());
(ssid, ap.proxy.strength().await.ok())
}
None => (None, None),
};
let network = network_from_nm(state, Some(&conn_type), ssid.as_deref()).with_signal(signal);
Ok((network, ap))
}
#[hotpath::measure]
async fn read_access_point(
conn: &Connection,
active: &ActiveConnectionProxy<'_>,
) -> Option<ActiveAp> {
let device_path = active.devices().await.ok()?.into_iter().next()?;
let wireless = WirelessDeviceProxy::builder(conn)
.path(device_path)
.ok()?
.build()
.await
.ok()?;
let ap_path = wireless.active_access_point().await.ok()?;
if ap_path.as_str() == "/" {
return None;
}
let proxy = AccessPointProxy::builder(conn)
.path(ap_path.clone())
.ok()?
.build()
.await
.ok()?;
Some(ActiveAp {
path: ap_path,
proxy,
})
}
pub struct NetworkProducer;
impl NetworkProducer {
pub fn new() -> Self {
Self
}
}
impl Default for NetworkProducer {
fn default() -> Self {
Self::new()
}
}
impl Producer for NetworkProducer {
fn name(&self) -> String {
"networkmanager".to_string()
}
fn run(self: Box<Self>, tx: MsgSender) -> ProducerFuture {
Box::pin(run(tx))
}
}
async fn run(tx: MsgSender) -> ProducerResult {
let conn = Connection::system().await?;
let nm = NetworkManagerProxy::new(&conn).await?;
let mut changes = select_all([
nm.receive_state_changed().await.map(|_| ()).boxed(),
nm.receive_primary_connection_changed()
.await
.map(|_| ())
.boxed(),
]);
let mut watched: Option<OwnedObjectPath> = None;
let mut strength: Option<BoxStream<'static, ()>> = None;
loop {
let (snapshot, ap) = read_snapshot(&conn, &nm).await;
if tx.send(Msg::Network(snapshot)).is_err() {
return Ok(());
}
let path = ap.as_ref().map(|ap| ap.path.clone());
if path != watched {
strength = match ap {
Some(ap) => Some(
ap.proxy
.receive_strength_changed()
.await
.map(|_| ())
.boxed(),
),
None => None,
};
watched = path;
}
match strength.as_mut() {
Some(signal) => match select(changes.next(), signal.next()).await {
Either::Left((None, _)) => return Ok(()),
Either::Right((None, _)) => {
strength = None;
watched = None;
}
_ => {}
},
None => {
if changes.next().await.is_none() {
return Ok(());
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offline_states_normalize_to_disconnected() {
for state in [10, 20, 30] {
let network = network_from_nm(state, Some("802-3-ethernet"), None);
assert_eq!(network.state(), NetworkState::Disconnected);
}
}
#[test]
fn connected_ethernet_normalizes_to_wired() {
for state in [40, 50, 60, 70] {
let network = network_from_nm(state, Some("802-3-ethernet"), None);
assert_eq!(network.state(), NetworkState::Wired);
assert_eq!(network.label(), "wired");
}
}
#[test]
fn connected_wireless_keeps_its_ssid() {
let network = network_from_nm(70, Some("802-11-wireless"), Some("home-net"));
assert_eq!(network.state(), NetworkState::Wireless);
assert_eq!(network.ssid(), Some("home-net"));
assert_eq!(network.label(), "home-net");
}
#[test]
fn connected_wireless_without_ssid_has_no_text_label() {
let network = network_from_nm(70, Some("802-11-wireless"), None);
assert_eq!(network.state(), NetworkState::Wireless);
assert_eq!(network.ssid(), None);
assert_eq!(network.label(), "");
}
#[test]
fn connected_with_unknown_type_normalizes_to_unknown() {
assert_eq!(
network_from_nm(70, Some("vpn"), None).state(),
NetworkState::Unknown
);
assert_eq!(
network_from_nm(70, None, None).state(),
NetworkState::Unknown
);
}
#[test]
fn unknown_nm_state_normalizes_to_unknown() {
assert_eq!(
network_from_nm(0, Some("802-3-ethernet"), None).state(),
NetworkState::Unknown
);
assert_eq!(
network_from_nm(99, Some("802-11-wireless"), Some("home-net")).state(),
NetworkState::Unknown
);
}
#[test]
fn ssid_is_dropped_when_the_link_is_not_wireless() {
let network = network_from_nm(70, Some("802-3-ethernet"), Some("home-net"));
assert_eq!(network.ssid(), None);
assert_eq!(network.label(), "wired");
}
}