diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index 5f032165..02cd1e97 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -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) } } } diff --git a/Modules/Net/popup.swift b/Modules/Net/popup.swift index 58bb49eb..acb42dc6 100644 --- a/Modules/Net/popup.swift +++ b/Modules/Net/popup.swift @@ -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)) }) } diff --git a/Modules/Net/readers.swift b/Modules/Net/readers.swift index 9eb0de19..40ed2653 100644 --- a/Modules/Net/readers.swift +++ b/Modules/Net/readers.swift @@ -24,15 +24,11 @@ struct ipResponse: Decodable { } internal class UsageReader: Reader { - typealias BandwidthUsage = (upload: Int64, download: Int64) - public var store: UnsafePointer? = 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 { } } + 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 { } 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? = 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 { let data: UnsafeMutablePointer? = unsafeBitCast(pointer.pointee.ifa_data, to: UnsafeMutablePointer.self) return (upload: Int64(data?.pointee.ifi_obytes ?? 0), download: Int64(data?.pointee.ifi_ibytes ?? 0)) } - - private func interfaceBandwidthUsage() -> BandwidthUsage { - var interfaceAddresses: UnsafeMutablePointer? = 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]> { diff --git a/Modules/Net/settings.swift b/Modules/Net/settings.swift index e1423d32..2e325290 100644 --- a/Modules/Net/settings.swift +++ b/Modules/Net/settings.swift @@ -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" + } } diff --git a/Stats/Supporting Files/de.lproj/Localizable.strings b/Stats/Supporting Files/de.lproj/Localizable.strings index 0f69d145..7e2998ca 100644 --- a/Stats/Supporting Files/de.lproj/Localizable.strings +++ b/Stats/Supporting Files/de.lproj/Localizable.strings @@ -135,6 +135,9 @@ "Network interface" = "Netzwerkschnittstelle"; "Total download" = "Empfangen"; "Total upload" = "Gesendet"; +"Reader type" = "Lesertyp"; +"Interface" = "Schnittstelle"; +"Process" = "Prozess"; // Battery "Level" = "Ladezustand"; diff --git a/Stats/Supporting Files/en.lproj/Localizable.strings b/Stats/Supporting Files/en.lproj/Localizable.strings index ea13a090..a634c9de 100644 --- a/Stats/Supporting Files/en.lproj/Localizable.strings +++ b/Stats/Supporting Files/en.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/es.lproj/Localizable.strings b/Stats/Supporting Files/es.lproj/Localizable.strings index e6e01690..08b79e3d 100644 --- a/Stats/Supporting Files/es.lproj/Localizable.strings +++ b/Stats/Supporting Files/es.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/fr.lproj/Localizable.strings b/Stats/Supporting Files/fr.lproj/Localizable.strings index fb88bc2b..10cbc734 100644 --- a/Stats/Supporting Files/fr.lproj/Localizable.strings +++ b/Stats/Supporting Files/fr.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/it.lproj/Localizable.strings b/Stats/Supporting Files/it.lproj/Localizable.strings index 4d51b9f5..0942c2fb 100644 --- a/Stats/Supporting Files/it.lproj/Localizable.strings +++ b/Stats/Supporting Files/it.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/ko.lproj/Localizable.strings b/Stats/Supporting Files/ko.lproj/Localizable.strings index 89c83683..8bc5e842 100644 --- a/Stats/Supporting Files/ko.lproj/Localizable.strings +++ b/Stats/Supporting Files/ko.lproj/Localizable.strings @@ -135,6 +135,9 @@ "Network interface" = "네트워크 인터페이스"; "Total download" = "총 다운로드"; "Total upload" = "총 업로드"; +"Reader type" = "리더 유형"; +"Interface" = "상호 작용"; +"Process" = "프로세스"; // Battery "Level" = "잔량"; diff --git a/Stats/Supporting Files/nb.lproj/Localizable.strings b/Stats/Supporting Files/nb.lproj/Localizable.strings index 09e74a3e..9e8d10de 100644 --- a/Stats/Supporting Files/nb.lproj/Localizable.strings +++ b/Stats/Supporting Files/nb.lproj/Localizable.strings @@ -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å"; diff --git a/Stats/Supporting Files/pl.lproj/Localizable.strings b/Stats/Supporting Files/pl.lproj/Localizable.strings index 6966d760..da734d45 100644 --- a/Stats/Supporting Files/pl.lproj/Localizable.strings +++ b/Stats/Supporting Files/pl.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/pt-BR.lproj/Localizable.strings b/Stats/Supporting Files/pt-BR.lproj/Localizable.strings index 2ebd2dc6..bbbbcde7 100644 --- a/Stats/Supporting Files/pt-BR.lproj/Localizable.strings +++ b/Stats/Supporting Files/pt-BR.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/ru.lproj/Localizable.strings b/Stats/Supporting Files/ru.lproj/Localizable.strings index 88d2731d..371f4ff0 100644 --- a/Stats/Supporting Files/ru.lproj/Localizable.strings +++ b/Stats/Supporting Files/ru.lproj/Localizable.strings @@ -136,6 +136,9 @@ "Network interface" = "Сетевой интерфейс"; "Total download" = "Всего скачано"; "Total upload" = "Всего выгружено"; +"Reader type" = "Метод чтения"; +"Interface" = "Интерфейс"; +"Process" = "Процессы"; // Battery "Level" = "Уровень заряда"; diff --git a/Stats/Supporting Files/tr.lproj/Localizable.strings b/Stats/Supporting Files/tr.lproj/Localizable.strings index 823b62fe..0bc0a404 100644 --- a/Stats/Supporting Files/tr.lproj/Localizable.strings +++ b/Stats/Supporting Files/tr.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/uk.lproj/Localizable.strings b/Stats/Supporting Files/uk.lproj/Localizable.strings index 400fac27..43ceceef 100644 --- a/Stats/Supporting Files/uk.lproj/Localizable.strings +++ b/Stats/Supporting Files/uk.lproj/Localizable.strings @@ -136,6 +136,9 @@ "Network interface" = "Мережевий інтерфейс"; "Total download" = "Загально завантажено"; "Total upload" = "Загально вислано"; +"Reader type" = "Метод читання"; +"Interface" = "Інтерфейс"; +"Process" = "Процеси"; // Battery "Level" = "Рівень заряду"; diff --git a/Stats/Supporting Files/vi.lproj/Localizable.strings b/Stats/Supporting Files/vi.lproj/Localizable.strings index 75d79bf1..eb6986ce 100644 --- a/Stats/Supporting Files/vi.lproj/Localizable.strings +++ b/Stats/Supporting Files/vi.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/zh-Hans.lproj/Localizable.strings b/Stats/Supporting Files/zh-Hans.lproj/Localizable.strings index 2457c8a6..73ea0a62 100644 --- a/Stats/Supporting Files/zh-Hans.lproj/Localizable.strings +++ b/Stats/Supporting Files/zh-Hans.lproj/Localizable.strings @@ -135,6 +135,9 @@ "Network interface" = "网络接口"; "Total download" = "共下载"; "Total upload" = "总上传"; +"Reader type" = "读卡器类型"; +"Interface" = "接口"; +"Process" = "处理"; // Battery "Level" = "电量"; diff --git a/Stats/Supporting Files/zh-Hant.lproj/Localizable.strings b/Stats/Supporting Files/zh-Hant.lproj/Localizable.strings index 3cfaef9a..cbcda83a 100644 --- a/Stats/Supporting Files/zh-Hant.lproj/Localizable.strings +++ b/Stats/Supporting Files/zh-Hant.lproj/Localizable.strings @@ -131,6 +131,9 @@ "Network interface" = "網路介面"; "Total download" = "共下載"; "Total upload" = "共上傳"; +"Reader type" = "讀卡器類型"; +"Interface" = "接口"; +"Process" = "處理"; // Battery "Level" = "電量"; diff --git a/StatsKit/helpers.swift b/StatsKit/helpers.swift index 0913f9b3..470b0330 100644 --- a/StatsKit/helpers.swift +++ b/StatsKit/helpers.swift @@ -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