use anyhow::{Result, bail};
use tokio::{process::Command, task::JoinHandle};
fn parse_scan_line(line: &str) -> Result<WifiNetwork> {
let fields = line
.replace(r"\:", ";")
.replace(":", "|")
.replace(";", ":")
.split("|")
.map(|s| s.to_string())
.collect::<Vec<_>>();
Ok(WifiNetwork {
in_use: fields[0] == "*",
bssid: fields[1].clone(),
ssid: fields[2].clone(),
mode: fields[3].clone(),
band: fields[4].clone(),
channel: fields[5].parse()?,
rate: fields[6].clone(),
signal: fields[7].parse()?,
security: fields[8].clone(),
})
}
#[derive(Debug)]
pub struct WifiNetwork {
pub in_use: bool,
pub bssid: String,
pub ssid: String,
pub mode: String,
pub band: String,
pub channel: usize,
pub rate: String,
pub signal: usize,
pub security: String,
}
#[derive(Debug)]
pub struct WifiDevice {
pub name: String,
pub device_type: String,
pub status: String,
pub wifi_networks: Vec<WifiNetwork>,
scan_pending: bool,
pub scan_task: Option<JoinHandle<Result<Vec<WifiNetwork>>>>,
pub connect_task: Option<JoinHandle<Result<()>>>,
pub connect_ssid: Option<String>,
}
impl WifiDevice {
pub fn new(name: String, device_type: String, status: String) -> Self {
Self {
name,
device_type,
status,
wifi_networks: vec![],
scan_pending: true,
scan_task: None,
connect_task: None,
connect_ssid: None,
}
}
pub fn scan(&mut self) {
let name = self.name.clone();
self.scan_task = Some(tokio::spawn(async move {
let output = Command::new("nmcli")
.args([
"-t",
"-f",
"IN-USE,BSSID,SSID,MODE,BAND,CHAN,RATE,SIGNAL,SECURITY",
"device",
"wifi",
"list",
"ifname",
&name,
"--rescan",
"yes",
])
.output()
.await?;
let raw_output = String::from_utf8(output.stdout)?;
raw_output
.lines()
.filter(|line| !line.is_empty())
.map(parse_scan_line)
.collect()
}));
}
pub fn connect(&mut self, ssid: String, password: Option<String>) {
if self.connect_task.is_some() {
return;
}
let name = self.name.clone();
self.connect_ssid = Some(ssid.clone());
self.connect_task = Some(tokio::spawn(async move {
let mut command = Command::new("nmcli");
command.args(["device", "wifi", "connect", &ssid, "ifname", &name]);
if let Some(password) = password {
command.args(["password", &password]);
}
let output = command.output().await?;
if !output.status.success() {
bail!(String::from_utf8_lossy(&output.stderr).trim().to_string());
}
Ok(())
}));
}
pub fn disconnect(&mut self) {
if self.connect_task.is_some() {
return;
}
let name = self.name.clone();
self.connect_task = Some(tokio::spawn(async move {
let output = Command::new("nmcli")
.args(["device", "disconnect", &name])
.output()
.await?;
if !output.status.success() {
bail!(String::from_utf8_lossy(&output.stderr).trim().to_string());
}
Ok(())
}));
}
pub async fn run(&mut self) -> Result<()> {
if let Some(scan_task) = self.scan_task.as_ref() {
if scan_task.is_finished() {
let task = self.scan_task.take().unwrap();
self.wifi_networks = task.await??;
}
}
if let Some(connect_task) = self.connect_task.as_ref() {
if connect_task.is_finished() {
let task = self.connect_task.take().unwrap();
let _ = task.await?;
self.connect_ssid = None;
self.scan_pending = true;
}
}
Ok(())
}
}
pub type WifiDevices = Vec<WifiDevice>;
pub struct WifiManager {
pub wifi_devices: WifiDevices,
pub find_devices_task: Option<JoinHandle<Result<WifiDevices>>>,
pub scanning_device: Option<String>,
}
impl WifiManager {
pub fn new() -> Self {
Self {
wifi_devices: vec![],
find_devices_task: None,
scanning_device: None,
}
}
pub fn find_devices(&mut self) {
self.find_devices_task = Some(tokio::spawn(async {
let output = Command::new("nmcli")
.args(vec!["-t", "-f", "DEVICE,TYPE,STATE", "device"])
.output()
.await?;
let raw_output = String::from_utf8(output.stdout)?
.replace(r"\:", ";")
.replace(":", "|")
.replace(";", ":");
let lines = raw_output.split("\n");
let mut devices = vec![];
for line in lines {
let info = line.split("|").collect::<Vec<_>>();
if info.len() < 3 {
continue;
}
if info[1] != "wifi" {
continue;
}
let name = info[0];
let device_type = info[1];
let status = info[2];
devices.push(WifiDevice::new(
name.to_string(),
device_type.to_string(),
status.to_string(),
));
}
Ok(devices)
}));
}
pub fn scan_all_devices(&mut self) {
for device in &mut self.wifi_devices {
device.scan_pending = true;
}
}
pub async fn run(&mut self) -> Result<()> {
if let Some(find_devices_task) = self.find_devices_task.as_ref() {
if find_devices_task.is_finished() {
let task = self.find_devices_task.take().unwrap();
self.wifi_devices = task.await??;
}
}
for device in &mut self.wifi_devices {
device.run().await?;
}
if let Some(name) = self.scanning_device.as_ref() {
let still_scanning = self
.wifi_devices
.iter()
.any(|d| &d.name == name && d.scan_task.is_some());
if !still_scanning {
self.scanning_device = None;
}
}
if self.scanning_device.is_none() && self.find_devices_task.is_none() {
if let Some(device) = self.wifi_devices.iter_mut().find(|d| d.scan_pending) {
device.scan_pending = false;
device.scan();
self.scanning_device = Some(device.name.clone());
}
}
Ok(())
}
}