mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
457 lines
18 KiB
Swift
457 lines
18 KiB
Swift
//
|
|
// readers.swift
|
|
// Disk
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 07/05/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import Kit
|
|
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<Disks> {
|
|
internal var list: Disks = Disks()
|
|
|
|
public override func read() {
|
|
let keys: [URLResourceKey] = [.volumeNameKey]
|
|
let removableState = Store.shared.bool(key: "Disk_removable", defaultValue: false)
|
|
let paths = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: [.skipHiddenVolumes])!
|
|
|
|
guard let session = DASessionCreate(kCFAllocatorDefault) else {
|
|
error("cannot create main DASessionCreate()", log: self.log)
|
|
return
|
|
}
|
|
|
|
var active: [String] = []
|
|
for url in paths {
|
|
if url.pathComponents.count == 1 || (url.pathComponents.count > 1 && url.pathComponents[1] == "Volumes") {
|
|
if let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, url as CFURL) {
|
|
if let diskName = DADiskGetBSDName(disk) {
|
|
let BSDName: String = String(cString: diskName)
|
|
active.append(BSDName)
|
|
|
|
if let d = self.list.first(where: { $0.BSDName == BSDName}), let idx = self.list.index(where: { $0.BSDName == BSDName}) {
|
|
if d.removable && !removableState {
|
|
self.list.remove(at: idx)
|
|
continue
|
|
}
|
|
|
|
if let path = d.path {
|
|
self.list.updateFreeSize(idx, newValue: self.freeDiskSpaceInBytes(path))
|
|
self.list.updateSMARTData(idx, smart: self.getSMARTDetails(for: BSDName))
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if var d = driveDetails(disk, removableState: removableState) {
|
|
if let path = d.path {
|
|
d.free = self.freeDiskSpaceInBytes(path)
|
|
d.size = self.totalDiskSpaceInBytes(path)
|
|
}
|
|
d.smart = self.getSMARTDetails(for: BSDName)
|
|
self.list.append(d)
|
|
self.list.sort()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
active.difference(from: self.list.map{ $0.BSDName }).forEach { (BSDName: String) in
|
|
if let idx = self.list.index(where: { $0.BSDName == BSDName }) {
|
|
self.list.remove(at: idx)
|
|
}
|
|
}
|
|
|
|
self.callback(self.list)
|
|
}
|
|
|
|
private func freeDiskSpaceInBytes(_ path: URL) -> Int64 {
|
|
do {
|
|
if let url = URL(string: path.absoluteString) {
|
|
let values = try url.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
|
|
if let capacity = values.volumeAvailableCapacityForImportantUsage, capacity != 0 {
|
|
return capacity
|
|
}
|
|
}
|
|
} catch let err {
|
|
error("error retrieving free space #1: \(err.localizedDescription)", log: self.log)
|
|
}
|
|
|
|
do {
|
|
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: path.path)
|
|
if let freeSpace = (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value {
|
|
return freeSpace
|
|
}
|
|
} catch let err {
|
|
error("error retrieving free space: \(err.localizedDescription)", log: self.log)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
private func totalDiskSpaceInBytes(_ path: URL) -> Int64 {
|
|
do {
|
|
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: path.path)
|
|
if let totalSpace = (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value {
|
|
return totalSpace
|
|
}
|
|
} catch let err {
|
|
error("error retrieving total space: \(err.localizedDescription)", log: self.log)
|
|
}
|
|
|
|
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<UnsafeMutablePointer<IOCFPlugInInterface>?>?
|
|
var smartInterface: UnsafeMutablePointer<UnsafeMutablePointer<IONVMeSMARTInterface>?>?
|
|
var score: Int32 = 0
|
|
|
|
var result = IOCreatePlugInInterfaceForService(disk, kIONVMeSMARTUserClientTypeID, kIOCFPlugInInterfaceID, &pluginInterface, &score)
|
|
guard result == kIOReturnSuccess else { return nil }
|
|
defer {
|
|
if pluginInterface != nil {
|
|
IODestroyPlugInInterface(pluginInterface)
|
|
}
|
|
}
|
|
|
|
result = withUnsafeMutablePointer(to: &smartInterface) {
|
|
$0.withMemoryRebound(to: Optional<LPVOID>.self, capacity: 1) {
|
|
pluginInterface?.pointee?.pointee.QueryInterface(pluginInterface, CFUUIDGetUUIDBytes(kIONVMeSMARTInterfaceID), $0) ?? KERN_NOT_FOUND
|
|
}
|
|
}
|
|
defer {
|
|
if smartInterface != nil {
|
|
_ = 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<Disks> {
|
|
internal var list: Disks = Disks()
|
|
|
|
init() {
|
|
super.init(.disk)
|
|
}
|
|
|
|
override func setup() {
|
|
self.setInterval(1)
|
|
}
|
|
|
|
public override func read() {
|
|
let keys: [URLResourceKey] = [.volumeNameKey]
|
|
let removableState = Store.shared.bool(key: "Disk_removable", defaultValue: false)
|
|
let paths = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys)!
|
|
|
|
guard let session = DASessionCreate(kCFAllocatorDefault) else {
|
|
error("cannot create a DASessionCreate()", log: self.log)
|
|
return
|
|
}
|
|
|
|
var active: [String] = []
|
|
for url in paths {
|
|
if url.pathComponents.count == 1 || (url.pathComponents.count > 1 && url.pathComponents[1] == "Volumes") {
|
|
if let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, url as CFURL) {
|
|
if let diskName = DADiskGetBSDName(disk) {
|
|
let BSDName: String = String(cString: diskName)
|
|
active.append(BSDName)
|
|
|
|
if let d = self.list.first(where: { $0.BSDName == BSDName}), let idx = self.list.index(where: { $0.BSDName == BSDName}) {
|
|
if d.removable && !removableState {
|
|
self.list.remove(at: idx)
|
|
continue
|
|
}
|
|
|
|
self.driveStats(idx, d)
|
|
continue
|
|
}
|
|
|
|
if let d = driveDetails(disk, removableState: removableState) {
|
|
self.list.append(d)
|
|
self.list.sort()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
active.difference(from: self.list.map{ $0.BSDName }).forEach { (BSDName: String) in
|
|
if let idx = self.list.index(where: { $0.BSDName == BSDName }) {
|
|
self.list.remove(at: idx)
|
|
}
|
|
}
|
|
|
|
self.callback(self.list)
|
|
}
|
|
|
|
private func driveStats(_ idx: Int, _ d: drive) {
|
|
guard let props = getIOProperties(d.parent) else {
|
|
return
|
|
}
|
|
|
|
if let statistics = props.object(forKey: "Statistics") as? NSDictionary {
|
|
let readBytes = statistics.object(forKey: "Bytes (Read)") as? Int64 ?? 0
|
|
let writeBytes = statistics.object(forKey: "Bytes (Write)") as? Int64 ?? 0
|
|
|
|
if d.activity.readBytes != 0 {
|
|
self.list.updateRead(idx, newValue: readBytes - d.activity.readBytes)
|
|
}
|
|
if d.activity.writeBytes != 0 {
|
|
self.list.updateWrite(idx, newValue: writeBytes - d.activity.writeBytes)
|
|
}
|
|
|
|
self.list.updateReadWrite(idx, read: readBytes, write: writeBytes)
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
private func driveDetails(_ disk: DADisk, removableState: Bool) -> drive? {
|
|
var d: drive = drive()
|
|
|
|
if let bsdName = DADiskGetBSDName(disk) {
|
|
d.BSDName = String(cString: bsdName)
|
|
}
|
|
|
|
if let diskDescription = DADiskCopyDescription(disk) {
|
|
if let dict = diskDescription as? [String: AnyObject] {
|
|
if let removable = dict[kDADiskDescriptionMediaRemovableKey as String] {
|
|
if removable as! Bool {
|
|
if !removableState {
|
|
return nil
|
|
}
|
|
d.removable = true
|
|
}
|
|
}
|
|
|
|
if let mediaName = dict[kDADiskDescriptionVolumeNameKey as String] {
|
|
d.mediaName = mediaName as! String
|
|
if d.mediaName == "Recovery" {
|
|
return nil
|
|
}
|
|
}
|
|
if d.mediaName == "" {
|
|
if let mediaName = dict[kDADiskDescriptionMediaNameKey as String] {
|
|
d.mediaName = mediaName as! String
|
|
if d.mediaName == "Recovery" {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
if let deviceModel = dict[kDADiskDescriptionDeviceModelKey as String] {
|
|
d.model = (deviceModel as! String).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
if let deviceProtocol = dict[kDADiskDescriptionDeviceProtocolKey as String] {
|
|
d.connectionType = deviceProtocol as! String
|
|
}
|
|
if let volumePath = dict[kDADiskDescriptionVolumePathKey as String] {
|
|
if let url = volumePath as? NSURL {
|
|
d.path = url as URL
|
|
|
|
if let components = url.pathComponents {
|
|
d.root = components.count == 1
|
|
|
|
if components.count > 1 && components[1] == "Volumes" {
|
|
if let name: String = url.lastPathComponent, name != "" {
|
|
d.mediaName = name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let volumeKind = dict[kDADiskDescriptionVolumeKindKey as String] {
|
|
d.fileSystem = volumeKind as! String
|
|
}
|
|
}
|
|
}
|
|
|
|
if d.path == nil {
|
|
return nil
|
|
}
|
|
|
|
let partitionLevel = d.BSDName.filter { "0"..."9" ~= $0 }.count
|
|
if let parent = getDeviceIOParent(DADiskCopyIOMedia(disk), level: Int(partitionLevel)) {
|
|
d.parent = parent
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
// https://opensource.apple.com/source/bless/bless-152/libbless/APFS/BLAPFSUtilities.c.auto.html
|
|
public func getDeviceIOParent(_ obj: io_registry_entry_t, level: Int) -> io_registry_entry_t? {
|
|
var parent: io_registry_entry_t = 0
|
|
|
|
if IORegistryEntryGetParentEntry(obj, kIOServicePlane, &parent) != KERN_SUCCESS {
|
|
return nil
|
|
}
|
|
|
|
for _ in 1...level where IORegistryEntryGetParentEntry(parent, kIOServicePlane, &parent) != KERN_SUCCESS {
|
|
IOObjectRelease(parent)
|
|
return nil
|
|
}
|
|
|
|
return parent
|
|
}
|
|
|
|
struct io {
|
|
var read: Int
|
|
var write: Int
|
|
}
|
|
|
|
public class ProcessReader: Reader<[Disk_process]> {
|
|
private let queue = DispatchQueue(label: "eu.exelban.Disk.processReader")
|
|
|
|
private var _list: [Int32: io] = [:]
|
|
private var list: [Int32: io] {
|
|
get {
|
|
self.queue.sync { self._list }
|
|
}
|
|
set {
|
|
self.queue.sync { self._list = newValue }
|
|
}
|
|
}
|
|
|
|
private var numberOfProcesses: Int {
|
|
Store.shared.int(key: "\(Disk.name)_processes", defaultValue: 5)
|
|
}
|
|
|
|
public override func setup() {
|
|
self.popup = true
|
|
self.setInterval(1)
|
|
}
|
|
|
|
public override func read() {
|
|
guard self.numberOfProcesses != 0, let output = runProcess(path: "/bin/ps", args: ["-Aceo pid,args", "-r"]) else { return }
|
|
|
|
var processes: [Disk_process] = []
|
|
output.enumerateLines { (line, _) -> Void in
|
|
var str = line.trimmingCharacters(in: .whitespaces)
|
|
let pidString = str.findAndCrop(pattern: "^\\d+")
|
|
if let range = str.range(of: pidString) {
|
|
str = str.replacingCharacters(in: range, with: "")
|
|
}
|
|
let name = str.findAndCrop(pattern: "^[^ ]+")
|
|
guard let pid = Int32(pidString) else { return }
|
|
|
|
var usage = rusage_info_current()
|
|
let result = withUnsafeMutablePointer(to: &usage) {
|
|
$0.withMemoryRebound(to: (rusage_info_t?.self), capacity: 1) {
|
|
proc_pid_rusage(pid, RUSAGE_INFO_CURRENT, $0)
|
|
}
|
|
}
|
|
guard result != -1 else { return }
|
|
|
|
let bytesRead = Int(usage.ri_diskio_bytesread)
|
|
let bytesWritten = Int(usage.ri_diskio_byteswritten)
|
|
|
|
if self.list[pid] == nil {
|
|
self.list[pid] = io(read: bytesRead, write: bytesWritten)
|
|
}
|
|
|
|
if let v = self.list[pid] {
|
|
let read = bytesRead - v.read
|
|
let write = bytesWritten - v.write
|
|
if read != 0 || write != 0 {
|
|
processes.append(Disk_process(pid: pid, name: name, read: read, write: write))
|
|
}
|
|
}
|
|
|
|
self.list[pid]?.read = bytesRead
|
|
self.list[pid]?.write = bytesWritten
|
|
}
|
|
|
|
processes.sort {
|
|
let firstMax = max($0.read, $0.write)
|
|
let secondMax = max($1.read, $1.write)
|
|
let firstMin = min($0.read, $0.write)
|
|
let secondMin = min($1.read, $1.write)
|
|
|
|
if firstMax == secondMax && firstMin != secondMin { // max values are the same, min not. Sort by min values
|
|
return firstMin < secondMin
|
|
}
|
|
return firstMax < secondMax // max values are not the same, sort by max value
|
|
}
|
|
|
|
self.callback(processes.suffix(self.numberOfProcesses).reversed())
|
|
}
|
|
}
|
|
|
|
private func runProcess(path: String, args: [String] = []) -> String? {
|
|
let task = Process()
|
|
task.launchPath = path
|
|
task.arguments = args
|
|
|
|
let outputPipe = Pipe()
|
|
defer {
|
|
outputPipe.fileHandleForReading.closeFile()
|
|
}
|
|
task.standardOutput = outputPipe
|
|
|
|
do {
|
|
try task.run()
|
|
} catch {
|
|
return nil
|
|
}
|
|
|
|
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
|
return String(decoding: outputData, as: UTF8.self)
|
|
}
|