Files
usb-server/internal/usb/usbdevfs.go
T
duffyduckandClaude Opus 5 9ed473a965 Fix HID transfers, harden the tunnel, add E2E crypto and direct peers
The HID failure came down to the endpoint type map being indexed by
endpoint number without the direction bit. A composite device can have
endpoint 1 as both interrupt IN (0x81) and bulk OUT (0x01); the last one
read won, so interrupt URBs were submitted as bulk and the kernel rejected
them. The device attached and stayed silent.

Endpoint data now comes from the raw descriptors read from /dev/bus/usb
rather than sysfs, which only ever exposes the active alternate setting —
a webcam's isochronous endpoints are invisible there because they only
exist after SET_INTERFACE. Two sysfs parsing bugs fell out of that too:
the numeric endpoint attributes are hex without a prefix (wMaxPacketSize
"0040" was read as 40, not 64), and bInterval was never read at all.

Reliability: three places could freeze the whole process. The share path
fed io.Pipe from the WebSocket read loop, so one slow USB transfer stalled
every tunnel and the keepalives with them. The relay wrote to client
sockets while holding the hub lock, so one peer that stopped reading
blocked routing and registration for everyone. Control transfers ran
inline in the protocol loop behind a 5s timeout. Also fixed: a use-after-
free where a discarded URB's memory could be collected while the kernel
still owned it, a reap loop that spun at 100% CPU on ioctl errors, a
missing attach timeout, a double close(done) panic, and Hash[:8] in the
relay's log line, which let a client with a short hash take the server
down.

Adds mode "both", so one client can offer and consume devices at once.
The tunnel and client-left callbacks became multicast for it: as plain
fields the second manager to register silently unhooked the first.

Tunnel traffic is now AES-256-GCM end to end, on the relay path as well
as directly. The key is derived from the three tokens, not from the group
hash — the relay is told the hash, so a key derived from it would protect
nothing from the one party in the middle. Group IDs are unchanged, so
existing setups keep working; only clients configured without the tokens
drop to unencrypted, relay-only operation.

Peers now try to connect directly, with the relay supplying the public
address neither side can determine for itself. Candidates are raced
because an unreachable address hangs until timeout rather than refusing.
Falling back to the relay is not an error.

Platform reach: cross-compiled targets for ARM, MIPS and RISC-V (the
Linux client needed no code changes — usbdevfs is not architecture
specific), multi-arch Docker images, an Android bridge that accepts
devices over SCM_RIGHTS because apps cannot open /dev/bus/usb, and macOS
builds via system_profiler enumeration.

Adds a Windows KMDF filter driver under driver/windows with its Go side.
UNTESTED: it has never been compiled or run, needs the WDK to build and
an EV certificate to distribute. Treat it as a starting point.

Adds "usb-client diag": says per machine whether sharing and using are
possible, what stands in the way, and what fixes it. Reports can be
uploaded to a relay to get them off machines that are awkward to copy
from.

96 tests, all green under -race. Builds for linux, windows and darwin on
amd64 and arm64.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:02:04 +02:00

582 lines
18 KiB
Go

