mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-15 00:34:08 +09:00
- add option to select the network reader (interface based or process based)
- small refactoring in Network reader
This commit is contained in:
@@ -28,11 +28,8 @@ public struct Network_interface {
|
||||
}
|
||||
|
||||
public struct Network_Usage: value_t {
|
||||
var download: Int64 = 0
|
||||
var upload: Int64 = 0
|
||||
|
||||
var totalDownload: Int64 = 0
|
||||
var totalUpload: Int64 = 0
|
||||
var bandwidth: Bandwidth = (0, 0)
|
||||
var total: Bandwidth = (0, 0)
|
||||
|
||||
var laddr: String? = nil // local ip
|
||||
var raddr: String? = nil // remote ip
|
||||
@@ -44,8 +41,7 @@ public struct Network_Usage: value_t {
|
||||
var ssid: String? = nil
|
||||
|
||||
mutating func reset() {
|
||||
self.download = 0
|
||||
self.upload = 0
|
||||
self.bandwidth = (0, 0)
|
||||
|
||||
self.laddr = nil
|
||||
self.raddr = nil
|
||||
@@ -142,7 +138,7 @@ public class Network: Module {
|
||||
|
||||
self.popupView.usageCallback(value!)
|
||||
if let widget = self.widget as? SpeedWidget {
|
||||
widget.setValue(upload: value!.upload, download: value!.download)
|
||||
widget.setValue(upload: value!.bandwidth.upload, download: value!.bandwidth.download)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,12 +306,12 @@ internal class Popup: NSView, Popup_p {
|
||||
public func usageCallback(_ value: Network_Usage) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if (self.window?.isVisible ?? false) || !self.initialized {
|
||||
self.uploadValue = value.upload
|
||||
self.downloadValue = value.download
|
||||
self.uploadValue = value.bandwidth.upload
|
||||
self.downloadValue = value.bandwidth.download
|
||||
self.setUploadDownloadFields()
|
||||
|
||||
self.totalUploadField?.stringValue = Units(bytes: value.totalUpload).getReadableMemory()
|
||||
self.totalDownloadField?.stringValue = Units(bytes: value.totalDownload).getReadableMemory()
|
||||
self.totalUploadField?.stringValue = Units(bytes: value.total.upload).getReadableMemory()
|
||||
self.totalDownloadField?.stringValue = Units(bytes: value.total.download).getReadableMemory()
|
||||
|
||||
if let interface = value.interface {
|
||||
self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName))"
|
||||
@@ -345,7 +345,7 @@ internal class Popup: NSView, Popup_p {
|
||||
self.initialized = true
|
||||
}
|
||||
|
||||
self.chart?.addValue(upload: Double(value.upload), download: Double(value.download))
|
||||
self.chart?.addValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,11 @@ struct ipResponse: Decodable {
|
||||
}
|
||||
|
||||
internal class UsageReader: Reader<Network_Usage> {
|
||||
typealias BandwidthUsage = (upload: Int64, download: Int64)
|
||||
|
||||
public var store: UnsafePointer<Store>? = nil
|
||||
|
||||
private var reachability: Reachability? = nil
|
||||
private var usage: Network_Usage = Network_Usage()
|
||||
|
||||
private var shouldReportSelectedInterfaceBandwidthOnly = true
|
||||
|
||||
private var primaryInterface: String {
|
||||
get {
|
||||
if let global = SCDynamicStoreCopyValue(nil, "State:/Network/Global/IPv4" as CFString), let name = global["PrimaryInterface"] as? String {
|
||||
@@ -51,6 +47,12 @@ internal class UsageReader: Reader<Network_Usage> {
|
||||
}
|
||||
}
|
||||
|
||||
private var reader: String {
|
||||
get {
|
||||
return self.store?.pointee.string(key: "Network_reader", defaultValue: "interface") ?? "interface"
|
||||
}
|
||||
}
|
||||
|
||||
public override func setup() {
|
||||
do {
|
||||
self.reachability = try Reachability()
|
||||
@@ -73,23 +75,108 @@ internal class UsageReader: Reader<Network_Usage> {
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
let currentUsage: BandwidthUsage
|
||||
if shouldReportSelectedInterfaceBandwidthOnly {
|
||||
currentUsage = interfaceBandwidthUsage()
|
||||
} else {
|
||||
currentUsage = allProcessesBandwidthUsage()
|
||||
let current: Bandwidth = self.reader == "interface" ? self.readInterfaceBandwidth() : self.readProcessBandwidth()
|
||||
|
||||
// allows to reset the value to 0 when first read
|
||||
if self.usage.bandwidth.upload != 0 {
|
||||
self.usage.bandwidth.upload = current.upload - self.usage.bandwidth.upload
|
||||
}
|
||||
if self.usage.bandwidth.download != 0 {
|
||||
self.usage.bandwidth.download = current.download - self.usage.bandwidth.download
|
||||
}
|
||||
|
||||
self.usage.upload = max(currentUsage.upload - self.usage.upload, 0)
|
||||
self.usage.download = max(currentUsage.download - self.usage.download, 0)
|
||||
self.usage.bandwidth.upload = max(self.usage.bandwidth.upload, 0) // prevent negative upload value
|
||||
self.usage.bandwidth.download = max(self.usage.bandwidth.download, 0) // prevent negative download value
|
||||
|
||||
self.usage.totalUpload += self.usage.upload
|
||||
self.usage.totalDownload += self.usage.download
|
||||
self.usage.total.upload += self.usage.bandwidth.upload
|
||||
self.usage.total.download += self.usage.bandwidth.download
|
||||
|
||||
self.callback(self.usage)
|
||||
|
||||
self.usage.upload = currentUsage.upload
|
||||
self.usage.download = currentUsage.download
|
||||
self.usage.bandwidth.upload = current.upload
|
||||
self.usage.bandwidth.download = current.download
|
||||
}
|
||||
|
||||
private func readInterfaceBandwidth() -> Bandwidth {
|
||||
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>? = nil
|
||||
var totalUpload: Int64 = 0
|
||||
var totalDownload: Int64 = 0
|
||||
guard getifaddrs(&interfaceAddresses) == 0 else {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
var pointer = interfaceAddresses
|
||||
while pointer != nil {
|
||||
defer { pointer = pointer?.pointee.ifa_next }
|
||||
|
||||
if String(cString: pointer!.pointee.ifa_name) != self.interfaceID {
|
||||
continue
|
||||
}
|
||||
|
||||
if let ip = getLocalIP(pointer!), self.usage.laddr != ip {
|
||||
self.usage.laddr = ip
|
||||
}
|
||||
|
||||
if let info = getBytesInfo(pointer!) {
|
||||
totalUpload += info.upload
|
||||
totalDownload += info.download
|
||||
}
|
||||
}
|
||||
freeifaddrs(interfaceAddresses)
|
||||
|
||||
return (totalUpload, totalDownload)
|
||||
}
|
||||
|
||||
private func readProcessBandwidth() -> Bandwidth {
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/bin/nettop"
|
||||
task.arguments = ["-P", "-L", "1", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"]
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
} catch let error {
|
||||
os_log(.error, log: log, "read bandwidth from processes %s", "\(error)")
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(decoding: outputData, as: UTF8.self)
|
||||
_ = String(decoding: errorData, as: UTF8.self)
|
||||
|
||||
if output.isEmpty {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
var totalUpload: Int64 = 0
|
||||
var totalDownload: Int64 = 0
|
||||
var firstLine = false
|
||||
output.enumerateLines { (line, _) -> () in
|
||||
if !firstLine {
|
||||
firstLine = true
|
||||
return
|
||||
}
|
||||
|
||||
let parsedLine = line.split(separator: ",")
|
||||
guard parsedLine.count >= 3 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let download = Int64(parsedLine[1]) {
|
||||
totalDownload += download
|
||||
}
|
||||
if let upload = Int64(parsedLine[2]) {
|
||||
totalUpload += upload
|
||||
}
|
||||
}
|
||||
|
||||
return (totalUpload, totalDownload)
|
||||
}
|
||||
|
||||
public func getDetails() {
|
||||
@@ -172,89 +259,6 @@ internal class UsageReader: Reader<Network_Usage> {
|
||||
let data: UnsafeMutablePointer<if_data>? = unsafeBitCast(pointer.pointee.ifa_data, to: UnsafeMutablePointer<if_data>.self)
|
||||
return (upload: Int64(data?.pointee.ifi_obytes ?? 0), download: Int64(data?.pointee.ifi_ibytes ?? 0))
|
||||
}
|
||||
|
||||
private func interfaceBandwidthUsage() -> BandwidthUsage {
|
||||
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>? = nil
|
||||
var totalUpload: Int64 = 0
|
||||
var totalDownload: Int64 = 0
|
||||
guard getifaddrs(&interfaceAddresses) == 0 else {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
var pointer = interfaceAddresses
|
||||
while pointer != nil {
|
||||
defer { pointer = pointer?.pointee.ifa_next }
|
||||
|
||||
if String(cString: pointer!.pointee.ifa_name) != self.interfaceID {
|
||||
continue
|
||||
}
|
||||
|
||||
if let ip = getLocalIP(pointer!), self.usage.laddr != ip {
|
||||
self.usage.laddr = ip
|
||||
}
|
||||
|
||||
if let info = getBytesInfo(pointer!) {
|
||||
totalUpload += info.upload
|
||||
totalDownload += info.download
|
||||
}
|
||||
}
|
||||
freeifaddrs(interfaceAddresses)
|
||||
|
||||
return (totalUpload, totalDownload)
|
||||
}
|
||||
|
||||
private func allProcessesBandwidthUsage() -> BandwidthUsage {
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/bin/nettop"
|
||||
task.arguments = ["-P", "-L", "1", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"]
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
} catch let error {
|
||||
print(error)
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(decoding: outputData, as: UTF8.self)
|
||||
_ = String(decoding: errorData, as: UTF8.self)
|
||||
|
||||
if output.isEmpty {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
var totalUpload: Int64 = 0
|
||||
var totalDownload: Int64 = 0
|
||||
var firstLine = false
|
||||
output.enumerateLines { (line, _) -> () in
|
||||
if !firstLine {
|
||||
firstLine = true
|
||||
return
|
||||
}
|
||||
|
||||
let parsedLine = line.split(separator: ",")
|
||||
guard parsedLine.count >= 3 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let download = Int(parsedLine[1]) {
|
||||
totalDownload += Int64(download)
|
||||
}
|
||||
if let upload = Int(parsedLine[2]) {
|
||||
totalUpload += Int64(upload)
|
||||
}
|
||||
}
|
||||
|
||||
return (totalUpload, totalDownload)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ProcessReader: Reader<[Network_Process]> {
|
||||
|
||||
@@ -16,6 +16,7 @@ import SystemConfiguration
|
||||
|
||||
internal class Settings: NSView, Settings_v {
|
||||
private var numberOfProcesses: Int = 8
|
||||
private var readerType: String = "interface"
|
||||
|
||||
public var callback: (() -> Void) = {}
|
||||
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
|
||||
@@ -30,6 +31,7 @@ internal class Settings: NSView, Settings_v {
|
||||
self.title = title
|
||||
self.store = store
|
||||
self.numberOfProcesses = store.pointee.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses)
|
||||
self.readerType = store.pointee.string(key: "\(self.title)_reader", defaultValue: self.readerType)
|
||||
|
||||
super.init(frame: CGRect(
|
||||
x: 0,
|
||||
@@ -56,22 +58,30 @@ internal class Settings: NSView, Settings_v {
|
||||
self.subviews.forEach{ $0.removeFromSuperview() }
|
||||
|
||||
let rowHeight: CGFloat = 30
|
||||
let num: CGFloat = 1
|
||||
|
||||
self.addNetworkSelector()
|
||||
let num: CGFloat = 2
|
||||
|
||||
self.addSubview(SelectTitleRow(
|
||||
frame: NSRect(x: Constants.Settings.margin, y: rowHeight + (Constants.Settings.margin*2), width: self.frame.width - (Constants.Settings.margin*2), height: 30),
|
||||
frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin + (rowHeight + Constants.Settings.margin) * 2, width: self.frame.width - (Constants.Settings.margin*2), height: 30),
|
||||
title: LocalizedString("Number of top processes"),
|
||||
action: #selector(changeNumberOfProcesses),
|
||||
items: NumbersOfProcesses.map{ "\($0)" },
|
||||
selected: "\(self.numberOfProcesses)"
|
||||
))
|
||||
|
||||
self.addSubview(SelectRow(
|
||||
frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin + (rowHeight + Constants.Settings.margin) * 1, width: self.frame.width - (Constants.Settings.margin*2), height: 30),
|
||||
title: LocalizedString("Reader type"),
|
||||
action: #selector(changeReaderType),
|
||||
items: NetworkReaders,
|
||||
selected: self.readerType
|
||||
))
|
||||
|
||||
self.addInterfaceSelector()
|
||||
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: (rowHeight*(num+1)) + (Constants.Settings.margin*(2+num))))
|
||||
}
|
||||
|
||||
private func addNetworkSelector() {
|
||||
private func addInterfaceSelector() {
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: self.frame.width, height: 30))
|
||||
|
||||
let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (view.frame.height - 16)/2, width: view.frame.width - 52, height: 17), LocalizedString("Network interface"))
|
||||
@@ -79,8 +89,9 @@ internal class Settings: NSView, Settings_v {
|
||||
rowTitle.textColor = .textColor
|
||||
|
||||
self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 200 - Constants.Settings.margin*2, y: 0, width: 200, height: 30))
|
||||
self.button!.target = self
|
||||
self.button?.target = self
|
||||
self.button?.action = #selector(self.handleSelection)
|
||||
self.button?.isEnabled = self.readerType == "interface"
|
||||
|
||||
let selectedInterface = self.store.pointee.string(key: "\(self.title)_interface", defaultValue: "")
|
||||
let menu = NSMenu()
|
||||
@@ -130,4 +141,13 @@ internal class Settings: NSView, Settings_v {
|
||||
self.callbackWhenUpdateNumberOfProcesses()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func changeReaderType(_ sender: NSMenuItem) {
|
||||
guard let key = sender.representedObject as? String else {
|
||||
return
|
||||
}
|
||||
self.readerType = key
|
||||
self.store.pointee.set(key: "\(self.title)_reader", value: key)
|
||||
self.button?.isEnabled = self.readerType == "interface"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user