From fd0fe6ce1afae9cfdf0f70acfc957bc1fde9e7ae Mon Sep 17 00:00:00 2001 From: zyxkad Date: Wed, 3 Jul 2024 10:29:52 -0600 Subject: [PATCH 1/6] add back product and maunfacturer for USB details --- enumerator/usb_darwin.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/enumerator/usb_darwin.go b/enumerator/usb_darwin.go index 1f51c47..1ba7ed7 100644 --- a/enumerator/usb_darwin.go +++ b/enumerator/usb_darwin.go @@ -1,5 +1,5 @@ // -// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // @@ -70,14 +70,16 @@ func extractPortInfo(service io_registry_entry_t) (*PortDetails, error) { vid, _ := usbDevice.GetIntProperty("idVendor", C.kCFNumberSInt16Type) pid, _ := usbDevice.GetIntProperty("idProduct", C.kCFNumberSInt16Type) serialNumber, _ := usbDevice.GetStringProperty("USB Serial Number") - //product, _ := usbDevice.GetStringProperty("USB Product Name") - //manufacturer, _ := usbDevice.GetStringProperty("USB Vendor Name") - //fmt.Println(product + " - " + manufacturer) + + product := usbDevice.GetName() + manufacturer, _ := usbDevice.GetStringProperty("USB Vendor Name") port.IsUSB = true port.VID = fmt.Sprintf("%04X", vid) port.PID = fmt.Sprintf("%04X", pid) port.SerialNumber = serialNumber + port.Product = product + port.Manufacturer = manufacturer } return port, nil } @@ -220,6 +222,12 @@ func (me *io_registry_entry_t) GetClass() string { return C.GoString(&class[0]) } +func (me *io_registry_entry_t) GetName() string { + name := make([]C.char, 128 /* the size of io_name_t */) + C.IOObjectGetClass(C.io_object_t(*me), &name[0]) + return C.GoString(&name[0]) +} + // io_iterator_t type io_iterator_t C.io_iterator_t From a3a363334eae7ccbf3d86cac828a0fd0f72527d7 Mon Sep 17 00:00:00 2001 From: zyxkad Date: Wed, 3 Jul 2024 10:31:06 -0600 Subject: [PATCH 2/6] fix header --- enumerator/usb_darwin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enumerator/usb_darwin.go b/enumerator/usb_darwin.go index 1ba7ed7..9debe36 100644 --- a/enumerator/usb_darwin.go +++ b/enumerator/usb_darwin.go @@ -1,5 +1,5 @@ // -// Copyright 2014-2023 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // From 19cff0ac8de6b09fa4962f6cd33d0fc5cf102d68 Mon Sep 17 00:00:00 2001 From: zyxkad Date: Wed, 3 Jul 2024 10:32:10 -0600 Subject: [PATCH 3/6] uncomment Manufacturer field --- enumerator/enumerator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enumerator/enumerator.go b/enumerator/enumerator.go index 2bd5043..782e59d 100644 --- a/enumerator/enumerator.go +++ b/enumerator/enumerator.go @@ -17,7 +17,7 @@ type PortDetails struct { PID string SerialNumber string - // Manufacturer string + Manufacturer string // Product is an OS-dependent string that describes the serial port, it may // be not always available and it may be different across OS. From e9fe4681a418d726793bad4ac5e90fc722408941 Mon Sep 17 00:00:00 2001 From: zyxkad Date: Wed, 3 Jul 2024 13:23:43 -0600 Subject: [PATCH 4/6] fix invoking wrong function instead of IORegistryEntryGetName --- enumerator/usb_darwin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enumerator/usb_darwin.go b/enumerator/usb_darwin.go index 9debe36..2d2bddb 100644 --- a/enumerator/usb_darwin.go +++ b/enumerator/usb_darwin.go @@ -224,7 +224,7 @@ func (me *io_registry_entry_t) GetClass() string { func (me *io_registry_entry_t) GetName() string { name := make([]C.char, 128 /* the size of io_name_t */) - C.IOObjectGetClass(C.io_object_t(*me), &name[0]) + C.IORegistryEntryGetName(C.io_object_t(*me), &name[0]) return C.GoString(&name[0]) } From 1b30f49e450913571c2727f2c81e75baf4cce103 Mon Sep 17 00:00:00 2001 From: zyxkad Date: Thu, 4 Jul 2024 21:33:14 -0600 Subject: [PATCH 5/6] use purego for darwin no cgo --- .../{usb_darwin.go => usb_darwin_cgo.go} | 2 + enumerator/usb_darwin_nocgo.go | 390 ++++++++++++++++++ go.mod | 1 + go.sum | 2 + 4 files changed, 395 insertions(+) rename enumerator/{usb_darwin.go => usb_darwin_cgo.go} (99%) create mode 100644 enumerator/usb_darwin_nocgo.go diff --git a/enumerator/usb_darwin.go b/enumerator/usb_darwin_cgo.go similarity index 99% rename from enumerator/usb_darwin.go rename to enumerator/usb_darwin_cgo.go index 2d2bddb..7c4f6c3 100644 --- a/enumerator/usb_darwin.go +++ b/enumerator/usb_darwin_cgo.go @@ -4,6 +4,8 @@ // license that can be found in the LICENSE file. // +//go:build darwin && cgo + package enumerator // #cgo LDFLAGS: -framework CoreFoundation -framework IOKit diff --git a/enumerator/usb_darwin_nocgo.go b/enumerator/usb_darwin_nocgo.go new file mode 100644 index 0000000..54d66aa --- /dev/null +++ b/enumerator/usb_darwin_nocgo.go @@ -0,0 +1,390 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +//go:build darwin && !cgo + +package enumerator + +import ( + "bytes" + "errors" + "fmt" + "time" + "unsafe" + + "github.com/ebitengine/purego" +) + +var libraryLoadError = lib.Load() + +func nativeGetDetailedPortsList() ([]*PortDetails, error) { + var ports []*PortDetails + + services, err := getAllServices("IOSerialBSDClient") + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + for _, service := range services { + port, err := extractPortInfo(service) + service.Release() + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + ports = append(ports, port) + } + return ports, nil +} + +func extractPortInfo(service io_registry_entry_t) (*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. + for retries := 5; retries > 0; retries-- { + name, err := service.GetStringProperty("IOCalloutDevice") + if err == nil { + port.Name = name + break + } + if retries == 0 { + return nil, fmt.Errorf("error extracting port info from device: %w", err) + } + time.Sleep(50 * time.Millisecond) + } + port.IsUSB = false + + validUSBDeviceClass := map[string]bool{ + "IOUSBDevice": true, + "IOUSBHostDevice": true, + } + usbDevice := service + var searchErr error + for !validUSBDeviceClass[usbDevice.GetClass()] { + if usbDevice, searchErr = usbDevice.GetParent("IOService"); searchErr != nil { + break + } + } + if searchErr == nil { + // It's an IOUSBDevice + vid, _ := usbDevice.GetIntProperty("idVendor", kCFNumberSInt16Type) + pid, _ := usbDevice.GetIntProperty("idProduct", kCFNumberSInt16Type) + serialNumber, _ := usbDevice.GetStringProperty("USB Serial Number") + + product := usbDevice.GetName() + manufacturer, _ := usbDevice.GetStringProperty("USB Vendor Name") + + port.IsUSB = true + port.VID = fmt.Sprintf("%04X", vid) + port.PID = fmt.Sprintf("%04X", pid) + port.SerialNumber = serialNumber + port.Product = product + port.Manufacturer = manufacturer + } + return port, nil +} + +func getAllServices(serviceType string) ([]io_registry_entry_t, error) { + i, err := getMatchingServices(lib.IOServiceMatching(serviceType)) + if err != nil { + return nil, err + } + defer i.Release() + + var services []io_registry_entry_t + tries := 0 + for tries < 5 { + // Extract all elements from iterator + if service, ok := i.Next(); ok { + services = append(services, io_registry_entry_t(service)) + continue + } + // If the list of services is empty or the iterator is still valid return the result + if len(services) == 0 || i.IsValid() { + return services, nil + } + // Otherwise empty the result and retry + for _, s := range services { + s.Release() + } + services = services[:0] + i.Reset() + tries++ + } + // Give up if the iteration continues to fail... + return nil, fmt.Errorf("IOServiceGetMatchingServices failed, data changed while iterating") +} + +// getMatchingServices look up registered IOService objects that match a matching dictionary. +func getMatchingServices(matcher cfMutableDictionaryRef) (io_iterator_t, error) { + var i io_iterator_t + res := lib.IOServiceGetMatchingServices(lib.kIOMasterPortDefault, cfDictionaryRef(matcher), &i) + if res.Successed() { + return 0, fmt.Errorf("IOServiceGetMatchingServices failed (code %d)", res) + } + return i, nil +} + +type library struct { + // IOKit + kIOMasterPortDefault uintptr + + IOIteratorIsValid func(io_iterator_t) bool + IOIteratorNext func(io_iterator_t) io_object_t + IOIteratorReset func(io_iterator_t) + IOObjectGetClass func(io_object_t, *io_name_t) kern_return_t + IOObjectRelease func(io_object_t) int + IORegistryEntryCreateCFProperty func(io_registry_entry_t, cfStringRef, cfAllocatorRef, uint32) cfTypeRef + IORegistryEntryGetName func(io_registry_entry_t, *io_name_t) kern_return_t + IORegistryEntryGetParentEntry func(io_registry_entry_t, string, *io_registry_entry_t) kern_return_t + IOServiceGetMatchingServices func(uintptr, cfDictionaryRef, *io_iterator_t) kern_return_t + IOServiceMatching func(string) cfMutableDictionaryRef + + // CoreFoundation + kCFAllocatorDefault cfAllocatorRef + + CFNumberGetValue func(cfNumberRef, cfNumberType, unsafe.Pointer) bool + CFRelease func(cfTypeRef) + CFStringCreateWithCString func(cfAllocatorRef, string, cfStringEncoding) cfStringRef + CFStringGetCString func(cfStringRef, *byte, int, cfStringEncoding) bool + CFStringGetCStringPtr func(cfStringRef, cfStringEncoding) string +} + +var lib library + +func (l *library) Load() error { + if err := l.loadIOKit(); err != nil { + return err + } + if err := l.loadCF(); err != nil { + return err + } + return nil +} + +func (l *library) loadIOKit() error { + iokitLib, err := purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + return err + } + + l.kIOMasterPortDefault = 0 // purego.Dlsym(iokitLib, "kIOMasterPortDefault") + + purego.RegisterLibFunc(&l.IOIteratorIsValid, iokitLib, "IOIteratorIsValid") + purego.RegisterLibFunc(&l.IOIteratorNext, iokitLib, "IOIteratorNext") + purego.RegisterLibFunc(&l.IOObjectGetClass, iokitLib, "IOObjectGetClass") + purego.RegisterLibFunc(&l.IOObjectRelease, iokitLib, "IOObjectRelease") + purego.RegisterLibFunc(&l.IORegistryEntryCreateCFProperty, iokitLib, "IORegistryEntryCreateCFProperty") + purego.RegisterLibFunc(&l.IORegistryEntryGetName, iokitLib, "IORegistryEntryGetName") + purego.RegisterLibFunc(&l.IORegistryEntryGetParentEntry, iokitLib, "IORegistryEntryGetParentEntry") + purego.RegisterLibFunc(&l.IOServiceGetMatchingServices, iokitLib, "IOServiceGetMatchingServices") + purego.RegisterLibFunc(&l.IOServiceMatching, iokitLib, "IOServiceMatching") + return nil +} + +func (l *library) loadCF() error { + cfLib, err := purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + return err + } + + ptr, err := purego.Dlsym(cfLib, "kCFAllocatorDefault") + if err != nil { + return err + } + l.kCFAllocatorDefault = *((*cfAllocatorRef)(unsafe.Pointer(ptr))) + + purego.RegisterLibFunc(&l.CFNumberGetValue, cfLib, "CFNumberGetValue") + purego.RegisterLibFunc(&l.CFRelease, cfLib, "CFRelease") + purego.RegisterLibFunc(&l.CFStringCreateWithCString, cfLib, "CFStringCreateWithCString") + purego.RegisterLibFunc(&l.CFStringGetCString, cfLib, "CFStringGetCString") + purego.RegisterLibFunc(&l.CFStringGetCStringPtr, cfLib, "CFStringGetCStringPtr") + return nil +} + +func (l *library) GoString(buf []byte) string { + i := bytes.IndexByte(buf, 0) + if i < 0 { + return string(buf) + } + return string(buf[:i]) +} + +func (l *library) ToCFString(s string) cfStringRef { + return l.CFStringCreateWithCString(l.kCFAllocatorDefault, s, kCFStringEncodingUTF8) +} + +func (l *library) FromCFString(p cfStringRef) string { + return l.CFStringGetCStringPtr(p, kCFStringEncodingUTF8) +} + +func (l *library) CFStringToBuf(p cfStringRef, buf []byte) bool { + return l.CFStringGetCString(p, &buf[0], len(buf), kCFStringEncodingUTF8) +} + +type ( + kern_return_t uint32 + io_name_t [128]byte + io_object_t uintptr + io_iterator_t io_object_t + io_registry_entry_t io_object_t +) + +const ( + KERN_SUCCESS kern_return_t = 0 +) + +func (r kern_return_t) Successed() bool { + return r == KERN_SUCCESS +} + +func (r kern_return_t) Failed() bool { + return r != KERN_SUCCESS +} + +func (s *io_name_t) AsPtr() *byte { + return &s[0] +} + +func (s *io_name_t) String() string { + return lib.GoString(s[:]) +} + +func (o io_object_t) Release() { + lib.IOObjectRelease(o) +} + +func (o io_object_t) GetClass() string { + var class io_name_t + if lib.IOObjectGetClass(o, &class).Failed() { + return "" + } + return class.String() +} + +// IsValid checks if an iterator is still valid. +// Some iterators will be made invalid if changes are made to the +// structure they are iterating over. This function checks the iterator +// is still valid and should be called when Next returns zero. +// An invalid iterator can be Reset and the iteration restarted. +func (i io_iterator_t) IsValid() bool { + return lib.IOIteratorIsValid(i) +} + +func (i io_iterator_t) Next() (io_object_t, bool) { + o := lib.IOIteratorNext(i) + if o == 0 { + return 0, false + } + return o, true +} + +func (i io_iterator_t) Reset() { + lib.IOIteratorReset(i) +} + +func (i io_iterator_t) Release() { + io_object_t(i).Release() +} + +func (e io_registry_entry_t) GetParent(plane string) (io_registry_entry_t, error) { + var parent io_registry_entry_t + if lib.IORegistryEntryGetParentEntry(e, plane, &parent).Failed() { + return 0, errors.New("No parent device available") + } + return parent, nil +} + +func (e io_registry_entry_t) CreateCFProperty(key string) (cfTypeRef, error) { + k := lib.ToCFString(key) + defer k.Release() + property := lib.IORegistryEntryCreateCFProperty(e, k, lib.kCFAllocatorDefault, 0) + if property == 0 { + return 0, errors.New("Property not found: " + key) + } + return property, nil +} + +func (e io_registry_entry_t) GetStringProperty(key string) (string, error) { + property, err := e.CreateCFProperty(key) + if err != nil { + return "", err + } + defer property.Release() + + if str := lib.CFStringGetCStringPtr(cfStringRef(property), kCFStringEncodingMacRoman); str == "" { + return str, nil + } + var name io_name_t + if !lib.CFStringGetCString(cfStringRef(property), name.AsPtr(), len(name), kCFStringEncodingUTF8) { + return "", fmt.Errorf("Property '%s' can't be converted", key) + } + return name.String(), nil +} + +func (e io_registry_entry_t) GetIntProperty(key string, intType cfNumberType) (int64, error) { + property, err := e.CreateCFProperty(key) + if err != nil { + return 0, err + } + defer property.Release() + var res int64 + if !lib.CFNumberGetValue(cfNumberRef(property), intType, unsafe.Pointer(&res)) { + return res, fmt.Errorf("Property '%s' can't be converted or has been truncated", key) + } + return res, nil +} + +func (e io_registry_entry_t) Release() { + io_object_t(e).Release() +} + +func (e io_registry_entry_t) GetClass() string { + return io_object_t(e).GetClass() +} + +func (e io_registry_entry_t) GetName() string { + var name io_name_t + if lib.IORegistryEntryGetName(e, &name).Failed() { + return "" + } + return name.String() +} + +type ( + cfIndex int + cfStringEncoding uint32 + cfTypeRef uintptr + + cfAllocatorRef cfTypeRef + cfDictionaryRef cfTypeRef + cfMutableDictionaryRef cfTypeRef + cfNumberRef cfTypeRef + cfNumberType cfIndex + cfStringRef cfTypeRef +) + +const ( + kCFNumberSInt8Type cfNumberType = 1 + kCFNumberSInt16Type cfNumberType = 2 + kCFNumberSInt32Type cfNumberType = 3 + kCFNumberSInt64Type cfNumberType = 4 +) + +const ( + kCFStringEncodingMacRoman cfStringEncoding = 0x0 + kCFStringEncodingUTF8 cfStringEncoding = 0x08000100 +) + +func (t cfTypeRef) Release() { + lib.CFRelease(t) +} + +func (s cfStringRef) String() string { + return lib.CFStringGetCStringPtr(s, kCFStringEncodingUTF8) +} + +func (s cfStringRef) Release() { + cfTypeRef(s).Release() +} diff --git a/go.mod b/go.mod index e89755e..14c5df2 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ebitengine/purego v0.7.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4cef3dc..8f53db0 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglD github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= +github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From bc847fb5fdc0036306b57c873337d869121c7f46 Mon Sep 17 00:00:00 2001 From: zyxkad Date: Thu, 4 Jul 2024 21:36:05 -0600 Subject: [PATCH 6/6] fix conditions --- enumerator/usb_darwin_nocgo.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/enumerator/usb_darwin_nocgo.go b/enumerator/usb_darwin_nocgo.go index 54d66aa..94bdd99 100644 --- a/enumerator/usb_darwin_nocgo.go +++ b/enumerator/usb_darwin_nocgo.go @@ -21,6 +21,10 @@ import ( var libraryLoadError = lib.Load() func nativeGetDetailedPortsList() ([]*PortDetails, error) { + if libraryLoadError != nil { + return nil, libraryLoadError + } + var ports []*PortDetails services, err := getAllServices("IOSerialBSDClient") @@ -120,7 +124,7 @@ func getAllServices(serviceType string) ([]io_registry_entry_t, error) { func getMatchingServices(matcher cfMutableDictionaryRef) (io_iterator_t, error) { var i io_iterator_t res := lib.IOServiceGetMatchingServices(lib.kIOMasterPortDefault, cfDictionaryRef(matcher), &i) - if res.Successed() { + if res.Failed() { return 0, fmt.Errorf("IOServiceGetMatchingServices failed (code %d)", res) } return i, nil