Read Wi-Fi status through NetworkManager D-Bus
This commit is contained in:
1 parent
b0583eb993
commit
e448d433fc
2 files changed
+227
-41
No files matched your search
@@ -1,4 +1,6 @@
|
|||||||
//! Bounded, off-UI-thread adapters. A missing service is `None`, never a fake state.
|
//! Bounded, off-UI-thread adapters. A missing service is `None`, never a fake state.
|
||||||
|
mod network;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env, fs,
|
env, fs,
|
||||||
path::Path,
|
path::Path,
|
||||||
@@ -334,7 +336,7 @@ impl StatusBus {
|
|||||||
|
|
||||||
fn poll(system_bus: &mut StatusBus, session_bus: &mut StatusBus) -> Snapshot {
|
fn poll(system_bus: &mut StatusBus, session_bus: &mut StatusBus) -> Snapshot {
|
||||||
Snapshot {
|
Snapshot {
|
||||||
network: network(),
|
network: system_bus.query(network::read).flatten(),
|
||||||
bluetooth: bluetooth(),
|
bluetooth: bluetooth(),
|
||||||
audio: audio(),
|
audio: audio(),
|
||||||
battery: battery(),
|
battery: battery(),
|
||||||
@@ -346,46 +348,6 @@ fn poll(system_bus: &mut StatusBus, session_bus: &mut StatusBus) -> Snapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn network() -> Option<Network> {
|
|
||||||
// No Wi-Fi device means no Wi-Fi control (wired connectivity isn't called Wi-Fi).
|
|
||||||
let devices = run("nmcli", &["-t", "-f", "TYPE", "device", "status"]).ok()?;
|
|
||||||
if !devices.lines().any(|line| line == "wifi") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let enabled = run("nmcli", &["radio", "wifi"]).ok()? == "enabled";
|
|
||||||
let mut connection = None;
|
|
||||||
let mut signal = 0;
|
|
||||||
if enabled {
|
|
||||||
let rows = run(
|
|
||||||
"nmcli",
|
|
||||||
&[
|
|
||||||
"-t",
|
|
||||||
"-f",
|
|
||||||
"IN-USE,SIGNAL,SSID",
|
|
||||||
"device",
|
|
||||||
"wifi",
|
|
||||||
"list",
|
|
||||||
"--rescan",
|
|
||||||
"no",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
for line in rows.lines() {
|
|
||||||
if let Some((strength, ssid)) = line.strip_prefix("*:").and_then(|s| s.split_once(':'))
|
|
||||||
{
|
|
||||||
signal = strength.parse::<u8>().ok()?.min(100);
|
|
||||||
connection = Some(ssid.replace("\\:", ":").replace("\\\\", "\\"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(Network {
|
|
||||||
enabled,
|
|
||||||
connection,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bluetooth() -> Option<Bluetooth> {
|
fn bluetooth() -> Option<Bluetooth> {
|
||||||
let info = run("bluetoothctl", &["show"]).ok()?;
|
let info = run("bluetoothctl", &["show"]).ok()?;
|
||||||
let enabled = info
|
let enabled = info
|
||||||
@@ -575,6 +537,42 @@ fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
pub(super) struct TestBus {
|
||||||
|
daemon: std::process::Child,
|
||||||
|
pub(super) address: String,
|
||||||
|
}
|
||||||
|
impl TestBus {
|
||||||
|
pub(super) fn new() -> Self {
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
let mut daemon = Command::new("dbus-daemon")
|
||||||
|
.args(["--session", "--nofork", "--nopidfile", "--print-address=1"])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
let mut address = String::new();
|
||||||
|
BufReader::new(daemon.stdout.take().unwrap())
|
||||||
|
.read_line(&mut address)
|
||||||
|
.unwrap();
|
||||||
|
Self {
|
||||||
|
daemon,
|
||||||
|
address: address.trim().to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(super) fn connect(&self) -> zbus::blocking::Connection {
|
||||||
|
zbus::blocking::connection::Builder::address(self.address.as_str())
|
||||||
|
.unwrap()
|
||||||
|
.method_timeout(Duration::from_millis(200))
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for TestBus {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.daemon.kill();
|
||||||
|
let _ = self.daemon.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct MockLogin1(Arc<std::sync::atomic::AtomicU8>);
|
struct MockLogin1(Arc<std::sync::atomic::AtomicU8>);
|
||||||
|
|
||||||
#[zbus::interface(name = "org.freedesktop.login1.Manager")]
|
#[zbus::interface(name = "org.freedesktop.login1.Manager")]
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
//! Read NetworkManager directly; no scans or network changes are requested.
|
||||||
|
use super::Network;
|
||||||
|
use zbus::{blocking::Connection, zvariant::OwnedObjectPath};
|
||||||
|
|
||||||
|
const SERVICE: &str = "org.freedesktop.NetworkManager";
|
||||||
|
const ROOT: &str = "/org/freedesktop/NetworkManager";
|
||||||
|
|
||||||
|
fn property<T: TryFrom<zbus::zvariant::OwnedValue>>(
|
||||||
|
connection: &Connection,
|
||||||
|
path: &str,
|
||||||
|
interface: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> zbus::Result<T>
|
||||||
|
where
|
||||||
|
T::Error: Into<zbus::Error>,
|
||||||
|
{
|
||||||
|
// Explicit Get avoids creating short-lived property-cache subscriptions.
|
||||||
|
let value: zbus::zvariant::OwnedValue = connection
|
||||||
|
.call_method(
|
||||||
|
Some(SERVICE),
|
||||||
|
path,
|
||||||
|
Some("org.freedesktop.DBus.Properties"),
|
||||||
|
"Get",
|
||||||
|
&(interface, name),
|
||||||
|
)?
|
||||||
|
.body()
|
||||||
|
.deserialize()?;
|
||||||
|
value.try_into().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read(connection: &Connection) -> zbus::Result<Option<Network>> {
|
||||||
|
let devices: Vec<OwnedObjectPath> = property(connection, ROOT, SERVICE, "Devices")?;
|
||||||
|
let mut wifi = Vec::new();
|
||||||
|
for device in devices {
|
||||||
|
let kind: u32 = property(
|
||||||
|
connection,
|
||||||
|
device.as_str(),
|
||||||
|
"org.freedesktop.NetworkManager.Device",
|
||||||
|
"DeviceType",
|
||||||
|
)?;
|
||||||
|
if kind == 2 {
|
||||||
|
wifi.push(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if wifi.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
// Stable selection when several Wi-Fi adapters have active connections.
|
||||||
|
wifi.sort_by(|a, b| a.as_str().cmp(b.as_str()));
|
||||||
|
let enabled = property(connection, ROOT, SERVICE, "WirelessEnabled")?;
|
||||||
|
let mut result = Network {
|
||||||
|
enabled,
|
||||||
|
connection: None,
|
||||||
|
signal: 0,
|
||||||
|
};
|
||||||
|
if enabled {
|
||||||
|
for device in wifi {
|
||||||
|
let ap: OwnedObjectPath = property(
|
||||||
|
connection,
|
||||||
|
device.as_str(),
|
||||||
|
"org.freedesktop.NetworkManager.Device.Wireless",
|
||||||
|
"ActiveAccessPoint",
|
||||||
|
)?;
|
||||||
|
if ap.as_str() == "/" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ssid: Vec<u8> = property(
|
||||||
|
connection,
|
||||||
|
ap.as_str(),
|
||||||
|
"org.freedesktop.NetworkManager.AccessPoint",
|
||||||
|
"Ssid",
|
||||||
|
)?;
|
||||||
|
let strength: u8 = property(
|
||||||
|
connection,
|
||||||
|
ap.as_str(),
|
||||||
|
"org.freedesktop.NetworkManager.AccessPoint",
|
||||||
|
"Strength",
|
||||||
|
)?;
|
||||||
|
result.connection = Some(String::from_utf8_lossy(&ssid).into_owned());
|
||||||
|
result.signal = strength.min(100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
wifi: bool,
|
||||||
|
enabled: bool,
|
||||||
|
active: bool,
|
||||||
|
strength: u8,
|
||||||
|
}
|
||||||
|
struct Manager(Arc<Mutex<State>>);
|
||||||
|
#[zbus::interface(name = "org.freedesktop.NetworkManager")]
|
||||||
|
impl Manager {
|
||||||
|
#[zbus(property)]
|
||||||
|
fn devices(&self) -> Vec<OwnedObjectPath> {
|
||||||
|
vec![OwnedObjectPath::try_from("/device").unwrap()]
|
||||||
|
}
|
||||||
|
#[zbus(property)]
|
||||||
|
fn wireless_enabled(&self) -> bool {
|
||||||
|
self.0.lock().unwrap().enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Device(Arc<Mutex<State>>);
|
||||||
|
#[zbus::interface(name = "org.freedesktop.NetworkManager.Device")]
|
||||||
|
impl Device {
|
||||||
|
#[zbus(property)]
|
||||||
|
fn device_type(&self) -> u32 {
|
||||||
|
if self.0.lock().unwrap().wifi { 2 } else { 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Wireless(Arc<Mutex<State>>);
|
||||||
|
#[zbus::interface(name = "org.freedesktop.NetworkManager.Device.Wireless")]
|
||||||
|
impl Wireless {
|
||||||
|
#[zbus(property)]
|
||||||
|
fn active_access_point(&self) -> OwnedObjectPath {
|
||||||
|
OwnedObjectPath::try_from(if self.0.lock().unwrap().active {
|
||||||
|
"/ap"
|
||||||
|
} else {
|
||||||
|
"/"
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Ap(Arc<Mutex<State>>);
|
||||||
|
#[zbus::interface(name = "org.freedesktop.NetworkManager.AccessPoint")]
|
||||||
|
impl Ap {
|
||||||
|
#[zbus(property)]
|
||||||
|
fn ssid(&self) -> Vec<u8> {
|
||||||
|
b"wifi:with\\escapes".to_vec()
|
||||||
|
}
|
||||||
|
#[zbus(property)]
|
||||||
|
fn strength(&self) -> u8 {
|
||||||
|
self.0.lock().unwrap().strength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires dbus-daemon; uses a private test bus"]
|
||||||
|
fn fresh_network_state_and_service_recovery() {
|
||||||
|
let bus = crate::status::tests::TestBus::new();
|
||||||
|
let state = Arc::new(Mutex::new(State::default()));
|
||||||
|
let server = zbus::blocking::connection::Builder::address(bus.address.as_str())
|
||||||
|
.unwrap()
|
||||||
|
.name(SERVICE)
|
||||||
|
.unwrap()
|
||||||
|
.serve_at(ROOT, Manager(state.clone()))
|
||||||
|
.unwrap()
|
||||||
|
.serve_at("/device", Device(state.clone()))
|
||||||
|
.unwrap()
|
||||||
|
.serve_at("/device", Wireless(state.clone()))
|
||||||
|
.unwrap()
|
||||||
|
.serve_at("/ap", Ap(state.clone()))
|
||||||
|
.unwrap()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let client = bus.connect();
|
||||||
|
assert!(read(&client).unwrap().is_none());
|
||||||
|
state.lock().unwrap().wifi = true;
|
||||||
|
let value = read(&client).unwrap().unwrap();
|
||||||
|
assert!(!value.enabled && value.connection.is_none());
|
||||||
|
state.lock().unwrap().enabled = true;
|
||||||
|
assert!(read(&client).unwrap().unwrap().connection.is_none());
|
||||||
|
{
|
||||||
|
let mut s = state.lock().unwrap();
|
||||||
|
s.active = true;
|
||||||
|
s.strength = 73;
|
||||||
|
}
|
||||||
|
let value = read(&client).unwrap().unwrap();
|
||||||
|
assert_eq!(value.connection.as_deref(), Some("wifi:with\\escapes"));
|
||||||
|
assert_eq!(value.signal, 73);
|
||||||
|
state.lock().unwrap().strength = 250;
|
||||||
|
assert_eq!(read(&client).unwrap().unwrap().signal, 100);
|
||||||
|
server.release_name(SERVICE).unwrap();
|
||||||
|
assert!(read(&client).is_err());
|
||||||
|
server.request_name(SERVICE).unwrap();
|
||||||
|
assert_eq!(read(&client).unwrap().unwrap().signal, 100);
|
||||||
|
server.object_server().remove::<Ap, _>("/ap").unwrap();
|
||||||
|
assert!(read(&client).is_err());
|
||||||
|
state.lock().unwrap().active = false;
|
||||||
|
assert!(read(&client).unwrap().unwrap().connection.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user