| #include "usb_monitor.hpp" |
| |
| #include <stdplus/print.hpp> |
| |
| #include <sstream> |
| #include <string_view> |
| |
| namespace usbdevice |
| { |
| |
| /** |
| * @brief Get the root hub path for a given USB device. |
| * |
| * This function walks up the udev device tree from the given usbDevice |
| * to find its root USB hub. The root hub is identified as a 'platform' |
| * subsystem device that is not itself a 'usb_device'. |
| * |
| * @param usbDevice A pointer to the udev_device representing the USB device. |
| * @return A string containing the syspath of the root USB hub, or an empty |
| * string if not found or if usbDevice is null. |
| */ |
| std::string getUsbHub(struct udev_device* usbDevice) |
| { |
| if (!usbDevice) |
| { |
| return ""; |
| } |
| |
| // To find the root hub, we walk up the device chain until we find the |
| // device that is a 'platform' subsystem device but not a 'usb_device' |
| // itself. This is the main controller like 'usb1' or 'usb2'. |
| struct udev_device* parent = usbDevice; |
| while ((parent = udev_device_get_parent(parent)) != NULL) |
| { |
| const char* subsystem = udev_device_get_subsystem(parent); |
| const char* devtype = udev_device_get_devtype(parent); |
| |
| if (subsystem && strcmp(subsystem, "platform") == 0 && |
| (!devtype || strcmp(devtype, "usb_device") != 0)) |
| { |
| return udev_device_get_syspath(parent); |
| } |
| } |
| return ""; |
| } |
| |
| /** |
| * @brief Retrieves the value of a system attribute for a udev device. |
| * |
| * @param dev A pointer to the udev_device. |
| * @param key The name of the system attribute to retrieve. |
| * @return A string containing the value of the system attribute, or an empty |
| * string if the attribute is not found or dev is null. |
| */ |
| std::string GetSysAttrValue(udev_device* dev, std::string_view key) |
| { |
| const char* value = udev_device_get_sysattr_value(dev, key.data()); |
| if (value == nullptr) |
| { |
| return std::string(); |
| } |
| |
| return std::string(value); |
| } |
| |
| UsbMonitor::UsbMonitor(Udev::UdevUniquePtr udevContext, |
| Udev::MonitorUniquePtr udevMonitor, |
| boost::asio::io_context& io) : |
| udevCtx(std::move(udevContext)), udevMonitor(std::move(udevMonitor)), |
| udevDescriptor(io) |
| {} |
| |
| UsbMonitor::~UsbMonitor() {} |
| |
| std::unique_ptr<UsbMonitor> UsbMonitor::create(boost::asio::io_context& io) |
| { |
| auto ctx = Udev::UdevUniquePtr(udev_new()); |
| if (!ctx) |
| { |
| stdplus::println(stderr, "Failed to create udev context."); |
| return nullptr; |
| } |
| |
| auto udevMonitor = Udev::MonitorUniquePtr( |
| udev_monitor_new_from_netlink(ctx.get(), "udev")); |
| if (!udevMonitor) |
| { |
| stdplus::println(stderr, "Failed to create udev monitor."); |
| return nullptr; |
| } |
| |
| return std::unique_ptr<UsbMonitor>( |
| new UsbMonitor(std::move(ctx), std::move(udevMonitor), io)); |
| } |
| |
| std::vector<UsbInterface> UsbMonitor::getEndpoints() |
| { |
| int ret = 0; |
| |
| Udev::EnumerateUniquePtr enumerate(udev_enumerate_new(udevCtx.get())); |
| if (!enumerate) |
| { |
| stdplus::println(stderr, "Failed to create udev enumeration context."); |
| return {}; |
| } |
| |
| ret = udev_enumerate_add_match_subsystem(enumerate.get(), "usb"); |
| if (ret < 0) |
| { |
| stdplus::println(stderr, |
| "udev_enumerate_add_match_subsystem failed. Error: {}", |
| ret); |
| return {}; |
| } |
| |
| ret = udev_enumerate_add_match_property(enumerate.get(), "DEVTYPE", |
| "usb_interface"); |
| if (ret < 0) |
| { |
| stdplus::println( |
| stderr, "udev_enumerate_add_match_property failed. Error: {}", ret); |
| return {}; |
| } |
| |
| ret = udev_enumerate_scan_devices(enumerate.get()); |
| if (ret < 0) |
| { |
| stdplus::println(stderr, |
| "udev_enumerate_scan_devices failed. Error: {}", ret); |
| return {}; |
| } |
| |
| struct udev_list_entry* devices = |
| udev_enumerate_get_list_entry(enumerate.get()); |
| if (!devices) |
| { |
| stdplus::println(stderr, "No USB interfaces found."); |
| return {}; |
| } |
| |
| struct udev_list_entry* devListEntry; |
| std::vector<UsbInterface> usbInfInfo; |
| |
| udev_list_entry_foreach(devListEntry, devices) |
| { |
| const char* infPath = udev_list_entry_get_name(devListEntry); |
| Udev::DeviceUniquePtr usbInf( |
| udev_device_new_from_syspath(udevCtx.get(), infPath)); |
| if (!usbInf) |
| { |
| continue; |
| } |
| |
| std::optional<UsbInterface> devInfo = |
| processUsbInf(usbInf.get(), infPath); |
| if (devInfo.has_value()) |
| { |
| usbInfInfo.push_back(std::move(*devInfo)); |
| } |
| } |
| return usbInfInfo; |
| } |
| |
| std::optional<UsbInterface> UsbMonitor::processUsbInf(udev_device* usbInf, |
| std::string_view infPath) |
| { |
| if (!usbInf) |
| { |
| return std::nullopt; |
| } |
| |
| if (infPath.empty() || (infPath.find_last_of("/") == std::string::npos)) |
| { |
| return std::nullopt; |
| } |
| |
| udev_device* usbDevice = udev_device_get_parent_with_subsystem_devtype( |
| usbInf, "usb", "usb_device"); |
| if (!usbDevice) |
| { |
| return std::nullopt; |
| } |
| |
| struct UsbInterface device; |
| |
| // Eg: 1-1.7. This includes the hub as well. |
| const char* port = udev_device_get_sysname(usbDevice); |
| if (port == nullptr) |
| { |
| return std::nullopt; |
| } |
| |
| // Extract jsut the port from the path that includes USB hub. |
| // Eg: 1-1.7 Then 1 is the USB hub and 1.7 is the port. |
| std::string portPathOnly; |
| std::string fullPortPath(port); |
| size_t hyphenPos = fullPortPath.find('-'); |
| // Check if a hyphen was found and it's not the last character |
| if (hyphenPos != std::string::npos && hyphenPos < fullPortPath.length() - 1) |
| { |
| // Extract the substring after the first hyphen |
| portPathOnly = fullPortPath.substr(hyphenPos + 1); |
| } |
| else |
| { |
| // If there's no hyphen (e.g., a root device), we will use the full name |
| portPathOnly = fullPortPath; |
| } |
| |
| device.name = (sdbusplus::message::object_path() / |
| infPath.substr(infPath.find_last_of("/"))) |
| .str; |
| |
| // Device level properties |
| device.rootHubPath = getUsbHub(usbDevice); |
| device.vendorId = GetSysAttrValue(usbDevice, "idVendor"); |
| device.productId = GetSysAttrValue(usbDevice, "idProduct"); |
| device.manufacturer = GetSysAttrValue(usbDevice, "manufacturer"); |
| device.product = GetSysAttrValue(usbDevice, "product"); |
| device.configuration = GetSysAttrValue(usbDevice, "bConfigurationValue"); |
| |
| // Interface descriptor level properties |
| device.interfacePath = infPath; |
| device.port = portPathOnly; |
| device.interfaceString = GetSysAttrValue(usbInf, "iInterface"); |
| device.interfaceNum = GetSysAttrValue(usbInf, "bInterfaceNumber"); |
| device.interfaceClass = GetSysAttrValue(usbInf, "bInterfaceClass"); |
| device.interfaceSubclass = GetSysAttrValue(usbInf, "bInterfaceSubClass"); |
| device.interfaceProtocol = GetSysAttrValue(usbInf, "bInterfaceProtocol"); |
| |
| return device; |
| } |
| |
| int UsbMonitor::monitor(AddCallback onAdd, RemoveCallback onRemove, |
| void* context) |
| { |
| int ret = udev_monitor_filter_add_match_subsystem_devtype( |
| udevMonitor.get(), "usb", "usb_interface"); |
| if (ret < 0) |
| { |
| stdplus::println( |
| stderr, |
| "udev_monitor_filter_add_match_subsystem_devtype failed. Error: {}", |
| ret); |
| return ret; |
| } |
| |
| ret = udev_monitor_enable_receiving(udevMonitor.get()); |
| if (ret < 0) |
| { |
| stdplus::println( |
| stderr, "udev_monitor_enable_receiving failed. Error: {}", ret); |
| return ret; |
| } |
| |
| int fd = udev_monitor_get_fd(udevMonitor.get()); |
| if (fd < 0) |
| { |
| stdplus::println(stderr, "Failed to get udev monitor fd. Error: {}", |
| fd); |
| return fd; |
| } |
| |
| onAddCallback = onAdd; |
| onRemoveCallback = onRemove; |
| cbContext = context; |
| |
| try |
| { |
| udevDescriptor.assign(fd); |
| } |
| catch (const boost::system::system_error& e) |
| { |
| stdplus::println(stderr, |
| "Assign failure. Error code: {} Error message: {}", |
| e.code().value(), e.what()); |
| return -e.code().value(); |
| } |
| |
| udevDescriptor.async_wait(boost::asio::posix::stream_descriptor::wait_read, |
| [this](const boost::system::error_code& error) { |
| this->handleUdevEvent(error); |
| }); |
| return 0; |
| } |
| |
| void UsbMonitor::handleUdevEvent(const boost::system::error_code& ec) |
| { |
| if (ec == boost::asio::error::operation_aborted) |
| { |
| stdplus::println(stderr, "handleUdevEvent aborted: {}", ec.what()); |
| return; |
| } |
| |
| // Handle other errors |
| if (ec) |
| { |
| stdplus::println(stderr, "handleUdevEvent error: {}", ec.message()); |
| // Not re-arming incase of persistant errors. |
| // TODO: identify error cases where we can re-arm this again. |
| return; |
| } |
| |
| if (onAddCallback == nullptr) |
| { |
| stdplus::println(stderr, "onAddCallback is null"); |
| } |
| |
| if (onRemoveCallback == nullptr) |
| { |
| stdplus::println(stderr, "onRemoveCallback is null"); |
| } |
| |
| while (onAddCallback && onRemoveCallback) |
| { |
| Udev::DeviceUniquePtr usbInf( |
| udev_monitor_receive_device(udevMonitor.get())); |
| if (!usbInf) |
| { |
| break; |
| } |
| |
| const char* path = udev_device_get_syspath(usbInf.get()); |
| if (path == nullptr) |
| { |
| stdplus::println(stderr, "Failed to get syspath for udev event"); |
| continue; |
| } |
| |
| const char* action = udev_device_get_action(usbInf.get()); |
| if (action == nullptr) |
| { |
| stdplus::println(stderr, "Failed to get action for udev event"); |
| continue; |
| } |
| |
| if (strcmp(action, "remove") == 0) |
| { |
| onRemoveCallback(path, cbContext); |
| } |
| else if (strcmp(action, "add") == 0) |
| { |
| std::optional<UsbInterface> devInfo = |
| processUsbInf(usbInf.get(), path); |
| if (!devInfo.has_value()) |
| { |
| stdplus::println( |
| stderr, "Failed to process newly added USB interface {}.", |
| path); |
| continue; |
| } |
| onAddCallback(*devInfo, cbContext); |
| } |
| } |
| |
| // Re-arm the wait operation for the next event. |
| udevDescriptor.async_wait(boost::asio::posix::stream_descriptor::wait_read, |
| [this](const boost::system::error_code& error) { |
| this->handleUdevEvent(error); |
| }); |
| } |
| |
| } // namespace usbdevice |