diff --git a/enumerator/enumerator.go b/enumerator/enumerator.go index 1d7fbb3..ce63502 100644 --- a/enumerator/enumerator.go +++ b/enumerator/enumerator.go @@ -22,18 +22,38 @@ type PortDetails struct { // SerialNumber is the USB serial number, when available. SerialNumber string // Configuration is the USB configuration string, when available. + // Requires active USB probing enabled. Configuration string // Manufacturer is the USB iManufacturer string, when available. + // Requires active USB probing enabled. Manufacturer string // Product is the USB iProduct string, when available. + // Requires active USB probing enabled. Product string } +// All is a vid/pid filter that accepts all devices +var All = func(vid, pid string) bool { return true } + // GetDetailedPortsList retrieve ports details like USB VID/PID. // Please note that this function may not be available on all OS: // in that case a FunctionNotImplemented error is returned. -func GetDetailedPortsList() ([]*PortDetails, error) { - return nativeGetDetailedPortsList() +// +// Getting some USB fields requires active USB probing (see PortDetails +// struct), which may interfere with correct operation on some devices. +// Active USB probing is disabled by default, to enable it you must provide +// at least one filter function to allow the probing of specific devices based on +// vid and pid. If no filters are provided then no devices will be actively probed. +// If a device match any of the filters provided then the device will be actively probed. +func GetDetailedPortsList(activeUSBProbeFilters ...func(vid, pid string) bool) ([]*PortDetails, error) { + return nativeGetDetailedPortsList(func(vid, pid string) bool { + for _, filter := range activeUSBProbeFilters { + if filter(vid, pid) { + return true + } + } + return false + }) } // PortEnumerationError is the error type for serial ports enumeration diff --git a/enumerator/example_getdetailedportlist_test.go b/enumerator/example_getdetailedportlist_test.go index 2e70ded..fe87891 100644 --- a/enumerator/example_getdetailedportlist_test.go +++ b/enumerator/example_getdetailedportlist_test.go @@ -14,7 +14,15 @@ import ( ) func ExampleGetDetailedPortsList() { - ports, err := enumerator.GetDetailedPortsList() + // Passing enumerator.All actively probes every USB device to retrieve the + // Manufacturer, Product and Configuration fields. This may interfere with + // the normal operation of some devices, so in production code you should + // prefer a filter that only allows probing specific VID/PID pairs, e.g.: + // + // enumerator.GetDetailedPortsList(func(vid, pid string) bool { + // return vid == "2341" // only probe Arduino devices + // }) + ports, err := enumerator.GetDetailedPortsList(enumerator.All) if err != nil { log.Fatal(err) } diff --git a/enumerator/usb_darwin.go b/enumerator/usb_darwin.go index f8f5a8d..abb1d66 100644 --- a/enumerator/usb_darwin.go +++ b/enumerator/usb_darwin.go @@ -49,7 +49,7 @@ import ( "unsafe" ) -func nativeGetDetailedPortsList() ([]*PortDetails, error) { +func nativeGetDetailedPortsList(shouldProbeUSB func(vid, pid string) bool) ([]*PortDetails, error) { var ports []*PortDetails services, err := getAllServices("IOSerialBSDClient") @@ -63,7 +63,7 @@ func nativeGetDetailedPortsList() ([]*PortDetails, error) { }() for _, service := range services { - port, err := extractPortInfo(io_registry_entry_t(service)) + port, err := extractPortInfo(io_registry_entry_t(service), shouldProbeUSB) if err != nil { return nil, &PortEnumerationError{causedBy: err} } @@ -72,7 +72,7 @@ func nativeGetDetailedPortsList() ([]*PortDetails, error) { return ports, nil } -func extractPortInfo(service io_registry_entry_t) (*PortDetails, error) { +func extractPortInfo(service io_registry_entry_t, shouldProbeUSB func(vid, pid string) bool) (*PortDetails, error) { port := &PortDetails{} // If called too early the port may still not be ready or fully enumerated // so we retry 5 times before returning error. @@ -116,7 +116,6 @@ func extractPortInfo(service io_registry_entry_t) (*PortDetails, error) { serialNumber, _ := usbDevice.GetStringProperty("kUSBSerialNumberString") vendor, _ := usbDevice.GetStringProperty("kUSBVendorString") product, _ := usbDevice.GetStringProperty("kUSBProductString") - configuration, _ := usbDevice.GetUSBConfigurationString() port.IsUSB = true port.VID = fmt.Sprintf("%04X", vid) @@ -124,7 +123,14 @@ func extractPortInfo(service io_registry_entry_t) (*PortDetails, error) { port.SerialNumber = serialNumber port.Manufacturer = vendor port.Product = product - port.Configuration = configuration + + // Retrieving the USB configuration string requires actively opening + // the device and issuing control requests, which may interfere with + // the device's normal operation. Only do this if the caller + // explicitly allowed probing for this VID/PID. + if shouldProbeUSB(port.VID, port.PID) { + port.Configuration, _ = usbDevice.GetUSBConfigurationString() + } } return port, nil } diff --git a/enumerator/usb_freebsd.go b/enumerator/usb_freebsd.go index c682bf9..086c06f 100644 --- a/enumerator/usb_freebsd.go +++ b/enumerator/usb_freebsd.go @@ -6,7 +6,7 @@ package enumerator -func nativeGetDetailedPortsList() ([]*PortDetails, error) { +func nativeGetDetailedPortsList(_ func(vid, pid string) bool) ([]*PortDetails, error) { // TODO return nil, &PortEnumerationError{} } diff --git a/enumerator/usb_linux.go b/enumerator/usb_linux.go index 00c1bd4..f82b71a 100644 --- a/enumerator/usb_linux.go +++ b/enumerator/usb_linux.go @@ -17,8 +17,12 @@ import ( "go.bug.st/serial" ) -func nativeGetDetailedPortsList() ([]*PortDetails, error) { - // Retrieve the port list +func nativeGetDetailedPortsList(_ func(vid, pid string) bool) ([]*PortDetails, error) { + // Retrieve the port list. + // Note: on Linux all the USB details (including Configuration, Manufacturer + // and Product) are read from sysfs, which is populated by the kernel at + // enumeration time and does not require actively probing the device, so + // the active-probe filter is not needed on this platform. ports, err := serial.GetPortsList() if err != nil { return nil, &PortEnumerationError{causedBy: err} diff --git a/enumerator/usb_openbsd.go b/enumerator/usb_openbsd.go index c682bf9..086c06f 100644 --- a/enumerator/usb_openbsd.go +++ b/enumerator/usb_openbsd.go @@ -6,7 +6,7 @@ package enumerator -func nativeGetDetailedPortsList() ([]*PortDetails, error) { +func nativeGetDetailedPortsList(_ func(vid, pid string) bool) ([]*PortDetails, error) { // TODO return nil, &PortEnumerationError{} } diff --git a/enumerator/usb_wasm.go b/enumerator/usb_wasm.go index 0096108..25698f9 100644 --- a/enumerator/usb_wasm.go +++ b/enumerator/usb_wasm.go @@ -6,6 +6,6 @@ package enumerator -func nativeGetDetailedPortsList() ([]*PortDetails, error) { +func nativeGetDetailedPortsList(_ func(vid, pid string) bool) ([]*PortDetails, error) { return nil, &PortEnumerationError{} } diff --git a/enumerator/usb_windows.go b/enumerator/usb_windows.go index aedc5d7..69938a3 100644 --- a/enumerator/usb_windows.go +++ b/enumerator/usb_windows.go @@ -117,7 +117,7 @@ func (dev *deviceInfo) openDevRegKey(scope windows.DICS_FLAG, hwProfile uint32, return setupDiOpenDevRegKey(dev.set, dev.data, scope, hwProfile, keyType, samDesired) } -func nativeGetDetailedPortsList() ([]*PortDetails, error) { +func nativeGetDetailedPortsList(shouldProbeUSB func(vid, pid string) bool) ([]*PortDetails, error) { guids, err := windows.SetupDiClassGuidsFromNameEx("Ports", "") if err != nil { return nil, &PortEnumerationError{causedBy: err} @@ -147,7 +147,7 @@ func nativeGetDetailedPortsList() ([]*PortDetails, error) { } details.Name = portName - if err := retrievePortDetailsFromDevInfo(device, details); err != nil { + if err := retrievePortDetailsFromDevInfo(device, details, shouldProbeUSB); err != nil { return nil, &PortEnumerationError{causedBy: err} } res = append(res, details) @@ -172,7 +172,7 @@ func retrievePortNameFromDevInfo(device *deviceInfo) (string, error) { return syscall.UTF16ToString(name[:]), nil } -func retrievePortDetailsFromDevInfo(device *deviceInfo, details *PortDetails) error { +func retrievePortDetailsFromDevInfo(device *deviceInfo, details *PortDetails, shouldProbeUSB func(vid, pid string) bool) error { deviceID, err := device.getInstanceID() if err != nil { return err @@ -206,7 +206,11 @@ func retrievePortDetailsFromDevInfo(device *deviceInfo, details *PortDetails) er } } - if details.IsUSB { + // Retrieving iManufacturer/iProduct/iConfiguration strings requires actively + // probing the USB device via the parent hub, which may interfere with the + // device's normal operation. Only do this if the caller explicitly allowed + // probing for this VID/PID. + if details.IsUSB && shouldProbeUSB(details.VID, details.PID) { if hub, port, err := findUsbHubAndPortConnectedToDevice(device); err == nil { defer hub.Close() diff --git a/portlist/portlist.go b/portlist/portlist.go index f95b901..d9ec155 100644 --- a/portlist/portlist.go +++ b/portlist/portlist.go @@ -10,6 +10,7 @@ package main import ( + "flag" "fmt" "log" @@ -17,7 +18,15 @@ import ( ) func main() { - ports, err := enumerator.GetDetailedPortsList() + probe := flag.Bool("probe", false, "actively probe USB devices to retrieve manufacturer, product and configuration strings") + flag.Parse() + + var filters []func(vid, pid string) bool + if *probe { + filters = append(filters, enumerator.All) + } + + ports, err := enumerator.GetDetailedPortsList(filters...) if err != nil { log.Fatal(err) }