From 3b5dcd505616050ced7dee664b2ac6b3531d30a7 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 18 Apr 2023 17:32:15 +0200 Subject: [PATCH] feat: initialized SMART reads for disks that are SMART capable (#1407) --- Modules/Disk/header.h | 34 +++++++++++++++++ Modules/Disk/main.swift | 54 ++++++++++++++------------- Modules/Disk/readers.swift | 76 +++++++++++++++++++++++++++++++++++++- 3 files changed, 136 insertions(+), 28 deletions(-) diff --git a/Modules/Disk/header.h b/Modules/Disk/header.h index 92c3bdde..70cf0492 100644 --- a/Modules/Disk/header.h +++ b/Modules/Disk/header.h @@ -10,3 +10,37 @@ // #include +#include + +struct nvme_smart_log { + UInt8 critical_warning; + UInt8 temperature[2]; + UInt8 avail_spare; + UInt8 spare_thresh; + UInt8 percent_used; + UInt8 rsvd6[26]; + UInt8 data_units_read[16]; + UInt8 data_units_written[16]; + UInt8 host_reads[16]; + UInt8 host_writes[16]; + UInt8 ctrl_busy_time[16]; + UInt32 power_cycles[4]; + UInt32 power_on_hours[4]; + UInt32 unsafe_shutdowns[4]; + UInt32 media_errors[4]; + UInt16 temp_sensor[8]; + UInt32 thm_temp1_trans_count; + UInt32 thm_temp2_trans_count; + UInt32 thm_temp1_total_time; + UInt32 thm_temp2_total_time; + UInt8 rsvd232[280]; +}; + +typedef struct IONVMeSMARTInterface { + IUNKNOWN_C_GUTS; + + UInt16 version; + UInt16 revision; + + IOReturn ( *SMARTReadData )( void * interface, struct nvme_smart_log * NVMeSMARTData ); +} IONVMeSMARTInterface; diff --git a/Modules/Disk/main.swift b/Modules/Disk/main.swift index 42417dde..aa225985 100644 --- a/Modules/Disk/main.swift +++ b/Modules/Disk/main.swift @@ -20,8 +20,13 @@ public struct stats { var writeBytes: Int64 = 0 } +public struct smart_t { + var temperature: Int = 0 + var life: Int = 0 +} + public struct drive { - var parent: io_registry_entry_t = 0 + var parent: io_object_t = 0 var mediaName: String = "" var BSDName: String = "" @@ -38,6 +43,7 @@ public struct drive { var free: Int64 = 0 var activity: stats = stats() + var smart: smart_t? = nil } public class Disks { @@ -126,6 +132,12 @@ public class Disks { self.array[idx].activity.write = newValue } } + + func updateSMARTData(_ idx: Int, smart: smart_t) { + self.queue.async(flags: .barrier) { + self.array[idx].smart = smart + } + } } public struct Disk_process: IOProcess_p { @@ -171,9 +183,9 @@ public class Disk: Module { private let settingsView: Settings = Settings() private let portalView: Portal = Portal() - private var capacityReader: CapacityReader? = nil - private var activityReader: ActivityReader? = nil - private var processReader: ProcessReader? = nil + private var capacityReader: CapacityReader = CapacityReader() + private var activityReader: ActivityReader = ActivityReader() + private var processReader: ProcessReader = ProcessReader() private var selectedDisk: String = "" private var notificationLevelState: Bool = false @@ -191,27 +203,23 @@ public class Disk: Module { ) guard self.available else { return } - self.capacityReader = CapacityReader() - self.activityReader = ActivityReader() - self.processReader = ProcessReader() - self.selectedDisk = Store.shared.string(key: "\(Disk.name)_disk", defaultValue: self.selectedDisk) - self.capacityReader?.callbackHandler = { [unowned self] value in + self.capacityReader.callbackHandler = { [unowned self] value in if let value = value { self.capacityCallback(value) } } - self.capacityReader?.readyCallback = { [unowned self] in + self.capacityReader.readyCallback = { [unowned self] in self.readyHandler() } - self.activityReader?.callbackHandler = { [unowned self] value in + self.activityReader.callbackHandler = { [unowned self] value in if let value = value { self.activityCallback(value) } } - self.processReader?.callbackHandler = { [unowned self] value in + self.processReader.callbackHandler = { [unowned self] value in if let list = value { self.popupView.processCallback(list) } @@ -219,34 +227,28 @@ public class Disk: Module { self.settingsView.selectedDiskHandler = { [unowned self] value in self.selectedDisk = value - self.capacityReader?.read() + self.capacityReader.read() } self.settingsView.callback = { [unowned self] in - self.capacityReader?.read() + self.capacityReader.read() } self.settingsView.setInterval = { [unowned self] value in - self.capacityReader?.setInterval(value) + self.capacityReader.setInterval(value) } self.settingsView.callbackWhenUpdateNumberOfProcesses = { self.popupView.numberOfProcessesUpdated() DispatchQueue.global(qos: .background).async { - self.processReader?.read() + self.processReader.read() } } - if let reader = self.capacityReader { - self.addReader(reader) - } - if let reader = self.activityReader { - self.addReader(reader) - } - if let reader = self.processReader { - self.addReader(reader) - } + self.addReader(self.capacityReader) + self.addReader(self.activityReader) + self.addReader(self.processReader) } public override func widgetDidSet(_ type: widget_t) { - if type == .speed && self.capacityReader?.interval != 1 { + if type == .speed && self.capacityReader.interval != 1 { self.settingsView.setUpdateInterval(value: 1) } } diff --git a/Modules/Disk/readers.swift b/Modules/Disk/readers.swift index faa05029..cf634678 100644 --- a/Modules/Disk/readers.swift +++ b/Modules/Disk/readers.swift @@ -11,8 +11,26 @@ import Cocoa import Kit -import IOKit -import Darwin +import IOKit.storage + +let kIONVMeSMARTUserClientTypeID = CFUUIDGetConstantUUIDWithBytes(nil, + 0xAA, 0x0F, 0xA6, 0xF9, + 0xC2, 0xD6, 0x45, 0x7F, + 0xB1, 0x0B, 0x59, 0xA1, + 0x32, 0x53, 0x29, 0x2F +) +let kIONVMeSMARTInterfaceID = CFUUIDGetConstantUUIDWithBytes(nil, + 0xCC, 0xD1, 0xDB, 0x19, + 0xFD, 0x9A, 0x4D, 0xAF, + 0xBF, 0x95, 0x12, 0x45, + 0x4B, 0x23, 0x0A, 0xB6 +) +let kIOCFPlugInInterfaceID = CFUUIDGetConstantUUIDWithBytes(nil, + 0xC2, 0x44, 0xE8, 0x58, + 0x10, 0x9C, 0x11, 0xD4, + 0x91, 0xD4, 0x00, 0x50, + 0xE4, 0xC6, 0x42, 0x6F +) internal class CapacityReader: Reader { internal var list: Disks = Disks() @@ -43,6 +61,9 @@ internal class CapacityReader: Reader { if let path = d.path { self.list.updateFreeSize(idx, newValue: self.freeDiskSpaceInBytes(path)) + if let smart = self.getSMARTDetails(for: BSDName) { + self.list.updateSMARTData(idx, smart: smart) + } } continue @@ -53,6 +74,9 @@ internal class CapacityReader: Reader { d.free = self.freeDiskSpaceInBytes(path) d.size = self.totalDiskSpaceInBytes(path) } + if let smart = self.getSMARTDetails(for: BSDName) { + d.smart = smart + } self.list.append(d) self.list.sort() } @@ -117,6 +141,54 @@ internal class CapacityReader: Reader { return 0 } + + private func getSMARTDetails(for BSDName: String) -> smart_t? { + var disk = IOServiceGetMatchingService(kIOMasterPortDefault, IOBSDNameMatching(kIOMasterPortDefault, 0, BSDName.cString(using: .utf8))) + guard disk != kIOReturnSuccess else { return nil } + defer { IOObjectRelease(disk) } + + var parent = disk + while IOObjectConformsTo(disk, kIOBlockStorageDeviceClass) == 0 { + let error = IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) + if error != kIOReturnSuccess || parent == kIOReturnSuccess { return nil } + disk = parent + } + + guard IOObjectConformsTo(disk, kIOBlockStorageDeviceClass) > 0, + let raw = IORegistryEntryCreateCFProperty(disk, "NVMe SMART Capable" as CFString, kCFAllocatorDefault, 0), + let val = raw.takeRetainedValue() as? Bool, val else { + return nil + } + + var pluginInterface: UnsafeMutablePointer?>? + var smartInterface: UnsafeMutablePointer?>? + var score: Int32 = 0 + + var result = IOCreatePlugInInterfaceForService(disk, kIONVMeSMARTUserClientTypeID, kIOCFPlugInInterfaceID, &pluginInterface, &score) + guard result == kIOReturnSuccess else { return nil } + defer { IODestroyPlugInInterface(pluginInterface) } + + result = withUnsafeMutablePointer(to: &smartInterface) { + $0.withMemoryRebound(to: Optional.self, capacity: 1) { + pluginInterface?.pointee?.pointee.QueryInterface(pluginInterface, CFUUIDGetUUIDBytes(kIONVMeSMARTInterfaceID), $0) ?? KERN_NOT_FOUND + } + } + defer { _ = pluginInterface?.pointee?.pointee.Release(smartInterface) } + + guard result == KERN_SUCCESS, let smart = smartInterface?.pointee else { return nil } + var smartData: nvme_smart_log = nvme_smart_log() + guard smart.pointee.SMARTReadData(smartInterface, &smartData) == kIOReturnSuccess else { return nil } + + let temperatures: [UInt8] = [UInt8(smartData.temperature.1), UInt8(smartData.temperature.0)] + var temperature: UInt16 = 0 + let data = NSData(bytes: temperatures, length: 2) + data.getBytes(&temperature, length: 2) + + return smart_t( + temperature: Int(UInt16(bigEndian: temperature) - 273), + life: 100 - Int(smartData.percent_used) + ) + } } internal class ActivityReader: Reader {