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