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>
582 lines
18 KiB
Go
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(¶ms.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(¶ms.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)
|
|
}
|