diff --git a/CMakeLists.txt b/CMakeLists.txt index bd609f7..0e26eb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,10 @@ add_library(WiFiDriver src/RadiotapBuilder.h src/RtlUsbAdapter.cpp src/RtlUsbAdapter.h + src/UsbDeviceLock.cpp + src/UsbDeviceLock.h + src/UsbOpen.cpp + src/UsbOpen.h src/SelectedChannel.h src/RateDefinitions.h src/RxPacket.h diff --git a/demo/main.cpp b/demo/main.cpp index 82d8b4f..d125b8c 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -17,6 +17,7 @@ #endif #include "RtlUsbAdapter.h" #include "SignalStop.h" +#include "UsbOpen.h" #include "WiFiDriver.h" #define USB_VENDOR_ID 0x0bda @@ -454,35 +455,28 @@ int main() { return 1; } - // Check if the kernel driver attached - if (libusb_kernel_driver_active(dev_handle, 0)) { - rc = libusb_detach_kernel_driver(dev_handle, 0); // detach driver - } - - /* Skip USB reset if DEVOURER_SKIP_RESET=1. Used when picking up a chip - * with firmware already running (e.g. after a patched-rtw88 sysfs unbind): - * USB reset would clobber fw state and force us to re-run fwdl. */ logger->info("init-timing: demo.open_device = {} ms", ms_since_start()); - if (!std::getenv("DEVOURER_SKIP_RESET")) { - libusb_reset_device(dev_handle); - } else { - logger->info("DEVOURER_SKIP_RESET set — skipping libusb_reset_device"); - } + /* Claim-before-reset: the kernel's exclusive interface claim is the primary + * guard against a second devourer driving this adapter — it returns BUSY, and + * bailing on BUSY *before* the reset keeps a second launch from re-enumerating + * the adapter out from under the process that already owns it. DEVOURER_SKIP_RESET + * still suppresses the reset for a warm pickup (firmware already running). + * See src/UsbOpen.h. */ + std::shared_ptr usb_lock; + rc = devourer::claim_interface_then_reset( + dev_handle, 0, logger, std::getenv("DEVOURER_SKIP_RESET") == nullptr, + usb_lock); logger->info("init-timing: demo.usb_reset = {} ms", ms_since_start()); - /* Set the USB configuration ourselves rather than relying on the kernel - * having done it — required when the device was never kernel-configured - * (e.g. drivers_autoprobe off / a truly cold chip), where bulk transfers - * otherwise fail with errno=3 (ESRCH). No-op if config 1 is already active. */ - { - int cfg = 0; - if (libusb_get_configuration(dev_handle, &cfg) != 0 || cfg != 1) - libusb_set_configuration(dev_handle, 1); + if (rc != 0) { + /* BUSY => another process owns the adapter; any other error => open failed. + * Either way, exit cleanly rather than asserting. */ + libusb_close(dev_handle); + libusb_exit(ctx); + return 1; } - rc = libusb_claim_interface(dev_handle, 0); - assert(rc == 0); WiFiDriver wifi_driver(logger); - auto rtlDevice = wifi_driver.CreateRtlDevice(dev_handle, ctx); + auto rtlDevice = wifi_driver.CreateRtlDevice(dev_handle, ctx, usb_lock); if (!rtlDevice) { /* The factory returns null when the plugged chip's generation wasn't * compiled in (per-chip CMake options); it already logged which. */ diff --git a/src/RtlUsbAdapter.cpp b/src/RtlUsbAdapter.cpp index e47f492..e922989 100644 --- a/src/RtlUsbAdapter.cpp +++ b/src/RtlUsbAdapter.cpp @@ -82,8 +82,10 @@ void RtlUsbAdapter::bulk_read_async_loop( } RtlUsbAdapter::RtlUsbAdapter(libusb_device_handle *dev_handle, Logger_t logger, - libusb_context *ctx) - : _dev_handle{dev_handle}, _ctx{ctx}, _logger{logger} { + libusb_context *ctx, + std::shared_ptr usb_lock) + : _dev_handle{dev_handle}, _ctx{ctx}, _logger{logger}, + _usb_lock{std::move(usb_lock)} { libusb_device_descriptor desc{}; if (libusb_get_device_descriptor(libusb_get_device(_dev_handle), &desc) == LIBUSB_SUCCESS) { diff --git a/src/RtlUsbAdapter.h b/src/RtlUsbAdapter.h index d4d864d..8c531d3 100644 --- a/src/RtlUsbAdapter.h +++ b/src/RtlUsbAdapter.h @@ -16,6 +16,10 @@ #include "hal_com_reg.h" #include "logger.h" +namespace devourer { +class UsbDeviceLock; +} + #define rtw_read8 rtw_read #define rtw_read16 rtw_read #define rtw_read32 rtw_read @@ -68,9 +72,19 @@ class RtlUsbAdapter { std::shared_ptr> _tx_wedged = std::make_shared>(false); + /* Exclusive per-adapter USB lock, acquired by WiFiDriver::CreateRtlDevice and + * held for the device's lifetime (see UsbDeviceLock.h). shared_ptr because + * RtlUsbAdapter is a copyable value type copied into every sub-manager + * (EepromManager / RadioManagementModule / HalModule / the device itself); + * all copies share the one lock, so it releases only when the last copy — and + * thus the whole device — is destroyed. Null when no lock was taken (graceful + * degradation on a lock-infrastructure error). */ + std::shared_ptr _usb_lock; + public: RtlUsbAdapter(libusb_device_handle *dev_handle, Logger_t logger, - libusb_context *ctx = nullptr); + libusb_context *ctx = nullptr, + std::shared_ptr usb_lock = nullptr); /* Kernel-style async RX: keep n_urbs concurrent bulk-IN transfers in flight on * the discovered bulk-IN endpoint, invoking on_data(buf,len) for each non-empty diff --git a/src/UsbDeviceLock.cpp b/src/UsbDeviceLock.cpp new file mode 100644 index 0000000..bb02405 --- /dev/null +++ b/src/UsbDeviceLock.cpp @@ -0,0 +1,154 @@ +#include "UsbDeviceLock.h" + +#if defined(__ANDROID__) || defined(_MSC_VER) || defined(__APPLE__) +#include +#else +#include +#endif + +#include +#include +#include + +namespace devourer { + +namespace { +/* Stable identity of the physical adapter: bus number + USB port path, e.g. + * "3-1.4" (bus 3, hub-port chain 1 -> 4). The port path is what a real OS keys a + * device node to; it survives a VID:PID re-enumeration (unlike the device + * address, which the kernel reassigns on every reset). Falls back to the device + * address only when the backend can't report a port path. */ +std::string device_key(libusb_device *dev) { + if (dev == nullptr) + return "unknown"; + std::string key = std::to_string(libusb_get_bus_number(dev)); + uint8_t ports[8] = {0}; + int pc = libusb_get_port_numbers(dev, ports, sizeof(ports)); + if (pc > 0) { + for (int i = 0; i < pc; ++i) + key += (i == 0 ? "-" : ".") + std::to_string(ports[i]); + } else { + key += "-a" + std::to_string(libusb_get_device_address(dev)); + } + return key; +} +} // namespace + +} // namespace devourer + +#if defined(_WIN32) +/* ------------------------------------------------------------------ Windows */ +#include + +namespace devourer { + +UsbDeviceLock::Result UsbDeviceLock::try_acquire(libusb_device *dev, + std::string *reason) { + if (held()) + return Result::Acquired; + _key = device_key(dev); + /* Named mutex in the session-local namespace (no "Global\\" prefix — one user + * session's devourer instances are what must not collide). A named mutex is + * released when the owning process exits; a crash leaves it ABANDONED, which + * the next waiter still acquires — so no stale lock either way. */ + const std::string name = "devourer-usb-" + _key; + _handle = ::CreateMutexA(nullptr, FALSE, name.c_str()); + if (_handle == nullptr) { + if (reason != nullptr) + *reason = "cannot create lock mutex for adapter " + _key; + return Result::Error; + } + DWORD w = ::WaitForSingleObject(static_cast(_handle), 0); + if (w == WAIT_TIMEOUT) { + ::CloseHandle(static_cast(_handle)); + _handle = nullptr; + if (reason != nullptr) + *reason = "adapter " + _key + + " is already in use by another devourer process"; + return Result::Busy; + } + /* WAIT_OBJECT_0 (clean) or WAIT_ABANDONED (prior owner crashed) -> we own it */ + return Result::Acquired; +} + +bool UsbDeviceLock::held() const { return _handle != nullptr; } + +UsbDeviceLock::~UsbDeviceLock() { + if (_handle != nullptr) { + ::ReleaseMutex(static_cast(_handle)); + ::CloseHandle(static_cast(_handle)); + _handle = nullptr; + } +} + +} // namespace devourer + +#else +/* -------------------------------------------------------------------- POSIX */ +#include +#include +#include + +namespace devourer { + +UsbDeviceLock::Result UsbDeviceLock::try_acquire(libusb_device *dev, + std::string *reason) { + if (held()) + return Result::Acquired; + _key = device_key(dev); + + const char *tmp = std::getenv("TMPDIR"); + std::string dir = (tmp != nullptr && *tmp != '\0') ? tmp : "/tmp"; + while (!dir.empty() && dir.back() == '/') + dir.pop_back(); + _path = dir + "/devourer-usb-" + _key + ".lock"; + + /* 0666 so a second user on the same host can still open O_RDWR and flock the + * same inode (cross-user contention on one adapter must still be caught); + * O_NOFOLLOW so a pre-planted symlink in the world-writable tmpdir can't + * redirect the open. */ + _fd = ::open(_path.c_str(), O_CREAT | O_RDWR | O_NOFOLLOW, 0666); + if (_fd < 0) { + if (reason != nullptr) + *reason = "cannot open lock file " + _path; + return Result::Error; + } + /* Non-blocking exclusive advisory lock. flock is tied to the open file + * description and is dropped by the kernel when the fd is closed — including + * the implicit close on process exit / SIGKILL — so it self-heals. */ + if (::flock(_fd, LOCK_EX | LOCK_NB) != 0) { + ::close(_fd); + _fd = -1; + if (reason != nullptr) + *reason = "adapter " + _key + + " is already in use by another devourer process"; + return Result::Busy; + } + /* Best-effort: record our pid in the file for a human debugging a refusal. + * Purely diagnostic — the lock is the flock, not the file contents. */ + if (::ftruncate(_fd, 0) == 0) { + const std::string pid = std::to_string(::getpid()) + "\n"; + ssize_t wr = ::write(_fd, pid.data(), pid.size()); + (void)wr; + } + return Result::Acquired; +} + +bool UsbDeviceLock::held() const { return _fd >= 0; } + +UsbDeviceLock::~UsbDeviceLock() { + if (_fd >= 0) { + /* Closing the fd releases the flock. Deliberately do NOT unlink the lock + * file: unlinking races a concurrent waiter (it could lock a now-orphaned + * inode while a third process re-creates and locks a fresh one). The file + * is a 0-to-few-byte pid stamp reused across runs — the standard lockfile + * trade-off. */ + ::flock(_fd, LOCK_UN); + ::close(_fd); + _fd = -1; + } +} + +} // namespace devourer + +#endif diff --git a/src/UsbDeviceLock.h b/src/UsbDeviceLock.h new file mode 100644 index 0000000..cfd9c02 --- /dev/null +++ b/src/UsbDeviceLock.h @@ -0,0 +1,68 @@ +#ifndef DEVOURER_USB_DEVICE_LOCK_H +#define DEVOURER_USB_DEVICE_LOCK_H + +#include + +struct libusb_device; + +namespace devourer { + +/* Cross-platform advisory *exclusive* lock for a single physical USB adapter, + * keyed to its bus number + port path (stable across a VID:PID re-enumeration). + * + * WHY: nothing in userspace stops two devourer instances from opening and + * driving the same adapter at once. When that happens the two bring-ups race + * the chip reset / firmware download / register writes and wedge it (observed: + * processes stuck in uninterruptible USB I/O that even `kill -9` won't clear). + * A real OS serialises access to a device node; this gives devourer the same + * guarantee at the library boundary — the second instance's acquisition fails, + * so `CreateRtlDevice` refuses instead of racing. + * + * LIFETIME: the lock is held for this object's lifetime and released + * automatically when the owning process exits — normally, via SIGKILL, or on a + * crash — because the kernel closes the backing fd (POSIX) / handle (Windows) + * on process death. There are therefore never stale locks to clean up, which is + * exactly the failure mode a PID-file scheme suffers after `kill -9`. + * + * FAIL-OPEN vs FAIL-CLOSED: genuine contention (another live process holds the + * adapter) returns Busy — the caller should refuse. An *infrastructure* failure + * (can't create the lock file, e.g. a read-only tmpdir) returns Error — the + * caller should log and proceed without exclusivity, so a quirky environment + * never bricks an otherwise-working open. */ +class UsbDeviceLock { +public: + enum class Result { + Acquired, /* we now hold the exclusive lock */ + Busy, /* another process holds it — caller should refuse */ + Error, /* couldn't create/lock the primitive — caller may proceed */ + }; + + UsbDeviceLock() = default; + ~UsbDeviceLock(); + UsbDeviceLock(const UsbDeviceLock &) = delete; + UsbDeviceLock &operator=(const UsbDeviceLock &) = delete; + UsbDeviceLock(UsbDeviceLock &&) = delete; + UsbDeviceLock &operator=(UsbDeviceLock &&) = delete; + + /* Try to take the exclusive lock for the adapter behind `dev`. On a non-null + * `reason`, a human-readable explanation is written for Busy/Error. Idempotent + * guard: calling twice on an already-held lock returns Acquired. */ + Result try_acquire(libusb_device *dev, std::string *reason); + + bool held() const; + /* The lock key (e.g. "3-1.4" = bus 3, port path 1.4). Empty until acquired. */ + const std::string &key() const { return _key; } + +private: + std::string _key; +#if defined(_WIN32) + void *_handle = nullptr; /* HANDLE from CreateMutex, kept opaque in the header */ +#else + int _fd = -1; + std::string _path; +#endif +}; + +} // namespace devourer + +#endif /* DEVOURER_USB_DEVICE_LOCK_H */ diff --git a/src/UsbOpen.cpp b/src/UsbOpen.cpp new file mode 100644 index 0000000..4c61ee1 --- /dev/null +++ b/src/UsbOpen.cpp @@ -0,0 +1,91 @@ +#include "UsbOpen.h" + +#include "UsbDeviceLock.h" + +#if defined(__ANDROID__) || defined(_MSC_VER) || defined(__APPLE__) +#include +#else +#include +#endif + +#include + +namespace devourer { + +int claim_interface_then_reset(libusb_device_handle *handle, int iface, + const Logger_t &logger, bool do_reset, + std::shared_ptr &out_lock) { + if (handle == nullptr) + return LIBUSB_ERROR_NO_DEVICE; + libusb_device *dev = libusb_get_device(handle); + + /* Advisory exclusive lock FIRST — before detach / claim / reset — so a second + * devourer is turned away without disturbing the device at all. This is the + * gate that actually holds on platforms where the kernel claim below does not + * arbitrate (WinUSB). Genuine contention -> refuse; a lock-infrastructure + * failure (e.g. read-only tmpdir) degrades to a warning and we lean on the + * kernel claim instead. */ + auto lock = std::make_shared(); + std::string why; + switch (lock->try_acquire(dev, &why)) { + case UsbDeviceLock::Result::Busy: + logger->error("USB adapter in use — refusing to open ({})", why); + return LIBUSB_ERROR_BUSY; + case UsbDeviceLock::Result::Error: + logger->warn("USB adapter lock unavailable ({}) — leaning on the kernel " + "interface claim for exclusivity", + why); + lock.reset(); + break; + case UsbDeviceLock::Result::Acquired: + logger->info("USB adapter {} locked for exclusive access", lock->key()); + break; + } + + if (libusb_kernel_driver_active(handle, iface) == 1) + libusb_detach_kernel_driver(handle, iface); + + /* Configure before claiming: a chip that was never kernel-configured is in + * config 0, where claim / bulk I/O fail with ESRCH. No-op if already set. */ + int cfg = 0; + if (libusb_get_configuration(handle, &cfg) != 0 || cfg != 1) + libusb_set_configuration(handle, 1); + + /* The kernel's exclusive claim — a redundant second gate on Linux usbfs + * (reports BUSY); a no-op success on WinUSB, where the lock above is the + * real guard. */ + int rc = libusb_claim_interface(handle, iface); + if (rc != 0) { + if (rc == LIBUSB_ERROR_BUSY) + logger->error("USB interface {} is BUSY — another process is already " + "using this adapter; refusing to open", + iface); + else + logger->error("libusb_claim_interface({}) failed: {} ({})", iface, + libusb_error_name(rc), rc); + return rc; + } + + if (do_reset) { + /* We hold the lock and the interface, so this re-enumeration can't disturb + * anyone else. If the reset invalidates the handle, report it; otherwise + * re-assert the claim (the reset may have released it). */ + int rrc = libusb_reset_device(handle); + if (rrc == LIBUSB_ERROR_NOT_FOUND) { + logger->error("libusb_reset_device: device re-enumerated, handle is " + "stale — close and reopen"); + return rrc; + } + rc = libusb_claim_interface(handle, iface); + if (rc != 0) { + logger->error("re-claim of interface {} after reset failed: {} ({})", + iface, libusb_error_name(rc), rc); + return rc; + } + } + + out_lock = std::move(lock); + return 0; +} + +} // namespace devourer diff --git a/src/UsbOpen.h b/src/UsbOpen.h new file mode 100644 index 0000000..1eb036d --- /dev/null +++ b/src/UsbOpen.h @@ -0,0 +1,45 @@ +#ifndef DEVOURER_USB_OPEN_H +#define DEVOURER_USB_OPEN_H + +#include + +#include "logger.h" + +struct libusb_device_handle; + +namespace devourer { + +class UsbDeviceLock; + +/* Prepare an already-opened USB handle for use with the OS-friendly ordering — + * *lock and claim before reset*: + * + * 1. take the advisory exclusive UsbDeviceLock (see UsbDeviceLock.h) FIRST, + * before touching the device. This is the cross-platform gate: on Windows + * (WinUSB) libusb_claim_interface is not a kernel arbitration point and + * does not report BUSY, so the lock — not the claim — is what turns a + * second devourer away there, and taking it up front means that second + * process resets nothing. `out_lock` receives it; the caller must keep it + * alive for as long as it drives the adapter (hand it to CreateRtlDevice, + * which then does NOT re-acquire); + * 2. detach an attached kernel driver on `iface`; + * 3. set configuration 1 (a cold, never-kernel-configured chip sits in config + * 0, where claim / bulk I/O fail with ESRCH); no-op if already set; + * 4. libusb_claim_interface(iface) — the kernel's exclusive per-interface lock + * (this reports BUSY on Linux usbfs, a redundant second gate); + * 5. only now, holding both the lock and the interface, and only if `do_reset`, + * libusb_reset_device() — safe, since we are the sole owner — then re-claim. + * + * Returns 0 on success; a libusb error code otherwise (already logged via + * `logger`). LIBUSB_ERROR_BUSY means "adapter in use by another process" (from + * either the lock or the claim) — the caller should close the handle and exit + * without touching the device. On success `out_lock` holds the exclusive lock + * (or is null if the lock could only degrade to a warning — e.g. a read-only + * tmpdir — in which case the kernel claim still guards). */ +int claim_interface_then_reset(libusb_device_handle *handle, int iface, + const Logger_t &logger, bool do_reset, + std::shared_ptr &out_lock); + +} // namespace devourer + +#endif /* DEVOURER_USB_OPEN_H */ diff --git a/src/WiFiDriver.cpp b/src/WiFiDriver.cpp index 89b473e..e13fb48 100644 --- a/src/WiFiDriver.cpp +++ b/src/WiFiDriver.cpp @@ -1,11 +1,14 @@ #include "WiFiDriver.h" #include +#include +#include #include #include #include "RtlUsbAdapter.h" +#include "UsbDeviceLock.h" #if defined(DEVOURER_HAVE_JAGUAR1) #include "jaguar1/RtlJaguarDevice.h" #endif @@ -53,7 +56,8 @@ WiFiDriver::WiFiDriver(Logger_t logger) : _logger{std::move(logger)} {} std::unique_ptr WiFiDriver::CreateRtlDevice(libusb_device_handle *dev_handle, - libusb_context *ctx) { + libusb_context *ctx, + std::shared_ptr usb_lock) { uint16_t pid = 0; libusb_device *dev = libusb_get_device(dev_handle); if (dev != nullptr) { @@ -62,13 +66,41 @@ WiFiDriver::CreateRtlDevice(libusb_device_handle *dev_handle, pid = desc.idProduct; } + /* Exclusive per-adapter USB lock (see UsbDeviceLock.h). The recommended open + * path (devourer::claim_interface_then_reset) already took this lock BEFORE + * the reset and hands it in here — in that case we must NOT re-acquire (a + * same-process flock on the same file would self-conflict); we just hold it + * for the device lifetime. A caller that opened/claimed the handle itself + * passes null, so acquire our own here as a best-effort second gate: genuine + * contention -> refuse (return nullptr, same contract as an unsupported chip); + * a lock-infrastructure error degrades to a warning. Either way the lock rides + * into the RtlUsbAdapter below and is released at device destruction. */ + if (!usb_lock) { + auto lock = std::make_shared(); + std::string lock_why; + switch (lock->try_acquire(dev, &lock_why)) { + case devourer::UsbDeviceLock::Result::Busy: + _logger->error("USB adapter in use — refusing to open ({})", lock_why); + return nullptr; + case devourer::UsbDeviceLock::Result::Error: + _logger->warn("USB adapter lock unavailable ({}) — proceeding without " + "exclusive access", + lock_why); + break; + case devourer::UsbDeviceLock::Result::Acquired: + _logger->info("USB adapter {} locked for exclusive access", lock->key()); + usb_lock = std::move(lock); + break; + } + } + uint8_t chip_id = read_chip_id(dev_handle); if (is_8822b_chip_id(chip_id)) { #if defined(DEVOURER_HAVE_JAGUAR2) _logger->info("Creating RtlJaguar2Device (PID 0x{:04x}, chip-id 0x{:02x})", pid, chip_id); return std::make_unique( - RtlUsbAdapter(dev_handle, _logger, ctx), _logger); + RtlUsbAdapter(dev_handle, _logger, ctx, usb_lock), _logger); #else _logger->error("RTL8822B (chip-id 0x{:02x}) detected but Jaguar2 support " "not compiled in (DEVOURER_JAGUAR2=OFF)", @@ -101,7 +133,7 @@ WiFiDriver::CreateRtlDevice(libusb_device_handle *dev_handle, _logger->info("Creating RtlJaguar3Device (PID 0x{:04x}, chip-id 0x{:02x})", pid, chip_id); return std::make_unique( - RtlUsbAdapter(dev_handle, _logger, ctx), _logger, variant); + RtlUsbAdapter(dev_handle, _logger, ctx, usb_lock), _logger, variant); #else _logger->error("Jaguar3 chip (chip-id 0x{:02x}) detected but Jaguar3 " "support not compiled in", @@ -117,7 +149,7 @@ WiFiDriver::CreateRtlDevice(libusb_device_handle *dev_handle, * async URB queue (bulk_read_async_loop) whose event pump needs the same * libusb context the handle was opened on (as Jaguar2/3 already do above). */ return std::make_unique( - RtlUsbAdapter(dev_handle, _logger, ctx), _logger); + RtlUsbAdapter(dev_handle, _logger, ctx, usb_lock), _logger); #else _logger->error("Jaguar1 chip (PID 0x{:04x}, chip-id 0x{:02x}) detected but " "Jaguar1 support not compiled in", diff --git a/src/WiFiDriver.h b/src/WiFiDriver.h index b9ff705..a4ee3bb 100644 --- a/src/WiFiDriver.h +++ b/src/WiFiDriver.h @@ -9,6 +9,10 @@ struct libusb_device_handle; struct libusb_context; +namespace devourer { +class UsbDeviceLock; +} + class WiFiDriver { Logger_t _logger; @@ -18,10 +22,18 @@ class WiFiDriver { /* Constructs the right device for the chip behind `dev_handle`: * RtlJaguarDevice for Jaguar wave-1 (8812/8811/8821/8814AU) or * RtlJaguar3Device for Jaguar3 (8822CU/8812EU/8822EU). Returns the common - * IRtlDevice interface. See CreateRtlDevice() for how the family is chosen. */ + * IRtlDevice interface. See CreateRtlDevice() for how the family is chosen. + * + * `usb_lock` is the exclusive per-adapter lock. Pass the one returned by + * devourer::claim_interface_then_reset (the recommended open path) so it is + * held for the device lifetime and not re-acquired. When null (a caller that + * opened/claimed the handle itself), CreateRtlDevice acquires its own as a + * best-effort second gate, and returns nullptr if the adapter is already in + * use. */ std::unique_ptr CreateRtlDevice(libusb_device_handle *dev_handle, - libusb_context *ctx = nullptr); + libusb_context *ctx = nullptr, + std::shared_ptr usb_lock = nullptr); }; #endif /* WIFI_DRIVER_H */ diff --git a/txdemo/main.cpp b/txdemo/main.cpp index 80fe632..337fcde 100644 --- a/txdemo/main.cpp +++ b/txdemo/main.cpp @@ -37,6 +37,7 @@ #endif #include "RtlUsbAdapter.h" #include "SignalStop.h" +#include "UsbOpen.h" #include "WiFiDriver.h" #include "RadiotapBuilder.h" #include "logger.h" @@ -257,26 +258,22 @@ int main(int argc, char **argv) { logger->info("Vendor/Product ID: {:04x}:{:04x}", desc.idVendor, desc.idProduct); - if (libusb_kernel_driver_active(handle, 0)) { - rc = libusb_detach_kernel_driver(handle, 0); - if (rc != 0) logger->error("libusb_detach_kernel_driver: {}", rc); - } - logger->info("init-timing: txdemo.open_device = {} ms", ms_since_start()); - if (!termux_mode && !std::getenv("DEVOURER_SKIP_RESET")) { - libusb_reset_device(handle); - } + /* Claim-before-reset (see src/UsbOpen.h): the exclusive interface claim is the + * primary guard — a second devourer on this adapter gets BUSY and we bail here + * before the reset, so it can't re-enumerate the adapter out from under the + * owner. Reset skipped in termux_mode (forked child shares the fd) and for + * DEVOURER_SKIP_RESET (warm pickup). */ + std::shared_ptr usb_lock; + rc = devourer::claim_interface_then_reset( + handle, 0, logger, + !termux_mode && std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock); logger->info("init-timing: txdemo.usb_reset = {} ms", ms_since_start()); - - /* Self-set the USB configuration (don't rely on the kernel) so we run on a - * truly cold chip; no-op if config 1 is already active. */ - { - int cfg = 0; - if (libusb_get_configuration(handle, &cfg) != 0 || cfg != 1) - libusb_set_configuration(handle, 1); + if (rc != 0) { + libusb_close(handle); + libusb_exit(context); + return 1; } - rc = libusb_claim_interface(handle, 0); - assert(rc == 0); /* USB-wire sentinel writes — gated behind DEVOURER_USB_SENTINEL=1. Used by * tools/usbmon_pcap_diff.py --phase-split to delimit the init phase in a @@ -384,7 +381,7 @@ int main(int argc, char **argv) { } WiFiDriver wifi_driver{logger}; - auto rtlDevice = wifi_driver.CreateRtlDevice(handle); + auto rtlDevice = wifi_driver.CreateRtlDevice(handle, nullptr, usb_lock); if (!rtlDevice) { /* Factory returns null when this chip's generation wasn't compiled in * (per-chip CMake options); it already logged which. */ diff --git a/txdemo/precoder_demo/main.cpp b/txdemo/precoder_demo/main.cpp index 919ba2e..0d5ecc8 100644 --- a/txdemo/precoder_demo/main.cpp +++ b/txdemo/precoder_demo/main.cpp @@ -54,6 +54,7 @@ #endif #include "RtlUsbAdapter.h" +#include "UsbOpen.h" #include "WiFiDriver.h" #include "logger.h" @@ -188,18 +189,21 @@ int main(int argc, char **argv) { } } - if (libusb_kernel_driver_active(handle, 0)) { - rc = libusb_detach_kernel_driver(handle, 0); - if (rc != 0) logger->error("libusb_detach_kernel_driver: {}", rc); + /* Claim-before-reset (see src/UsbOpen.h): the exclusive claim is the primary + * guard — a second devourer on this adapter gets BUSY here and bails before + * the reset, so it can't re-enumerate the adapter out from under the owner. */ + std::shared_ptr usb_lock; + rc = devourer::claim_interface_then_reset( + handle, 0, logger, + termux_fd == 0 && std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock); + if (rc != 0) { + libusb_close(handle); + libusb_exit(context); + return 1; } - if (termux_fd == 0 && !std::getenv("DEVOURER_SKIP_RESET")) { - libusb_reset_device(handle); - } - rc = libusb_claim_interface(handle, 0); - assert(rc == 0); WiFiDriver wifi_driver{logger}; - auto rtlDevice = wifi_driver.CreateRtlDevice(handle); + auto rtlDevice = wifi_driver.CreateRtlDevice(handle, nullptr, usb_lock); // 2.4 GHz channel 6 is the plan's matrix-validated cell for these chips. int channel = 6; diff --git a/txdemo/stream_duplex_demo/main.cpp b/txdemo/stream_duplex_demo/main.cpp index b6f0e69..11062ee 100644 --- a/txdemo/stream_duplex_demo/main.cpp +++ b/txdemo/stream_duplex_demo/main.cpp @@ -64,6 +64,7 @@ #if defined(DEVOURER_HAVE_JAGUAR1) #include "jaguar1/RtlJaguarDevice.h" #endif +#include "UsbOpen.h" #include "WiFiDriver.h" #include "logger.h" #include "stream_stdin.h" @@ -332,17 +333,21 @@ int main(int argc, char **argv) { } } - if (libusb_kernel_driver_active(handle, 0)) { - libusb_detach_kernel_driver(handle, 0); + /* Claim-before-reset (see src/UsbOpen.h): the exclusive claim is the primary + * guard — a second devourer on this adapter gets BUSY here and bails before + * the reset, so it can't re-enumerate the adapter out from under the owner. */ + std::shared_ptr usb_lock; + rc = devourer::claim_interface_then_reset( + handle, 0, logger, + termux_fd == 0 && std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock); + if (rc != 0) { + libusb_close(handle); + libusb_exit(context); + return 1; } - if (termux_fd == 0 && !std::getenv("DEVOURER_SKIP_RESET")) { - libusb_reset_device(handle); - } - rc = libusb_claim_interface(handle, 0); - assert(rc == 0); WiFiDriver wifi_driver{logger}; - auto rtlDevice = wifi_driver.CreateRtlDevice(handle); + auto rtlDevice = wifi_driver.CreateRtlDevice(handle, nullptr, usb_lock); int channel = 6; if (const char *ch_env = std::getenv("DEVOURER_CHANNEL")) { diff --git a/txdemo/stream_tx_demo/main.cpp b/txdemo/stream_tx_demo/main.cpp index 56607f3..c1a9e54 100644 --- a/txdemo/stream_tx_demo/main.cpp +++ b/txdemo/stream_tx_demo/main.cpp @@ -68,6 +68,7 @@ #if defined(DEVOURER_HAVE_JAGUAR1) #include "jaguar1/RtlJaguarDevice.h" #endif +#include "UsbOpen.h" #include "WiFiDriver.h" #include "logger.h" #include "stream_stdin.h" @@ -171,18 +172,21 @@ int main(int argc, char **argv) { } } - if (libusb_kernel_driver_active(handle, 0)) { - rc = libusb_detach_kernel_driver(handle, 0); - if (rc != 0) logger->error("libusb_detach_kernel_driver: {}", rc); + /* Claim-before-reset (see src/UsbOpen.h): the exclusive claim is the primary + * guard — a second devourer on this adapter gets BUSY here and bails before + * the reset, so it can't re-enumerate the adapter out from under the owner. */ + std::shared_ptr usb_lock; + rc = devourer::claim_interface_then_reset( + handle, 0, logger, + termux_fd == 0 && std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock); + if (rc != 0) { + libusb_close(handle); + libusb_exit(context); + return 1; } - if (termux_fd == 0 && !std::getenv("DEVOURER_SKIP_RESET")) { - libusb_reset_device(handle); - } - rc = libusb_claim_interface(handle, 0); - assert(rc == 0); WiFiDriver wifi_driver{logger}; - auto rtlDevice = wifi_driver.CreateRtlDevice(handle); + auto rtlDevice = wifi_driver.CreateRtlDevice(handle, nullptr, usb_lock); /* Jaguar1-only research features (TXAGC override, fast-retune hopping) aren't * on the IRtlDevice contract — downcast for them; jag is null on Jaguar3, and * the downcast plus its call sites compile out when Jaguar1 isn't built. */ diff --git a/txdemo/svc_tx_demo/main.cpp b/txdemo/svc_tx_demo/main.cpp index 203306d..07674fa 100644 --- a/txdemo/svc_tx_demo/main.cpp +++ b/txdemo/svc_tx_demo/main.cpp @@ -58,6 +58,7 @@ #include "RadiotapBuilder.h" #include "RtlUsbAdapter.h" +#include "UsbOpen.h" #include "WiFiDriver.h" #include "logger.h" #include "stream_stdin.h" @@ -142,15 +143,21 @@ int main(int argc, char** argv) { if (handle == NULL) { logger->error("No supported device"); libusb_exit(context); return 1; } } - if (libusb_kernel_driver_active(handle, 0)) - libusb_detach_kernel_driver(handle, 0); - if (termux_fd == 0 && !std::getenv("DEVOURER_SKIP_RESET")) - libusb_reset_device(handle); - rc = libusb_claim_interface(handle, 0); - assert(rc == 0); + /* Claim-before-reset (see src/UsbOpen.h): the exclusive claim is the primary + * guard — a second devourer on this adapter gets BUSY here and bails before + * the reset, so it can't re-enumerate the adapter out from under the owner. */ + std::shared_ptr usb_lock; + rc = devourer::claim_interface_then_reset( + handle, 0, logger, + termux_fd == 0 && std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock); + if (rc != 0) { + libusb_close(handle); + libusb_exit(context); + return 1; + } WiFiDriver wifi_driver{logger}; - auto rtlDevice = wifi_driver.CreateRtlDevice(handle); + auto rtlDevice = wifi_driver.CreateRtlDevice(handle, nullptr, usb_lock); int channel = 6; if (const char* ch = std::getenv("DEVOURER_CHANNEL")) channel = std::atoi(ch);