From c3f5c6b901aaa81cb265f709217f155586ceeeed Mon Sep 17 00:00:00 2001 From: Anatol Pomazau Date: Wed, 16 Feb 2022 17:30:55 -0800 Subject: [PATCH] Add support for MacOSX Implements #2 --- README.md | 2 +- examples/nvme_darwin_test.go | 38 ++++++++ examples/other_test.go | 6 +- ...scsi__linux_test.go => scsi_linux_test.go} | 0 nvme.go | 27 +++++- nvme_darwin.cpp | 94 +++++++++++++++++++ nvme_darwin.go | 66 +++++++++++++ nvme_darwin.h | 26 +++++ nvme_linux.go | 75 ++++++++------- nvme_other.go | 16 ++-- 10 files changed, 298 insertions(+), 52 deletions(-) create mode 100644 examples/nvme_darwin_test.go rename examples/{scsi__linux_test.go => scsi_linux_test.go} (100%) create mode 100644 nvme_darwin.cpp create mode 100644 nvme_darwin.go create mode 100644 nvme_darwin.h diff --git a/README.md b/README.md index bf0d43a..6e174f0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Smart.go tries to match functionality provided by [smartctl](https://www.smartmo Currently this library support SATA, SCSI and NVMe drives. Different drive types provide different set of monitoring information and API reflects it. -At this point the library works with Linux OS only. We are looking for help in porting it to other OS as well. +At this point the library works at Linux and partialy at MacOSX. We are looking for help with porting it to other platforms. ## Example diff --git a/examples/nvme_darwin_test.go b/examples/nvme_darwin_test.go new file mode 100644 index 0000000..3e88627 --- /dev/null +++ b/examples/nvme_darwin_test.go @@ -0,0 +1,38 @@ +package test + +import ( + "bytes" + "fmt" + "os/exec" + "testing" + + "github.com/anatol/smart.go" + "github.com/stretchr/testify/require" +) + +func TestNVMe(t *testing.T) { + path := "disk0" + + out, err := exec.Command("smartctl", "-a", path).CombinedOutput() + fmt.Println(string(out)) + //require.NoError(t, err) it fails at macosx because of GetLogPage() + + dev, err := smart.OpenNVMe(path) + require.NoError(t, err) + defer dev.Close() + + c, ns, err := dev.Identify() + require.NoError(t, err) + + require.Equal(t, 0x106b, int(c.VendorID)) + require.Equal(t, 0x106b, int(c.Ssvid)) + require.Contains(t, string(bytes.TrimSpace(c.ModelNumber[:])), "APPLE SSD") + require.Equal(t, 1, int(c.Nn)) + + require.Len(t, ns, 1) + require.Equal(t, 244276265, int(ns[0].Nsze)) + + sm, err := dev.ReadSMART() + require.NoError(t, err) + require.Less(t, uint16(300), sm.Temperature) +} diff --git a/examples/other_test.go b/examples/other_test.go index b704f6d..96b1062 100644 --- a/examples/other_test.go +++ b/examples/other_test.go @@ -1,6 +1,6 @@ -// go:build !linux -//go:build !linux -// +build !linux +// go:build !linux&& !darwin +//go:build !linux && !darwin +// +build !linux,!darwin package test diff --git a/examples/scsi__linux_test.go b/examples/scsi_linux_test.go similarity index 100% rename from examples/scsi__linux_test.go rename to examples/scsi_linux_test.go diff --git a/nvme.go b/nvme.go index a54d27d..3e998e3 100644 --- a/nvme.go +++ b/nvme.go @@ -1188,10 +1188,29 @@ const ( nvmeLogDeviceSelftest = 0x6 ) -type NVMeDevice struct { - fd int -} - func (d *NVMeDevice) Type() string { return "nvme" } + +func (d *NVMeDevice) Identify() (*NvmeIdentController, []NvmeIdentNamespace, error) { + controller, err := d.readControllerIdentifyData() + if err != nil { + return nil, nil, err + } + + var ns []NvmeIdentNamespace + // QEMU has 256 namespaces for some reason, TODO: clarify + for i := 0; i < int(controller.Nn); i++ { + namespace, err := d.readNamespaceIdentifyData(i + 1) + if err != nil { + return nil, nil, err + } + if namespace.Nsze == 0 { + continue + } + + ns = append(ns, *namespace) + } + + return controller, ns, nil +} diff --git a/nvme_darwin.cpp b/nvme_darwin.cpp new file mode 100644 index 0000000..ff3b628 --- /dev/null +++ b/nvme_darwin.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +#include "nvme_darwin.h" + +bool is_smart_capable(io_object_t dev) { + CFTypeRef smartCapableKey = IORegistryEntryCreateCFProperty( + dev, CFSTR(kIOPropertyNVMeSMARTCapableKey), kCFAllocatorDefault, 0); + if (smartCapableKey) { + CFRelease(smartCapableKey); + return true; + } + + return false; +} + +// *path is a string that has a format like "disk0". +unsigned int smart_nvme_open_darwin(const char *path, smart_nvme_darwin *nvme) { + // see also https://gist.github.com/AlanQuatermain/250538 + SInt32 score = 0; + IOReturn kr; + HRESULT hr; + + CFMutableDictionaryRef matcher = + IOBSDNameMatching(kIOMasterPortDefault, 0, path); + io_object_t disk = IOServiceGetMatchingService(kIOMasterPortDefault, matcher); + + while (!is_smart_capable(disk)) { + io_object_t prevdisk = disk; + + // Find this device's parent and try again. + IOReturn err = IORegistryEntryGetParentEntry(disk, kIOServicePlane, &disk); + if (err != kIOReturnSuccess || !disk) { + IOObjectRelease(prevdisk); + break; + } + } + + if (!disk) { + printf("no disk found"); + goto exit1; + } + + nvme->disk = disk; + + kr = IOCreatePlugInInterfaceForService( + nvme->disk, kIONVMeSMARTUserClientTypeID, kIOCFPlugInInterfaceID, + &nvme->plugin, &score); + + if (kr != kIOReturnSuccess) + goto exit2; + + hr = (*nvme->plugin) + ->QueryInterface(nvme->plugin, + CFUUIDGetUUIDBytes(kIONVMeSMARTInterfaceID), + (void **)&nvme->smartIfNVMe); + if (hr != S_OK) + goto exit3; + + return 0; + +exit3: + IODestroyPlugInInterface(nvme->plugin); +exit2: + IOObjectRelease(nvme->disk); +exit1: + return -1; +} + +unsigned int smart_nvme_identify_darwin(smart_nvme_darwin *nvme, void *buffer, + unsigned int nsid) { + IOReturn err = + (*nvme->smartIfNVMe)->GetIdentifyData(nvme->smartIfNVMe, buffer, nsid); + if (err) + return ENOSYS; + + return 0; +} + +unsigned int smart_nvme_readsmart_darwin(smart_nvme_darwin *nvme, + void *buffer) { + IOReturn err = + (*nvme->smartIfNVMe) + ->SMARTReadData(nvme->smartIfNVMe, (struct NVMeSMARTData *)buffer); + if (err) + return ENOSYS; + + return 0; +} + +void smart_nvme_close_darwin(smart_nvme_darwin *nvme) { + (*nvme->smartIfNVMe)->Release(nvme->smartIfNVMe); + IODestroyPlugInInterface(nvme->plugin); + IOObjectRelease(nvme->disk); +} diff --git a/nvme_darwin.go b/nvme_darwin.go new file mode 100644 index 0000000..b590b5a --- /dev/null +++ b/nvme_darwin.go @@ -0,0 +1,66 @@ +package smart + +// #cgo darwin LDFLAGS: -framework IOKit -framework CoreFoundation +// #include "nvme_darwin.h" +import "C" + +import ( + "bytes" + "encoding/binary" + "fmt" + "unsafe" +) + +type NVMeDevice struct { + data C.smart_nvme_darwin +} + +func OpenNVMe(name string) (*NVMeDevice, error) { + dev := NVMeDevice{} + + if res := C.smart_nvme_open_darwin(C.CString(name), &dev.data); res != 0 { + return nil, fmt.Errorf("open darwin device error: 0x%x", res) + } + + return &dev, nil +} + +func (d *NVMeDevice) Close() error { + C.smart_nvme_close_darwin(&d.data) + return nil +} + +func (d *NVMeDevice) ReadSMART() (*NvmeSMARTLog, error) { + buf := make([]byte, 512) + if err := C.smart_nvme_readsmart_darwin(&d.data, unsafe.Pointer(&buf[0])); err != 0 { + return nil, fmt.Errorf("smart_nvme_readsmart_darwin: %v", err) + } + var sl NvmeSMARTLog + if err := binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &sl); err != nil { + return nil, err + } + + return &sl, nil +} + +func (d *NVMeDevice) readControllerIdentifyData() (*NvmeIdentController, error) { + buf := make([]byte, 4096) + if err := C.smart_nvme_identify_darwin(&d.data, unsafe.Pointer(&buf[0]), 0); err != 0 { + return nil, fmt.Errorf("smart_nvme_identify_darwin: %v", err) + } + var controller NvmeIdentController + if err := binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &controller); err != nil { + return nil, err + } + + return &controller, nil +} + +func (d *NVMeDevice) readNamespaceIdentifyData(nsid int) (*NvmeIdentNamespace, error) { + var n NvmeIdentNamespace + if err := C.smart_nvme_identify_darwin(&d.data, unsafe.Pointer(&n), C.uint(nsid)); err != 0 { + return nil, fmt.Errorf("smart_nvme_identify_darwin: %X", err) + } + + return &n, nil +} diff --git a/nvme_darwin.h b/nvme_darwin.h new file mode 100644 index 0000000..5d0e71c --- /dev/null +++ b/nvme_darwin.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct smart_nvme_darwin { + IONVMeSMARTInterface **smartIfNVMe; + IOCFPlugInInterface **plugin; + io_object_t disk; +} smart_nvme_darwin; + +unsigned int smart_nvme_open_darwin(const char *path, smart_nvme_darwin *nvme); +unsigned int smart_nvme_identify_darwin(smart_nvme_darwin *nvme, void *buffer, + unsigned int nsid); +unsigned int smart_nvme_readsmart_darwin(smart_nvme_darwin *nvme, void *buffer); +void smart_nvme_close_darwin(smart_nvme_darwin *nvme); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/nvme_linux.go b/nvme_linux.go index 1221032..2cbf3ac 100644 --- a/nvme_linux.go +++ b/nvme_linux.go @@ -38,6 +38,10 @@ type nvmePassthruCmd64 struct { result uint64 } +type NVMeDevice struct { + fd int +} + func OpenNVMe(name string) (*NVMeDevice, error) { fd, err := unix.Open(name, unix.O_RDWR, 0600) if err != nil { @@ -54,44 +58,13 @@ func (d *NVMeDevice) Close() error { return unix.Close(d.fd) } -func (d *NVMeDevice) Identify() (*NvmeIdentController, []NvmeIdentNamespace, error) { - buf := make([]byte, 4096) - if err := nvmeReadIdentify(d.fd, 0, 1, buf); err != nil { - return nil, nil, err - } - var controller NvmeIdentController - if err := binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &controller); err != nil { - return nil, nil, err - } - - var ns []NvmeIdentNamespace - // QEMU has 256 namespaces for some reason, TODO: clarify - for i := 0; i < int(controller.Nn); i++ { - buf2 := make([]byte, 4096) - var n NvmeIdentNamespace - if err := nvmeReadIdentify(d.fd, uint32(i+1), 0, buf2); err != nil { - return nil, nil, err - } - if err := binary.Read(bytes.NewBuffer(buf2), binary.LittleEndian, &n); err != nil { - return nil, nil, err - } - if n.Nsze == 0 { - continue - } - - ns = append(ns, n) - } - - return &controller, ns, nil -} - func (d *NVMeDevice) ReadSMART() (*NvmeSMARTLog, error) { - buf3 := make([]byte, 512) - if err := nvmeReadLogPage(d.fd, nvmeLogSmartInformation, buf3); err != nil { + buf := make([]byte, 512) + if err := nvmeReadLogPage(d.fd, nvmeLogSmartInformation, buf); err != nil { return nil, err } var sl NvmeSMARTLog - if err := binary.Read(bytes.NewBuffer(buf3), binary.LittleEndian, &sl); err != nil { + if err := binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &sl); err != nil { return nil, err } @@ -116,14 +89,40 @@ func nvmeReadLogPage(fd int, logID uint8, buf []byte) error { return ioctl(uintptr(fd), nvmeIoctlAdmin64Cmd, uintptr(unsafe.Pointer(&cmd))) } -func nvmeReadIdentify(fd int, nsid, cns uint32, data []byte) error { +func (d *NVMeDevice) readIdentifyData(nsid, cns int, data []byte) error { cmd := nvmePassthruCmd64{ opcode: nvmeAdminIdentify, - nsid: nsid, + nsid: uint32(nsid), addr: uint64(uintptr(unsafe.Pointer(&data[0]))), dataLen: uint32(len(data)), - cdw10: cns, + cdw10: uint32(cns), } - return ioctl(uintptr(fd), nvmeIoctlAdmin64Cmd, uintptr(unsafe.Pointer(&cmd))) + return ioctl(uintptr(d.fd), nvmeIoctlAdmin64Cmd, uintptr(unsafe.Pointer(&cmd))) +} + +func (d *NVMeDevice) readControllerIdentifyData() (*NvmeIdentController, error) { + buf := make([]byte, 4096) + if err := d.readIdentifyData(0, 1, buf); err != nil { + return nil, err + } + var controller NvmeIdentController + if err := binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &controller); err != nil { + return nil, err + } + + return &controller, nil +} + +func (d *NVMeDevice) readNamespaceIdentifyData(nsid int) (*NvmeIdentNamespace, error) { + buf := make([]byte, 4096) + if err := d.readIdentifyData(nsid, 0, buf); err != nil { + return nil, err + } + var namespace NvmeIdentNamespace + if err := binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &namespace); err != nil { + return nil, err + } + + return &namespace, nil } diff --git a/nvme_other.go b/nvme_other.go index b3731dc..38a2f59 100644 --- a/nvme_other.go +++ b/nvme_other.go @@ -1,6 +1,6 @@ -// go:build !linux -//go:build !linux -// +build !linux +// go:build !linux && !darwin +//go:build !linux && !darwin +// +build !linux,!darwin package smart @@ -12,10 +12,14 @@ func (d *NVMeDevice) Close() error { return ErrOSUnsupported } -func (d *NVMeDevice) Identify() (*NvmeIdentController, []NvmeIdentNamespace, error) { - return nil, nil, ErrOSUnsupported +func (d *NVMeDevice) ReadSMART() (*NvmeSMARTLog, error) { + return nil, ErrOSUnsupported } -func (d *NVMeDevice) ReadSMART() (*NvmeSMARTLog, error) { +func (d *NVMeDevice) readControllerIdentifyData() (*NvmeIdentController, error) { + return nil, ErrOSUnsupported +} + +func (d *NVMeDevice) readNamespaceIdentifyData(nsid int) (*NvmeIdentNamespace, error) { return nil, ErrOSUnsupported }