use futures_util::stream::{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>>;
}
async fn read_snapshot(conn: &Connection, nm: &NetworkManagerProxy<'_>) -> Option<Network> {
match try_read(conn, nm).await {
Ok(network) => Some(network),
Err(e) => {
warn!("networkmanager: reading network state failed: {e}");
None
}
}
}
async fn try_read(conn: &Connection, nm: &NetworkManagerProxy<'_>) -> zbus::Result<Network> {
let state = nm.state().await?;
let primary = nm.primary_connection().await?;
if primary.as_str() == "/" {
return Ok(network_from_nm(state, None, None));
}
let active = ActiveConnectionProxy::builder(conn)
.path(primary)?
.build()
.await?;
let conn_type = active.type_().await?;
let ssid = if conn_type == "802-11-wireless" {
read_ssid(conn, &active).await
} else {
None
};
Ok(network_from_nm(state, Some(&conn_type), ssid.as_deref()))
}
async fn read_ssid(conn: &Connection, active: &ActiveConnectionProxy<'_>) -> Option<String> {
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 access_point = AccessPointProxy::builder(conn)
.path(ap_path)
.ok()?
.build()
.await
.ok()?;
let bytes = access_point.ssid().await.ok()?;
Some(String::from_utf8_lossy(&bytes).into_owned())
}
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?;
if tx
.send(Msg::Network(read_snapshot(&conn, &nm).await))
.is_err()
{
return Ok(());
}
let mut changes = select_all([
nm.receive_state_changed().await.map(|_| ()).boxed(),
nm.receive_primary_connection_changed()
.await
.map(|_| ())
.boxed(),
]);
while changes.next().await.is_some() {
if tx
.send(Msg::Network(read_snapshot(&conn, &nm).await))
.is_err()
{
return Ok(());
}
}
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");
}
}