Skip to content

Commit

Permalink
feat: systemprofilestream part of profiler
Browse files Browse the repository at this point in the history
  • Loading branch information
tuna-f1sh committed Feb 7, 2025
1 parent 767d79a commit 9570814
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 57 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ regex_icon = ["dep:regex"] # icon name lookup with regex
cli_generate = ["dep:clap_complete", "dep:clap_mangen"] # for generating man and completions
native = ["nusb", "udev"] # pure Rust USB and udev bindings
ffi = ["libusb", "udevlib"] # C bindings for libusb and libudev
watch = ["crossterm", "futures-lite", "chrono"] # watch mode
watch = ["crossterm", "futures-lite", "chrono", "nusb"] # watch mode
default = ["native", "regex_icon", "watch"] # default native Rust USB (nusb, udevrs) with regex icon name lookup

[[bin]]
Expand Down
2 changes: 2 additions & 0 deletions src/profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub mod libusb;
pub mod macos;
#[cfg(feature = "nusb")]
pub mod nusb;
#[cfg(all(feature = "nusb", feature = "watch"))]
pub mod watch;

/// Transfer direction
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
Expand Down
2 changes: 1 addition & 1 deletion src/profiler/nusb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ impl NusbProfiler {
Ok(extra)
}

fn build_spdevice(
pub(crate) fn build_spdevice(
&mut self,
device_info: &nusb::DeviceInfo,
with_extra: bool,
Expand Down
101 changes: 101 additions & 0 deletions src/profiler/watch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! Leverages the usb HotplugEvent to create a stream of system USB devices
//!
//! See the watch cli for a usage example.
use super::nusb::NusbProfiler;
use super::{Device, SystemProfile, WatchEvent};
use crate::error::Error;
use ::nusb::hotplug::HotplugEvent;
use ::nusb::watch_devices;
use chrono::Local;
use futures_lite::stream::Stream;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};

/// A stream that yields an updated [`SystemProfile`] when a USB device is connected or disconnected
pub struct SystemProfileStream {
spusb: Arc<Mutex<SystemProfile>>,
watch_stream: Pin<Box<dyn Stream<Item = HotplugEvent> + Send>>,
}

impl SystemProfileStream {
/// Create a new [`SystemProfileStream`]
///
/// Will create a new [`SystemProfile`] and watch for USB devices
pub fn new() -> Result<Self, Error> {
let spusb = Arc::new(Mutex::new(super::get_spusb_with_extra()?));
let watch_stream = Box::pin(watch_devices()?);
Ok(Self {
spusb,
watch_stream,
})
}

/// Create a new [`SystemProfileStream`] with a initial [`SystemProfile`]
pub fn new_with_spusb(spusb: Arc<Mutex<SystemProfile>>) -> Result<Self, Error> {
let watch_stream = Box::pin(watch_devices()?);
Ok(Self {
spusb,
watch_stream,
})
}

/// Get the [`SystemProfile`] from the stream
pub fn get_profile(&self) -> Arc<Mutex<SystemProfile>> {
Arc::clone(&self.spusb)
}
}

impl Stream for SystemProfileStream {
type Item = Arc<Mutex<SystemProfile>>;

fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let mut profiler = NusbProfiler::new();

match Pin::new(&mut this.watch_stream).poll_next(cx) {
Poll::Ready(Some(event)) => {
let mut spusb = this.spusb.lock().unwrap();

match event {
HotplugEvent::Connected(device) => {
let mut cyme_device: Device =
profiler.build_spdevice(&device, true).unwrap();
cyme_device.last_event = Some(WatchEvent::Connected(Local::now()));

// is it existing? TODO this is a mess, need to take existing, put devices into new and replace since might have new descriptors
if let Some(existing) = spusb.get_node_mut(&cyme_device.port_path()) {
let devices = std::mem::take(&mut existing.devices);
cyme_device.devices = devices;
*existing = cyme_device;
// else we have to stick into tree at correct place
} else if cyme_device.is_trunk_device() {
let bus = spusb.get_bus_mut(cyme_device.location_id.bus).unwrap();
if let Some(bd) = bus.devices.as_mut() {
bd.push(cyme_device);
} else {
bus.devices = Some(vec![cyme_device]);
}
} else if let Ok(parent_path) = cyme_device.parent_path() {
if let Some(parent) = spusb.get_node_mut(&parent_path) {
if let Some(bd) = parent.devices.as_mut() {
bd.push(cyme_device);
} else {
parent.devices = Some(vec![cyme_device]);
}
}
}
}
HotplugEvent::Disconnected(id) => {
if let Some(device) = spusb.get_id_mut(&id) {
device.last_event = Some(WatchEvent::Disconnected(Local::now()));
}
}
}
Poll::Ready(Some(Arc::clone(&this.spusb)))
}
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
65 changes: 10 additions & 55 deletions src/watch.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
//! Watch for USB devices being connected and disconnected.
//!
//! TODO ideas:
//!
//! - Use cyme::display
//! - Make this into a full TUI with expanding device details
//! - Adjustable PrintSettings with keybindings
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent},
execute, terminal,
};
use nusb::hotplug::HotplugEvent;
use std::io::stdout;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

use cyme::display::*;
use cyme::error::{Error, ErrorKind, Result};
use cyme::profiler::{Device, Filter, SystemProfile, WatchEvent};
use cyme::profiler::{watch::SystemProfileStream, Filter, SystemProfile};
use futures_lite::stream::StreamExt;

pub fn watch_usb_devices(
mut spusb: SystemProfile,
spusb: SystemProfile,
filter: Option<Filter>,
mut print_settings: PrintSettings,
) -> Result<()> {
Expand All @@ -31,71 +24,33 @@ pub fn watch_usb_devices(
print_settings.device_blocks =
Some(DeviceBlocks::default_blocks(print_settings.verbosity > 0));
}
print_settings.device_blocks.as_mut().map(|b| {
if let Some(b) = print_settings.device_blocks.as_mut() {
b.insert(0, DeviceBlocks::EventIcon);
b.push(DeviceBlocks::LastEvent);
});
}

let stop_flag = Arc::new(Mutex::new(false));
let stop_flag_clone = Arc::clone(&stop_flag);

let mut stdout = stdout();
execute!(stdout, terminal::Clear(terminal::ClearType::All))
.map_err(|e| Error::new(ErrorKind::Other("crossterm"), &e.to_string()))?;
let watch = nusb::watch_devices().map_err(|e| Error::new(ErrorKind::Nusb, &e.to_string()))?;
// TODO this requires a rethink of writer since raw needs \n\r
//terminal::enable_raw_mode()?;

// first draw
draw_devices(&spusb, &print_settings)?;

let profile_stream = SystemProfileStream::new_with_spusb(Arc::new(Mutex::new(spusb)))
.map_err(|e| Error::new(ErrorKind::Nusb, &e.to_string()))?;

thread::spawn(move || {
futures_lite::future::block_on(async {
let mut watch_stream = watch;

while let Some(event) = watch_stream.next().await {
match event {
HotplugEvent::Connected(device) => {
// TODO profile device with extra using nusb profiler
// requires to be part of crate
let mut cyme_device: Device = Device::from(&device);
cyme_device.last_event = Some(WatchEvent::Connected(chrono::Local::now()));
// is it existing? TODO this is a mess, need to take existing, put devices into new and replace since might have new descriptors
if let Some(existing) = spusb.get_node_mut(&cyme_device.port_path()) {
let devices = std::mem::take(&mut existing.devices);
cyme_device.devices = devices;
// TODO actually profile extra since descriptors might be new
cyme_device.extra = existing.extra.clone();
*existing = cyme_device;
// else we have to stick into tree at correct place
} else if cyme_device.is_trunk_device() {
let bus = spusb.get_bus_mut(cyme_device.location_id.bus).unwrap();
if let Some(bd) = bus.devices.as_mut() {
bd.push(cyme_device);
} else {
bus.devices = Some(vec![cyme_device]);
}
} else if let Ok(parent_path) = cyme_device.parent_path() {
if let Some(parent) = spusb.get_node_mut(&parent_path) {
if let Some(bd) = parent.devices.as_mut() {
bd.push(cyme_device);
} else {
parent.devices = Some(vec![cyme_device]);
}
}
}
}
HotplugEvent::Disconnected(id) => {
if let Some(device) = spusb.get_id_mut(&id) {
device.last_event =
Some(WatchEvent::Disconnected(chrono::Local::now()));
}
}
}

futures_lite::pin!(profile_stream);
while let Some(spusb) = profile_stream.next().await {
let mut spusb = spusb.lock().unwrap();
// HACK this is prabably over kill, does sort and filter of whole tree every time - filter could be done once and on insert instead
cyme::display::prepare(&mut spusb, &filter, &print_settings);

draw_devices(&spusb, &print_settings).unwrap();
}
});
Expand Down

0 comments on commit 9570814

Please sign in to comment.