use std::path::{Path, PathBuf};
use udev::{EventType, MonitorBuilder, MonitorSocket};
use drmkit_core::{CoreError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct HotplugEvent {
pub card: Option<PathBuf>,
pub connector_id: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Source {
#[default]
Udev,
Kernel,
}
pub struct HotplugMonitor {
socket: MonitorSocket,
}
impl HotplugMonitor {
pub fn open(source: Source) -> Result<Self> {
let builder = match source {
Source::Udev => MonitorBuilder::new(),
Source::Kernel => MonitorBuilder::new_kernel(),
};
let socket = builder
.and_then(|builder| builder.match_subsystem("drm"))
.and_then(MonitorBuilder::listen)
.map_err(|error| CoreError::Io(io_errno(&error)))?;
Ok(Self { socket })
}
pub fn dispatch(&mut self, out: &mut Vec<HotplugEvent>) {
for event in self.socket.iter() {
let device = event.device();
let hotplug = device
.property_value("HOTPLUG")
.and_then(std::ffi::OsStr::to_str);
if !is_hotplug(event.event_type(), hotplug) {
continue;
}
out.push(HotplugEvent {
card: device.devnode().map(Path::to_path_buf),
connector_id: parse_connector_id(
device
.property_value("CONNECTOR")
.and_then(std::ffi::OsStr::to_str),
),
});
}
}
}
impl std::fmt::Debug for HotplugMonitor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::os::fd::AsFd as _;
f.debug_struct("HotplugMonitor")
.field("fd", &self.socket.as_fd())
.finish()
}
}
impl std::os::fd::AsFd for HotplugMonitor {
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
self.socket.as_fd()
}
}
fn is_hotplug(event_type: EventType, hotplug: Option<&str>) -> bool {
event_type == EventType::Change && hotplug == Some("1")
}
fn parse_connector_id(value: Option<&str>) -> Option<u32> {
value?.parse().ok()
}
fn io_errno(error: &std::io::Error) -> rustix::io::Errno {
rustix::io::Errno::from_io_error(error).unwrap_or(rustix::io::Errno::IO)
}
#[cfg(test)]
mod tests {
use super::{EventType, is_hotplug, parse_connector_id};
#[test]
fn only_a_change_carrying_hotplug_counts() {
assert!(is_hotplug(EventType::Change, Some("1")));
}
#[test]
fn an_ordinary_change_is_ignored() {
assert!(!is_hotplug(EventType::Change, None));
assert!(!is_hotplug(EventType::Change, Some("0")));
}
#[test]
fn an_add_or_remove_is_not_a_hotplug() {
assert!(!is_hotplug(EventType::Add, Some("1")));
assert!(!is_hotplug(EventType::Remove, Some("1")));
assert!(!is_hotplug(EventType::Bind, Some("1")));
}
#[test]
fn the_connector_hint_is_read_when_it_is_a_number() {
assert_eq!(parse_connector_id(Some("393")), Some(393));
assert_eq!(parse_connector_id(Some("0")), Some(0));
}
#[test]
fn anything_else_means_re_enumerate() {
assert_eq!(parse_connector_id(None), None);
assert_eq!(parse_connector_id(Some("")), None);
assert_eq!(parse_connector_id(Some("393x")), None);
assert_eq!(parse_connector_id(Some("-1")), None);
assert_eq!(parse_connector_id(Some("4294967296")), None);
}
}