- add an option to select a network interface

- update logic when the module is not available (now reader initializing after the module is initialized)
- update a Network popup view
This commit is contained in:
Serhiy Mytrovtsiy
2020-07-06 19:25:41 +02:00
parent 40d44248d9
commit 5e51770cbc
13 changed files with 309 additions and 182 deletions

View File

@@ -40,7 +40,7 @@ struct Battery_Usage: value_t {
}
public class Battery: Module {
private var usageReader: UsageReader = UsageReader()
private var usageReader: UsageReader? = nil
private let popupView: Popup = Popup()
public init(_ store: UnsafePointer<Store>?) {
@@ -49,15 +49,20 @@ public class Battery: Module {
popup: self.popupView,
settings: nil
)
guard self.available else { return }
self.usageReader.readyCallback = { [unowned self] in
self.usageReader = UsageReader()
self.usageReader?.readyCallback = { [unowned self] in
self.readyHandler()
}
self.usageReader.callbackHandler = { [unowned self] value in
self.usageReader?.callbackHandler = { [unowned self] value in
self.usageCallback(value)
}
self.addReader(self.usageReader)
if let reader = self.usageReader {
self.addReader(reader)
}
}
public override func isAvailable() -> Bool {

View File

@@ -35,19 +35,22 @@ public class CPU: Module {
private let popupView: Popup = Popup()
private var settingsView: Settings
private var loadReader: LoadReader? = LoadReader()
private var loadReader: LoadReader? = nil
private let smc: UnsafePointer<SMCService>?
public init(_ store: UnsafePointer<Store>, _ smc: UnsafePointer<SMCService>) {
self.smc = smc
self.settingsView = Settings("CPU", store: store)
self.loadReader!.store = store
super.init(
store: store,
popup: self.popupView,
settings: self.settingsView
)
guard self.available else { return }
self.loadReader = LoadReader()
self.loadReader?.store = store
self.settingsView.callback = { [unowned self] in
self.loadReader?.read()
@@ -60,7 +63,9 @@ public class CPU: Module {
self.loadCallback(value)
}
self.addReader(self.loadReader!)
if let reader = self.loadReader {
self.addReader(reader)
}
}
private func loadCallback(_ value: CPU_Load?) {

View File

@@ -63,7 +63,7 @@ struct DiskList: value_t {
public class Disk: Module {
private let popupView: Popup = Popup()
private var capacityReader: CapacityReader = CapacityReader()
private var capacityReader: CapacityReader? = nil
private var settingsView: Settings
private var selectedDisk: String = ""
@@ -75,21 +75,26 @@ public class Disk: Module {
popup: self.popupView,
settings: self.settingsView
)
guard self.available else { return }
self.capacityReader = CapacityReader()
self.selectedDisk = store!.pointee.string(key: "\(self.config.name)_disk", defaultValue: self.selectedDisk)
self.capacityReader.readyCallback = { [unowned self] in
self.capacityReader?.readyCallback = { [unowned self] in
self.readyHandler()
}
self.capacityReader.callbackHandler = { [unowned self] value in
self.capacityReader?.callbackHandler = { [unowned self] value in
self.capacityCallback(value: value)
}
self.settingsView.selectedDiskHandler = { [unowned self] value in
self.selectedDisk = value
self.capacityReader.read()
self.capacityReader?.read()
}
self.addReader(self.capacityReader)
if let reader = self.capacityReader {
self.addReader(reader)
}
}
private func capacityCallback(value: DiskList?) {

View File

@@ -50,7 +50,7 @@ internal class Settings: NSView, Settings_v {
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
rowTitle.textColor = .textColor
self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 164, y: -1, width: 140, height: 30))
self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 140 - Constants.Settings.margin*2, y: -1, width: 140, height: 30))
self.button!.target = self
self.button?.action = #selector(self.handleSelection)

View File

@@ -35,7 +35,7 @@ public struct RAM_Usage: value_t {
public class Memory: Module {
private let popupView: Popup = Popup()
private var usageReader: UsageReader = UsageReader()
private var usageReader: UsageReader? = nil
public init(_ store: UnsafePointer<Store>?) {
super.init(
@@ -43,15 +43,20 @@ public class Memory: Module {
popup: self.popupView,
settings: nil
)
guard self.available else { return }
self.usageReader.readyCallback = { [unowned self] in
self.usageReader = UsageReader()
self.usageReader?.readyCallback = { [unowned self] in
self.readyHandler()
}
self.usageReader.callbackHandler = { [unowned self] value in
self.usageReader?.callbackHandler = { [unowned self] value in
self.loadCallback(value: value)
}
self.addReader(self.usageReader)
if let reader = self.usageReader {
self.addReader(reader)
}
}
private func loadCallback(value: RAM_Usage?) {

View File

@@ -12,67 +12,94 @@
import Cocoa
import StatsKit
import ModuleKit
import SystemConfiguration
public enum Network_t: String {
case wifi
case ethernet
case bluetooth
case other
}
public struct Network_interface {
var displayName: String = ""
var BSDName: String = ""
var address: String = ""
}
public struct Network_Usage: value_t {
var active: Bool = false
var download: Int64 = 0
var upload: Int64 = 0
var laddr: String? = nil // local ip
var paddr: String? = nil // remote ip
var iaddr: String? = nil // mac adress
var raddr: String? = nil // remote ip
var interface: Network_interface? = nil
var connectionType: Network_t? = nil
var countryCode: String? = nil
var networkName: String? = nil
var ssid: String? = nil
mutating func reset() {
self.active = false
self.download = 0
self.upload = 0
self.laddr = nil
self.paddr = nil
self.iaddr = nil
self.raddr = nil
self.interface = nil
self.connectionType = nil
self.countryCode = nil
self.networkName = nil
self.ssid = nil
}
public var widget_value: Double = 0
}
public class Network: Module {
private var usageReader: UsageReader = UsageReader()
private var usageReader: UsageReader?
private let popupView: Popup = Popup()
private var settingsView: Settings
public init(_ store: UnsafePointer<Store>?) {
self.usageReader.store = store
self.settingsView = Settings("Network", store: store!)
super.init(
store: store,
popup: self.popupView,
settings: nil
settings: self.settingsView
)
guard self.available else { return }
self.usageReader.readyCallback = { [unowned self] in
self.usageReader = UsageReader()
self.usageReader?.store = store
self.usageReader?.readyCallback = { [unowned self] in
self.readyHandler()
}
self.usageReader.callbackHandler = { [unowned self] value in
self.usageReader?.callbackHandler = { [unowned self] value in
self.usageCallback(value)
}
self.addReader(self.usageReader)
self.settingsView.callback = { [unowned self] in
self.usageReader?.getDetails()
self.usageReader?.read()
}
if let reader = self.usageReader {
self.addReader(reader)
}
}
public override func isAvailable() -> Bool {
var list: [String] = []
for interface in SCNetworkInterfaceCopyAll() as NSArray {
if let displayName = SCNetworkInterfaceGetLocalizedDisplayName(interface as! SCNetworkInterface) {
list.append(displayName as String)
}
}
return list.count > 0
}
private func usageCallback(_ value: Network_Usage?) {

View File

@@ -15,7 +15,7 @@ import StatsKit
internal class Popup: NSView {
let dashboardHeight: CGFloat = 90
let detailsHeight: CGFloat = 88
let detailsHeight: CGFloat = 110
private var dashboardView: NSView? = nil
@@ -31,7 +31,8 @@ internal class Popup: NSView {
private var publicIPField: ValueField? = nil
private var localIPField: ValueField? = nil
private var networkTypeField: ValueField? = nil
private var interfaceField: ValueField? = nil
private var ssidField: ValueField? = nil
private var macAdressField: ValueField? = nil
private var initialized: Bool = false
@@ -137,19 +138,20 @@ internal class Popup: NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: separator.frame.origin.y - self.detailsHeight, width: self.frame.width, height: self.detailsHeight))
self.publicIPField = PopupRow(view, n: 3, title: "Public IP:", value: "")
self.localIPField = PopupRow(view, n: 2, title: "Local IP:", value: "")
self.networkTypeField = PopupRow(view, n: 1, title: "Network:", value: "")
self.publicIPField = PopupRow(view, n: 4, title: "Public IP:", value: "")
self.localIPField = PopupRow(view, n: 3, title: "Local IP:", value: "")
self.interfaceField = PopupRow(view, n: 2, title: "Interface:", value: "")
self.ssidField = PopupRow(view, n: 1, title: "Network:", value: "")
self.macAdressField = PopupRow(view, n: 0, title: "Physical address:", value: "")
self.publicIPField?.addTracking()
self.localIPField?.addTracking()
self.networkTypeField?.addTracking()
self.ssidField?.addTracking()
self.macAdressField?.addTracking()
self.publicIPField?.isSelectable = true
self.localIPField?.isSelectable = true
self.networkTypeField?.isSelectable = true
self.ssidField?.isSelectable = true
self.macAdressField?.isSelectable = true
self.addSubview(view)
@@ -157,49 +159,38 @@ internal class Popup: NSView {
public func usageCallback(_ value: Network_Usage) {
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.initialized && value.active {
if !(self.window?.isVisible ?? false) && self.initialized {
return
}
self.uploadValue = value.upload
self.downloadValue = value.download
self.setUploadDownloadFields()
if !value.active {
self.publicIPField?.stringValue = "No connection"
self.localIPField?.stringValue = "No connection"
self.networkTypeField?.stringValue = "No connection"
self.macAdressField?.stringValue = "No connection"
return
}
if var publicIP = value.paddr, self.publicIPField?.stringValue != publicIP {
if value.countryCode != nil {
publicIP = "\(publicIP) (\(value.countryCode!))"
}
self.publicIPField?.stringValue = publicIP
if let interface = value.interface {
self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName))"
self.macAdressField?.stringValue = interface.address
} else {
self.publicIPField?.stringValue = "Unknown"
}
if value.laddr != nil && self.localIPField?.stringValue != value.laddr {
self.localIPField?.stringValue = value.laddr!
}
if value.iaddr != nil && self.macAdressField?.stringValue != value.iaddr {
self.macAdressField?.stringValue = value.iaddr!
self.interfaceField?.stringValue = "Unknown"
self.macAdressField?.stringValue = "Unknown"
}
if value.connectionType != nil {
var networkType = ""
if value.connectionType == .wifi {
networkType = "\(value.networkName ?? "unknown") (WiFi)"
} else if value.connectionType == .ethernet {
networkType = "Ethernet"
}
if self.networkTypeField?.stringValue != networkType {
self.networkTypeField?.stringValue = networkType
if value.connectionType == .wifi {
self.ssidField?.stringValue = value.ssid ?? "Unknown"
} else {
self.ssidField?.stringValue = "Unavailable"
}
if self.publicIPField?.stringValue != value.raddr {
if value.raddr == nil {
self.publicIPField?.stringValue = "Unknown"
} else {
if value.countryCode == nil {
self.publicIPField?.stringValue = value.raddr!
} else {
self.publicIPField?.stringValue = "\(value.raddr!) (\(value.countryCode!))"
}
}
}
if self.localIPField?.stringValue != value.laddr {
self.localIPField?.stringValue = value.laddr ?? "Unknown"
}
self.initialized = true
})
@@ -214,7 +205,7 @@ extension ValueField {
}
public override func mouseEntered(with: NSEvent) {
guard self.stringValue != "No connection" && self.stringValue != "Unknown" else {
guard self.stringValue != "No connection" && self.stringValue != "Unknown" && self.stringValue != "Unavailable" else {
return
}
@@ -226,7 +217,7 @@ extension ValueField {
}
public override func mouseDown(with: NSEvent) {
guard self.stringValue != "No connection" && self.stringValue != "Unknown" else {
guard self.stringValue != "No connection" && self.stringValue != "Unknown" && self.stringValue != "Unavailable" else {
return
}

View File

@@ -17,8 +17,15 @@ import Reachability
import os.log
import CoreWLAN
struct ipResponse: Decodable {
var ip: String
var country: String
var cc: String
}
internal class UsageReader: Reader<Network_Usage> {
public var store: UnsafePointer<Store>? = nil
private var reachability: Reachability? = nil
private var usage: Network_Usage = Network_Usage()
@@ -27,16 +34,16 @@ internal class UsageReader: Reader<Network_Usage> {
if let global = SCDynamicStoreCopyValue(nil, "State:/Network/Global/IPv4" as CFString), let name = global["PrimaryInterface"] as? String {
return name
}
return "eth0"
return ""
}
}
private var interfaceID: String {
get {
return self.store?.pointee.string(key: "network_interface", defaultValue: self.primaryInterface) ?? self.primaryInterface
return self.store?.pointee.string(key: "Network_interface", defaultValue: self.primaryInterface) ?? self.primaryInterface
}
set {
self.store?.pointee.set(key: "network_interface", value: newValue)
self.store?.pointee.set(key: "Network_interface", value: newValue)
}
}
@@ -49,8 +56,7 @@ internal class UsageReader: Reader<Network_Usage> {
}
self.reachability!.whenReachable = { _ in
self.usage.reset()
self.readInformation()
self.getDetails()
}
self.reachability!.whenUnreachable = { _ in
self.usage.reset()
@@ -59,14 +65,6 @@ internal class UsageReader: Reader<Network_Usage> {
}
public override func read() {
guard self.reachability?.connection != .unavailable else {
if self.usage.active {
self.usage.reset()
self.callback(self.usage)
}
return
}
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>? = nil
var upload: Int64 = 0
var download: Int64 = 0
@@ -80,14 +78,14 @@ internal class UsageReader: Reader<Network_Usage> {
continue
}
if let ip = getLocalIP(pointer!), self.usage.laddr != ip {
self.usage.laddr = ip
}
if let info = getBytesInfo(pointer!) {
upload += info.upload
download += info.download
}
if let ip = getLocalIP(pointer!), self.usage.laddr != ip {
self.usage.laddr = ip
}
}
freeifaddrs(interfaceAddresses)
@@ -96,52 +94,53 @@ internal class UsageReader: Reader<Network_Usage> {
self.usage.download = download - self.usage.download
}
if self.usage.upload < 0 {
self.usage.upload = 0
}
if self.usage.download < 0 {
self.usage.download = 0
}
self.callback(self.usage)
self.usage.upload = upload
self.usage.download = download
}
private func readInformation() {
guard self.reachability != nil && self.reachability!.connection != .unavailable else { return }
public func getDetails() {
self.usage.reset()
self.usage.active = true
DispatchQueue.global(qos: .background).async {
self.usage.paddr = self.getPublicIP()
self.getPublicIP()
}
if self.reachability!.connection == .wifi {
self.usage.connectionType = .wifi
if let interface = CWWiFiClient.shared().interface() {
self.usage.networkName = interface.ssid()
self.usage.countryCode = interface.countryCode()
self.usage.iaddr = interface.hardwareAddress()
if self.interfaceID != "" {
for interface in SCNetworkInterfaceCopyAll() as NSArray {
if let bsdName = SCNetworkInterfaceGetBSDName(interface as! SCNetworkInterface),
bsdName as String == self.interfaceID,
let type = SCNetworkInterfaceGetInterfaceType(interface as! SCNetworkInterface),
let displayName = SCNetworkInterfaceGetLocalizedDisplayName(interface as! SCNetworkInterface),
let address = SCNetworkInterfaceGetHardwareAddressString(interface as! SCNetworkInterface) {
self.usage.interface = Network_interface(displayName: displayName as String, BSDName: bsdName as String, address: address as String)
switch type {
case kSCNetworkInterfaceTypeEthernet:
self.usage.connectionType = .ethernet
case kSCNetworkInterfaceTypeIEEE80211, kSCNetworkInterfaceTypeWWAN:
self.usage.connectionType = .wifi
case kSCNetworkInterfaceTypeBluetooth:
self.usage.connectionType = .bluetooth
default:
self.usage.connectionType = .other
}
}
}
} else {
self.usage.connectionType = .ethernet
self.usage.iaddr = getMacAddress()
}
}
private func getDataUsageInfo(from infoPointer: UnsafeMutablePointer<ifaddrs>) -> (upload: Int64, download: Int64)? {
let pointer = infoPointer
let addr = pointer.pointee.ifa_addr.pointee
guard addr.sa_family == UInt8(AF_LINK) else { return nil }
var networkData: UnsafeMutablePointer<if_data>? = nil
networkData = unsafeBitCast(pointer.pointee.ifa_data, to: UnsafeMutablePointer<if_data>.self)
return (upload: Int64(networkData?.pointee.ifi_obytes ?? 0), download: Int64(networkData?.pointee.ifi_ibytes ?? 0))
}
private func getBytesInfo(_ pointer: UnsafeMutablePointer<ifaddrs>) -> (upload: Int64, download: Int64)? {
let addr = pointer.pointee.ifa_addr.pointee
guard addr.sa_family == UInt8(AF_LINK) else {
return nil
}
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))
if let interface = CWWiFiClient.shared().interface(), self.usage.connectionType == .wifi {
self.usage.ssid = interface.ssid()
self.usage.countryCode = interface.countryCode()
}
}
private func getLocalIP(_ pointer: UnsafeMutablePointer<ifaddrs>) -> String? {
@@ -157,73 +156,35 @@ internal class UsageReader: Reader<Network_Usage> {
return String(cString: ip)
}
private func getPublicIP() -> String? {
let url = URL(string: "https://api.ipify.org")
private func getPublicIP() {
let url = URL(string: "https://api.myip.com")
var address: String? = nil
do {
if let url = url {
address = try String(contentsOf: url)
if address!.contains("<") {
address = nil
if address != nil {
let jsonData = address!.data(using: .utf8)
let response: ipResponse = try JSONDecoder().decode(ipResponse.self, from: jsonData!)
self.usage.countryCode = response.cc
self.usage.raddr = response.ip
}
}
} catch let error {
os_log(.error, log: log, "get public ip %s", "\(error)")
}
}
private func getBytesInfo(_ pointer: UnsafeMutablePointer<ifaddrs>) -> (upload: Int64, download: Int64)? {
let addr = pointer.pointee.ifa_addr.pointee
return address
}
// https://stackoverflow.com/questions/31835418/how-to-get-mac-address-from-os-x-with-swift
private func getMacAddress() -> String? {
var macAddressAsString : String?
if let intfIterator = findEthernetInterfaces() {
if let macAddress = getMACAddress(intfIterator) {
macAddressAsString = macAddress.map( { String(format:"%02x", $0) } ).joined(separator: ":")
}
IOObjectRelease(intfIterator)
}
return macAddressAsString
}
private func findEthernetInterfaces() -> io_iterator_t? {
let matchingDictUM = IOServiceMatching("IOEthernetInterface");
if matchingDictUM == nil {
guard addr.sa_family == UInt8(AF_LINK) else {
return nil
}
let matchingDict = matchingDictUM! as NSMutableDictionary
matchingDict["IOPropertyMatch"] = [ "IOPrimaryInterface" : true]
var matchingServices : io_iterator_t = 0
if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &matchingServices) != KERN_SUCCESS {
return nil
}
return matchingServices
}
private func getMACAddress(_ intfIterator : io_iterator_t) -> [UInt8]? {
var macAddress : [UInt8]?
var intfService = IOIteratorNext(intfIterator)
while intfService != 0 {
var controllerService : io_object_t = 0
if IORegistryEntryGetParentEntry(intfService, kIOServicePlane, &controllerService) == KERN_SUCCESS {
let dataUM = IORegistryEntryCreateCFProperty(controllerService, "IOMACAddress" as CFString, kCFAllocatorDefault, 0)
if dataUM != nil {
let data = (dataUM!.takeRetainedValue() as! CFData) as Data
macAddress = [0, 0, 0, 0, 0, 0]
data.copyBytes(to: &macAddress!, count: macAddress!.count)
}
IOObjectRelease(controllerService)
}
IOObjectRelease(intfService)
intfService = IOIteratorNext(intfIterator)
}
return macAddress
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))
}
}

106
Modules/Net/settings.swift Normal file
View File

@@ -0,0 +1,106 @@
//
// settings.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 06/07/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import StatsKit
import ModuleKit
import SystemConfiguration
internal class Settings: NSView, Settings_v {
public var callback: (() -> Void) = {}
private let title: String
private let store: UnsafePointer<Store>
private var button: NSPopUpButton?
private var list: [Network_interface] = []
public init(_ title: String, store: UnsafePointer<Store>) {
self.title = title
self.store = store
super.init(frame: CGRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: Constants.Settings.width - (Constants.Settings.margin*2), height: 0))
for interface in SCNetworkInterfaceCopyAll() as NSArray {
if let bsdName = SCNetworkInterfaceGetBSDName(interface as! SCNetworkInterface),
let displayName = SCNetworkInterfaceGetLocalizedDisplayName(interface as! SCNetworkInterface) {
self.list.append(Network_interface(displayName: displayName as String, BSDName: bsdName as String))
}
}
self.wantsLayer = true
self.canDrawConcurrently = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func load(widget: widget_t) {
self.subviews.forEach{ $0.removeFromSuperview() }
self.addNetworkSelector()
self.setFrameSize(NSSize(width: self.frame.width, height: 30 + (Constants.Settings.margin*2)))
}
private func addNetworkSelector() {
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: self.frame.width, height: 29))
let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (view.frame.height - 16)/2, width: view.frame.width - 52, height: 17), "Network interface")
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
rowTitle.textColor = .textColor
self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 200 - Constants.Settings.margin*2, y: -1, width: 200, height: 30))
self.button!.target = self
self.button?.action = #selector(self.handleSelection)
let selectedInterface = self.store.pointee.string(key: "\(self.title)_interface", defaultValue: "")
let menu = NSMenu()
let autodetection = NSMenuItem(title: "Autodetection", action: nil, keyEquivalent: "")
menu.addItem(autodetection)
menu.addItem(NSMenuItem.separator())
self.list.forEach { (interface: Network_interface) in
let interfaceMenu = NSMenuItem(title: "\(interface.displayName) (\(interface.BSDName))", action: nil, keyEquivalent: "")
interfaceMenu.identifier = NSUserInterfaceItemIdentifier(rawValue: interface.BSDName)
menu.addItem(interfaceMenu)
if selectedInterface != "" && selectedInterface == interface.BSDName {
interfaceMenu.state = .on
}
}
if selectedInterface == "" {
self.button?.selectItem(withTitle: "Autodetection")
}
self.button?.menu = menu
view.addSubview(rowTitle)
view.addSubview(self.button!)
self.addSubview(view)
}
@objc func handleSelection(_ sender: NSPopUpButton) {
guard let item = sender.selectedItem else { return }
if item.title == "Autodetection" {
self.store.pointee.remove("\(self.title)_interface")
} else {
if let bsdName = item.identifier?.rawValue {
self.store.pointee.set(key: "\(self.title)_interface", value: bsdName)
}
}
self.callback()
}
}

View File

@@ -27,6 +27,7 @@ public class Sensors: Module {
popup: self.popupView,
settings: self.settingsView
)
guard self.available else { return }
self.checkIfNoSensorsEnabled()
self.popupView.setup(self.sensorsReader.list)