feat: added top processes to the Disk module (#1370)

This commit is contained in:
Serhiy Mytrovtsiy
2023-04-06 18:17:25 +02:00
parent ac084bca28
commit a1758c3233
5 changed files with 425 additions and 49 deletions

View File

@@ -128,28 +128,62 @@ public class Disks {
}
}
public struct Disk_process: IOProcess_p {
private var base: DataSizeBase {
DataSizeBase(rawValue: Store.shared.string(key: "\(Disk.name)_base", defaultValue: "byte")) ?? .byte
}
public var pid: Int32
public var name: String
public var icon: NSImage = Constants.defaultProcessIcon
var read: Int
var write: Int
public var input: String {
Units(bytes: Int64(self.read)).getReadableSpeed(base: self.base)
}
public var output: String {
Units(bytes: Int64(self.write)).getReadableSpeed(base: self.base)
}
init(pid: Int32, name: String, read: Int, write: Int) {
self.pid = pid
self.name = name
self.read = read
self.write = write
if let app = NSRunningApplication(processIdentifier: pid) {
if let name = app.localizedName {
self.name = name
}
if let icon = app.icon {
self.icon = icon
}
}
}
}
public class Disk: Module {
private let popupView: Popup
private let settingsView: Settings
private let portalView: Portal
public static let name: String = "Disk"
private let popupView: Popup = Popup()
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 selectedDisk: String = ""
private var notificationLevelState: Bool = false
private var notificationID: String? = nil
private var notificationLevel: String {
get {
return Store.shared.string(key: "\(self.config.name)_notificationLevel", defaultValue: "Disabled")
}
Store.shared.string(key: "\(Disk.name)_notificationLevel", defaultValue: "Disabled")
}
public init() {
self.popupView = Popup()
self.settingsView = Settings("Disk")
self.portalView = Portal("Disk")
super.init(
popup: self.popupView,
settings: self.settingsView,
@@ -159,7 +193,9 @@ public class Disk: Module {
self.capacityReader = CapacityReader()
self.activityReader = ActivityReader()
self.selectedDisk = Store.shared.string(key: "\(self.config.name)_disk", defaultValue: self.selectedDisk)
self.processReader = ProcessReader()
self.selectedDisk = Store.shared.string(key: "\(Disk.name)_disk", defaultValue: self.selectedDisk)
self.capacityReader?.callbackHandler = { [unowned self] value in
if let value = value {
@@ -175,6 +211,11 @@ public class Disk: Module {
self.activityCallback(value)
}
}
self.processReader?.callbackHandler = { [unowned self] value in
if let list = value {
self.popupView.processCallback(list)
}
}
self.settingsView.selectedDiskHandler = { [unowned self] value in
self.selectedDisk = value
@@ -186,6 +227,12 @@ public class Disk: Module {
self.settingsView.setInterval = { [unowned self] value in
self.capacityReader?.setInterval(value)
}
self.settingsView.callbackWhenUpdateNumberOfProcesses = {
self.popupView.numberOfProcessesUpdated()
DispatchQueue.global(qos: .background).async {
self.processReader?.read()
}
}
if let reader = self.capacityReader {
self.addReader(reader)
@@ -193,6 +240,9 @@ public class Disk: Module {
if let reader = self.activityReader {
self.addReader(reader)
}
if let reader = self.processReader {
self.addReader(reader)
}
}
public override func widgetDidSet(_ type: widget_t) {

View File

@@ -13,8 +13,6 @@ import Cocoa
import Kit
internal class Popup: PopupWrapper {
private let emptyView: EmptyView = EmptyView(height: 30, isHidden: false, msg: localizedString("No disks are available"))
private var readColorState: Color = .secondBlue
private var readColor: NSColor {
var value = NSColor.systemRed
@@ -32,46 +30,67 @@ internal class Popup: PopupWrapper {
return value
}
private var disks: NSStackView = {
let view = NSStackView()
view.spacing = Constants.Popup.margins
view.orientation = .vertical
return view
}()
private var processes: IOProcessView = IOProcessView(
countKey: "\(Disk.name)_processes",
inputColorKey: "\(Disk.name)_readColor",
outputColorKey: "\(Disk.name)_writeColor"
)
public init() {
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 30))
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.readColorState = Color.fromString(Store.shared.string(key: "\(Disk.name)_readColor", defaultValue: self.readColorState.key))
self.writeColorState = Color.fromString(Store.shared.string(key: "\(Disk.name)_writeColor", defaultValue: self.writeColorState.key))
self.orientation = .vertical
self.spacing = Constants.Popup.margins
self.distribution = .fill
self.spacing = 0
self.addArrangedSubview(self.disks)
self.addArrangedSubview(separatorView(localizedString("Top processes"), width: self.frame.width))
self.addArrangedSubview(self.processes)
self.recalculateHeight()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func recalculateHeight() {
let h = self.subviews.map({ $0.bounds.height }).reduce(0, +)
if self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
internal func capacityCallback(_ value: Disks) {
defer {
if value.isEmpty && self.emptyView.superview == nil {
self.addArrangedSubview(self.emptyView)
} else if !value.isEmpty && self.emptyView.superview != nil {
self.emptyView.removeFromSuperview()
}
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
if h > 0 && self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
let h = self.disks.subviews.map({ $0.bounds.height + self.disks.spacing }).reduce(0, +) - self.disks.spacing
if h > 0 && self.disks.frame.size.height != h {
self.disks.setFrameSize(NSSize(width: self.frame.width, height: h))
self.recalculateHeight()
}
}
self.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }.forEach { (v: DiskView) in
self.disks.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }.forEach { (v: DiskView) in
if !value.map({$0.BSDName}).contains(v.BSDName) {
v.removeFromSuperview()
}
}
value.forEach { (drive: drive) in
if let view = self.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }).first(where: { $0.BSDName == drive.BSDName }) {
if let view = self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }).first(where: { $0.BSDName == drive.BSDName }) {
view.updateFree(free: drive.free)
} else {
self.addArrangedSubview(DiskView(
self.disks.addArrangedSubview(DiskView(
width: self.frame.width,
BSDName: drive.BSDName,
name: drive.mediaName,
@@ -84,7 +103,7 @@ internal class Popup: PopupWrapper {
}
internal func activityCallback(_ value: Disks) {
let views = self.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }
let views = self.disks.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }
value.reversed().forEach { (drive: drive) in
if let view = views.first(where: { $0.name == drive.mediaName }) {
view.updateReadWrite(read: drive.activity.read, write: drive.activity.write)
@@ -92,6 +111,15 @@ internal class Popup: PopupWrapper {
}
}
internal func processCallback(_ list: [Disk_process]) {
self.processes.update(list)
}
internal func numberOfProcessesUpdated() {
self.processes.reinit()
self.recalculateHeight()
}
// MARK: - Settings
public override func settings() -> NSView? {
@@ -122,10 +150,11 @@ internal class Popup: PopupWrapper {
self.writeColorState = newValue
Store.shared.set(key: "\(Disk.name)_writeColor", value: key)
if let color = newValue.additional as? NSColor {
for view in self.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
for view in self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
view.setChartColor(write: color)
}
}
self.processes.updateColors()
}
@objc private func toggleReadColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
@@ -135,10 +164,11 @@ internal class Popup: PopupWrapper {
self.readColorState = newValue
Store.shared.set(key: "\(Disk.name)_readColor", value: key)
if let color = newValue.additional as? NSColor {
for view in self.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
for view in self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
view.setChartColor(read: color)
}
}
self.processes.updateColors()
}
}
@@ -246,7 +276,7 @@ internal class NameView: NSStackView {
let readView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 32, height: activity.frame.height))
let readField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: nameField.frame.width, height: readView.frame.height))
readField.stringValue = "R"
let readState: NSView = NSView(frame: NSRect(x: 13, y: (readView.frame.height-10)/2, width: 9, height: 9))
let readState: NSView = NSView(frame: NSRect(x: 13, y: (readView.frame.height-9)/2, width: 10, height: 10))
readState.wantsLayer = true
readState.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.75).cgColor
readState.layer?.cornerRadius = 2
@@ -254,7 +284,7 @@ internal class NameView: NSStackView {
let writeView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 32, height: activity.frame.height))
let writeField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: nameField.frame.width, height: readView.frame.height))
writeField.stringValue = "W"
let writeState: NSView = NSView(frame: NSRect(x: 17, y: (writeView.frame.height-10)/2, width: 9, height: 9))
let writeState: NSView = NSView(frame: NSRect(x: 17, y: (writeView.frame.height-10)/2, width: 10, height: 10))
writeState.wantsLayer = true
writeState.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.75).cgColor
writeState.layer?.cornerRadius = 2
@@ -551,3 +581,184 @@ internal class LegendView: NSView {
}
}
}
public protocol IOProcess_p {
var pid: Int32 { get }
var name: String { get }
var icon: NSImage { get }
var input: String { get }
var output: String { get }
}
public class IOProcessView: NSStackView {
private let countKey: String
private let inputColorKey: String
private let outputColorKey: String
private var initialized: Bool = false
private var count: Int {
Store.shared.int(key: countKey, defaultValue: 5)
}
private var readColor: NSColor {
Color.fromString(Store.shared.string(key: inputColorKey, defaultValue: Color.secondBlue.key)).additional as! NSColor
}
private var writeColor: NSColor {
Color.fromString(Store.shared.string(key: outputColorKey, defaultValue: Color.secondRed.key)).additional as! NSColor
}
private var inputBoxView: NSView?
private var outputBoxView: NSView?
public var height: CGFloat {
CGFloat((self.count+1) * 22)
}
init(countKey: String, inputColorKey: String, outputColorKey: String) {
self.countKey = countKey
self.inputColorKey = inputColorKey
self.outputColorKey = outputColorKey
super.init(frame: NSRect.zero)
self.orientation = .vertical
self.spacing = 1
self.reinit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func reinit() {
self.subviews.forEach({ $0.removeFromSuperview() })
self.addArrangedSubview(self.legendRow())
for _ in 0..<count {
self.addArrangedSubview(TopProcess())
}
self.setFrameSize(NSSize(width: self.frame.width, height: self.height))
}
private func legendRow() -> NSView {
let view: NSStackView = NSStackView()
view.spacing = 50
view.orientation = .horizontal
view.heightAnchor.constraint(equalToConstant: 21).isActive = true
let inputView: NSView = NSView()
inputView.widthAnchor.constraint(equalToConstant: 10).isActive = true
inputView.heightAnchor.constraint(equalToConstant: 10).isActive = true
inputView.wantsLayer = true
inputView.layer?.backgroundColor = self.readColor.cgColor
inputView.layer?.cornerRadius = 2
self.inputBoxView = inputView
let outputView: NSView = NSView()
outputView.widthAnchor.constraint(equalToConstant: 10).isActive = true
outputView.heightAnchor.constraint(equalToConstant: 10).isActive = true
outputView.wantsLayer = true
outputView.layer?.backgroundColor = self.writeColor.cgColor
outputView.layer?.cornerRadius = 2
self.outputBoxView = outputView
view.addArrangedSubview(NSView())
view.addArrangedSubview(inputView)
view.addArrangedSubview(outputView)
return view
}
public func update(_ list: [IOProcess_p]) {
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.initialized {
return
}
for (i, p) in self.subviews.compactMap({ $0 as? TopProcess }).enumerated() {
if list.count != self.count && self.initialized {
p.clear()
}
if list.indices.contains(i) {
p.set(list[i])
}
}
self.initialized = true
})
}
public func updateColors() {
self.inputBoxView?.layer?.backgroundColor = self.readColor.cgColor
self.outputBoxView?.layer?.backgroundColor = self.writeColor.cgColor
}
}
public class TopProcess: NSStackView {
private var imageView: NSImageView = NSImageView()
private var labelView: NSTextField = LabelField()
private var inputView: NSTextField = ValueField()
private var outputView: NSTextField = ValueField()
init() {
super.init(frame: NSRect.zero)
self.orientation = .horizontal
self.spacing = 0
self.alignment = .centerY
self.layer?.cornerRadius = 3
self.labelView.cell?.truncatesLastVisibleLine = true
self.inputView.font = NSFont.systemFont(ofSize: 10, weight: .regular)
self.outputView.font = NSFont.systemFont(ofSize: 10, weight: .regular)
self.addArrangedSubview(self.imageView)
self.addArrangedSubview(self.labelView)
self.addArrangedSubview(NSView())
self.addArrangedSubview(self.inputView)
self.addArrangedSubview(self.outputView)
self.addTrackingArea(NSTrackingArea(
rect: NSRect(x: 0, y: 0, width: 264, height: 21),
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
owner: self,
userInfo: nil
))
NSLayoutConstraint.activate([
self.imageView.widthAnchor.constraint(equalToConstant: 12),
self.labelView.heightAnchor.constraint(equalToConstant: 16),
self.inputView.widthAnchor.constraint(equalToConstant: 60),
self.outputView.widthAnchor.constraint(equalToConstant: 60),
self.heightAnchor.constraint(equalToConstant: 21)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func mouseEntered(with: NSEvent) {
self.layer?.backgroundColor = .init(gray: 0.01, alpha: 0.05)
}
public override func mouseExited(with: NSEvent) {
self.layer?.backgroundColor = .none
}
public func set(_ process: IOProcess_p) {
self.imageView.image = process.icon
self.labelView.stringValue = process.name
self.inputView.stringValue = process.input
self.outputView.stringValue = process.output
self.toolTip = "pid: \(process.pid)"
}
public func clear() {
self.inputView.stringValue = "-"
self.outputView.stringValue = "-"
self.toolTip = ""
}
}

View File

@@ -13,15 +13,13 @@ import Cocoa
import Kit
internal class Portal: NSStackView, Portal_p {
internal var name: String
internal var name: String { Disk.name }
private var circle: PieChartView? = nil
private var initialized: Bool = false
init(_ title: String) {
self.name = title
init() {
super.init(frame: NSRect.zero)
self.wantsLayer = true

View File

@@ -285,3 +285,104 @@ public func getDeviceIOParent(_ obj: io_registry_entry_t, level: Int) -> io_regi
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 read() {
guard self.numberOfProcesses != 0 else { return }
guard 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)
}

View File

@@ -16,24 +16,25 @@ internal class Settings: NSStackView, Settings_v {
private var removableState: Bool = false
private var updateIntervalValue: Int = 10
private var notificationLevel: String = "Disabled"
private var numberOfProcesses: Int = 5
public var selectedDiskHandler: (String) -> Void = {_ in }
public var callback: (() -> Void) = {}
public var setInterval: ((_ value: Int) -> Void) = {_ in }
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
private let title: String
private var selectedDisk: String
private var button: NSPopUpButton?
private var intervalSelectView: NSView? = nil
private var list: [String] = []
public init(_ title: String) {
self.title = title
self.selectedDisk = Store.shared.string(key: "\(self.title)_disk", defaultValue: "")
self.removableState = Store.shared.bool(key: "\(self.title)_removable", defaultValue: self.removableState)
self.updateIntervalValue = Store.shared.int(key: "\(self.title)_updateInterval", defaultValue: self.updateIntervalValue)
self.notificationLevel = Store.shared.string(key: "\(self.title)_notificationLevel", defaultValue: self.notificationLevel)
public init() {
self.selectedDisk = Store.shared.string(key: "\(Disk.name)_disk", defaultValue: "")
self.removableState = Store.shared.bool(key: "\(Disk.name)_removable", defaultValue: self.removableState)
self.updateIntervalValue = Store.shared.int(key: "\(Disk.name)_updateInterval", defaultValue: self.updateIntervalValue)
self.notificationLevel = Store.shared.string(key: "\(Disk.name)_notificationLevel", defaultValue: self.notificationLevel)
self.numberOfProcesses = Store.shared.int(key: "\(Disk.name)_processes", defaultValue: self.numberOfProcesses)
super.init(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
@@ -56,6 +57,13 @@ internal class Settings: NSStackView, Settings_v {
public func load(widgets: [widget_t]) {
self.subviews.forEach{ $0.removeFromSuperview() }
self.addArrangedSubview(selectSettingsRowV1(
title: localizedString("Number of top processes"),
action: #selector(changeNumberOfProcesses),
items: NumbersOfProcesses.map{ "\($0)" },
selected: "\(self.numberOfProcesses)"
))
self.intervalSelectView = selectSettingsRowV1(
title: localizedString("Update interval"),
action: #selector(changeUpdateInterval),
@@ -122,10 +130,18 @@ internal class Settings: NSStackView, Settings_v {
})
}
@objc private func changeNumberOfProcesses(_ sender: NSMenuItem) {
if let value = Int(sender.title) {
self.numberOfProcesses = value
Store.shared.set(key: "\(Disk.name)_processes", value: value)
self.callbackWhenUpdateNumberOfProcesses()
}
}
@objc private func handleSelection(_ sender: NSPopUpButton) {
guard let item = sender.selectedItem else { return }
self.selectedDisk = item.title
Store.shared.set(key: "\(self.title)_disk", value: item.title)
Store.shared.set(key: "\(Disk.name)_disk", value: item.title)
self.selectedDiskHandler(item.title)
}
@@ -138,7 +154,7 @@ internal class Settings: NSStackView, Settings_v {
}
self.removableState = state! == .on ? true : false
Store.shared.set(key: "\(self.title)_removable", value: self.removableState)
Store.shared.set(key: "\(Disk.name)_removable", value: self.removableState)
self.callback()
}
@@ -152,15 +168,15 @@ internal class Settings: NSStackView, Settings_v {
guard let key = sender.representedObject as? String else { return }
if key == "Disabled" {
Store.shared.set(key: "\(self.title)_notificationLevel", value: key)
Store.shared.set(key: "\(Disk.name)_notificationLevel", value: key)
} else if let value = Double(key.replacingOccurrences(of: "%", with: "")) {
Store.shared.set(key: "\(self.title)_notificationLevel", value: "\(value/100)")
Store.shared.set(key: "\(Disk.name)_notificationLevel", value: "\(value/100)")
}
}
public func setUpdateInterval(value: Int) {
self.updateIntervalValue = value
Store.shared.set(key: "\(self.title)_updateInterval", value: value)
Store.shared.set(key: "\(Disk.name)_updateInterval", value: value)
self.setInterval(value)
}
}