231 lines
7.5 KiB
Rust
231 lines
7.5 KiB
Rust
//! This module handles pulling and parsing data from an `avahi-browse` subprocess in a way that the GUI can render
|
|
|
|
use std::{
|
|
convert::TryFrom,
|
|
io::BufRead,
|
|
net::IpAddr,
|
|
process::{Child, Stdio},
|
|
str::FromStr,
|
|
sync::mpsc::{self, Receiver, Sender},
|
|
thread::{self, JoinHandle},
|
|
};
|
|
|
|
use thiserror::Error;
|
|
|
|
/// Defines possible errors generated by the packet parsing process
|
|
#[derive(Debug, Error)]
|
|
pub enum PacketParseError {
|
|
#[error("Failed to parse mode char: {0}")]
|
|
ModeParseError(char),
|
|
#[error("Failed to internet protocol type: {0}")]
|
|
IpTypeParseError(String),
|
|
#[error("Ran out of arguments")]
|
|
NotEnoughArgsError,
|
|
#[error(transparent)]
|
|
AddrParseError(#[from] std::net::AddrParseError),
|
|
#[error(transparent)]
|
|
IntParseError(#[from] std::num::ParseIntError),
|
|
}
|
|
|
|
/// Tries to unwrap a string from an arg iter, or just returns an error
|
|
fn try_unwrap_arg(arg: Option<&str>) -> Result<&str, PacketParseError> {
|
|
arg.ok_or(PacketParseError::NotEnoughArgsError)
|
|
}
|
|
|
|
/// Defines the type of packet received from the `avahi-browse` subprocess
|
|
#[derive(Debug)]
|
|
pub enum PacketMode {
|
|
/// A new service was discovered
|
|
New,
|
|
/// An existing service was updated
|
|
Update,
|
|
/// An existing service was removed
|
|
Remove,
|
|
}
|
|
|
|
impl TryFrom<char> for PacketMode {
|
|
type Error = PacketParseError;
|
|
|
|
/// Parses one of avahi's update chars into something we can make use of
|
|
fn try_from(c: char) -> Result<Self, Self::Error> {
|
|
match c {
|
|
'+' => Ok(PacketMode::New),
|
|
'=' => Ok(PacketMode::Update),
|
|
'-' => Ok(PacketMode::Remove),
|
|
_ => Err(PacketParseError::ModeParseError(c)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Used to specify weather a packet came over IPv4 or IPv6
|
|
#[derive(Debug)]
|
|
pub enum IpType {
|
|
V4,
|
|
V6,
|
|
}
|
|
|
|
impl TryFrom<String> for IpType {
|
|
type Error = PacketParseError;
|
|
|
|
/// Parses the type of IP address from the avahi-browse output
|
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
|
match s.as_str() {
|
|
"IPv4" => Ok(IpType::V4),
|
|
"IPv6" => Ok(IpType::V6),
|
|
_ => Err(PacketParseError::IpTypeParseError(s)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The metadat for a single service
|
|
#[derive(Debug)]
|
|
pub struct Service {
|
|
/// The name of the service
|
|
pub name: String,
|
|
/// The mDNS hostname of the service
|
|
pub hostname: String,
|
|
/// The IP address serving the service
|
|
pub ip: IpAddr,
|
|
/// The port the service is listening on
|
|
pub port: u16,
|
|
/// All additional data
|
|
pub data: Vec<String>,
|
|
}
|
|
|
|
/// An information packet parsed from a `avahi-browse` subprocess
|
|
#[derive(Debug)]
|
|
pub struct MdnsPacket {
|
|
/// The type of packet received
|
|
pub mode: PacketMode,
|
|
/// The interface the packet was received on
|
|
pub interface_name: String,
|
|
/// The internet protocol type of the packet
|
|
pub internet_protocol: IpType,
|
|
/// The hostname / service name
|
|
pub hostname: String,
|
|
/// The type of the service
|
|
pub service_type: String,
|
|
/// The domain of the service
|
|
pub domain: String,
|
|
/// The service defined in this packet
|
|
pub service: Option<Service>,
|
|
}
|
|
|
|
impl TryFrom<String> for MdnsPacket {
|
|
type Error = PacketParseError;
|
|
|
|
/// Parses a packet from the `avahi-browse` subprocess
|
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
|
// Convert the incoming data into an iter over its components
|
|
let mut iter = s.split(';');
|
|
|
|
// Load the data before constructing the structure
|
|
// Grabs the first char of the first arg, and reads it as a mode
|
|
let mode = PacketMode::try_from(
|
|
try_unwrap_arg(iter.next())?
|
|
.chars()
|
|
.next()
|
|
.ok_or(PacketParseError::NotEnoughArgsError)?,
|
|
)?;
|
|
// Grabs the second arg, and reads it as an interface name
|
|
let interface_name = try_unwrap_arg(iter.next())?.to_string();
|
|
// Grabs the third arg, and reads it as an internet protocol type
|
|
let internet_protocol = IpType::try_from(try_unwrap_arg(iter.next())?.to_string())?;
|
|
// Grabs the fourth arg, and reads it as a hostname
|
|
let hostname = try_unwrap_arg(iter.next())?
|
|
.to_string()
|
|
.replace("\\.", ".")
|
|
.replace("\\0", " ");
|
|
// Grabs the fifth arg, and reads it as a service type
|
|
let service_type = try_unwrap_arg(iter.next())?.to_string();
|
|
// Grabs the sixth arg, and reads it as a domain
|
|
let domain = try_unwrap_arg(iter.next())?.to_string();
|
|
|
|
Ok(Self {
|
|
mode,
|
|
interface_name,
|
|
internet_protocol,
|
|
hostname,
|
|
service_type: service_type.clone(),
|
|
domain,
|
|
// Reads the remaining args as service data
|
|
// We do this by assuming there is a service, and turning any "no args" errors into a `None` value
|
|
service: match (|| -> Result<Service, PacketParseError> {
|
|
Ok(Service {
|
|
name: service_type,
|
|
hostname: try_unwrap_arg(iter.next())?.to_string(),
|
|
ip: IpAddr::from_str(try_unwrap_arg(iter.next())?)?,
|
|
port: u16::from_str(try_unwrap_arg(iter.next())?)?,
|
|
data: iter.map(|s| s.to_string()).collect(),
|
|
})
|
|
})() {
|
|
Ok(s) => Some(s),
|
|
Err(e) => {
|
|
// eprintln!("{}", e);
|
|
None
|
|
}
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Encapsulates everything needed to read from and control an `avahi-browse` subprocess
|
|
#[derive(Debug)]
|
|
pub struct AvahiSubprocess {
|
|
thread: JoinHandle<()>,
|
|
|
|
/// This stream sends parsed packets back to the main thread
|
|
pub packet_stream: Receiver<MdnsPacket>,
|
|
}
|
|
|
|
impl AvahiSubprocess {
|
|
/// Creates a new `avahi-browse` subprocess
|
|
pub fn spawn() -> Self {
|
|
// Create a channel to send parsed packets back to the main thread
|
|
let (packet_sender, packet_receiver) = mpsc::channel::<MdnsPacket>();
|
|
|
|
let thread = thread::spawn(move || {
|
|
// Spawn avahi-browse
|
|
let child = std::process::Command::new("avahi-browse")
|
|
.arg("-p")
|
|
.arg("-r")
|
|
.arg("-a")
|
|
.stdout(Stdio::piped())
|
|
.spawn()
|
|
.expect("Failed to spawn avahi-browse subprocess");
|
|
|
|
// Create a stream from the stdout of the child
|
|
let stream = child
|
|
.stdout
|
|
.expect("Failed to get stdout of avahi-browse subprocess");
|
|
let stream = std::io::BufReader::new(stream);
|
|
let stream = stream.lines();
|
|
|
|
// Parse the lines from the stream
|
|
for line in stream {
|
|
let line = line.expect("Failed to read line from avahi-browse subprocess");
|
|
|
|
match MdnsPacket::try_from(line.to_string()) {
|
|
Ok(packet) => {
|
|
// Send the packet to the main thread
|
|
// println!("{:?}", packet);
|
|
packet_sender
|
|
.send(packet)
|
|
.expect("Failed to send packet to main thread");
|
|
}
|
|
Err(e) => {
|
|
// Log the error
|
|
eprintln!("Failed to parse packet: {}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Return the subprocess
|
|
Self {
|
|
thread,
|
|
packet_stream: packet_receiver,
|
|
}
|
|
}
|
|
}
|