How it works...

To get system notifications about events on USB devices, we are using a library called libudev. It provides only a plain C interface, and so we created simple C++ wrappers to make coding easier.

For our wrapper classes, we declared a namespace named usb:

namespace usb {

It contains two classes. The first class is Device, which gives us a C++ interface to a low-level libudev object called udev_device.

We defined a constructor that created an instance of Device from a udev_device pointer and a destructor to release the udev_device. Internally, libudev uses reference counting for its object, and so our destructor calls a function to decrease the reference count of the udev_device:

    ~Device() {
udev_device_unref(dev);
}
    Device(const Device& other) : dev(other.dev) {
udev_device_ref(dev);
}

This way, we can copy the Device instances without memory or file descriptor leaks.

Besides the constructors and the destructor, the Device class has only two methods: action and attr. The action method returns the most recent USB device action:

    std::string action() const { 
return udev_device_get_action(dev);
}

The attr method returns any sysfs attribute associated with the device:

    std::string attr(const char* name) const {
const char* val = udev_device_get_sysattr_value(dev,
name);
return val ? val : "";
}

The Monitor class also has a constructor and a destructor, but we made it noncopyable by disabling the copy constructor:

    Monitor(const Monitor& other) = delete;

The constructor initializes the libudev instance using a static variable to ensure it is initialized only once:

      struct udev* udev = udev_new();

It also sets up the monitoring filter and enables monitoring:

      udev_monitor_filter_add_match_subsystem_devtype(
mon, "usb", NULL);
udev_monitor_enable_receiving(mon);

The wait method contains the most important monitoring logic. It accepts a function-like process object that is called each time an event is detected:

Device wait(std::function<bool(const Device&)> process) {

The function should return true if the event and the device it originates from are what we need; otherwise, it returns false to indicate that wait should keep working.

Internally, the wait function creates a file descriptor that is used to deliver device events to the program:

 

      fds[0].fd = udev_monitor_get_fd(mon);

Then it sets up the monitoring loop. Despite its name, the poll function does not check the status of devices constantly; it waits for events on the specified file descriptors. We pass -1 as a timeout, indicating that we intend to wait for events forever:

int ret = poll(fds, 1, -1);

The poll function returns only in the case of an error or a new USB event. We handle an error condition by throwing an exception:

          if (ret < 0) {
throw std::system_error(errno,
std::system_category(),
"Poll failed");
}

For each event, we create a new instance of Device and pass it to the process. If process returns true, we exit the wait loop, returning the instance of Device to the caller:

            Device d(udev_monitor_receive_device(mon));
if (process(d)) {
return d;
};

Let's see how we can use these classes in our application. In the main function, we create an instance of Monitor and invoke its wait function. We use a lambda function to process each action:

usb::Device d = mon.wait([](auto& d) {

In the lambda function, we print information about all of the events:

    std::cout << "Check [" << id << "] action: " 
<< d.action() << std::endl;

We also check for the specific action and device id:

    return d.action() == "bind" && 
id == "8086:0808";

Once found, we display information about its function and power requirements:

  std::cout << d.attr("product")
<< " connected, uses up to "
<< d.attr("bMaxPower") << std::endl;

Running this application initially does not produce any output:

However, once we insert a USB device (a USB microphone in my case), we can see the following output:

The application can wait for a specific USB device and handle it after it is connected. It does this without busy looping, relying on the information provided by the operating system. As a result, the application spends most of the time sleeping while the poll call is blocked by the operating system.