//go:build windows package usb import ( "fmt" "log" "strings" "unsafe" "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) { 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 } }