From 6a3b5afc8ab2b91cb9fa2bdea8bc470d8260d4d3 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Fri, 14 Aug 2020 00:04:51 +0200 Subject: [PATCH] - write Disks reader from scratch - add disk read/write state to popup --- Modules/Disk/main.swift | 81 ++++++++--------- Modules/Disk/popup.swift | 71 ++++++++++++--- Modules/Disk/readers.swift | 175 ++++++++++++++++++++---------------- Modules/Disk/settings.swift | 2 +- StatsKit/SystemKit.swift | 9 +- StatsKit/extensions.swift | 37 ++++++++ 6 files changed, 244 insertions(+), 131 deletions(-) diff --git a/Modules/Disk/main.swift b/Modules/Disk/main.swift index e75b8cbd..7c8688f4 100644 --- a/Modules/Disk/main.swift +++ b/Modules/Disk/main.swift @@ -13,24 +13,40 @@ import Cocoa import StatsKit import ModuleKit -struct diskInfo { - var name: String = "" +public struct stats { + var read: Int64 = 0 + var write: Int64 = 0 + + var readBytes: Int64 = 0 + var writeBytes: Int64 = 0 + var readOperations: Int64 = 0 + var writeOperations: Int64 = 0 + var readTime: Int64 = 0 + var writeTime: Int64 = 0 +} + +struct drive { + var parent: io_registry_entry_t = 0 + + var mediaName: String = "" + var BSDName: String = "" + + var root: Bool = false + var removable: Bool = false + var model: String = "" var path: URL? - var connection: String = "" + var connectionType: String = "" var fileSystem: String = "" - var totalSize: Int64 = 0 - var freeSize: Int64 = 0 + var size: Int64 = 0 + var free: Int64 = 0 - var mediaBSDName: String = "" - var root: Bool = false - - var removable: Bool = false + var stats: stats? = nil } struct DiskList: value_t { - var list: [diskInfo] = [] + var list: [drive] = [] public var widget_value: Double { get { @@ -38,40 +54,40 @@ struct DiskList: value_t { } } - func getDiskByBSDName(_ name: String) -> diskInfo? { - if let idx = self.list.firstIndex(where: { $0.mediaBSDName == name }) { + func getDiskByBSDName(_ name: String) -> drive? { + if let idx = self.list.firstIndex(where: { $0.BSDName == name }) { return self.list[idx] } return nil } - func getDiskByName(_ name: String) -> diskInfo? { - if let idx = self.list.firstIndex(where: { $0.name == name }) { + func getDiskByName(_ name: String) -> drive? { + if let idx = self.list.firstIndex(where: { $0.mediaName == name }) { return self.list[idx] } return nil } - func getRootDisk() -> diskInfo? { + func getRootDisk() -> drive? { if let idx = self.list.firstIndex(where: { $0.root }) { return self.list[idx] } return nil } -} - -public struct IO { - var read: Int = 0 - var write: Int = 0 + + mutating func removeDiskByBSDName(_ name: String) { + if let idx = self.list.firstIndex(where: { $0.BSDName == name }) { + self.list.remove(at: idx) + } + } } public class Disk: Module { private let popupView: Popup = Popup() private var capacityReader: CapacityReader? = nil - private var ioReader: IOReader? = nil private var settingsView: Settings private var selectedDisk: String = "" @@ -89,17 +105,12 @@ public class Disk: Module { self.capacityReader?.store = store self.selectedDisk = store.pointee.string(key: "\(self.config.name)_disk", defaultValue: self.selectedDisk) - self.ioReader = IOReader() - self.capacityReader?.readyCallback = { [unowned self] in self.readyHandler() } self.capacityReader?.callbackHandler = { [unowned self] value in self.capacityCallback(value: value) } - self.ioReader?.callbackHandler = { [unowned self] value in - self.ioCallback(value: value) - } self.settingsView.selectedDiskHandler = { [unowned self] value in self.selectedDisk = value @@ -115,9 +126,6 @@ public class Disk: Module { if let reader = self.capacityReader { self.addReader(reader) } - if let reader = self.ioReader { - self.addReader(reader) - } } private func capacityCallback(value: DiskList?) { @@ -127,7 +135,7 @@ public class Disk: Module { self.popupView.usageCallback(value!) self.settingsView.setList(value!) - var d: diskInfo? = value!.getDiskByName(self.selectedDisk) + var d = value!.getDiskByName(self.selectedDisk) if d == nil { d = value!.getRootDisk() } @@ -136,8 +144,8 @@ public class Disk: Module { return } - let total = d!.totalSize - let free = d!.freeSize + let total = d!.size + let free = d!.free let usedSpace = total - free let percentage = Double(usedSpace) / Double(total) @@ -150,15 +158,8 @@ public class Disk: Module { if let widget = self.widget as? MemoryWidget { widget.setValue((free, usedSpace)) } - } - - private func ioCallback(value: IO?) { - if value == nil { - return - } - if let widget = self.widget as? SpeedWidget { - widget.setValue(upload: Int64(value!.write), download: Int64(value!.read)) + widget.setValue(upload: d?.stats?.write ?? 0, download: d?.stats?.read ?? 0) } } } diff --git a/Modules/Disk/popup.swift b/Modules/Disk/popup.swift index a3b48b39..434c227a 100644 --- a/Modules/Disk/popup.swift +++ b/Modules/Disk/popup.swift @@ -33,20 +33,20 @@ internal class Popup: NSView { self.list = [:] } - value.list.reversed().forEach { (d: diskInfo) in - if self.list[d.name] == nil { + value.list.reversed().forEach { (d: drive) in + if self.list[d.mediaName] == nil { DispatchQueue.main.async(execute: { - self.list[d.name] = DiskView( + self.list[d.mediaName] = DiskView( NSRect(x: 0, y: (self.diskFullHeight + Constants.Popup.margins) * CGFloat(self.list.count), width: self.frame.width, height: self.diskFullHeight), - name: d.name, - size: d.totalSize, - free: d.freeSize, + name: d.mediaName, + size: d.size, + free: d.free, path: d.path ) - self.addSubview(self.list[d.name]!) + self.addSubview(self.list[d.mediaName]!) }) } else { - self.list[d.name]?.update(free: d.freeSize) + self.list[d.mediaName]?.update(free: d.free, read: d.stats?.read, write: d.stats?.write) } } @@ -73,6 +73,9 @@ internal class DiskView: NSView { private var percentageField: NSTextField? = nil private var usedBarSpace: NSView? = nil + private var readState: NSView? = nil + private var writeState: NSView? = nil + private var mainView: NSView public init(_ frame: NSRect, name: String, size: Int64, free: Int64, path: URL?) { @@ -106,13 +109,46 @@ internal class DiskView: NSView { private func addName() { let nameWidth = self.name.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 4 - let view: NSView = NSView(frame: NSRect(x: 0, y: self.mainView.frame.height - nameHeight, width: nameWidth, height: nameHeight)) + let view: NSView = NSView(frame: NSRect(x: 0, y: self.mainView.frame.height - nameHeight, width: self.mainView.frame.width, height: nameHeight)) let nameField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: nameWidth, height: view.frame.height)) nameField.stringValue = self.name + let activityView: NSView = NSView(frame: NSRect(x: view.frame.width-66, y: 0, width: 66, height: view.frame.height-2)) + + let readView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: activityView.frame.width/2, height: activityView.frame.height)) + + let readField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: nameWidth, height: readView.frame.height)) + readField.stringValue = "R" + readView.addSubview(readField) + + let readState: NSView = NSView(frame: NSRect(x: 15, y: 6, width: 9, height: 9)) + readState.wantsLayer = true + readState.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.75).cgColor + readState.layer?.cornerRadius = 2 + readView.addSubview(readState) + + let writeView: NSView = NSView(frame: NSRect(x: activityView.frame.width/2, y: 0, width: activityView.frame.width/2, height: activityView.frame.height)) + + let writeField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: nameWidth, height: readView.frame.height)) + writeField.stringValue = "W" + writeView.addSubview(writeField) + + let writeState: NSView = NSView(frame: NSRect(x: 17, y: 6, width: 9, height: 9)) + writeState.wantsLayer = true + writeState.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.75).cgColor + writeState.layer?.cornerRadius = 2 + writeView.addSubview(writeState) + + activityView.addSubview(readView) + activityView.addSubview(writeView) + view.addSubview(nameField) + view.addSubview(activityView) self.mainView.addSubview(view) + + self.readState = readState + self.writeState = writeState } private func addLegend(free: Int64) { @@ -150,7 +186,15 @@ internal class DiskView: NSView { self.mainView.addSubview(view) } - public func update(free: Int64) { + private func setReadState(_ state: Bool) { + self.readState?.layer?.backgroundColor = state ? NSColor.systemBlue.cgColor : NSColor.lightGray.withAlphaComponent(0.75).cgColor + } + + private func setWriteState(_ state: Bool) { + self.writeState?.layer?.backgroundColor = state ? NSColor.systemRed.cgColor : NSColor.lightGray.withAlphaComponent(0.75).cgColor + } + + public func update(free: Int64, read: Int64?, write: Int64?) { DispatchQueue.main.async(execute: { if self.legendField != nil { self.legendField?.stringValue = "Used \(Units(bytes: (self.size - free)).getReadableMemory()) from \(Units(bytes: self.size).getReadableMemory())" @@ -162,6 +206,13 @@ internal class DiskView: NSView { let width: CGFloat = ((self.mainView.frame.width - 2) * percentage) / 1 self.usedBarSpace?.setFrameSize(NSSize(width: width, height: self.usedBarSpace!.frame.height)) } + + if read != nil { + self.setReadState(read != 0) + } + if write != nil { + self.setWriteState(write != 0) + } }) } diff --git a/Modules/Disk/readers.swift b/Modules/Disk/readers.swift index f884dd69..b3a3ce62 100644 --- a/Modules/Disk/readers.swift +++ b/Modules/Disk/readers.swift @@ -14,6 +14,7 @@ import ModuleKit import StatsKit import IOKit import Darwin +import os.log internal class CapacityReader: Reader { private var disks: DiskList = DiskList() @@ -24,45 +25,80 @@ internal class CapacityReader: Reader { let removableState = store?.pointee.bool(key: "Disk_removable", defaultValue: false) ?? false let paths = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys)! - if let session = DASessionCreate(kCFAllocatorDefault) { - 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) - - if let d: diskInfo = self.disks.getDiskByBSDName(BSDName) { - if let idx = self.disks.list.firstIndex(where: { $0.mediaBSDName == BSDName }) { - if d.removable && !removableState { - self.disks.list.remove(at: idx) - continue - } - - if let path = self.disks.list[idx].path { - self.disks.list[idx].freeSize = freeDiskSpaceInBytes(path.absoluteString) - } + guard let session = DASessionCreate(kCFAllocatorDefault) else { + os_log(.error, log: log, "cannot create a DASessionCreate()") + 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: drive = self.disks.getDiskByBSDName(BSDName) { + if let idx = self.disks.list.firstIndex(where: { $0.BSDName == BSDName }) { + if d.removable && !removableState { + self.disks.list.remove(at: idx) + continue + } + + if let path = self.disks.list[idx].path { + self.disks.list[idx].free = self.freeDiskSpaceInBytes(path.absoluteString) + self.driveStats(self.disks.list[idx].parent, &self.disks.list[idx].stats) } - continue - } - - if let d = getDisk(disk, removableState: removableState) { - self.disks.list.append(d) - self.disks.list.sort{ $1.removable } } + continue + } + + if let d = driveDetails(disk, removableState: removableState) { + self.disks.list.append(d) + self.disks.list.sort{ $1.removable } } } } } } + if active.count < self.disks.list.count { + let missingDisks = active.difference(from: self.disks.list.map{ $0.BSDName }) + + missingDisks.forEach { (BSDName: String) in + self.disks.removeDiskByBSDName(BSDName) + } + } + self.callback(self.disks) } - private func getDisk(_ disk: DADisk, removableState: Bool) -> diskInfo? { - var d: diskInfo = diskInfo() + // https://opensource.apple.com/source/bless/bless-152/libbless/APFS/BLAPFSUtilities.c.auto.html + public func getDeviceIOParent(_ obj: io_registry_entry_t, fileSystem: String = "") -> io_registry_entry_t? { + var parent: io_registry_entry_t = 0 + + if IORegistryEntryGetParentEntry(obj, kIOServicePlane, &parent) != KERN_SUCCESS { + return nil + } + + if IORegistryEntryGetParentEntry(parent, kIOServicePlane, &parent) != KERN_SUCCESS { + IOObjectRelease(parent) + return nil + } + + if IORegistryEntryGetParentEntry(parent, kIOServicePlane, &parent) != KERN_SUCCESS { + IOObjectRelease(parent) + return nil + } + + return parent + } + + private func driveDetails(_ disk: DADisk, removableState: Bool) -> drive? { + var d: drive = drive() if let bsdName = DADiskGetBSDName(disk) { - d.mediaBSDName = String(cString: bsdName) + d.BSDName = String(cString: bsdName) } if let diskDescription = DADiskCopyDescription(disk) { @@ -77,16 +113,16 @@ internal class CapacityReader: Reader { } if let mediaName = dict[kDADiskDescriptionMediaNameKey as String] { - d.name = mediaName as! String + d.mediaName = mediaName as! String } if let mediaSize = dict[kDADiskDescriptionMediaSizeKey as String] { - d.totalSize = Int64(truncating: mediaSize as! NSNumber) + d.size = Int64(truncating: mediaSize as! NSNumber) } if let deviceModel = dict[kDADiskDescriptionDeviceModelKey as String] { d.model = (deviceModel as! String).trimmingCharacters(in: .whitespacesAndNewlines) } if let deviceProtocol = dict[kDADiskDescriptionDeviceProtocolKey as String] { - d.connection = deviceProtocol as! String + d.connectionType = deviceProtocol as! String } if let volumePath = dict[kDADiskDescriptionVolumePathKey as String] { let url = volumePath as? NSURL @@ -94,7 +130,7 @@ internal class CapacityReader: Reader { if url!.pathComponents!.count > 1 && url!.pathComponents![1] == "Volumes" { let lastPath: String = (url?.lastPathComponent)! if lastPath != "" { - d.name = lastPath + d.mediaName = lastPath d.path = URL(string: "/Volumes/\(lastPath)") } } else if url!.pathComponents!.count == 1 { @@ -110,10 +146,42 @@ internal class CapacityReader: Reader { } if d.path != nil { - d.freeSize = freeDiskSpaceInBytes(d.path!.absoluteString) + d.free = freeDiskSpaceInBytes(d.path!.absoluteString) } - return d.name == "Recovery" ? nil : d + if let parent = self.getDeviceIOParent(DADiskCopyIOMedia(disk), fileSystem: d.fileSystem) { + d.parent = parent + self.driveStats(parent, &d.stats) + } + + return d + } + + private func driveStats(_ entry: io_registry_entry_t, _ diskStats: UnsafeMutablePointer) { + guard let props = getIOProperties(entry) else { + return + } + + if let statistics = props.object(forKey: "Statistics") as? NSDictionary { + if diskStats.pointee == nil { + diskStats.initialize(to: stats()) + } + + let readBytes = statistics.object(forKey: "Bytes (Read)") as? Int64 ?? 0 + let writeBytes = statistics.object(forKey: "Bytes (Write)") as? Int64 ?? 0 + + diskStats.pointee?.read = readBytes - (diskStats.pointee?.readBytes ?? 0) + diskStats.pointee?.write = writeBytes - (diskStats.pointee?.writeBytes ?? 0) + + diskStats.pointee?.readBytes = readBytes + diskStats.pointee?.writeBytes = writeBytes + diskStats.pointee?.readOperations = statistics.object(forKey: "Operations (Read)") as? Int64 ?? 0 + diskStats.pointee?.writeOperations = statistics.object(forKey: "Operations (Read)") as? Int64 ?? 0 + diskStats.pointee?.readTime = statistics.object(forKey: "Total Time (Read)") as? Int64 ?? 0 + diskStats.pointee?.writeTime = statistics.object(forKey: "Total Time (Read)") as? Int64 ?? 0 + } + + return } private func freeDiskSpaceInBytes(_ path: String) -> Int64 { @@ -126,46 +194,3 @@ internal class CapacityReader: Reader { } } } - -// https://gist.github.com/kainjow/0e7650cc797a52261e0f4ba851477c2f -internal class IOReader: Reader { - public var stats: IO = IO() - - public override func read() { - let initialNumPids = proc_listallpids(nil, 0) - let buffer = UnsafeMutablePointer.allocate(capacity: Int(initialNumPids)) - defer { - buffer.deallocate() - } - - let bufferLength = initialNumPids * Int32(MemoryLayout.size) - let numPids = proc_listallpids(buffer, bufferLength) - - var read: Int = 0 - var write: Int = 0 - for i in 0..? @@ -249,7 +248,7 @@ public class SystemKit { if result == KERN_SUCCESS { totalSize = Double(vmStats.max_mem) } else { - os_log(.error, log: self.log, "host_basic_info(): %v", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")) + os_log(.error, log: self.log, "host_basic_info(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")) return nil } @@ -283,7 +282,7 @@ public class SystemKit { ) } - os_log(.error, log: self.log, "host_statistics64(): %v", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")) + os_log(.error, log: self.log, "host_statistics64(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")) return nil } diff --git a/StatsKit/extensions.swift b/StatsKit/extensions.swift index f381821d..062d8ac4 100644 --- a/StatsKit/extensions.swift +++ b/StatsKit/extensions.swift @@ -648,6 +648,14 @@ public extension Array where Element : Equatable { } } +public extension Array where Element : Hashable { + func difference(from other: [Element]) -> [Element] { + let thisSet = Set(self) + let otherSet = Set(other) + return Array(thisSet.symmetricDifference(otherSet)) + } +} + public func FindAndToggleNSControlState(_ view: NSView?, state: NSControl.StateValue) { if let control = view?.subviews.first(where: { $0 is NSControl }) { ToggleNSControlState(control as? NSControl, state: state) @@ -852,3 +860,32 @@ public struct TopProcess { self.icon = icon } } + +public func getIOParent(_ obj: io_registry_entry_t) -> io_registry_entry_t? { + var parent: io_registry_entry_t = 0 + + if IORegistryEntryGetParentEntry(obj, kIOServicePlane, &parent) != KERN_SUCCESS { + return nil + } + + if (IOObjectConformsTo(parent, "IOBlockStorageDriver") == 0) { + IOObjectRelease(parent) + return nil + } + + return parent +} + +public func getIOProperties(_ entry: io_registry_entry_t) -> NSDictionary? { + var properties: Unmanaged? = nil + + if IORegistryEntryCreateCFProperties(entry, &properties, kCFAllocatorDefault, 0) != kIOReturnSuccess { + return nil + } + + defer { + properties?.release() + } + + return properties?.takeUnretainedValue() +}