blob: ad0c402c4109205c82cb1015aedd78e9d405ad94 [file]
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use android_hardware_serialservice::aidl::android::hardware::serialservice::SerialPortInfo::SerialPortInfo;
use anyhow::Result;
use futures::stream::BoxStream;
use std::sync::{Arc, Mutex};
use tokio::task::JoinHandle;
use tokio_stream::StreamExt;
use ueventd::device::{Device, FilesystemAttributeMap};
use ueventd::event::{DeviceEvent, DeviceType, EventType};
use crate::driver_type_finder::DriverTypeFinder;
#[mockall::automock]
pub trait DeviceEventCallback {
fn on_device_added(&mut self, info: SerialPortInfo);
fn on_device_removed(&mut self, name: &str);
}
/// Handles the stream of /dev events coming from ueventd Watcher.
pub struct DeviceEventsHandler {
stream: BoxStream<'static, DeviceEvent>,
callback: Box<dyn DeviceEventCallback + Send>,
driver_type_finder: Arc<Mutex<dyn DriverTypeFinder + Send>>,
}
struct UsbDeviceId {
vendor_id: i32,
product_id: i32,
}
impl DeviceEventsHandler {
pub async fn start_new(
stream: BoxStream<'static, DeviceEvent>,
callback: Box<dyn DeviceEventCallback + Send>,
driver_type_finder: Arc<Mutex<dyn DriverTypeFinder + Send>>,
) -> JoinHandle<()> {
let handler = DeviceEventsHandler { stream, callback, driver_type_finder };
tokio::spawn(handler.run())
}
async fn run(mut self) {
while let Some(device_event) = self.stream.next().await {
self.handle_device_event(device_event).await;
}
}
async fn handle_device_event(&mut self, device_event: DeviceEvent) {
let DeviceType::DeviceNode { devnode_path } = device_event.device_type else {
log::error!("unexpected device type {:?}", device_event.device_type);
return;
};
if !devnode_path.starts_with("/dev/") {
log::error!("device name doesn't start with /dev/ in {}", devnode_path.display());
return;
};
// Remove "/dev/" from the path, e.g. "/dev/ttyACM0" -> "ttyACM0"
let name = &devnode_path.to_str().unwrap()[5..];
match device_event.event_type {
EventType::Add => {
let Ok(driver_type) = ({
let driver_type_finder = self.driver_type_finder.lock().unwrap();
driver_type_finder.find(&devnode_path)
}) else {
log::debug!("unsupported device type {}", devnode_path.display());
return;
};
// E.g. /sys/<device-dir>/device/subsystem -> .../bus/usb
// If such a dir doesn't exist, we report "virtual" subsystem
let subsystem_opt = device_event.device.device().and_then(|d| d.subsystem());
let usb_device_id = UsbDeviceId::find_for_device(&device_event.device);
let info = SerialPortInfo {
name: name.to_string(),
subsystem: subsystem_opt.unwrap_or("virtual".to_string()),
driverType: driver_type,
vendorId: usb_device_id.vendor_id,
productId: usb_device_id.product_id,
};
self.callback.on_device_added(info);
}
EventType::Remove => {
self.callback.on_device_removed(name);
}
EventType::Change => {
// We don't care about change events for now.
}
}
}
}
impl UsbDeviceId {
// Check each parent folder until we find idVendor file,
// e.g. from /sys/devices/pci0000:00/0000:00:14.0/usb3/3-8/3-8:1.1/tty/ttyACM0
// to /sys/devices/pci0000:00/0000:00:14.0/usb3/3-8
fn find_for_device(device: &Device) -> Self {
let subsystem = "usb";
let mut current_opt = device.parent_with_subsystem(subsystem);
while let Some(current) = current_opt {
let attrs = current.sysattrs();
if let Ok(vendor_id) = Self::read_hex_attr(&attrs, "idVendor") {
let product_id = Self::read_hex_attr(&attrs, "idProduct");
return Self { vendor_id, product_id: product_id.unwrap_or(-1) };
}
current_opt = current.parent_with_subsystem(subsystem);
}
Self { vendor_id: -1, product_id: -1 }
}
fn read_hex_attr(attrs: &FilesystemAttributeMap, name: &str) -> Result<i32> {
let attr_value = attrs.get(name)?;
let hex_value = attr_value.trim();
Ok(i32::from_str_radix(hex_value, 16)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
use futures::StreamExt;
use mockall::predicate::eq;
use std::collections::HashMap;
use std::path::Path;
use ueventd::device::Device;
use ueventd::mock_sysfs::{MockSysfs, SysfsFile};
use crate::driver_type_finder::MockDriverTypeFinder;
fn init_test_logging() {
android_logger::init_once(
android_logger::Config::default()
.with_tag("device_events_handler_tests")
.with_max_level(log::LevelFilter::Debug),
);
}
#[tokio::test]
async fn test_handle_add_serial_device() {
init_test_logging();
let (device, _sysfs_dir) = create_usb_device_in_mock_sysfs();
let stream = tokio_stream::iter(vec![DeviceEvent {
event_type: EventType::Add,
device_type: DeviceType::DeviceNode {
devnode_path: Path::new("/dev/ttyACM0").to_path_buf(),
},
device,
extra_data: None,
}])
.boxed();
let mut callback = MockDeviceEventCallback::new();
callback.expect_on_device_added().times(1).returning(|info| {
assert_eq!(info.name, "ttyACM0".to_string());
assert_eq!(info.subsystem, "usb".to_string());
assert_eq!(info.driverType, "serial".to_string());
assert_eq!(info.vendorId, 0x0694);
assert_eq!(info.productId, 0x0009);
});
callback.expect_on_device_removed().never();
let mut driver_type_finder = MockDriverTypeFinder::new();
driver_type_finder
.expect_find()
.with(eq(Path::new("/dev/ttyACM0")))
.times(1)
.returning(|_| Ok("serial".to_string()));
let handle = DeviceEventsHandler::start_new(
stream,
Box::new(callback),
Arc::new(Mutex::new(driver_type_finder)) as Arc<Mutex<dyn DriverTypeFinder + Send>>,
)
.await;
handle.await.unwrap();
}
#[tokio::test]
async fn test_handle_add_device_without_usb_id() {
init_test_logging();
let (device, _sysfs_dir) = create_uart_device_in_mock_sysfs();
let stream = tokio_stream::iter(vec![DeviceEvent {
event_type: EventType::Add,
device_type: DeviceType::DeviceNode {
devnode_path: Path::new("/dev/ttyS0").to_path_buf(),
},
device,
extra_data: None,
}])
.boxed();
let mut callback = MockDeviceEventCallback::new();
callback.expect_on_device_added().times(1).returning(|info| {
assert_eq!(info.name, "ttyS0".to_string());
assert_eq!(info.subsystem, "serial-base".to_string());
assert_eq!(info.driverType, "serial".to_string());
assert_eq!(info.vendorId, -1);
assert_eq!(info.productId, -1);
});
callback.expect_on_device_removed().never();
let mut driver_type_finder = MockDriverTypeFinder::new();
driver_type_finder
.expect_find()
.with(eq(Path::new("/dev/ttyS0")))
.times(1)
.returning(|_| Ok("serial".to_string()));
let handle = DeviceEventsHandler::start_new(
stream,
Box::new(callback),
Arc::new(Mutex::new(driver_type_finder)) as Arc<Mutex<dyn DriverTypeFinder + Send>>,
)
.await;
handle.await.unwrap();
}
#[tokio::test]
async fn test_handle_add_and_remove_serial_device() {
init_test_logging();
let (device, _sysfs_dir) = create_usb_device_in_mock_sysfs();
let stream = tokio_stream::iter(vec![
DeviceEvent {
event_type: EventType::Add,
device_type: DeviceType::DeviceNode {
devnode_path: Path::new("/dev/ttyACM0").to_path_buf(),
},
device: device.clone(),
extra_data: None,
},
DeviceEvent {
event_type: EventType::Remove,
device_type: DeviceType::DeviceNode {
devnode_path: Path::new("/dev/ttyACM0").to_path_buf(),
},
device,
extra_data: None,
},
])
.boxed();
let mut callback = MockDeviceEventCallback::new();
callback.expect_on_device_added().times(1).returning(|info| {
assert_eq!(info.name, "ttyACM0".to_string());
assert_eq!(info.subsystem, "usb".to_string());
assert_eq!(info.driverType, "serial".to_string());
assert_eq!(info.vendorId, 0x0694);
assert_eq!(info.productId, 0x0009);
});
callback.expect_on_device_removed().times(1).returning(|name| {
assert_eq!(name, "ttyACM0");
});
let mut driver_type_finder = MockDriverTypeFinder::new();
driver_type_finder
.expect_find()
.with(eq(Path::new("/dev/ttyACM0")))
.times(1)
.returning(|_| Ok("serial".to_string()));
let handle = DeviceEventsHandler::start_new(
stream,
Box::new(callback),
Arc::new(Mutex::new(driver_type_finder)) as Arc<Mutex<dyn DriverTypeFinder + Send>>,
)
.await;
handle.await.unwrap();
}
#[tokio::test]
async fn test_handle_add_alien_device() {
init_test_logging();
let (device, _sysfs_dir) = create_usb_device_in_mock_sysfs();
let stream = tokio_stream::iter(vec![DeviceEvent {
event_type: EventType::Add,
device_type: DeviceType::DeviceNode {
devnode_path: Path::new("/dev/alien").to_path_buf(),
},
device,
extra_data: None,
}])
.boxed();
let mut callback = MockDeviceEventCallback::new();
callback.expect_on_device_added().never();
callback.expect_on_device_removed().never();
let mut driver_type_finder = MockDriverTypeFinder::new();
driver_type_finder
.expect_find()
.with(eq(Path::new("/dev/alien")))
.times(1)
.returning(|_| Err(anyhow!("Driver type not found")));
let handle = DeviceEventsHandler::start_new(
stream,
Box::new(callback),
Arc::new(Mutex::new(driver_type_finder)) as Arc<Mutex<dyn DriverTypeFinder + Send>>,
)
.await;
handle.await.unwrap();
}
fn create_usb_device_in_mock_sysfs() -> (Device, MockSysfs) {
let sysfs = SysfsFile::Dir(HashMap::from([
(
"devices/pci0000:00/0000:00:14.0/usb3/3-8",
SysfsFile::Dir(HashMap::from([
(
"3-8:1.1",
SysfsFile::Dir(HashMap::from([
(
"tty/ttyACM0",
SysfsFile::Dir(HashMap::from([
(
"device",
SysfsFile::Dir(HashMap::from([
(
"subsystem",
SysfsFile::Symlink(
"../../../../../../../../../bus/usb",
),
),
("uevent", SysfsFile::RegularFile("")),
])),
),
(
"subsystem",
SysfsFile::Symlink("../../../../../../../../class/tty"),
),
("uevent", SysfsFile::RegularFile("")),
])),
),
("subsystem", SysfsFile::Symlink("../../../../../../bus/usb")),
("uevent", SysfsFile::RegularFile("")),
])),
),
("subsystem", SysfsFile::Symlink("../../../../../bus/usb")),
("idVendor", SysfsFile::RegularFile("0694\n")),
("idProduct", SysfsFile::RegularFile("0009\n")),
("uevent", SysfsFile::RegularFile("")),
])),
),
("bus/usb", SysfsFile::Dir(HashMap::new())),
("class/tty", SysfsFile::Dir(HashMap::new())),
]));
let sysfs_dir = match MockSysfs::new(sysfs) {
Ok(ms) => ms,
Err(e) => panic!("Could not create mock sysfs: {}", e),
};
let sysfs_path =
sysfs_dir.path().join("devices/pci0000:00/0000:00:14.0/usb3/3-8/3-8:1.1/tty/ttyACM0");
let device = Device::with_root_and_syspath(sysfs_dir.path(), &sysfs_path).unwrap();
(device, sysfs_dir)
}
fn create_uart_device_in_mock_sysfs() -> (Device, MockSysfs) {
let sysfs = SysfsFile::Dir(HashMap::from([
(
"devices/pci0000:00/0000:00:1e.0/dw-apb-uart.6/dw-apb-uart.6:0/dw-apb-uart.6:0.0",
SysfsFile::Dir(HashMap::from([
(
"tty/ttyS0",
SysfsFile::Dir(HashMap::from([
(
"device",
SysfsFile::Dir(HashMap::from([
(
"subsystem",
SysfsFile::Symlink(
"../../../../../../../../../bus/serial-base",
),
),
("uevent", SysfsFile::RegularFile("")),
])),
),
(
"subsystem",
SysfsFile::Symlink("../../../../../../../../../class/tty"),
),
("uevent", SysfsFile::RegularFile("")),
])),
),
("subsystem", SysfsFile::Symlink("../../../../../../../bus/serial-base")),
("uevent", SysfsFile::RegularFile("")),
])),
),
("bus/serial-base", SysfsFile::Dir(HashMap::new())),
("class/tty", SysfsFile::Dir(HashMap::new())),
]));
let sysfs_dir = match MockSysfs::new(sysfs) {
Ok(ms) => ms,
Err(e) => panic!("Could not create mock sysfs: {}", e),
};
let sysfs_path = sysfs_dir.path().join(
"devices/pci0000:00/0000:00:1e.0/dw-apb-uart.6/dw-apb-uart.6:0/dw-apb-uart.6:0.0/tty/ttyS0");
let device = Device::with_root_and_syspath(sysfs_dir.path(), &sysfs_path).unwrap();
(device, sysfs_dir)
}
}