Archived
1

Finished the app

This commit is contained in:
Evan Pratten 2021-12-17 13:44:08 -05:00
parent 309e463578
commit 42a0e33b0d
3 changed files with 226 additions and 26 deletions

148
src/gui.rs Normal file
View File

@ -0,0 +1,148 @@
//! This module handles rendering parsed data to a window
use std::{collections::HashMap, sync::mpsc::Receiver};
use eframe::{egui, epi};
use crate::ipc::{MdnsPacket, Service};
/// Defines a single host record
#[derive(Debug)]
struct HostRecord {
pub name: String,
pub services: HashMap<String, Vec<Service>>,
}
/// This is the GUI, and everything it needs to keep state
pub struct BrowseApp {
/// The data stream coming from the subprocess
packet_stream: Receiver<MdnsPacket>,
/// The currently rendered hosts
current_hosts: HashMap<String, HashMap<String, HostRecord>>,
}
impl BrowseApp {
pub fn new(packet_stream: Receiver<MdnsPacket>) -> Self {
Self {
packet_stream,
current_hosts: HashMap::new(),
}
}
}
impl epi::App for BrowseApp {
fn update(&mut self, ctx: &eframe::egui::CtxRef, frame: &mut eframe::epi::Frame<'_>) {
// Poll the packet stream for new data, and update the hosts list accordingly
for packet in self.packet_stream.try_iter() {
match packet.mode {
crate::ipc::PacketMode::New => {
// Create the interface if not already existing
if !self.current_hosts.contains_key(&packet.interface_name) {
self.current_hosts
.insert(packet.interface_name.clone(), HashMap::new());
}
// Add the host to the interface
self.current_hosts
.get_mut(&packet.interface_name)
.unwrap()
.insert(
packet.hostname.clone(),
HostRecord {
name: packet.hostname.clone(),
services: match packet.service {
Some(service) => {
let mut services = HashMap::new();
services.insert(service.name.clone(), vec![service]);
services
}
None => HashMap::new(),
},
},
);
}
crate::ipc::PacketMode::Update => {
// Get the interface
let interface = self.current_hosts.get_mut(&packet.interface_name).unwrap();
// Get the host
let host = interface.get_mut(&packet.hostname).unwrap();
// Only update the host if the new service is real
if let Some(service) = packet.service {
// If the service name does not exist, create the service on the host
if !host.services.contains_key(&service.name) {
host.services.insert(service.name.clone(), vec![service]);
} else {
// Update an existing service if the address is not already used
let services = host.services.get_mut(&service.name).unwrap();
if !services.iter().any(|s| {
format!("{} {}:{}", s.hostname, s.ip, s.port)
== format!(
"{} {}:{}",
service.hostname, service.ip, service.port
)
}) {
services.push(service);
}
}
}
}
crate::ipc::PacketMode::Remove => {
// TODO: I don't want to actually remove data from the view. Maybe this can get implemented in the future?
}
}
}
// Create a menu bar with quit button
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::menu::menu(ui, "File", |ui| {
if ui.button("Quit").clicked() {
frame.quit();
}
});
});
});
// Create a scrollable area to display the packet data
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
// Split the GUI into interfaces
for (interface, hosts) in &self.current_hosts {
// Create a header for the interface
ui.add(egui::Label::new(interface).heading());
ui.separator();
// Render each host
for host in hosts.values() {
// Render the packet data
egui::CollapsingHeader::new(&host.name).show(ui, |ui| {
// Render every service name as a dropdown
for service_name in host.services.keys() {
egui::CollapsingHeader::new(service_name).show(ui, |ui| {
// Render provider for the service as its own dropdown, containing optional metadata
for service in host.services.get(service_name).unwrap() {
egui::CollapsingHeader::new(format!(
"{} (IP: {} Port: {})",
service.hostname, service.ip, service.port
))
.show(ui, |ui| {
// Render the metadata
ui.label(service.data.join("\n"));
});
}
});
}
});
}
}
});
});
}
fn name(&self) -> &str {
"NetBrowse"
}
}

View File

@ -3,7 +3,9 @@
use std::{
convert::TryFrom,
io::BufRead,
net::IpAddr,
process::{Child, Stdio},
str::FromStr,
sync::mpsc::{self, Receiver, Sender},
thread::{self, JoinHandle},
};
@ -19,6 +21,10 @@ pub enum PacketParseError {
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
@ -71,6 +77,21 @@ impl TryFrom<String> for IpType {
}
}
/// 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 {
@ -86,8 +107,8 @@ pub struct MdnsPacket {
pub service_type: String,
/// The domain of the service
pub domain: String,
/// Additional service metadata
pub metadata: Vec<String>,
/// The service defined in this packet
pub service: Option<Service>,
}
impl TryFrom<String> for MdnsPacket {
@ -98,26 +119,52 @@ impl TryFrom<String> for MdnsPacket {
// 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 {
// Grabs the first char of the first arg, and reads it as a mode
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
interface_name: try_unwrap_arg(iter.next())?.to_string(),
// Grabs the third arg, and reads it as an internet protocol type
internet_protocol: IpType::try_from(try_unwrap_arg(iter.next())?.to_string())?,
// Grabs the fourth arg, and reads it as a hostname
hostname: try_unwrap_arg(iter.next())?.to_string(),
// Grabs the fifth arg, and reads it as a service type
service_type: try_unwrap_arg(iter.next())?.to_string(),
// Grabs the sixth arg, and reads it as a domain
domain: try_unwrap_arg(iter.next())?.to_string(),
// Grabs the remaining args, and reads them as metadata
metadata: iter.map(|s| s.to_string()).collect(),
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
}
},
})
}
}
@ -161,7 +208,7 @@ impl AvahiSubprocess {
match MdnsPacket::try_from(line.to_string()) {
Ok(packet) => {
// Send the packet to the main thread
println!("{:?}", packet);
// println!("{:?}", packet);
packet_sender
.send(packet)
.expect("Failed to send packet to main thread");

View File

@ -65,10 +65,12 @@
)]
use colored::Colorize;
use eframe::{egui::Vec2, NativeOptions};
use gui::BrowseApp;
mod gui;
mod ipc;
fn main() {
// Check for avahi-browse on this system
if let Err(_) = which::which("avahi-browse") {
eprintln!("{}", "`avahi-browse` was not found in system $PATH".red());
@ -78,6 +80,9 @@ fn main() {
// Spawn the subprocess
let subprocess = ipc::AvahiSubprocess::spawn();
loop{}
}
// Build the egui app and run it
let app = BrowseApp::new(subprocess.packet_stream);
let mut options = NativeOptions::default();
options.initial_window_size = Some(Vec2::new(500.0, 800.0));
eframe::run_native(Box::new(app), options);
}