//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 } }