Files
external_libcamera/src/libcamera/device_enumerator_udev.cpp
Barnabás Pőcze 2b266a4ab2 libcamera: device_enumerator_udev: Handle duplicate devices
It is possible that same device is processed multiple times, leading to
multiple `MediaDevice`s being instantiated, mostly likely leading to
a fatal error:

  Trying to register a camera with a duplicated ID xyzw...

There is a time window after the `udev_monitor` has been created in `init()`
and the first (and only) enumeration done in `enumerate()`. If e.g. a UVC
camera is connected in this time frame, then it is possible that it will be
processed both by the `udevNotify()` and the initial `enumerate()`, leading
to the fatal error. This can be reproduced as follows:

  1. $ gdb --args cam -m
  2. (gdb) break libcamera::DeviceEnumeratorUdev::enumerate
  3. (gdb) run
  4. when the breakpoint is hit, connect a usb camera
  5. (gdb) continue
  6. observe fatal error

To address this, keep track of the devnums of all devices reported by
udev, and reject devices with already known devnums. This ensures that
the same device won't be reported multiple times (assuming that udev
reports "add" / "remove" events in the correct order).

Closes: https://gitlab.freedesktop.org/camera/libcamera/-/issues/293
Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
2025-12-16 10:49:27 +01:00

381 lines
8.8 KiB
C++

