- write Disks reader from scratch

- add disk read/write state to popup
This commit is contained in:
Serhiy Mytrovtsiy
2020-08-14 00:04:51 +02:00
parent 9e65322659
commit 6a3b5afc8a
6 changed files with 244 additions and 131 deletions

View File

@@ -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)
}
}
}

View File

@@ -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)
}
})
}

View File

@@ -14,6 +14,7 @@ import ModuleKit
import StatsKit
import IOKit
import Darwin
import os.log
internal class CapacityReader: Reader<DiskList> {
private var disks: DiskList = DiskList()
@@ -24,45 +25,80 @@ internal class CapacityReader: Reader<DiskList> {
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<DiskList> {
}
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<DiskList> {
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<DiskList> {
}
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<stats?>) {
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<DiskList> {
}
}
}
// https://gist.github.com/kainjow/0e7650cc797a52261e0f4ba851477c2f
internal class IOReader: Reader<IO> {
public var stats: IO = IO()
public override func read() {
let initialNumPids = proc_listallpids(nil, 0)
let buffer = UnsafeMutablePointer<pid_t>.allocate(capacity: Int(initialNumPids))
defer {
buffer.deallocate()
}
let bufferLength = initialNumPids * Int32(MemoryLayout<pid_t>.size)
let numPids = proc_listallpids(buffer, bufferLength)
var read: Int = 0
var write: Int = 0
for i in 0..<numPids {
let pid = buffer[Int(i)]
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)
}
}
if result == kIOReturnSuccess {
read += Int(usage.ri_diskio_bytesread)
write += Int(usage.ri_diskio_byteswritten)
}
}
if self.stats.read != 0 && self.stats.write != 0 {
self.stats.read = read - self.stats.read
self.stats.write = write - self.stats.write
}
self.callback(self.stats)
self.stats.read = read
self.stats.write = write
}
}

View File

@@ -95,7 +95,7 @@ internal class Settings: NSView, Settings_v {
}
internal func setList(_ list: DiskList) {
let disks = list.list.map{ $0.name }
let disks = list.list.map{ $0.mediaName }
DispatchQueue.main.async(execute: {
if self.button?.itemTitles.count != disks.count {
self.button?.removeAllItems()

View File

@@ -114,7 +114,7 @@ public class SystemKit {
return String(cString: UnsafeRawPointer(pointer).assumingMemoryBound(to: CChar.self))
}
os_log(.error, log: self.log, "error call sysctl(): %v", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
os_log(.error, log: self.log, "error call sysctl(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
@@ -146,7 +146,7 @@ public class SystemKit {
return cpu_s(physicalCores: Int8(data.physical_cpu), logicalCores: Int8(data.logical_cpu), name: name)
}
os_log(.error, log: self.log, "hostInfo.withMemoryRebound(): %v", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
os_log(.error, log: self.log, "hostInfo.withMemoryRebound(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
@@ -157,7 +157,6 @@ public class SystemKit {
let result = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("IOPCIDevice"), &iterator)
if result == kIOReturnSuccess {
while device != 0 {
device = IOIteratorNext(iterator)
var serviceDictionary: Unmanaged<CFMutableDictionary>?
@@ -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
}

View File

@@ -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<CFMutableDictionary>? = nil
if IORegistryEntryCreateCFProperties(entry, &properties, kCFAllocatorDefault, 0) != kIOReturnSuccess {
return nil
}
defer {
properties?.release()
}
return properties?.takeUnretainedValue()
}