- add option to select the network reader (interface based or process based)

- small refactoring in Network reader
This commit is contained in:
Serhiy Mytrovtsiy
2020-11-27 18:11:14 +01:00
parent 6b58302089
commit 19e20ea063
20 changed files with 188 additions and 117 deletions

View File

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

View File

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

View File

@@ -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]> {

View File

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

View File

@@ -135,6 +135,9 @@
"Network interface" = "Netzwerkschnittstelle";
"Total download" = "Empfangen";
"Total upload" = "Gesendet";
"Reader type" = "Lesertyp";
"Interface" = "Schnittstelle";
"Process" = "Prozess";
// Battery
"Level" = "Ladezustand";

View File

@@ -136,6 +136,9 @@
"Network interface" = "Network interface";
"Total download" = "Total download";
"Total upload" = "Total upload";
"Reader type" = "Reader type";
"Interface" = "Interface";
"Process" = "Process";
// Battery
"Level" = "Level";

View File

@@ -132,6 +132,9 @@
"Network interface" = "Interfaz de red";
"Total download" = "Descarga total";
"Total upload" = "Carga total";
"Reader type" = "Tipo de lector";
"Interface" = "Interfaz";
"Process" = "Proceso";
// Battery
"Level" = "Nivel";

View File

@@ -135,6 +135,9 @@
"Network interface" = "Interface réseau";
"Total download" = "Total download";
"Total upload" = "Total upload";
"Reader type" = "Type de lecteur";
"Interface" = "Interface";
"Process" = "Processus";
// Battery
"Level" = "Niveau";

View File

@@ -136,6 +136,9 @@
"Network interface" = "Interfaccia di rete";
"Total download" = "Download totale";
"Total upload" = "Upload totale";
"Reader type" = "Tipo di lettore";
"Interface" = "Interfaccia";
"Process" = "Processi";
// Battery
"Level" = "Livello";

View File

@@ -135,6 +135,9 @@
"Network interface" = "네트워크 인터페이스";
"Total download" = "총 다운로드";
"Total upload" = "총 업로드";
"Reader type" = "리더 유형";
"Interface" = "상호 작용";
"Process" = "프로세스";
// Battery
"Level" = "잔량";

View File

@@ -136,6 +136,9 @@
"Network interface" = "Nettverksgrensesnitt";
"Total download" = "Totalt lastet ned";
"Total upload" = "Totalt lastet opp";
"Reader type" = "Lesertype";
"Interface" = "Grensesnitt";
"Process" = "Prosess";
// Battery
"Level" = "Nivå";

View File

@@ -136,6 +136,9 @@
"Network interface" = "Interfejs sieciowy";
"Total download" = "Całkowicie pobrano";
"Total upload" = "Całkowicie przesłano";
"Reader type" = "Sposób odczytu";
"Interface" = "Interfejs";
"Process" = "Procesy";
// Battery
"Level" = "Poziom naładowania";

View File

@@ -136,6 +136,9 @@
"Network interface" = "Interface de rede";
"Total download" = "Download total";
"Total upload" = "Upload total";
"Reader type" = "Tipo de leitor";
"Interface" = "Interface";
"Process" = "Processo";
// Battery
"Level" = "Nível";

View File

@@ -136,6 +136,9 @@
"Network interface" = "Сетевой интерфейс";
"Total download" = "Всего скачано";
"Total upload" = "Всего выгружено";
"Reader type" = "Метод чтения";
"Interface" = "Интерфейс";
"Process" = "Процессы";
// Battery
"Level" = "Уровень заряда";

View File

@@ -135,6 +135,9 @@
"Network interface" = "Ağ arayüzü";
"Total download" = "Tamamen indirildi";
"Total upload" = "Tamamen yüklendi";
"Reader type" = "Okuma yöntemi";
"Interface" = "Arayüz";
"Process" = "Süreçler";
// Battery
"Level" = "Doluluk";

View File

@@ -136,6 +136,9 @@
"Network interface" = "Мережевий інтерфейс";
"Total download" = "Загально завантажено";
"Total upload" = "Загально вислано";
"Reader type" = "Метод читання";
"Interface" = "Інтерфейс";
"Process" = "Процеси";
// Battery
"Level" = "Рівень заряду";

View File

@@ -136,6 +136,9 @@
"Network interface" = "Giao diện mạng";
"Total download" = "Tổng tải";
"Total upload" = "Tổng upload";
"Reader type" = "Loại trình đọc";
"Interface" = "Giao diện";
"Process" = "Quá trình";
// Battery
"Level" = "Dung lượng Pin";

View File

@@ -135,6 +135,9 @@
"Network interface" = "网络接口";
"Total download" = "共下载";
"Total upload" = "总上传";
"Reader type" = "读卡器类型";
"Interface" = "接口";
"Process" = "处理";
// Battery
"Level" = "电量";

View File

@@ -131,6 +131,9 @@
"Network interface" = "網路介面";
"Total download" = "共下載";
"Total upload" = "共上傳";
"Reader type" = "讀卡器類型";
"Interface" = "接口";
"Process" = "處理";
// Battery
"Level" = "電量";

View File

@@ -83,6 +83,12 @@ public let ShortLong: [KeyValue_t] = [
public let ReaderUpdateIntervals: [Int] = [1, 2, 3, 5, 10, 15, 30]
public let NumbersOfProcesses: [Int] = [3, 5, 8, 10, 15]
public typealias Bandwidth = (upload: Int64, download: Int64)
public let NetworkReaders: [KeyValue_t] = [
KeyValue_t(key: "interface", value: "Interface based"),
KeyValue_t(key: "process", value: "Processes based"),
]
public struct Units {
public let bytes: Int64