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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
//go:build linux
|
||||
|
||||
package usb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Externally supplied file descriptors, keyed by bus ID.
|
||||
//
|
||||
// Android is the reason this exists. Apps there cannot open /dev/bus/usb:
|
||||
// access goes through the framework, which shows a permission dialog and
|
||||
// returns an already-open descriptor. A small Java shim obtains it and passes
|
||||
// it to this process, which then drives the device through the same usbdevfs
|
||||
// ioctls as anywhere else — the kernel interface is identical, only the way
|
||||
// the descriptor is obtained differs.
|
||||
var (
|
||||
adoptedMu sync.Mutex
|
||||
adoptedFDs = make(map[string]int)
|
||||
)
|
||||
|
||||
// AdoptDeviceFD registers an already-open usbdevfs file descriptor for a bus
|
||||
// ID. The next OpenDevice for that bus ID takes it instead of opening a path.
|
||||
//
|
||||
// Ownership transfers: the descriptor is closed when the resulting handle is
|
||||
// closed, or by ReleaseAdoptedFDs if it is never claimed.
|
||||
func AdoptDeviceFD(busID string, fd int) error {
|
||||
if busID == "" {
|
||||
return fmt.Errorf("bus ID is required")
|
||||
}
|
||||
if fd < 0 {
|
||||
return fmt.Errorf("invalid file descriptor %d", fd)
|
||||
}
|
||||
|
||||
// Reject a descriptor that is not actually usable, so the failure is
|
||||
// reported here rather than as a confusing ioctl error much later.
|
||||
if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != nil {
|
||||
return fmt.Errorf("file descriptor %d is not open: %w", fd, err)
|
||||
}
|
||||
|
||||
adoptedMu.Lock()
|
||||
defer adoptedMu.Unlock()
|
||||
|
||||
if old, exists := adoptedFDs[busID]; exists && old != fd {
|
||||
unix.Close(old)
|
||||
}
|
||||
adoptedFDs[busID] = fd
|
||||
return nil
|
||||
}
|
||||
|
||||
// takeAdoptedFD removes and returns a registered descriptor, if any.
|
||||
func takeAdoptedFD(busID string) (int, bool) {
|
||||
adoptedMu.Lock()
|
||||
defer adoptedMu.Unlock()
|
||||
|
||||
fd, ok := adoptedFDs[busID]
|
||||
if ok {
|
||||
delete(adoptedFDs, busID)
|
||||
}
|
||||
return fd, ok
|
||||
}
|
||||
|
||||
// HasAdoptedFD reports whether a descriptor is registered for a bus ID.
|
||||
func HasAdoptedFD(busID string) bool {
|
||||
adoptedMu.Lock()
|
||||
defer adoptedMu.Unlock()
|
||||
_, ok := adoptedFDs[busID]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ReleaseAdoptedFDs closes every registered descriptor that was never claimed.
|
||||
func ReleaseAdoptedFDs() {
|
||||
adoptedMu.Lock()
|
||||
defer adoptedMu.Unlock()
|
||||
|
||||
for busID, fd := range adoptedFDs {
|
||||
unix.Close(fd)
|
||||
delete(adoptedFDs, busID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build !linux
|
||||
|
||||
package usb
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Adopting an external file descriptor only makes sense where devices are
|
||||
// driven through usbdevfs, which is Linux-only.
|
||||
|
||||
func AdoptDeviceFD(busID string, fd int) error {
|
||||
return fmt.Errorf("adopting USB file descriptors is only supported on Linux")
|
||||
}
|
||||
|
||||
func takeAdoptedFD(busID string) (int, bool) { return 0, false }
|
||||
|
||||
func HasAdoptedFD(busID string) bool { return false }
|
||||
|
||||
func ReleaseAdoptedFDs() {}
|
||||
@@ -0,0 +1,169 @@
|
||||
package usb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// USB descriptor types
|
||||
const (
|
||||
DescTypeDevice = 0x01
|
||||
DescTypeConfiguration = 0x02
|
||||
DescTypeInterface = 0x04
|
||||
DescTypeEndpoint = 0x05
|
||||
)
|
||||
|
||||
// ParsedDescriptors holds everything we extract from a device's raw
|
||||
// descriptor blob (device descriptor followed by all configuration
|
||||
// descriptors, as returned by reading a usbdevfs device file).
|
||||
type ParsedDescriptors struct {
|
||||
VendorID uint16
|
||||
ProductID uint16
|
||||
BcdDevice uint16
|
||||
DeviceClass uint8
|
||||
DeviceSubClass uint8
|
||||
DeviceProtocol uint8
|
||||
NumConfigs uint8
|
||||
|
||||
// Configs holds every configuration, each with every interface
|
||||
// alternate setting and its endpoints.
|
||||
Configs []ConfigDescriptor
|
||||
}
|
||||
|
||||
// ConfigDescriptor is one USB configuration
|
||||
type ConfigDescriptor struct {
|
||||
Value uint8 // bConfigurationValue
|
||||
Interfaces []Interface // every alternate setting, in descriptor order
|
||||
}
|
||||
|
||||
// ParseDescriptors parses a raw descriptor blob: an 18-byte device
|
||||
// descriptor followed by one or more complete configuration descriptors.
|
||||
//
|
||||
// Reading a usbdevfs file (/dev/bus/usb/BBB/DDD) from offset 0 yields
|
||||
// exactly this layout, which is the only way to see interface alternate
|
||||
// settings — sysfs only exposes the currently active one.
|
||||
func ParseDescriptors(data []byte) (*ParsedDescriptors, error) {
|
||||
if len(data) < 18 {
|
||||
return nil, fmt.Errorf("descriptor blob too short: %d bytes", len(data))
|
||||
}
|
||||
if data[1] != DescTypeDevice {
|
||||
return nil, fmt.Errorf("first descriptor is type 0x%02x, expected device (0x01)", data[1])
|
||||
}
|
||||
|
||||
pd := &ParsedDescriptors{
|
||||
DeviceClass: data[4],
|
||||
DeviceSubClass: data[5],
|
||||
DeviceProtocol: data[6],
|
||||
VendorID: binary.LittleEndian.Uint16(data[8:10]),
|
||||
ProductID: binary.LittleEndian.Uint16(data[10:12]),
|
||||
BcdDevice: binary.LittleEndian.Uint16(data[12:14]),
|
||||
NumConfigs: data[17],
|
||||
}
|
||||
|
||||
// Walk the remaining descriptors. Configuration descriptors start a new
|
||||
// config; interface descriptors start a new alternate setting; endpoint
|
||||
// descriptors attach to the most recent interface. Class-specific
|
||||
// descriptors (HID, UVC, audio) are skipped by their bLength.
|
||||
pos := int(data[0]) // skip the device descriptor using its own bLength
|
||||
if pos < 18 {
|
||||
pos = 18
|
||||
}
|
||||
|
||||
var curConfig *ConfigDescriptor
|
||||
var curIface *Interface
|
||||
|
||||
for pos+2 <= len(data) {
|
||||
bLength := int(data[pos])
|
||||
bType := data[pos+1]
|
||||
|
||||
// A zero-length descriptor would loop forever; a descriptor running
|
||||
// past the end of the blob means the device returned garbage.
|
||||
if bLength < 2 || pos+bLength > len(data) {
|
||||
break
|
||||
}
|
||||
|
||||
switch bType {
|
||||
case DescTypeConfiguration:
|
||||
if bLength >= 9 {
|
||||
pd.Configs = append(pd.Configs, ConfigDescriptor{Value: data[pos+5]})
|
||||
curConfig = &pd.Configs[len(pd.Configs)-1]
|
||||
curIface = nil
|
||||
}
|
||||
|
||||
case DescTypeInterface:
|
||||
if bLength >= 9 && curConfig != nil {
|
||||
curConfig.Interfaces = append(curConfig.Interfaces, Interface{
|
||||
Number: data[pos+2],
|
||||
AltSetting: data[pos+3],
|
||||
Class: data[pos+5],
|
||||
SubClass: data[pos+6],
|
||||
Protocol: data[pos+7],
|
||||
})
|
||||
curIface = &curConfig.Interfaces[len(curConfig.Interfaces)-1]
|
||||
}
|
||||
|
||||
case DescTypeEndpoint:
|
||||
if bLength >= 7 && curIface != nil {
|
||||
curIface.Endpoints = append(curIface.Endpoints, Endpoint{
|
||||
Address: data[pos+2],
|
||||
TransferType: data[pos+3] & 0x03,
|
||||
MaxPacketSize: binary.LittleEndian.Uint16(data[pos+4 : pos+6]),
|
||||
Interval: data[pos+6],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pos += bLength
|
||||
}
|
||||
|
||||
if len(pd.Configs) == 0 {
|
||||
return nil, fmt.Errorf("no configuration descriptor found")
|
||||
}
|
||||
|
||||
return pd, nil
|
||||
}
|
||||
|
||||
// FindConfig returns the configuration with the given bConfigurationValue,
|
||||
// or nil if the device has no such configuration.
|
||||
func (pd *ParsedDescriptors) FindConfig(value uint8) *ConfigDescriptor {
|
||||
for i := range pd.Configs {
|
||||
if pd.Configs[i].Value == value {
|
||||
return &pd.Configs[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllEndpoints returns every endpoint across every alternate setting of the
|
||||
// given configuration, keyed by full bEndpointAddress (direction bit
|
||||
// included). Endpoints only present in a non-zero alternate setting — the
|
||||
// isochronous endpoints of webcams, for example — are included, which is
|
||||
// what makes the endpoint type map correct after a SET_INTERFACE.
|
||||
func (c *ConfigDescriptor) AllEndpoints() map[uint8]Endpoint {
|
||||
eps := make(map[uint8]Endpoint)
|
||||
for _, iface := range c.Interfaces {
|
||||
for _, ep := range iface.Endpoints {
|
||||
// Alternate settings reuse addresses with identical transfer
|
||||
// types in practice; keep the first one we see so alt 0 wins.
|
||||
if _, seen := eps[ep.Address]; !seen {
|
||||
eps[ep.Address] = ep
|
||||
}
|
||||
}
|
||||
}
|
||||
return eps
|
||||
}
|
||||
|
||||
// ActiveInterfaces returns one Interface per interface number, using
|
||||
// alternate setting 0 — the set of interfaces that must be claimed.
|
||||
func (c *ConfigDescriptor) ActiveInterfaces() []Interface {
|
||||
var result []Interface
|
||||
seen := make(map[uint8]bool)
|
||||
for _, iface := range c.Interfaces {
|
||||
if iface.AltSetting != 0 || seen[iface.Number] {
|
||||
continue
|
||||
}
|
||||
seen[iface.Number] = true
|
||||
result = append(result, iface)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package usb
|
||||
|
||||
import "testing"
|
||||
|
||||
// buildDescriptorBlob assembles a device descriptor followed by raw
|
||||
// configuration bytes, the way a usbdevfs file read returns them.
|
||||
func buildDescriptorBlob(numConfigs uint8, configs ...[]byte) []byte {
|
||||
dev := []byte{
|
||||
18, // bLength
|
||||
0x01, // bDescriptorType = DEVICE
|
||||
0x00, 0x02, // bcdUSB 2.00
|
||||
0x00, // bDeviceClass (per-interface)
|
||||
0x00, // bDeviceSubClass
|
||||
0x00, // bDeviceProtocol
|
||||
64, // bMaxPacketSize0
|
||||
0x6d, 0x04, // idVendor 046d
|
||||
0x1c, 0xc0, // idProduct c01c
|
||||
0x10, 0x02, // bcdDevice 0210
|
||||
1, 2, 3, // string indices
|
||||
numConfigs,
|
||||
}
|
||||
blob := dev
|
||||
for _, c := range configs {
|
||||
blob = append(blob, c...)
|
||||
}
|
||||
return blob
|
||||
}
|
||||
|
||||
func ifaceDesc(number, alt, numEndpoints, class, subclass, protocol uint8) []byte {
|
||||
return []byte{9, 0x04, number, alt, numEndpoints, class, subclass, protocol, 0}
|
||||
}
|
||||
|
||||
func endpointDesc(addr, attrs uint8, maxPacket uint16, interval uint8) []byte {
|
||||
return []byte{7, 0x05, addr, attrs, byte(maxPacket), byte(maxPacket >> 8), interval}
|
||||
}
|
||||
|
||||
func configDesc(value uint8, body []byte) []byte {
|
||||
total := 9 + len(body)
|
||||
cfg := []byte{9, 0x02, byte(total), byte(total >> 8), 1, value, 0, 0x80, 250}
|
||||
return append(cfg, body...)
|
||||
}
|
||||
|
||||
func TestParseDescriptorsDeviceFields(t *testing.T) {
|
||||
blob := buildDescriptorBlob(1, configDesc(1, ifaceDesc(0, 0, 0, 3, 1, 1)))
|
||||
|
||||
pd, err := ParseDescriptors(blob)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDescriptors: %v", err)
|
||||
}
|
||||
|
||||
if pd.VendorID != 0x046d {
|
||||
t.Errorf("VendorID = %04x, want 046d", pd.VendorID)
|
||||
}
|
||||
if pd.ProductID != 0xc01c {
|
||||
t.Errorf("ProductID = %04x, want c01c", pd.ProductID)
|
||||
}
|
||||
if pd.BcdDevice != 0x0210 {
|
||||
t.Errorf("BcdDevice = %04x, want 0210", pd.BcdDevice)
|
||||
}
|
||||
if pd.NumConfigs != 1 {
|
||||
t.Errorf("NumConfigs = %d, want 1", pd.NumConfigs)
|
||||
}
|
||||
if len(pd.Configs) != 1 {
|
||||
t.Fatalf("got %d configs, want 1", len(pd.Configs))
|
||||
}
|
||||
}
|
||||
|
||||
// A composite device where endpoint number 1 appears twice with different
|
||||
// directions and different transfer types. Keying the endpoint map by number
|
||||
// alone collapses these two into one, which is what made the server submit
|
||||
// interrupt URBs with the bulk type and broke HID devices.
|
||||
func TestAllEndpointsKeepsDirectionsSeparate(t *testing.T) {
|
||||
body := ifaceDesc(0, 0, 2, 0x08, 0x06, 0x50) // mass storage
|
||||
body = append(body, endpointDesc(0x01, 0x02, 512, 0)...) // bulk OUT, EP1
|
||||
body = append(body, endpointDesc(0x82, 0x02, 512, 0)...) // bulk IN, EP2
|
||||
body = append(body, ifaceDesc(1, 0, 1, 0x03, 0x01, 0x01)...) // HID keyboard
|
||||
body = append(body, endpointDesc(0x81, 0x03, 8, 10)...) // interrupt IN, EP1
|
||||
|
||||
blob := buildDescriptorBlob(1, configDesc(1, body))
|
||||
pd, err := ParseDescriptors(blob)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDescriptors: %v", err)
|
||||
}
|
||||
|
||||
eps := pd.Configs[0].AllEndpoints()
|
||||
if len(eps) != 3 {
|
||||
t.Fatalf("got %d endpoints, want 3: %+v", len(eps), eps)
|
||||
}
|
||||
|
||||
if got := eps[0x01].TransferType; got != TransferTypeBulk {
|
||||
t.Errorf("EP 0x01 type = %d, want bulk (%d)", got, TransferTypeBulk)
|
||||
}
|
||||
if got := eps[0x81].TransferType; got != TransferTypeInterrupt {
|
||||
t.Errorf("EP 0x81 type = %d, want interrupt (%d) — direction bit must not collapse", got, TransferTypeInterrupt)
|
||||
}
|
||||
if got := eps[0x81].Interval; got != 10 {
|
||||
t.Errorf("EP 0x81 interval = %d, want 10", got)
|
||||
}
|
||||
if got := eps[0x82].MaxPacketSize; got != 512 {
|
||||
t.Errorf("EP 0x82 maxpacket = %d, want 512", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A webcam's isochronous endpoints only exist in a non-zero alternate
|
||||
// setting. sysfs shows only the active setting, so an endpoint map built from
|
||||
// it would classify these as bulk after a SET_INTERFACE.
|
||||
func TestAllEndpointsIncludesAlternateSettings(t *testing.T) {
|
||||
body := ifaceDesc(1, 0, 0, 0x0e, 0x02, 0x00) // video streaming, alt 0: no endpoints
|
||||
body = append(body, ifaceDesc(1, 1, 1, 0x0e, 0x02, 0x00)...)
|
||||
body = append(body, endpointDesc(0x81, 0x05, 1024, 1)...) // isochronous IN
|
||||
body = append(body, ifaceDesc(1, 2, 1, 0x0e, 0x02, 0x00)...)
|
||||
body = append(body, endpointDesc(0x81, 0x05, 2048, 1)...)
|
||||
|
||||
blob := buildDescriptorBlob(1, configDesc(1, body))
|
||||
pd, err := ParseDescriptors(blob)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDescriptors: %v", err)
|
||||
}
|
||||
|
||||
cfg := pd.Configs[0]
|
||||
if len(cfg.Interfaces) != 3 {
|
||||
t.Fatalf("got %d interface descriptors, want 3 (alt 0,1,2)", len(cfg.Interfaces))
|
||||
}
|
||||
|
||||
eps := cfg.AllEndpoints()
|
||||
ep, ok := eps[0x81]
|
||||
if !ok {
|
||||
t.Fatal("EP 0x81 missing — endpoints from non-zero alternate settings were dropped")
|
||||
}
|
||||
if ep.TransferType != TransferTypeIsochronous {
|
||||
t.Errorf("EP 0x81 type = %d, want isochronous (%d)", ep.TransferType, TransferTypeIsochronous)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveInterfacesOnlyAltZero(t *testing.T) {
|
||||
body := ifaceDesc(0, 0, 0, 0x01, 0x01, 0x00)
|
||||
body = append(body, ifaceDesc(1, 0, 0, 0x01, 0x02, 0x00)...)
|
||||
body = append(body, ifaceDesc(1, 1, 1, 0x01, 0x02, 0x00)...)
|
||||
body = append(body, endpointDesc(0x81, 0x05, 192, 1)...)
|
||||
|
||||
blob := buildDescriptorBlob(1, configDesc(1, body))
|
||||
pd, _ := ParseDescriptors(blob)
|
||||
|
||||
active := pd.Configs[0].ActiveInterfaces()
|
||||
if len(active) != 2 {
|
||||
t.Fatalf("got %d active interfaces, want 2 (one per interface number)", len(active))
|
||||
}
|
||||
for _, iface := range active {
|
||||
if iface.AltSetting != 0 {
|
||||
t.Errorf("interface %d has alt setting %d, want 0", iface.Number, iface.AltSetting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Class-specific descriptors (HID, UVC, audio) sit between the standard ones
|
||||
// and must be skipped by bLength rather than confusing the walk.
|
||||
func TestParseDescriptorsSkipsClassSpecific(t *testing.T) {
|
||||
hidDesc := []byte{9, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x3f, 0x00}
|
||||
|
||||
body := ifaceDesc(0, 0, 1, 0x03, 0x01, 0x01)
|
||||
body = append(body, hidDesc...)
|
||||
body = append(body, endpointDesc(0x81, 0x03, 8, 10)...)
|
||||
|
||||
blob := buildDescriptorBlob(1, configDesc(1, body))
|
||||
pd, err := ParseDescriptors(blob)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDescriptors: %v", err)
|
||||
}
|
||||
|
||||
eps := pd.Configs[0].AllEndpoints()
|
||||
if _, ok := eps[0x81]; !ok {
|
||||
t.Fatal("endpoint after a HID descriptor was not parsed")
|
||||
}
|
||||
if len(pd.Configs[0].Interfaces[0].Endpoints) != 1 {
|
||||
t.Errorf("got %d endpoints on the interface, want 1",
|
||||
len(pd.Configs[0].Interfaces[0].Endpoints))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindConfigSelectsByValue(t *testing.T) {
|
||||
blob := buildDescriptorBlob(2,
|
||||
configDesc(1, ifaceDesc(0, 0, 0, 0x03, 0, 0)),
|
||||
configDesc(2, ifaceDesc(0, 0, 0, 0x08, 0, 0)),
|
||||
)
|
||||
pd, err := ParseDescriptors(blob)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseDescriptors: %v", err)
|
||||
}
|
||||
|
||||
cfg := pd.FindConfig(2)
|
||||
if cfg == nil {
|
||||
t.Fatal("FindConfig(2) returned nil")
|
||||
}
|
||||
if cfg.Interfaces[0].Class != 0x08 {
|
||||
t.Errorf("got interface class %02x, want 08 — wrong configuration selected", cfg.Interfaces[0].Class)
|
||||
}
|
||||
if pd.FindConfig(9) != nil {
|
||||
t.Error("FindConfig(9) should return nil for a configuration that does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDescriptorsRejectsGarbage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"empty", nil},
|
||||
{"too short", []byte{18, 0x01, 0x00}},
|
||||
{"not a device descriptor", append([]byte{9, 0x02}, make([]byte, 20)...)},
|
||||
{"no configuration", buildDescriptorBlob(1)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, err := ParseDescriptors(tt.data); err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A truncated or zero-length descriptor must terminate the walk instead of
|
||||
// looping forever or reading past the buffer.
|
||||
func TestParseDescriptorsHandlesTruncation(t *testing.T) {
|
||||
blob := buildDescriptorBlob(1, configDesc(1, ifaceDesc(0, 0, 1, 3, 1, 1)))
|
||||
blob = append(blob, 0x00, 0x05) // zero bLength would spin forever
|
||||
blob = append(blob, 9, 0x04) // interface descriptor claiming 9 bytes, only 2 present
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
if _, err := ParseDescriptors(blob); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-done
|
||||
}
|
||||
+44
-27
@@ -2,43 +2,60 @@ package usb
|
||||
|
||||
// Device represents a USB device
|
||||
type Device struct {
|
||||
BusID string `json:"bus_id"` // e.g. "1-1.4"
|
||||
BusNum uint32 `json:"bus_num"`
|
||||
DevNum uint32 `json:"dev_num"`
|
||||
Speed uint32 `json:"speed"`
|
||||
VendorID uint16 `json:"vendor_id"`
|
||||
ProductID uint16 `json:"product_id"`
|
||||
BcdDevice uint16 `json:"bcd_device"`
|
||||
DeviceClass uint8 `json:"device_class"`
|
||||
DeviceSubClass uint8 `json:"device_sub_class"`
|
||||
DeviceProtocol uint8 `json:"device_protocol"`
|
||||
ConfigValue uint8 `json:"config_value"`
|
||||
NumConfigs uint8 `json:"num_configs"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
Product string `json:"product"`
|
||||
Serial string `json:"serial"`
|
||||
SysPath string `json:"sys_path"` // sysfs path
|
||||
DevPath string `json:"dev_path"` // /dev/bus/usb path
|
||||
Interfaces []Interface `json:"interfaces"`
|
||||
BusID string `json:"bus_id"` // e.g. "1-1.4"
|
||||
BusNum uint32 `json:"bus_num"`
|
||||
DevNum uint32 `json:"dev_num"`
|
||||
Speed uint32 `json:"speed"`
|
||||
VendorID uint16 `json:"vendor_id"`
|
||||
ProductID uint16 `json:"product_id"`
|
||||
BcdDevice uint16 `json:"bcd_device"`
|
||||
DeviceClass uint8 `json:"device_class"`
|
||||
DeviceSubClass uint8 `json:"device_sub_class"`
|
||||
DeviceProtocol uint8 `json:"device_protocol"`
|
||||
ConfigValue uint8 `json:"config_value"`
|
||||
NumConfigs uint8 `json:"num_configs"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
Product string `json:"product"`
|
||||
Serial string `json:"serial"`
|
||||
SysPath string `json:"sys_path"` // sysfs path
|
||||
DevPath string `json:"dev_path"` // /dev/bus/usb path
|
||||
|
||||
// Interfaces holds one entry per interface number at alternate setting 0.
|
||||
// These are the interfaces that get claimed when sharing the device.
|
||||
Interfaces []Interface `json:"interfaces"`
|
||||
|
||||
// Endpoints holds every endpoint of the active configuration across all
|
||||
// alternate settings, keyed by full bEndpointAddress (direction bit
|
||||
// included). Endpoints that only exist in a non-zero alternate setting
|
||||
// are included, so the transfer type stays correct after SET_INTERFACE.
|
||||
Endpoints map[uint8]Endpoint `json:"endpoints"`
|
||||
}
|
||||
|
||||
// Interface represents a USB interface
|
||||
// Interface represents a USB interface at one alternate setting
|
||||
type Interface struct {
|
||||
Number uint8 `json:"number"`
|
||||
Class uint8 `json:"class"`
|
||||
SubClass uint8 `json:"sub_class"`
|
||||
Protocol uint8 `json:"protocol"`
|
||||
Driver string `json:"driver"`
|
||||
Endpoints []Endpoint `json:"endpoints"`
|
||||
Number uint8 `json:"number"`
|
||||
AltSetting uint8 `json:"alt_setting"`
|
||||
Class uint8 `json:"class"`
|
||||
SubClass uint8 `json:"sub_class"`
|
||||
Protocol uint8 `json:"protocol"`
|
||||
Driver string `json:"driver"`
|
||||
Endpoints []Endpoint `json:"endpoints"`
|
||||
}
|
||||
|
||||
// Endpoint represents a USB endpoint
|
||||
type Endpoint struct {
|
||||
Address uint8 `json:"address"` // bEndpointAddress (bit 7=direction, bits 3:0=number)
|
||||
TransferType uint8 `json:"transfer_type"` // 0=control, 1=iso, 2=bulk, 3=interrupt
|
||||
Address uint8 `json:"address"` // bEndpointAddress (bit 7=direction, bits 3:0=number)
|
||||
TransferType uint8 `json:"transfer_type"` // 0=control, 1=iso, 2=bulk, 3=interrupt
|
||||
MaxPacketSize uint16 `json:"max_packet_size"`
|
||||
Interval uint8 `json:"interval"` // bInterval
|
||||
}
|
||||
|
||||
// IsIn reports whether this is an IN (device-to-host) endpoint.
|
||||
func (e Endpoint) IsIn() bool { return e.Address&0x80 != 0 }
|
||||
|
||||
// Number returns the endpoint number without the direction bit.
|
||||
func (e Endpoint) Number() uint8 { return e.Address & 0x0F }
|
||||
|
||||
// USB transfer types (from bmAttributes)
|
||||
const (
|
||||
TransferTypeControl = 0
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
//go:build windows
|
||||
|
||||
package usb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Interface to the usbshare filter driver (driver/windows).
|
||||
//
|
||||
// The structure layouts and IOCTL codes here must match public.h exactly.
|
||||
// They are marshalled by hand on both sides, so a mismatch corrupts memory
|
||||
// rather than failing cleanly — change one, change the other.
|
||||
|
||||
// GUID_DEVINTERFACE_USBSHARE from public.h.
|
||||
var guidDevInterfaceUsbShare = windows.GUID{
|
||||
Data1: 0x8f3d2a14,
|
||||
Data2: 0x6c7b,
|
||||
Data3: 0x4e59,
|
||||
Data4: [8]byte{0x9a, 0x1d, 0x3f, 0x5b, 0x7c, 0x8e, 0x2d, 0x40},
|
||||
}
|
||||
|
||||
// IOCTL codes, mirroring the USBSHARE_IOCTL macro.
|
||||
const (
|
||||
fileDeviceUsbShare = 0x8000
|
||||
methodBuffered = 0
|
||||
fileAnyAccess = 0
|
||||
)
|
||||
|
||||
func usbShareIOCTL(index uint32) uint32 {
|
||||
return (fileDeviceUsbShare << 16) | (fileAnyAccess << 14) | ((0x800 + index) << 2) | methodBuffered
|
||||
}
|
||||
|
||||
var (
|
||||
ioctlClaim = usbShareIOCTL(0)
|
||||
ioctlRelease = usbShareIOCTL(1)
|
||||
ioctlGetDescriptors = usbShareIOCTL(2)
|
||||
ioctlSubmit = usbShareIOCTL(3)
|
||||
ioctlCancel = usbShareIOCTL(4)
|
||||
ioctlSetInterface = usbShareIOCTL(5)
|
||||
ioctlClearHalt = usbShareIOCTL(6)
|
||||
ioctlReset = usbShareIOCTL(7)
|
||||
)
|
||||
|
||||
// Transfer types, matching USBSHARE_TRANSFER_* in public.h.
|
||||
const (
|
||||
winTransferControl = 0
|
||||
winTransferIsochronous = 1
|
||||
winTransferBulk = 2
|
||||
winTransferInterrupt = 3
|
||||
)
|
||||
|
||||
// Directions, matching USBSHARE_DIR_*.
|
||||
const (
|
||||
winDirOut = 0
|
||||
winDirIn = 1
|
||||
)
|
||||
|
||||
// winDeviceInfo mirrors USBSHARE_DEVICE_INFO (packed).
|
||||
type winDeviceInfo struct {
|
||||
VendorID uint16
|
||||
ProductID uint16
|
||||
BcdDevice uint16
|
||||
DeviceClass uint8
|
||||
DeviceSubClass uint8
|
||||
DeviceProtocol uint8
|
||||
ConfigurationValue uint8
|
||||
NumConfigurations uint8
|
||||
Speed uint32
|
||||
PortNumber uint32
|
||||
}
|
||||
|
||||
// winTransferHeader mirrors USBSHARE_TRANSFER (packed).
|
||||
type winTransferHeader struct {
|
||||
ID uint64
|
||||
EndpointAddress uint8
|
||||
Type uint8
|
||||
Direction uint8
|
||||
Reserved uint8
|
||||
BufferLength uint32
|
||||
Timeout uint32
|
||||
Setup [8]byte
|
||||
}
|
||||
|
||||
// winTransferResult mirrors USBSHARE_TRANSFER_RESULT (packed).
|
||||
type winTransferResult struct {
|
||||
ID uint64
|
||||
Status int32
|
||||
UsbdStatus uint32
|
||||
ActualLength uint32
|
||||
}
|
||||
|
||||
const (
|
||||
winTransferHeaderSize = 8 + 1 + 1 + 1 + 1 + 4 + 4 + 8 // 28
|
||||
winTransferResultSize = 8 + 4 + 4 + 4 // 20
|
||||
)
|
||||
|
||||
// DriverHandle is an open handle to a device claimed through the filter driver.
|
||||
type DriverHandle struct {
|
||||
handle windows.Handle
|
||||
info winDeviceInfo
|
||||
nextID uint64
|
||||
}
|
||||
|
||||
// OpenDriverDevice opens the filter driver's interface for a device path and
|
||||
// claims the device.
|
||||
//
|
||||
// Claiming stops the class driver from talking to the device, which is what
|
||||
// lets us drive it — and it is released automatically if this process dies,
|
||||
// because the driver ties the claim to the handle.
|
||||
func OpenDriverDevice(devicePath string) (*DriverHandle, error) {
|
||||
pathPtr, err := windows.UTF16PtrFromString(devicePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid device path: %w", err)
|
||||
}
|
||||
|
||||
handle, err := windows.CreateFile(
|
||||
pathPtr,
|
||||
windows.GENERIC_READ|windows.GENERIC_WRITE,
|
||||
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
|
||||
nil,
|
||||
windows.OPEN_EXISTING,
|
||||
windows.FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening %s: %w (is the usbshare driver installed?)", devicePath, err)
|
||||
}
|
||||
|
||||
h := &DriverHandle{handle: handle}
|
||||
|
||||
if err := h.claim(); err != nil {
|
||||
windows.CloseHandle(handle)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *DriverHandle) claim() error {
|
||||
out := make([]byte, unsafe.Sizeof(winDeviceInfo{}))
|
||||
var returned uint32
|
||||
|
||||
err := windows.DeviceIoControl(h.handle, ioctlClaim,
|
||||
nil, 0,
|
||||
&out[0], uint32(len(out)),
|
||||
&returned, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("claiming device: %w", err)
|
||||
}
|
||||
|
||||
h.info = *(*winDeviceInfo)(unsafe.Pointer(&out[0]))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close releases the device and closes the handle.
|
||||
func (h *DriverHandle) Close() error {
|
||||
var returned uint32
|
||||
windows.DeviceIoControl(h.handle, ioctlRelease, nil, 0, nil, 0, &returned, nil)
|
||||
return windows.CloseHandle(h.handle)
|
||||
}
|
||||
|
||||
// Info returns the device information reported at claim time.
|
||||
func (h *DriverHandle) Info() winDeviceInfo { return h.info }
|
||||
|
||||
// Descriptors reads the raw descriptor blob: device descriptor followed by
|
||||
// the configuration descriptors, the same layout Linux usbdevfs returns. It
|
||||
// is parsed by the same code on both platforms.
|
||||
func (h *DriverHandle) Descriptors() ([]byte, error) {
|
||||
// Ask with a generous buffer first; grow if the driver reports more.
|
||||
buf := make([]byte, 4096)
|
||||
var returned uint32
|
||||
|
||||
err := windows.DeviceIoControl(h.handle, ioctlGetDescriptors,
|
||||
nil, 0, &buf[0], uint32(len(buf)), &returned, nil)
|
||||
|
||||
if err == windows.ERROR_INSUFFICIENT_BUFFER || err == windows.ERROR_MORE_DATA {
|
||||
buf = make([]byte, returned)
|
||||
err = windows.DeviceIoControl(h.handle, ioctlGetDescriptors,
|
||||
nil, 0, &buf[0], uint32(len(buf)), &returned, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading descriptors: %w", err)
|
||||
}
|
||||
|
||||
return buf[:returned], nil
|
||||
}
|
||||
|
||||
// Transfer performs one USB transfer and blocks until it completes.
|
||||
//
|
||||
// For IN transfers data is the buffer to fill; for OUT transfers it holds the
|
||||
// payload to send. The returned count is how many bytes actually moved, which
|
||||
// matters for both directions.
|
||||
func (h *DriverHandle) Transfer(params *TransferParams) (int, error) {
|
||||
h.nextID++
|
||||
|
||||
header := winTransferHeader{
|
||||
ID: h.nextID,
|
||||
EndpointAddress: params.EndpointAddress,
|
||||
Type: params.Type,
|
||||
Direction: params.Direction,
|
||||
BufferLength: uint32(len(params.Data)),
|
||||
Timeout: params.TimeoutMS,
|
||||
Setup: params.Setup,
|
||||
}
|
||||
|
||||
// Input: header followed by the payload for OUT transfers.
|
||||
input := make([]byte, winTransferHeaderSize+len(params.Data))
|
||||
marshalTransferHeader(input, &header)
|
||||
if params.Direction == winDirOut && len(params.Data) > 0 {
|
||||
copy(input[winTransferHeaderSize:], params.Data)
|
||||
}
|
||||
|
||||
// Output: result header followed by the payload for IN transfers.
|
||||
output := make([]byte, winTransferResultSize+len(params.Data))
|
||||
var returned uint32
|
||||
|
||||
err := windows.DeviceIoControl(h.handle, ioctlSubmit,
|
||||
&input[0], uint32(len(input)),
|
||||
&output[0], uint32(len(output)),
|
||||
&returned, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("submitting transfer: %w", err)
|
||||
}
|
||||
if returned < winTransferResultSize {
|
||||
return 0, fmt.Errorf("driver returned %d bytes, expected at least %d",
|
||||
returned, winTransferResultSize)
|
||||
}
|
||||
|
||||
result := unmarshalTransferResult(output)
|
||||
if result.Status != 0 {
|
||||
return int(result.ActualLength), fmt.Errorf(
|
||||
"transfer failed: status 0x%08x, usbd 0x%08x",
|
||||
uint32(result.Status), result.UsbdStatus)
|
||||
}
|
||||
|
||||
if params.Direction == winDirIn && result.ActualLength > 0 {
|
||||
n := int(result.ActualLength)
|
||||
if n > len(params.Data) {
|
||||
n = len(params.Data)
|
||||
}
|
||||
copy(params.Data, output[winTransferResultSize:winTransferResultSize+n])
|
||||
}
|
||||
|
||||
return int(result.ActualLength), nil
|
||||
}
|
||||
|
||||
// TransferParams describes one transfer.
|
||||
type TransferParams struct {
|
||||
EndpointAddress uint8
|
||||
Type uint8
|
||||
Direction uint8
|
||||
Data []byte
|
||||
TimeoutMS uint32
|
||||
Setup [8]byte
|
||||
}
|
||||
|
||||
// SetInterface selects an alternate setting through the driver, so the USB
|
||||
// stack re-opens the pipes and reserves bandwidth for isochronous endpoints.
|
||||
func (h *DriverHandle) SetInterface(iface, alt uint8) error {
|
||||
input := []byte{iface, alt}
|
||||
var returned uint32
|
||||
|
||||
err := windows.DeviceIoControl(h.handle, ioctlSetInterface,
|
||||
&input[0], uint32(len(input)), nil, 0, &returned, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting interface %d to alt %d: %w", iface, alt, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearHalt clears a stall condition on an endpoint.
|
||||
func (h *DriverHandle) ClearHalt(endpoint uint8) error {
|
||||
input := []byte{endpoint}
|
||||
var returned uint32
|
||||
|
||||
err := windows.DeviceIoControl(h.handle, ioctlClearHalt,
|
||||
&input[0], 1, nil, 0, &returned, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clearing halt on endpoint 0x%02x: %w", endpoint, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset resets the device's port.
|
||||
func (h *DriverHandle) Reset() error {
|
||||
var returned uint32
|
||||
err := windows.DeviceIoControl(h.handle, ioctlReset, nil, 0, nil, 0, &returned, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resetting device: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalTransferHeader writes the header in the driver's packed layout.
|
||||
// Done field by field rather than by casting a struct: Go inserts padding
|
||||
// that the packed C structure does not have.
|
||||
func marshalTransferHeader(buf []byte, h *winTransferHeader) {
|
||||
binary.LittleEndian.PutUint64(buf[0:8], h.ID)
|
||||
buf[8] = h.EndpointAddress
|
||||
buf[9] = h.Type
|
||||
buf[10] = h.Direction
|
||||
buf[11] = h.Reserved
|
||||
binary.LittleEndian.PutUint32(buf[12:16], h.BufferLength)
|
||||
binary.LittleEndian.PutUint32(buf[16:20], h.Timeout)
|
||||
copy(buf[20:28], h.Setup[:])
|
||||
}
|
||||
|
||||
func unmarshalTransferResult(buf []byte) winTransferResult {
|
||||
return winTransferResult{
|
||||
ID: binary.LittleEndian.Uint64(buf[0:8]),
|
||||
Status: int32(binary.LittleEndian.Uint32(buf[8:12])),
|
||||
UsbdStatus: binary.LittleEndian.Uint32(buf[12:16]),
|
||||
ActualLength: binary.LittleEndian.Uint32(buf[16:20]),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//go:build darwin
|
||||
|
||||
package usb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Enumerate lists USB devices on macOS via system_profiler.
|
||||
//
|
||||
// This is enough to see and report what is attached, which is what the
|
||||
// diagnostics need. It is not enough to share anything: that requires opening
|
||||
// devices through IOKit, which has no equivalent here — see the platform
|
||||
// table in the README.
|
||||
//
|
||||
// Going through the command keeps the client cgo-free and therefore
|
||||
// cross-compilable from any machine.
|
||||
func Enumerate() ([]Device, error) {
|
||||
if external := ExternalDevices(); len(external) > 0 {
|
||||
// Devices handed in from outside are usable; report them first.
|
||||
return external, nil
|
||||
}
|
||||
|
||||
out, err := exec.Command("system_profiler", "-json", "SPUSBDataType").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("running system_profiler: %w", err)
|
||||
}
|
||||
|
||||
var report struct {
|
||||
Items []spUSBItem `json:"SPUSBDataType"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &report); err != nil {
|
||||
return nil, fmt.Errorf("parsing system_profiler output: %w", err)
|
||||
}
|
||||
|
||||
var devices []Device
|
||||
for _, item := range report.Items {
|
||||
collectItem(&devices, item)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
type spUSBItem struct {
|
||||
Name string `json:"_name"`
|
||||
VendorID string `json:"vendor_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Speed string `json:"device_speed"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
SerialNumber string `json:"serial_num"`
|
||||
LocationID string `json:"location_id"`
|
||||
Items []spUSBItem `json:"_items"`
|
||||
}
|
||||
|
||||
func collectItem(devices *[]Device, item spUSBItem) {
|
||||
if item.VendorID != "" {
|
||||
dev := Device{
|
||||
BusID: locationToBusID(item.LocationID),
|
||||
VendorID: parseHexID(item.VendorID),
|
||||
ProductID: parseHexID(item.ProductID),
|
||||
Speed: parseSpeedName(item.Speed),
|
||||
Manufacturer: item.Manufacturer,
|
||||
Product: item.Name,
|
||||
Serial: item.SerialNumber,
|
||||
}
|
||||
*devices = append(*devices, dev)
|
||||
}
|
||||
|
||||
for _, child := range item.Items {
|
||||
collectItem(devices, child)
|
||||
}
|
||||
}
|
||||
|
||||
// parseHexID turns "0x046d (Logitech Inc.)" into 0x046d.
|
||||
func parseHexID(id string) uint16 {
|
||||
id = strings.TrimSpace(id)
|
||||
if i := strings.Index(id, " "); i > 0 {
|
||||
id = id[:i]
|
||||
}
|
||||
id = strings.TrimPrefix(id, "0x")
|
||||
|
||||
v, err := strconv.ParseUint(id, 16, 16)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return uint16(v)
|
||||
}
|
||||
|
||||
// locationToBusID derives an identifier from the location ID, which encodes
|
||||
// the device's position in the port tree.
|
||||
func locationToBusID(locationID string) string {
|
||||
locationID = strings.TrimSpace(locationID)
|
||||
if i := strings.Index(locationID, " "); i > 0 {
|
||||
locationID = locationID[:i]
|
||||
}
|
||||
return strings.TrimPrefix(locationID, "0x")
|
||||
}
|
||||
|
||||
// parseSpeedName maps system_profiler's wording onto USB/IP speed codes.
|
||||
func parseSpeedName(speed string) uint32 {
|
||||
switch {
|
||||
case strings.Contains(speed, "low_speed"):
|
||||
return 1
|
||||
case strings.Contains(speed, "full_speed"):
|
||||
return 2
|
||||
case strings.Contains(speed, "high_speed"):
|
||||
return 3
|
||||
case strings.Contains(speed, "super_speed_plus"):
|
||||
return 6
|
||||
case strings.Contains(speed, "super_speed"):
|
||||
return 5
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,17 @@ import (
|
||||
|
||||
const sysfsUSBDevices = "/sys/bus/usb/devices"
|
||||
|
||||
// Enumerate lists all USB devices by reading sysfs
|
||||
// Enumerate lists all USB devices by reading sysfs, plus any device that was
|
||||
// registered from outside the process (see RegisterExternalDevice).
|
||||
func Enumerate() ([]Device, error) {
|
||||
entries, err := os.ReadDir(sysfsUSBDevices)
|
||||
if err != nil {
|
||||
// On Android sysfs is not readable by an app, but devices handed in
|
||||
// through the bridge still work. Only report a failure when there is
|
||||
// nothing at all to go on.
|
||||
if external := ExternalDevices(); len(external) > 0 {
|
||||
return external, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading sysfs: %w", err)
|
||||
}
|
||||
|
||||
@@ -46,7 +53,7 @@ func Enumerate() ([]Device, error) {
|
||||
devices = append(devices, *dev)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
return mergeExternal(devices), nil
|
||||
}
|
||||
|
||||
func isDevicePath(name string) bool {
|
||||
@@ -93,12 +100,63 @@ func readDevice(busID string) (*Device, error) {
|
||||
// Compute dev path
|
||||
dev.DevPath = fmt.Sprintf("/dev/bus/usb/%03d/%03d", dev.BusNum, dev.DevNum)
|
||||
|
||||
// Read interfaces
|
||||
// Read interfaces from sysfs. This gives us the bound kernel driver per
|
||||
// interface, which the raw descriptors don't contain.
|
||||
dev.Interfaces = readInterfaces(sysPath, busID)
|
||||
|
||||
// Overlay the raw descriptors from the usbdevfs file. Only these expose
|
||||
// interface alternate settings and correct endpoint attributes; sysfs
|
||||
// shows just the active alternate setting. Without the non-zero alternate
|
||||
// settings the endpoint type map is wrong for webcams and audio devices.
|
||||
applyRawDescriptors(dev)
|
||||
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
// applyRawDescriptors reads the device's descriptor blob from its usbdevfs
|
||||
// file and fills in Endpoints plus any interface data sysfs did not provide.
|
||||
// Failure is not fatal: reading /dev/bus/usb requires permissions we may not
|
||||
// have when merely listing devices, and the sysfs data alone is enough for
|
||||
// that. Sharing a device opens the same file anyway and would fail earlier.
|
||||
func applyRawDescriptors(dev *Device) {
|
||||
data, err := os.ReadFile(dev.DevPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pd, err := ParseDescriptors(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := pd.FindConfig(dev.ConfigValue)
|
||||
if cfg == nil {
|
||||
// The device is unconfigured, or sysfs and the descriptors disagree.
|
||||
// Fall back to the first configuration.
|
||||
if len(pd.Configs) == 0 {
|
||||
return
|
||||
}
|
||||
cfg = &pd.Configs[0]
|
||||
}
|
||||
|
||||
dev.Endpoints = cfg.AllEndpoints()
|
||||
|
||||
// Merge: keep the driver names from sysfs, take everything else from the
|
||||
// descriptors (which are authoritative and include endpoint intervals).
|
||||
drivers := make(map[uint8]string, len(dev.Interfaces))
|
||||
for _, iface := range dev.Interfaces {
|
||||
drivers[iface.Number] = iface.Driver
|
||||
}
|
||||
|
||||
ifaces := cfg.ActiveInterfaces()
|
||||
for i := range ifaces {
|
||||
ifaces[i].Driver = drivers[ifaces[i].Number]
|
||||
}
|
||||
if len(ifaces) > 0 {
|
||||
dev.Interfaces = ifaces
|
||||
}
|
||||
}
|
||||
|
||||
func readInterfaces(sysPath, busID string) []Interface {
|
||||
entries, err := os.ReadDir(sysPath)
|
||||
if err != nil {
|
||||
@@ -150,26 +208,14 @@ func readEndpoints(ifacePath string) []Endpoint {
|
||||
}
|
||||
|
||||
epPath := filepath.Join(ifacePath, name)
|
||||
addr, _ := strconv.ParseUint(readString(epPath, "bEndpointAddress"), 16, 8)
|
||||
|
||||
var transferType uint8
|
||||
switch readString(epPath, "type") {
|
||||
case "Control":
|
||||
transferType = TransferTypeControl
|
||||
case "Isoc":
|
||||
transferType = TransferTypeIsochronous
|
||||
case "Bulk":
|
||||
transferType = TransferTypeBulk
|
||||
case "Interrupt":
|
||||
transferType = TransferTypeInterrupt
|
||||
}
|
||||
|
||||
maxPkt := readUint32(epPath, "wMaxPacketSize")
|
||||
|
||||
// Every numeric endpoint attribute in sysfs is hex, without a 0x
|
||||
// prefix — wMaxPacketSize "0040" means 64, not 40.
|
||||
eps = append(eps, Endpoint{
|
||||
Address: uint8(addr),
|
||||
TransferType: transferType,
|
||||
MaxPacketSize: uint16(maxPkt),
|
||||
Address: readHex8(epPath, "bEndpointAddress"),
|
||||
TransferType: readHex8(epPath, "bmAttributes") & 0x03,
|
||||
MaxPacketSize: readHex16(epPath, "wMaxPacketSize"),
|
||||
Interval: readHex8(epPath, "bInterval"),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,221 @@
|
||||
|
||||
package usb
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
// Enumerate lists all USB devices (Windows stub)
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
modsetupapi = windows.NewLazySystemDLL("setupapi.dll")
|
||||
|
||||
procSetupDiGetClassDevsW = modsetupapi.NewProc("SetupDiGetClassDevsW")
|
||||
procSetupDiEnumDeviceInterfaces = modsetupapi.NewProc("SetupDiEnumDeviceInterfaces")
|
||||
procSetupDiGetDeviceInterfaceDetailW = modsetupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
|
||||
procSetupDiDestroyDeviceInfoList = modsetupapi.NewProc("SetupDiDestroyDeviceInfoList")
|
||||
)
|
||||
|
||||
const (
|
||||
digcfPresent = 0x00000002
|
||||
digcfDeviceInterface = 0x00000010
|
||||
)
|
||||
|
||||
type spDeviceInterfaceData struct {
|
||||
CbSize uint32
|
||||
InterfaceClassGuid windows.GUID
|
||||
Flags uint32
|
||||
Reserved uintptr
|
||||
}
|
||||
|
||||
// Enumerate lists USB devices reachable through the usbshare filter driver,
|
||||
// plus any device registered from outside this process.
|
||||
//
|
||||
// Only devices with the filter attached appear: Windows has no equivalent of
|
||||
// walking /sys/bus/usb, and without the filter there is no way to drive a
|
||||
// device from user mode anyway, so listing the others would only offer
|
||||
// devices that cannot actually be shared.
|
||||
func Enumerate() ([]Device, error) {
|
||||
return nil, fmt.Errorf("USB enumeration not yet implemented on Windows")
|
||||
devices, err := enumerateFiltered()
|
||||
if err != nil {
|
||||
if external := ExternalDevices(); len(external) > 0 {
|
||||
return external, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mergeExternal(devices), nil
|
||||
}
|
||||
|
||||
func enumerateFiltered() ([]Device, error) {
|
||||
handle, _, _ := procSetupDiGetClassDevsW.Call(
|
||||
uintptr(unsafe.Pointer(&guidDevInterfaceUsbShare)),
|
||||
0, 0,
|
||||
uintptr(digcfPresent|digcfDeviceInterface),
|
||||
)
|
||||
if handle == uintptr(windows.InvalidHandle) {
|
||||
return nil, fmt.Errorf("no USB devices with the usbshare filter found " +
|
||||
"(install driver/windows/usbshare.inf and attach it to the devices you want to share)")
|
||||
}
|
||||
defer procSetupDiDestroyDeviceInfoList.Call(handle)
|
||||
|
||||
var devices []Device
|
||||
|
||||
for index := uint32(0); ; index++ {
|
||||
var ifaceData spDeviceInterfaceData
|
||||
ifaceData.CbSize = uint32(unsafe.Sizeof(ifaceData))
|
||||
|
||||
ret, _, _ := procSetupDiEnumDeviceInterfaces.Call(
|
||||
handle, 0,
|
||||
uintptr(unsafe.Pointer(&guidDevInterfaceUsbShare)),
|
||||
uintptr(index),
|
||||
uintptr(unsafe.Pointer(&ifaceData)),
|
||||
)
|
||||
if ret == 0 {
|
||||
break // no more interfaces
|
||||
}
|
||||
|
||||
devicePath, err := interfaceDetailPath(handle, &ifaceData)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dev, err := describeFilteredDevice(devicePath)
|
||||
if err != nil {
|
||||
log.Printf("[usb] skipping %s: %v", devicePath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
devices = append(devices, *dev)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
// interfaceDetailPath resolves an interface to the device path used to open it.
|
||||
func interfaceDetailPath(handle uintptr, ifaceData *spDeviceInterfaceData) (string, error) {
|
||||
// First call determines the size.
|
||||
var required uint32
|
||||
procSetupDiGetDeviceInterfaceDetailW.Call(
|
||||
handle,
|
||||
uintptr(unsafe.Pointer(ifaceData)),
|
||||
0, 0,
|
||||
uintptr(unsafe.Pointer(&required)),
|
||||
0,
|
||||
)
|
||||
if required == 0 {
|
||||
return "", fmt.Errorf("could not determine the interface detail size")
|
||||
}
|
||||
|
||||
buf := make([]byte, required)
|
||||
|
||||
// SP_DEVICE_INTERFACE_DETAIL_DATA_W starts with cbSize, which must be set
|
||||
// to the size of the fixed part — 8 on 64-bit, counting the alignment of
|
||||
// the WCHAR array that follows — not the size of the whole buffer.
|
||||
*(*uint32)(unsafe.Pointer(&buf[0])) = 8
|
||||
|
||||
ret, _, err := procSetupDiGetDeviceInterfaceDetailW.Call(
|
||||
handle,
|
||||
uintptr(unsafe.Pointer(ifaceData)),
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(required),
|
||||
uintptr(unsafe.Pointer(&required)),
|
||||
0,
|
||||
)
|
||||
if ret == 0 {
|
||||
return "", fmt.Errorf("reading interface detail: %w", err)
|
||||
}
|
||||
|
||||
// The path is a null-terminated WCHAR string starting after cbSize.
|
||||
pathPtr := (*uint16)(unsafe.Pointer(&buf[4]))
|
||||
return windows.UTF16PtrToString(pathPtr), nil
|
||||
}
|
||||
|
||||
// describeFilteredDevice opens a device briefly to read its descriptors.
|
||||
//
|
||||
// Claiming it here means the class driver stops seeing it for the duration.
|
||||
// Enumeration therefore releases immediately: holding the claim would make
|
||||
// merely listing devices disrupt whatever is using them.
|
||||
func describeFilteredDevice(devicePath string) (*Device, error) {
|
||||
handle, err := OpenDriverDevice(devicePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer handle.Close()
|
||||
|
||||
descriptors, err := handle.Descriptors()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading descriptors: %w", err)
|
||||
}
|
||||
|
||||
parsed, err := ParseDescriptors(descriptors)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing descriptors: %w", err)
|
||||
}
|
||||
|
||||
info := handle.Info()
|
||||
|
||||
cfg := parsed.FindConfig(info.ConfigurationValue)
|
||||
if cfg == nil {
|
||||
cfg = &parsed.Configs[0]
|
||||
}
|
||||
|
||||
dev := &Device{
|
||||
BusID: busIDFromPath(devicePath),
|
||||
BusNum: 0,
|
||||
DevNum: uint32(info.PortNumber),
|
||||
Speed: translateWindowsSpeed(info.Speed),
|
||||
VendorID: parsed.VendorID,
|
||||
ProductID: parsed.ProductID,
|
||||
BcdDevice: parsed.BcdDevice,
|
||||
DeviceClass: parsed.DeviceClass,
|
||||
DeviceSubClass: parsed.DeviceSubClass,
|
||||
DeviceProtocol: parsed.DeviceProtocol,
|
||||
ConfigValue: cfg.Value,
|
||||
NumConfigs: parsed.NumConfigs,
|
||||
DevPath: devicePath,
|
||||
Interfaces: cfg.ActiveInterfaces(),
|
||||
Endpoints: cfg.AllEndpoints(),
|
||||
}
|
||||
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
// busIDFromPath derives a stable identifier from a Windows device path.
|
||||
//
|
||||
// Paths look like \\?\usb#vid_046d&pid_c52b#5&1a2b3c4d&0&2#{guid}. The
|
||||
// instance part is stable for as long as the device stays in the same port,
|
||||
// which is what peers need: they request devices by this ID.
|
||||
func busIDFromPath(devicePath string) string {
|
||||
trimmed := strings.TrimPrefix(devicePath, `\\?\`)
|
||||
if idx := strings.LastIndex(trimmed, "#{"); idx > 0 {
|
||||
trimmed = trimmed[:idx]
|
||||
}
|
||||
|
||||
// '#' separates the parts; '&' appears inside them. Neither is a problem
|
||||
// for transport, but a shorter, more readable ID helps in the UI.
|
||||
parts := strings.Split(trimmed, "#")
|
||||
if len(parts) >= 3 {
|
||||
return strings.ReplaceAll(parts[2], "&", "-")
|
||||
}
|
||||
return strings.ReplaceAll(trimmed, "#", "-")
|
||||
}
|
||||
|
||||
// translateWindowsSpeed maps USB_DEVICE_SPEED onto the USB/IP speed codes.
|
||||
func translateWindowsSpeed(speed uint32) uint32 {
|
||||
switch speed {
|
||||
case 0: // UsbLowSpeed
|
||||
return 1
|
||||
case 1: // UsbFullSpeed
|
||||
return 2
|
||||
case 2: // UsbHighSpeed
|
||||
return 3
|
||||
case 3: // UsbSuperSpeed
|
||||
return 5
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package usb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Externally registered devices.
|
||||
//
|
||||
// Normally devices are found by walking sysfs. That is not available to an
|
||||
// unprivileged Android app, which must go through the framework: it enumerates
|
||||
// devices itself, asks the user for permission, and receives an already-open
|
||||
// file descriptor plus the raw descriptor blob. Those devices are registered
|
||||
// here and merged into the enumeration, so everything above this layer works
|
||||
// the same whether a device came from sysfs or from outside.
|
||||
var (
|
||||
externalMu sync.RWMutex
|
||||
externalDevices = make(map[string]Device)
|
||||
)
|
||||
|
||||
// RegisterExternalDevice adds a device that was discovered outside this
|
||||
// process. descriptors is the raw blob (device descriptor followed by
|
||||
// configuration descriptors), exactly what a usbdevfs file read returns and
|
||||
// what Android's UsbDeviceConnection.getRawDescriptors() provides.
|
||||
func RegisterExternalDevice(busID string, descriptors []byte, meta ExternalDeviceMeta) error {
|
||||
if busID == "" {
|
||||
return fmt.Errorf("bus ID is required")
|
||||
}
|
||||
|
||||
parsed, err := ParseDescriptors(descriptors)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing descriptors for %s: %w", busID, err)
|
||||
}
|
||||
|
||||
cfg := parsed.FindConfig(meta.ConfigValue)
|
||||
if cfg == nil {
|
||||
cfg = &parsed.Configs[0]
|
||||
}
|
||||
|
||||
dev := Device{
|
||||
BusID: busID,
|
||||
BusNum: meta.BusNum,
|
||||
DevNum: meta.DevNum,
|
||||
Speed: meta.Speed,
|
||||
VendorID: parsed.VendorID,
|
||||
ProductID: parsed.ProductID,
|
||||
BcdDevice: parsed.BcdDevice,
|
||||
DeviceClass: parsed.DeviceClass,
|
||||
DeviceSubClass: parsed.DeviceSubClass,
|
||||
DeviceProtocol: parsed.DeviceProtocol,
|
||||
ConfigValue: cfg.Value,
|
||||
NumConfigs: parsed.NumConfigs,
|
||||
Manufacturer: meta.Manufacturer,
|
||||
Product: meta.Product,
|
||||
Serial: meta.Serial,
|
||||
Interfaces: cfg.ActiveInterfaces(),
|
||||
Endpoints: cfg.AllEndpoints(),
|
||||
}
|
||||
|
||||
externalMu.Lock()
|
||||
externalDevices[busID] = dev
|
||||
externalMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExternalDeviceMeta carries the fields that cannot be read from the
|
||||
// descriptor blob because they describe the device's place on the bus or come
|
||||
// from string descriptors the caller already resolved.
|
||||
type ExternalDeviceMeta struct {
|
||||
BusNum uint32
|
||||
DevNum uint32
|
||||
Speed uint32
|
||||
ConfigValue uint8
|
||||
Manufacturer string
|
||||
Product string
|
||||
Serial string
|
||||
}
|
||||
|
||||
// UnregisterExternalDevice removes a device registered from outside.
|
||||
func UnregisterExternalDevice(busID string) {
|
||||
externalMu.Lock()
|
||||
delete(externalDevices, busID)
|
||||
externalMu.Unlock()
|
||||
}
|
||||
|
||||
// ExternalDevices returns a snapshot of the externally registered devices.
|
||||
func ExternalDevices() []Device {
|
||||
externalMu.RLock()
|
||||
defer externalMu.RUnlock()
|
||||
|
||||
result := make([]Device, 0, len(externalDevices))
|
||||
for _, dev := range externalDevices {
|
||||
result = append(result, dev)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// HasExternalDevices reports whether any device came from outside.
|
||||
func HasExternalDevices() bool {
|
||||
externalMu.RLock()
|
||||
defer externalMu.RUnlock()
|
||||
return len(externalDevices) > 0
|
||||
}
|
||||
|
||||
// mergeExternal appends externally registered devices to a list from sysfs,
|
||||
// letting the external entry win on a bus ID collision — it carries a file
|
||||
// descriptor we can actually use, which the sysfs entry may not.
|
||||
func mergeExternal(devices []Device) []Device {
|
||||
externalMu.RLock()
|
||||
defer externalMu.RUnlock()
|
||||
|
||||
if len(externalDevices) == 0 {
|
||||
return devices
|
||||
}
|
||||
|
||||
result := make([]Device, 0, len(devices)+len(externalDevices))
|
||||
for _, dev := range devices {
|
||||
if _, overridden := externalDevices[dev.BusID]; !overridden {
|
||||
result = append(result, dev)
|
||||
}
|
||||
}
|
||||
for _, dev := range externalDevices {
|
||||
result = append(result, dev)
|
||||
}
|
||||
return result
|
||||
}
|
||||
+116
-14
@@ -3,8 +3,10 @@
|
||||
package usb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -81,7 +83,7 @@ type usbdevfsBulkTransfer struct {
|
||||
}
|
||||
|
||||
type usbdevfsSetIntf struct {
|
||||
Interface uint32
|
||||
Interface uint32
|
||||
AltSetting uint32
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ type usbdevfsURB struct {
|
||||
NumberOfPackets int32 // or StreamID
|
||||
ErrorCount int32
|
||||
Signr uint32
|
||||
UserContext uintptr
|
||||
UserContext uintptr
|
||||
// ISO packet descriptors follow in memory if Type == urbTypeISO
|
||||
}
|
||||
|
||||
@@ -126,10 +128,25 @@ 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
|
||||
// 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)
|
||||
@@ -293,7 +310,7 @@ type SubmitURBParams struct {
|
||||
Endpoint uint8
|
||||
Flags uint32
|
||||
Buffer []byte
|
||||
UserContext uintptr
|
||||
UserContext uintptr
|
||||
}
|
||||
|
||||
// SubmitURB submits an asynchronous URB
|
||||
@@ -310,7 +327,7 @@ func (h *DeviceHandle) SubmitURB(params *SubmitURBParams) (*usbdevfsURB, error)
|
||||
Buffer: bufPtr,
|
||||
BufferLength: int32(len(params.Buffer)),
|
||||
NumberOfPackets: -1, // 0xFFFFFFFF for non-ISO
|
||||
UserContext: params.UserContext,
|
||||
UserContext: params.UserContext,
|
||||
}
|
||||
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsSubmitURB, uintptr(unsafe.Pointer(urb)))
|
||||
@@ -320,6 +337,20 @@ func (h *DeviceHandle) SubmitURB(params *SubmitURBParams) (*usbdevfsURB, error)
|
||||
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
|
||||
@@ -327,7 +358,7 @@ func (h *DeviceHandle) ReapURB() (*usbdevfsURB, error) {
|
||||
if errno != 0 {
|
||||
return nil, fmt.Errorf("USBDEVFS_REAPURB: %w", errno)
|
||||
}
|
||||
return (*usbdevfsURB)(unsafe.Pointer(urbPtr)), nil
|
||||
return urbFromKernelPtr(urbPtr), nil
|
||||
}
|
||||
|
||||
// ReapURBNonBlock tries to reap a URB without blocking
|
||||
@@ -337,7 +368,7 @@ func (h *DeviceHandle) ReapURBNonBlock() (*usbdevfsURB, error) {
|
||||
if errno != 0 {
|
||||
return nil, fmt.Errorf("USBDEVFS_REAPURBNDELAY: %w", errno)
|
||||
}
|
||||
return (*usbdevfsURB)(unsafe.Pointer(urbPtr)), nil
|
||||
return urbFromKernelPtr(urbPtr), nil
|
||||
}
|
||||
|
||||
// DiscardURB cancels a submitted URB
|
||||
@@ -450,10 +481,81 @@ func ReadISOResults(mem []byte, numPackets int32) []ISOPacketResult {
|
||||
// ReapedURBInfo holds exported fields from a reaped URB needed for response building
|
||||
type ReapedURBInfo struct {
|
||||
UserContext uintptr
|
||||
Status int32
|
||||
Status int32
|
||||
ActualLength int32
|
||||
StartFrame int32
|
||||
ErrorCount 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
|
||||
@@ -463,13 +565,13 @@ func (h *DeviceHandle) ReapURBInfo() (*ReapedURBInfo, error) {
|
||||
if errno != 0 {
|
||||
return nil, fmt.Errorf("USBDEVFS_REAPURB: %w", errno)
|
||||
}
|
||||
urb := (*usbdevfsURB)(unsafe.Pointer(urbPtr))
|
||||
urb := urbFromKernelPtr(urbPtr)
|
||||
return &ReapedURBInfo{
|
||||
UserContext: urb.UserContext,
|
||||
Status: urb.Status,
|
||||
Status: urb.Status,
|
||||
ActualLength: urb.ActualLength,
|
||||
StartFrame: urb.StartFrame,
|
||||
ErrorCount: urb.ErrorCount,
|
||||
StartFrame: urb.StartFrame,
|
||||
ErrorCount: urb.ErrorCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//go:build darwin
|
||||
|
||||
package usb
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Device access on macOS would go through IOKit, which has no counterpart to
|
||||
// usbdevfs: there is no device node to open and drive with ioctls. Providing
|
||||
// it means writing an IOKit backend with cgo, which is a separate piece of
|
||||
// work — see the platform table in the README.
|
||||
//
|
||||
// These stubs exist so the client builds and its other functions (listing
|
||||
// devices, diagnostics, the relay, the web UI) work on macOS.
|
||||
|
||||
type DeviceHandle struct{}
|
||||
|
||||
func OpenDevice(devPath string, busID string) (*DeviceHandle, error) {
|
||||
return nil, fmt.Errorf("sharing USB devices is not implemented on macOS " +
|
||||
"(needs an IOKit backend); this machine can still run the relay")
|
||||
}
|
||||
|
||||
func (h *DeviceHandle) Close() error { return nil }
|
||||
func (h *DeviceHandle) Fd() int { return -1 }
|
||||
func (h *DeviceHandle) DisconnectDriver() error { return fmt.Errorf("not implemented on macOS") }
|
||||
func (h *DeviceHandle) ConnectDriver() error { return fmt.Errorf("not implemented on macOS") }
|
||||
func (h *DeviceHandle) ClaimInterface(uint32) error { return fmt.Errorf("not implemented on macOS") }
|
||||
func (h *DeviceHandle) ReleaseInterface(uint32) error { return fmt.Errorf("not implemented on macOS") }
|
||||
@@ -11,7 +11,7 @@ func OpenDevice(devPath string, busID string) (*DeviceHandle, error) {
|
||||
return nil, fmt.Errorf("USB device access not yet implemented on Windows")
|
||||
}
|
||||
|
||||
func (h *DeviceHandle) Close() error { return nil }
|
||||
func (h *DeviceHandle) Close() error { return nil }
|
||||
func (h *DeviceHandle) Fd() int { return -1 }
|
||||
func (h *DeviceHandle) DisconnectDriver() error { return fmt.Errorf("not implemented") }
|
||||
func (h *DeviceHandle) ConnectDriver() error { return fmt.Errorf("not implemented") }
|
||||
|
||||
Reference in New Issue
Block a user