#![deny(missing_docs)]
use super::Result;
use log::warn;
use std::collections::VecDeque;
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::UnixDatagram;
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::error::Error;
const BUF_SIZE: usize = 10_240;
const PATH_DEFAULT_CLIENT: &str = "/tmp";
const PATH_DEFAULT_SERVER: &str = "/var/run/wpa_supplicant/wlan0";
#[derive(Default)]
pub struct ClientBuilder {
cli_path: Option<PathBuf>,
ctrl_path: Option<PathBuf>,
}
impl ClientBuilder {
#[must_use]
pub fn cli_path<I, P>(mut self, cli_path: I) -> Self
where
I: Into<Option<P>>,
P: AsRef<Path> + Sized,
PathBuf: From<P>,
{
self.cli_path = cli_path.into().map(PathBuf::from);
self
}
#[must_use]
pub fn ctrl_path<I, P>(mut self, ctrl_path: I) -> Self
where
I: Into<Option<P>>,
P: AsRef<Path> + Sized,
PathBuf: From<P>,
{
self.ctrl_path = ctrl_path.into().map(PathBuf::from);
self
}
pub fn open(self) -> Result<Client> {
let mut counter = 0;
loop {
counter += 1;
let bind_filename = format!("wpa_ctrl_{}-{}", std::process::id(), counter);
let bind_filepath = self
.cli_path
.as_deref()
.unwrap_or_else(|| Path::new(PATH_DEFAULT_CLIENT))
.join(bind_filename);
match UnixDatagram::bind(&bind_filepath) {
Ok(socket) => {
socket.connect(self.ctrl_path.unwrap_or_else(|| PATH_DEFAULT_SERVER.into()))?;
socket.set_nonblocking(true)?;
return Ok(Client(ClientInternal {
buffer: [0; BUF_SIZE],
handle: socket,
filepath: bind_filepath,
}));
}
Err(ref e) if counter < 2 && e.kind() == std::io::ErrorKind::AddrInUse => {
std::fs::remove_file(bind_filepath)?;
continue;
}
Err(e) => return Err(e.into()),
};
}
}
}
struct ClientInternal {
buffer: [u8; BUF_SIZE],
handle: UnixDatagram,
filepath: PathBuf,
}
fn select(fd: RawFd, duration: Duration) -> Result<bool> {
let r = unsafe {
let mut raw_fd_set = {
let mut raw_fd_set = std::mem::MaybeUninit::<libc::fd_set>::uninit();
libc::FD_ZERO(raw_fd_set.as_mut_ptr());
raw_fd_set.assume_init()
};
libc::FD_SET(fd, &mut raw_fd_set);
libc::select(
fd + 1,
&mut raw_fd_set,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut libc::timeval {
tv_sec: duration.as_secs().try_into().unwrap(),
tv_usec: duration.subsec_micros().try_into().unwrap(),
},
)
};
if r >= 0 {
Ok(r > 0)
} else {
Err(Error::Wait)
}
}
impl ClientInternal {
pub fn pending(&mut self) -> Result<bool> {
select(self.handle.as_raw_fd(), Duration::from_secs(0))
}
pub fn recv(&mut self) -> Result<Option<String>> {
if self.pending()? {
let buf_len = self.handle.recv(&mut self.buffer)?;
std::str::from_utf8(&self.buffer[0..buf_len])
.map(|s| Some(s.to_owned()))
.map_err(std::convert::Into::into)
} else {
Ok(None)
}
}
fn request<F: FnMut(&str)>(&mut self, cmd: &str, mut cb: F) -> Result<String> {
self.handle.send(cmd.as_bytes())?;
loop {
select(self.handle.as_raw_fd(), Duration::from_secs(10))?;
match self.handle.recv(&mut self.buffer) {
Ok(len) => {
let s = std::str::from_utf8(&self.buffer[0..len])?;
if s.starts_with('<') {
cb(s);
} else {
return Ok(s.to_owned());
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
}
}
impl Drop for ClientInternal {
fn drop(&mut self) {
if let Err(e) = std::fs::remove_file(&self.filepath) {
warn!("Unable to unlink {:?}", e);
}
}
}
pub struct Client(ClientInternal);
impl Client {
#[must_use]
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn attach(mut self) -> Result<ClientAttached> {
if self.0.request("ATTACH", |_: &str| ())? == "OK\n" {
Ok(ClientAttached(self.0, VecDeque::new()))
} else {
Err(Error::Attach)
}
}
pub fn request(&mut self, cmd: &str) -> Result<String> {
self.0.request(cmd, |_: &str| ())
}
}
pub struct ClientAttached(ClientInternal, VecDeque<String>);
impl ClientAttached {
pub fn detach(mut self) -> Result<Client> {
if self.0.request("DETACH", |_: &str| ())? == "OK\n" {
Ok(Client(self.0))
} else {
Err(Error::Detach)
}
}
pub fn recv(&mut self) -> Result<Option<String>> {
if let Some(s) = self.1.pop_back() {
Ok(Some(s))
} else {
self.0.recv()
}
}
pub fn request(&mut self, cmd: &str) -> Result<String> {
let mut messages = VecDeque::new();
let r = self.0.request(cmd, |s: &str| messages.push_front(s.into()));
self.1.extend(messages);
r
}
}
#[cfg(test)]
mod test {
use serial_test::serial;
use super::*;
fn wpa_ctrl() -> Client {
Client::builder().open().unwrap()
}
#[test]
#[serial]
fn attach() {
wpa_ctrl()
.attach()
.unwrap()
.detach()
.unwrap()
.attach()
.unwrap()
.detach()
.unwrap();
}
#[test]
#[serial]
fn detach() {
let wpa = wpa_ctrl().attach().unwrap();
wpa.detach().unwrap();
}
#[test]
#[serial]
fn builder() {
wpa_ctrl();
}
#[test]
#[serial]
fn request() {
let mut wpa = wpa_ctrl();
assert_eq!(wpa.request("PING").unwrap(), "PONG\n");
let mut wpa_attached = wpa.attach().unwrap();
assert_eq!(wpa_attached.request("PING").unwrap(), "PONG\n");
}
#[test]
#[serial]
fn recv() {
let mut wpa = wpa_ctrl().attach().unwrap();
assert_eq!(wpa.recv().unwrap(), None);
assert_eq!(wpa.request("SCAN").unwrap(), "OK\n");
loop {
match wpa.recv().unwrap() {
Some(s) => {
assert_eq!(&s[3..], "CTRL-EVENT-SCAN-STARTED ");
break;
}
None => std::thread::sleep(std::time::Duration::from_millis(10)),
}
}
wpa.detach().unwrap();
}
}