/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2018-2019, Google Inc.
*
* udev-based device enumerator
*/
#include "libcamera/internal/device_enumerator_udev.h"
#include <algorithm>
#include <fcntl.h>
#include <libudev.h>
#include <list>
#include <map>
#include <string.h>
#include <string_view>
#include <sys/ioctl.h>
#include <sys/sysmacros.h>
#include <unistd.h>
#include <libcamera/base/event_notifier.h>
#include <libcamera/base/log.h>
#include "libcamera/internal/media_device.h"
namespace libcamera {
LOG_DECLARE_CATEGORY(DeviceEnumerator)
DeviceEnumeratorUdev::DeviceEnumeratorUdev()
: udev_(nullptr), monitor_(nullptr), notifier_(nullptr)
{
}
DeviceEnumeratorUdev::~DeviceEnumeratorUdev()
{
delete notifier_;
if (monitor_)
udev_monitor_unref(monitor_);
if (udev_)
udev_unref(udev_);
}
int DeviceEnumeratorUdev::init()
{
int ret;
if (udev_)
return -EBUSY;
udev_ = udev_new();
if (!udev_)
return -ENODEV;
monitor_ = udev_monitor_new_from_netlink(udev_, "udev");
if (!monitor_)
return -ENODEV;
ret = udev_monitor_filter_add_match_subsystem_devtype(monitor_, "media",
nullptr);
if (ret < 0)
return ret;
ret = udev_monitor_filter_add_match_subsystem_devtype(monitor_, "video4linux",
nullptr);
if (ret < 0)
return ret;
return 0;
}
int DeviceEnumeratorUdev::addUdevDevice(struct udev_device *dev)
{
const char *subsystem = udev_device_get_subsystem(dev);
if (!subsystem)
return -ENODEV;
/*
* Record that udev reported the given devnum. And reject if it has
* already been seen (e.g. device added between udev monitor creation
* in `init()` and `enumerate()`). This record is kept even if later
* in this function an error is encountered. Only a "remove" event
* from udev should erase it from `devices_`.
*/
const dev_t devnum = udev_device_get_devnum(dev);
if (devnum == makedev(0, 0))
return -ENODEV;
const auto [it, inserted] = devices_.insert(devnum);
if (!inserted)
return -EEXIST;
if (!strcmp(subsystem, "media")) {
std::unique_ptr<MediaDevice> media =
createDevice(udev_device_get_devnode(dev));
if (!media)
return -ENODEV;
DependencyMap deps;
int ret = populateMediaDevice(media.get(), &deps);
if (ret < 0) {
LOG(DeviceEnumerator, Warning)
<< "Failed to populate media device "
<< media->deviceNode()
<< " (" << media->driver() << "), skipping";
return ret;
}
if (!deps.empty()) {
LOG(DeviceEnumerator, Debug)
<< "Defer media device " << media->deviceNode()
<< " due to " << deps.size()
<< " missing dependencies";
pending_.emplace_back(std::move(media), std::move(deps));
MediaDeviceDeps *mediaDeps = &pending_.back();
for (const auto &dep : mediaDeps->deps_)
devMap_[dep.first] = mediaDeps;
return 0;
}
addDevice(std::move(media));
return 0;
}
if (!strcmp(subsystem, "video4linux")) {
addV4L2Device(devnum);
return 0;
}
return -ENODEV;
}
void DeviceEnumeratorUdev::removeUdevDevice(struct udev_device *dev)
{
const char *subsystem = udev_device_get_subsystem(dev);
if (subsystem && !strcmp(subsystem, "media"))
removeDevice(udev_device_get_devnode(dev));
devices_.erase(udev_device_get_devnum(dev));
}
int DeviceEnumeratorUdev::enumerate()
{
struct udev_enumerate *udev_enum = nullptr;
struct udev_list_entry *ents, *ent;
int ret;
udev_enum = udev_enumerate_new(udev_);
if (!udev_enum)
return -ENOMEM;
ret = udev_enumerate_add_match_subsystem(udev_enum, "media");
if (ret < 0)
goto done;
ret = udev_enumerate_add_match_subsystem(udev_enum, "video4linux");
if (ret < 0)
goto done;
ret = udev_enumerate_add_match_is_initialized(udev_enum);
if (ret < 0)
goto done;
ret = udev_enumerate_scan_devices(udev_enum);
if (ret < 0)
goto done;
ents = udev_enumerate_get_list_entry(udev_enum);
if (!ents)
goto done;
udev_list_entry_foreach(ent, ents) {
struct udev_device *dev;
const char *devnode;
const char *syspath = udev_list_entry_get_name(ent);
dev = udev_device_new_from_syspath(udev_, syspath);
if (!dev) {
LOG(DeviceEnumerator, Warning)
<< "Failed to get device for '"
<< syspath << "', skipping";
continue;
}
devnode = udev_device_get_devnode(dev);
if (!devnode) {
udev_device_unref(dev);
LOG(DeviceEnumerator, Warning)
<< "Failed to get device node for '"
<< syspath << "', skipping";
continue;
}
if (addUdevDevice(dev) < 0)
LOG(DeviceEnumerator, Warning)
<< "Failed to add device for '"
<< syspath << "', skipping";
udev_device_unref(dev);
}
done:
udev_enumerate_unref(udev_enum);
if (ret < 0)
return ret;
ret = udev_monitor_enable_receiving(monitor_);
if (ret < 0)
return ret;
int fd = udev_monitor_get_fd(monitor_);
notifier_ = new EventNotifier(fd, EventNotifier::Read);
notifier_->activated.connect(this, &DeviceEnumeratorUdev::udevNotify);
return 0;
}
int DeviceEnumeratorUdev::populateMediaDevice(MediaDevice *media, DependencyMap *deps)
{
std::set<dev_t> children;
/* Associate entities to device node paths. */
for (MediaEntity *entity : media->entities()) {
if (entity->deviceMajor() == 0 && entity->deviceMinor() == 0)
continue;
dev_t devnum = makedev(entity->deviceMajor(),
entity->deviceMinor());
/*
* If the devnum isn't in the orphans list, add it to the unmet
* dependencies.
*/
if (orphans_.find(devnum) == orphans_.end()) {
(*deps)[devnum].push_back(entity);
continue;
}
/*
* Otherwise take it from the orphans list. Don't remove the
* entry from the list yet as other entities in this media
* device may need the same device.
*/
std::string deviceNode = lookupDeviceNode(devnum);
if (deviceNode.empty())
return -EINVAL;
int ret = entity->setDeviceNode(deviceNode);
if (ret)
return ret;
children.insert(devnum);
}
/* Remove all found children from the orphans list. */
for (auto it = orphans_.begin(), last = orphans_.end(); it != last;) {
if (children.find(*it) != children.end())
it = orphans_.erase(it);
else
++it;
}
return 0;
}
/**
* \brief Lookup device node path from device number
* \param[in] devnum The device number
*
* Translate a device number given as \a devnum to a device node path.
*
* \return The device node path on success, or an empty string if the lookup
* fails
*/
std::string DeviceEnumeratorUdev::lookupDeviceNode(dev_t devnum)
{
struct udev_device *device;
const char *name;
std::string deviceNode = std::string();
device = udev_device_new_from_devnum(udev_, 'c', devnum);
if (!device)
return std::string();
name = udev_device_get_devnode(device);
if (name)
deviceNode = name;
udev_device_unref(device);
return deviceNode;
}
/**
* \brief Add a V4L2 device to the media device that it belongs to
* \param[in] devnum major:minor number of V4L2 device to add, as a dev_t
*
* Add V4L2 device identified by \a devnum to the MediaDevice that it belongs
* to, if such a MediaDevice has been created. Otherwise add the V4L2 device
* to the orphan list. If the V4L2 device is added to a MediaDevice, and it is
* the last V4L2 device that the MediaDevice needs, then the MediaDevice is
* added to the DeviceEnumerator, where it is available for pipeline handlers.
*
* \return 0 on success or a negative error code otherwise
*/
int DeviceEnumeratorUdev::addV4L2Device(dev_t devnum)
{
/*
* If the devnum doesn't belong to any media device, add it to the
* orphans list.
*/
auto it = devMap_.find(devnum);
if (it == devMap_.end()) {
orphans_.insert(devnum);
return 0;
}
/*
* Set the device node for all entities matching the devnum. Multiple
* entities can share the same device node, for instance for V4L2 M2M
* devices.
*/
std::string deviceNode = lookupDeviceNode(devnum);
if (deviceNode.empty())
return -EINVAL;
MediaDeviceDeps *deps = it->second;
for (MediaEntity *entity : deps->deps_[devnum]) {
int ret = entity->setDeviceNode(deviceNode);
if (ret)
return ret;
}
/*
* Remove the devnum from the unmet dependencies for this media device.
* If no more dependency is unmet, add the media device to the
* enumerator.
*/
deps->deps_.erase(devnum);
devMap_.erase(it);
if (deps->deps_.empty()) {
LOG(DeviceEnumerator, Debug)
<< "All dependencies for media device "
<< deps->media_->deviceNode() << " found";
addDevice(std::move(deps->media_));
pending_.remove(*deps);
}
return 0;
}
void DeviceEnumeratorUdev::udevNotify()
{
struct udev_device *dev = udev_monitor_receive_device(monitor_);
if (!dev) {
int err = errno;
LOG(DeviceEnumerator, Warning)
<< "Ignoring notfication received without a device: "
<< strerror(err);
return;
}
std::string_view action(udev_device_get_action(dev));
LOG(DeviceEnumerator, Debug)
<< action << " device " << udev_device_get_devnode(dev);
if (action == "add")
addUdevDevice(dev);
else if (action == "remove")
removeUdevDevice(dev);
udev_device_unref(dev);
}
} /* namespace libcamera */