//go:build linux
package usb
import (
"errors"
"fmt"
"os"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
// ioctl direction constants
const (
iocNone = 0
iocWrite = 1
iocRead = 2
)
// ioctl encoding helpers
func ioc(dir, typ, nr, size uintptr) uintptr {
return (dir << 30) | (size << 16) | (typ << 8) | nr
}
func ior(typ, nr, size uintptr) uintptr { return ioc(iocRead, typ, nr, size) }
func iow(typ, nr, size uintptr) uintptr { return ioc(iocWrite, typ, nr, size) }
func iowr(typ, nr, size uintptr) uintptr { return ioc(iocRead|iocWrite, typ, nr, size) }
func io_(typ, nr uintptr) uintptr { return ioc(iocNone, typ, nr, 0) }
// USBDEVFS_DISCONNECT_CLAIM flags
const disconnectClaimIfDriver = 0x01
// USB device file system ioctl numbers
var (
usbdevfsControl = iowr('U', 0, unsafe.Sizeof(usbdevfsCtrlTransfer{}))
usbdevfsBulk = iowr('U', 2, unsafe.Sizeof(usbdevfsBulkTransfer{}))
usbdevfsSetInterface = ior('U', 4, unsafe.Sizeof(usbdevfsSetIntf{}))
usbdevfsSetConfig = ior('U', 5, 4)
usbdevfsSubmitURB = ior('U', 10, unsafe.Sizeof(usbdevfsURB{}))
usbdevfsResetEP = ior('U', 3, 4)
usbdevfsDiscardURB = io_('U', 11)
usbdevfsReapURB = iow('U', 12, unsafe.Sizeof(uintptr(0)))
usbdevfsReapURBNDelay = iow('U', 13, unsafe.Sizeof(uintptr(0)))
usbdevfsClaimInterface = ior('U', 15, 4)
usbdevfsReleaseInterface = ior('U', 16, 4)
usbdevfsIoctl = iowr('U', 18, unsafe.Sizeof(usbdevfsIoctlArg{}))
usbdevfsReset = io_('U', 20)
usbdevfsClearHalt = ior('U', 21, 4)
usbdevfsDisconnect = io_('U', 22)
usbdevfsConnect = io_('U', 23)
usbdevfsGetCapabilities = ior('U', 26, 4)
usbdevfsDisconnectClaim = ior('U', 27, unsafe.Sizeof(usbdevfsDisconnectClaimArg{}))
usbdevfsGetSpeed = io_('U', 31)
)
// URB type constants
const (
urbTypeISO = 0
urbTypeInterrupt = 1
urbTypeControl = 2
urbTypeBulk = 3
)
// usbdevfs structures for ioctls
type usbdevfsCtrlTransfer struct {
RequestType uint8
Request uint8
Value uint16
Index uint16
Length uint16
Timeout uint32
Data uintptr
}
type usbdevfsBulkTransfer struct {
Endpoint uint32
Length uint32
Timeout uint32
Data uintptr
}
type usbdevfsSetIntf struct {
Interface uint32
AltSetting uint32
}
type usbdevfsISOPacketDesc struct {
Length uint32
ActualLength uint32
Status uint32
}
// usbdevfsIoctlArg is the argument for USBDEVFS_IOCTL (per-interface sub-ioctl)
type usbdevfsIoctlArg struct {
Ifno int32
IoctlCode int32
Data uintptr
}
// usbdevfsDisconnectClaimArg is the argument for USBDEVFS_DISCONNECT_CLAIM
type usbdevfsDisconnectClaimArg struct {
Interface uint32
Flags uint32
Driver [256]byte
}
type usbdevfsURB struct {
Type uint8
Endpoint uint8
Status int32
Flags uint32
Buffer uintptr
BufferLength int32
ActualLength int32
StartFrame int32
NumberOfPackets int32 // or StreamID
ErrorCount int32
Signr uint32
UserContext uintptr
// ISO packet descriptors follow in memory if Type == urbTypeISO
}
// DeviceHandle provides low-level USB device access via usbdevfs
type DeviceHandle struct {
fd int
busID string
devPath string
// adopted marks a descriptor handed to us from outside rather than
// opened here. It is closed on Close like any other, but the distinction
// matters for diagnostics: an adopted descriptor means the host process
// could not have opened the device itself.
adopted bool
}
// OpenDevice opens a USB device file for direct access.
//
// If an external file descriptor has been registered for this device (see
// AdoptDeviceFD) it is used instead of opening the path. That is how Android
// works: apps cannot open /dev/bus/usb themselves, so a small Java shim asks
// the system for permission and hands the resulting descriptor down.
func OpenDevice(devPath string, busID string) (*DeviceHandle, error) {
if fd, ok := takeAdoptedFD(busID); ok {
return &DeviceHandle{fd: fd, busID: busID, devPath: devPath, adopted: true}, nil
}
fd, err := unix.Open(devPath, unix.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", devPath, err)
}
return &DeviceHandle{
fd: fd,
busID: busID,
devPath: devPath,
}, nil
}
// Close closes the device handle
func (h *DeviceHandle) Close() error {
return unix.Close(h.fd)
}
// Fd returns the file descriptor
func (h *DeviceHandle) Fd() int {
return h.fd
}
// DisconnectDriver disconnects the kernel driver from the device
func (h *DeviceHandle) DisconnectDriver() error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsDisconnect, 0)
if errno != 0 {
return fmt.Errorf("USBDEVFS_DISCONNECT: %w", errno)
}
return nil
}
// ConnectDriver reconnects the kernel driver
func (h *DeviceHandle) ConnectDriver() error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsConnect, 0)
if errno != 0 {
return fmt.Errorf("USBDEVFS_CONNECT: %w", errno)
}
return nil
}
// DisconnectDriverForInterface disconnects the kernel driver from a specific interface
// Uses USBDEVFS_IOCTL with USBDEVFS_DISCONNECT sub-ioctl
func (h *DeviceHandle) DisconnectDriverForInterface(ifnum uint32) error {
arg := usbdevfsIoctlArg{
Ifno: int32(ifnum),
IoctlCode: int32(usbdevfsDisconnect),
Data: 0,
}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsIoctl, uintptr(unsafe.Pointer(&arg)))
if errno != 0 && errno != unix.ENODATA { // ENODATA = no driver bound, that's OK
return fmt.Errorf("USBDEVFS_IOCTL(DISCONNECT, iface %d): %w", ifnum, errno)
}
return nil
}
// DisconnectClaimInterface atomically disconnects kernel driver and claims an interface
// Uses USBDEVFS_DISCONNECT_CLAIM (available since Linux 3.7)
func (h *DeviceHandle) DisconnectClaimInterface(ifnum uint32) error {
arg := usbdevfsDisconnectClaimArg{
Interface: ifnum,
Flags: disconnectClaimIfDriver,
}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsDisconnectClaim, uintptr(unsafe.Pointer(&arg)))
if errno != 0 {
return fmt.Errorf("USBDEVFS_DISCONNECT_CLAIM(%d): %w", ifnum, errno)
}
return nil
}
// ClaimInterface claims exclusive access to a USB interface
func (h *DeviceHandle) ClaimInterface(ifnum uint32) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsClaimInterface, uintptr(unsafe.Pointer(&ifnum)))
if errno != 0 {
return fmt.Errorf("USBDEVFS_CLAIMINTERFACE(%d): %w", ifnum, errno)
}
return nil
}
// ReleaseInterface releases a claimed interface
func (h *DeviceHandle) ReleaseInterface(ifnum uint32) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsReleaseInterface, uintptr(unsafe.Pointer(&ifnum)))
if errno != 0 {
return fmt.Errorf("USBDEVFS_RELEASEINTERFACE(%d): %w", ifnum, errno)
}
return nil
}
// SetConfiguration sets the device configuration
func (h *DeviceHandle) SetConfiguration(config uint32) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsSetConfig, uintptr(unsafe.Pointer(&config)))
if errno != 0 {
return fmt.Errorf("USBDEVFS_SETCONFIGURATION(%d): %w", config, errno)
}
return nil
}
// SetInterface sets alternate setting for an interface
func (h *DeviceHandle) SetInterface(iface, altSetting uint32) error {
si := usbdevfsSetIntf{Interface: iface, AltSetting: altSetting}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsSetInterface, uintptr(unsafe.Pointer(&si)))
if errno != 0 {
return fmt.Errorf("USBDEVFS_SETINTERFACE(%d, %d): %w", iface, altSetting, errno)
}
return nil
}
// ClearHalt clears endpoint halt/stall condition
func (h *DeviceHandle) ClearHalt(endpoint uint32) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsClearHalt, uintptr(unsafe.Pointer(&endpoint)))
if errno != 0 {
return fmt.Errorf("USBDEVFS_CLEAR_HALT(%d): %w", endpoint, errno)
}
return nil
}
// ResetDevice resets the USB device
func (h *DeviceHandle) ResetDevice() error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsReset, 0)
if errno != 0 {
return fmt.Errorf("USBDEVFS_RESET: %w", errno)
}
return nil
}
// GetSpeed returns the device speed
func (h *DeviceHandle) GetSpeed() (uint32, error) {
r, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsGetSpeed, 0)
if errno != 0 {
return 0, fmt.Errorf("USBDEVFS_GET_SPEED: %w", errno)
}
return uint32(r), nil
}
// ControlTransfer performs a synchronous control transfer
func (h *DeviceHandle) ControlTransfer(requestType, request uint8, value, index, length uint16, timeout uint32, data []byte) (int, error) {
var dataPtr uintptr
if len(data) > 0 {
dataPtr = uintptr(unsafe.Pointer(&data[0]))
}
ct := usbdevfsCtrlTransfer{
RequestType: requestType,
Request: request,
Value: value,
Index: index,
Length: length,
Timeout: timeout,
Data: dataPtr,
}
r, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsControl, uintptr(unsafe.Pointer(&ct)))
if errno != 0 {
return 0, fmt.Errorf("USBDEVFS_CONTROL: %w", errno)
}
return int(r), nil
}
// SubmitURBParams holds parameters for async URB submission
type SubmitURBParams struct {
Type uint8
Endpoint uint8
Flags uint32
Buffer []byte
UserContext uintptr
}
// SubmitURB submits an asynchronous URB
func (h *DeviceHandle) SubmitURB(params *SubmitURBParams) (*usbdevfsURB, error) {
var bufPtr uintptr
if len(params.Buffer) > 0 {
bufPtr = uintptr(unsafe.Pointer(&params.Buffer[0]))
}
urb := &usbdevfsURB{
Type: params.Type,
Endpoint: params.Endpoint,
Flags: params.Flags,
Buffer: bufPtr,
BufferLength: int32(len(params.Buffer)),
NumberOfPackets: -1, // 0xFFFFFFFF for non-ISO
UserContext: params.UserContext,
}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsSubmitURB, uintptr(unsafe.Pointer(urb)))
if errno != 0 {
return nil, fmt.Errorf("USBDEVFS_SUBMITURB: %w", errno)
}
return urb, nil
}
// urbFromKernelPtr converts the uintptr USBDEVFS_REAPURB writes back into a
// *usbdevfsURB.
//
// go vet flags this as "possible misuse of unsafe.Pointer", correctly in
// general: the garbage collector cannot see a pointer stored in a uintptr, so
// the object could be collected before the conversion. It is safe here because
// the kernel only ever returns a pointer we submitted ourselves, and the
// caller keeps that URB reachable — in pendingURBs or unlinkedURBs on the
// server — from submission until after it has been reaped.
func urbFromKernelPtr(p uintptr) *usbdevfsURB {
//nolint:govet // see the comment above
return (*usbdevfsURB)(unsafe.Pointer(p))
}
// ReapURB blocks until a URB completes, then returns it
func (h *DeviceHandle) ReapURB() (*usbdevfsURB, error) {
var urbPtr uintptr
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsReapURB, uintptr(unsafe.Pointer(&urbPtr)))
if errno != 0 {
return nil, fmt.Errorf("USBDEVFS_REAPURB: %w", errno)
}
return urbFromKernelPtr(urbPtr), nil
}
// ReapURBNonBlock tries to reap a URB without blocking
func (h *DeviceHandle) ReapURBNonBlock() (*usbdevfsURB, error) {
var urbPtr uintptr
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsReapURBNDelay, uintptr(unsafe.Pointer(&urbPtr)))
if errno != 0 {
return nil, fmt.Errorf("USBDEVFS_REAPURBNDELAY: %w", errno)
}
return urbFromKernelPtr(urbPtr), nil
}
// DiscardURB cancels a submitted URB
func (h *DeviceHandle) DiscardURB(urb *usbdevfsURB) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsDiscardURB, uintptr(unsafe.Pointer(urb)))
if errno != 0 {
return fmt.Errorf("USBDEVFS_DISCARDURB: %w", errno)
}
return nil
}
// DiscardURBByPtr cancels a submitted URB given its raw pointer.
// Use this when the URB type is not accessible (e.g. from another package).
func (h *DeviceHandle) DiscardURBByPtr(ptr unsafe.Pointer) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsDiscardURB, uintptr(ptr))
if errno != 0 {
return fmt.Errorf("USBDEVFS_DISCARDURB: %w", errno)
}
return nil
}
// ResetEndpoint resets the host-side data toggle for an endpoint without
// sending any USB traffic to the device. Use after SET_CONFIGURATION to
// sync host controller toggle state with the device.
func (h *DeviceHandle) ResetEndpoint(endpoint uint32) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsResetEP, uintptr(unsafe.Pointer(&endpoint)))
if errno != 0 {
return fmt.Errorf("USBDEVFS_RESETEP(%d): %w", endpoint, errno)
}
return nil
}
// SubmitISOURBParams holds parameters for async ISO URB submission
type SubmitISOURBParams struct {
Endpoint uint8
Flags uint32
Buffer []byte
NumberOfPackets int32
PacketLengths []uint32 // length of each ISO packet
UserContext uintptr
}
// SubmitISOURB submits an asynchronous isochronous URB.
// Returns the URB pointer and the backing memory slice (must be kept alive until reap).
func (h *DeviceHandle) SubmitISOURB(params *SubmitISOURBParams) (urb *usbdevfsURB, mem []byte, err error) {
urbSize := unsafe.Sizeof(usbdevfsURB{})
isoDescSize := unsafe.Sizeof(usbdevfsISOPacketDesc{})
totalSize := urbSize + uintptr(params.NumberOfPackets)*isoDescSize
mem = make([]byte, totalSize)
urb = (*usbdevfsURB)(unsafe.Pointer(&mem[0]))
var bufPtr uintptr
if len(params.Buffer) > 0 {
bufPtr = uintptr(unsafe.Pointer(&params.Buffer[0]))
}
urb.Type = urbTypeISO
urb.Endpoint = params.Endpoint
urb.Flags = params.Flags
urb.Buffer = bufPtr
urb.BufferLength = int32(len(params.Buffer))
urb.NumberOfPackets = params.NumberOfPackets
urb.UserContext = params.UserContext
// Fill ISO packet descriptors (immediately following the URB in memory)
for i := int32(0); i < params.NumberOfPackets; i++ {
descOffset := urbSize + uintptr(i)*isoDescSize
desc := (*usbdevfsISOPacketDesc)(unsafe.Pointer(&mem[descOffset]))
if i < int32(len(params.PacketLengths)) {
desc.Length = params.PacketLengths[i]
}
}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsSubmitURB, uintptr(unsafe.Pointer(urb)))
if errno != 0 {
return nil, nil, fmt.Errorf("USBDEVFS_SUBMITURB (ISO): %w", errno)
}
return urb, mem, nil
}
// ISOPacketResult holds the result of one ISO packet after reaping
type ISOPacketResult struct {
Length uint32
ActualLength uint32
Status uint32
}
// ReadISOResults reads the ISO packet results from a reaped ISO URB's backing memory.
func ReadISOResults(mem []byte, numPackets int32) []ISOPacketResult {
urbSize := unsafe.Sizeof(usbdevfsURB{})
isoDescSize := unsafe.Sizeof(usbdevfsISOPacketDesc{})
results := make([]ISOPacketResult, numPackets)
for i := int32(0); i < numPackets; i++ {
offset := urbSize + uintptr(i)*isoDescSize
if int(offset+isoDescSize) > len(mem) {
break
}
desc := (*usbdevfsISOPacketDesc)(unsafe.Pointer(&mem[offset]))
results[i] = ISOPacketResult{
Length: desc.Length,
ActualLength: desc.ActualLength,
Status: desc.Status,
}
}
return results
}
// ReapedURBInfo holds exported fields from a reaped URB needed for response building
type ReapedURBInfo struct {
UserContext uintptr
Status int32
ActualLength int32
StartFrame int32
ErrorCount int32
}
// ErrNoURBReady is returned by ReapURBInfoNonBlock when no URB has completed.
var ErrNoURBReady = errors.New("no completed URB available")
// ErrDeviceGone is returned when the device has been unplugged or the file
// descriptor is no longer usable.
var ErrDeviceGone = errors.New("device gone")
// WaitForURB waits up to timeout for at least one URB to complete.
// It returns true if a URB is ready to be reaped, false on timeout.
//
// usbdevfs signals completed URBs via POLLOUT, so polling lets the reap loop
// stay responsive to shutdown without either spinning on a non-blocking ioctl
// or blocking indefinitely in USBDEVFS_REAPURB. The latter matters: a blocking
// reap can only be broken by closing the fd, which races with the fd being
// reused by another goroutine.
func (h *DeviceHandle) WaitForURB(timeout time.Duration) (bool, error) {
fds := []unix.PollFd{{Fd: int32(h.fd), Events: unix.POLLOUT}}
ms := int(timeout.Milliseconds())
if ms < 0 {
ms = 0
}
for {
n, err := unix.Poll(fds, ms)
if err == unix.EINTR {
continue // interrupted by a signal, not an error
}
if err != nil {
return false, fmt.Errorf("poll: %w", err)
}
if n == 0 {
return false, nil // timeout
}
// POLLERR/POLLHUP/POLLNVAL mean the device is gone or the fd was closed.
if fds[0].Revents&(unix.POLLERR|unix.POLLHUP|unix.POLLNVAL) != 0 {
return false, ErrDeviceGone
}
return fds[0].Revents&unix.POLLOUT != 0, nil
}
}
// ReapURBInfoNonBlock reaps one completed URB without blocking.
// Returns ErrNoURBReady if none has completed, ErrDeviceGone if the device
// has been disconnected.
func (h *DeviceHandle) ReapURBInfoNonBlock() (*ReapedURBInfo, error) {
var urbPtr uintptr
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsReapURBNDelay, uintptr(unsafe.Pointer(&urbPtr)))
if errno != 0 {
switch errno {
case unix.EAGAIN:
return nil, ErrNoURBReady
case unix.ENODEV, unix.ESHUTDOWN, unix.EBADF, unix.ENOENT:
return nil, ErrDeviceGone
default:
return nil, fmt.Errorf("USBDEVFS_REAPURBNDELAY: %w", errno)
}
}
if urbPtr == 0 {
return nil, ErrNoURBReady
}
urb := urbFromKernelPtr(urbPtr)
return &ReapedURBInfo{
UserContext: urb.UserContext,
Status: urb.Status,
ActualLength: urb.ActualLength,
StartFrame: urb.StartFrame,
ErrorCount: urb.ErrorCount,
}, nil
}
// ReapURBInfo blocks until a URB completes and returns exported info
func (h *DeviceHandle) ReapURBInfo() (*ReapedURBInfo, error) {
var urbPtr uintptr
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsReapURB, uintptr(unsafe.Pointer(&urbPtr)))
if errno != 0 {
return nil, fmt.Errorf("USBDEVFS_REAPURB: %w", errno)
}
urb := urbFromKernelPtr(urbPtr)
return &ReapedURBInfo{
UserContext: urb.UserContext,
Status: urb.Status,
ActualLength: urb.ActualLength,
StartFrame: urb.StartFrame,
ErrorCount: urb.ErrorCount,
}, nil
}
// GetFile returns an os.File wrapping the device fd (useful for epoll/select)
func (h *DeviceHandle) GetFile() *os.File {
return os.NewFile(uintptr(h.fd), h.devPath)
}