Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 18 additions & 24 deletions demo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#endif
#include "RtlUsbAdapter.h"
#include "SignalStop.h"
#include "UsbOpen.h"
#include "WiFiDriver.h"

#define USB_VENDOR_ID 0x0bda
Expand Down Expand Up @@ -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<devourer::UsbDeviceLock> 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. */
Expand Down
6 changes: 4 additions & 2 deletions src/RtlUsbAdapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<devourer::UsbDeviceLock> 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) {
Expand Down
16 changes: 15 additions & 1 deletion src/RtlUsbAdapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
#include "hal_com_reg.h"
#include "logger.h"

namespace devourer {
class UsbDeviceLock;
}

#define rtw_read8 rtw_read<uint8_t>
#define rtw_read16 rtw_read<uint16_t>
#define rtw_read32 rtw_read<uint32_t>
Expand Down Expand Up @@ -68,9 +72,19 @@ class RtlUsbAdapter {
std::shared_ptr<std::atomic<bool>> _tx_wedged =
std::make_shared<std::atomic<bool>>(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<devourer::UsbDeviceLock> _usb_lock;

public:
RtlUsbAdapter(libusb_device_handle *dev_handle, Logger_t logger,
libusb_context *ctx = nullptr);
libusb_context *ctx = nullptr,
std::shared_ptr<devourer::UsbDeviceLock> 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
Expand Down
154 changes: 154 additions & 0 deletions src/UsbDeviceLock.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#include "UsbDeviceLock.h"

#if defined(__ANDROID__) || defined(_MSC_VER) || defined(__APPLE__)
#include <libusb.h>
#else
#include <libusb-1.0/libusb.h>
#endif

#include <cstdint>
#include <cstdlib>
#include <string>

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 <windows.h>

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>(_handle), 0);
if (w == WAIT_TIMEOUT) {
::CloseHandle(static_cast<HANDLE>(_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>(_handle));
::CloseHandle(static_cast<HANDLE>(_handle));
_handle = nullptr;
}
}

} // namespace devourer

#else
/* -------------------------------------------------------------------- POSIX */
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>

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
68 changes: 68 additions & 0 deletions src/UsbDeviceLock.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#ifndef DEVOURER_USB_DEVICE_LOCK_H
#define DEVOURER_USB_DEVICE_LOCK_H

#include <string>

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 */
Loading
Loading