Skip to content

Commit

Permalink
Add support for MacOSX
Browse files Browse the repository at this point in the history
Implements #2
  • Loading branch information
anatol committed Feb 18, 2022
1 parent 71e2e3c commit 1c0fd2c
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 51 deletions.
1 change: 1 addition & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ColumnLimit: 140
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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 at Linux and partialy at MacOSX. We are looking for help with porting it to other platforms.

## Example

Here is an example of code that demonstrates the library usage.
Expand Down
38 changes: 38 additions & 0 deletions examples/nvme_darwin_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 3 additions & 3 deletions examples/other_test.go
Original file line number Diff line number Diff line change
@@ -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

Expand Down
File renamed without changes.
27 changes: 23 additions & 4 deletions nvme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
89 changes: 89 additions & 0 deletions nvme_darwin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT

#include "nvme_darwin.h"

static 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);
nvme->plugin = NULL;
exit2:
IOObjectRelease(nvme->disk);
nvme->disk = 0;
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);
nvme->smartIfNVMe = NULL;

IODestroyPlugInInterface(nvme->plugin);
nvme->plugin = NULL;

IOObjectRelease(nvme->disk);
nvme->disk = 0;
}
66 changes: 66 additions & 0 deletions nvme_darwin.go
Original file line number Diff line number Diff line change
@@ -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
}
25 changes: 25 additions & 0 deletions nvme_darwin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/storage/nvme/NVMeSMARTLibExternal.h>

#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
75 changes: 37 additions & 38 deletions nvme_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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
}
Loading

0 comments on commit 1c0fd2c

Please sign in to comment.