Files
macos-stats/Modules/Net/readers.swift
2020-11-13 13:57:33 +01:00

324 lines
11 KiB
Swift

//
// readers.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 24/05/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
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()
private var primaryInterface: String {
get {
if let global = SCDynamicStoreCopyValue(nil, "State:/Network/Global/IPv4" as CFString), let name = global["PrimaryInterface"] as? String {
return name
}
return ""
}
}
private var interfaceID: String {
get {
return self.store?.pointee.string(key: "Network_interface", defaultValue: self.primaryInterface) ?? self.primaryInterface
}
set {
self.store?.pointee.set(key: "Network_interface", value: newValue)
}
}
public override func setup() {
do {
self.reachability = try Reachability()
try self.reachability!.startNotifier()
} catch let error {
os_log(.error, log: log, "initialize Reachability error %s", "\(error)")
}
self.reachability!.whenReachable = { _ in
if self.active {
self.getDetails()
}
}
self.reachability!.whenUnreachable = { _ in
if self.active {
self.usage.reset()
self.callback(self.usage)
}
}
}
public override func read() {
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>? = nil
var upload: Int64 = 0
var download: Int64 = 0
guard getifaddrs(&interfaceAddresses) == 0 else { return }
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!) {
upload += info.upload
download += info.download
}
}
freeifaddrs(interfaceAddresses)
if self.usage.upload != 0 && self.usage.download != 0 {
self.usage.upload = upload - self.usage.upload
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.usage.totalUpload += self.usage.upload
self.usage.totalDownload += self.usage.download
self.callback(self.usage)
self.usage.upload = upload
self.usage.download = download
}
public func getDetails() {
self.usage.reset()
DispatchQueue.global(qos: .background).async {
self.getPublicIP()
}
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
}
}
}
}
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? {
var addr = pointer.pointee.ifa_addr.pointee
guard addr.sa_family == UInt8(AF_INET) else {
return nil
}
var ip = [CChar](repeating: 0, count: Int(NI_MAXHOST))
getnameinfo(&addr, socklen_t(addr.sa_len), &ip, socklen_t(ip.count), nil, socklen_t(0), NI_NUMERICHOST)
return String(cString: ip)
}
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 != 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
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))
}
}
public class ProcessReader: Reader<[Network_Process]> {
private let store: UnsafePointer<Store>
private let title: String
private var previous: [Network_Process] = []
private var numberOfProcesses: Int {
get {
return self.store.pointee.int(key: "\(self.title)_processes", defaultValue: 8)
}
}
init(_ title: String, store: UnsafePointer<Store>) {
self.title = title
self.store = store
super.init()
}
public override func setup() {
self.popup = true
}
public override func read() {
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
}
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
}
var list: [Network_Process] = []
var firstLine = false
output.enumerateLines { (line, _) -> () in
if !firstLine {
firstLine = true
return
}
let parsedLine = line.split(separator: ",")
guard parsedLine.count >= 3 else {
return
}
var process = Network_Process()
process.time = Date()
let nameArray = parsedLine[0].split(separator: ".")
if let pid = nameArray.last {
process.pid = String(pid)
}
if let app = NSRunningApplication(processIdentifier: pid_t(process.pid) ?? 0) {
process.name = app.localizedName ?? nameArray.dropLast().joined(separator: ".")
process.icon = app.icon
} else {
process.name = nameArray.dropLast().joined(separator: ".")
}
if let download = Int(parsedLine[1]) {
process.download = download
}
if let upload = Int(parsedLine[2]) {
process.upload = upload
}
list.append(process)
}
var processes: [Network_Process] = []
if self.previous.count == 0 {
self.previous = list
processes = list
} else {
self.previous.forEach { (pp: Network_Process) in
if let i = list.firstIndex(where: { $0.pid == pp.pid }) {
let p = list[i]
var download = p.download - pp.download
var upload = p.upload - pp.upload
let time = download == 0 && upload == 0 ? pp.time : Date()
list[i].time = time
if download < 0 {
download = 0
}
if upload < 0 {
upload = 0
}
processes.append(Network_Process(time: time, name: p.name, pid: p.pid, download: download, upload: upload, icon: p.icon))
}
}
self.previous = list
}
processes.sort {
if $0.download != $1.download {
return $0.download < $1.download
} else if $0.upload < $1.upload {
return $0.upload < $1.upload
} else {
return $0.time < $1.time
}
}
self.callback(processes.suffix(self.numberOfProcesses).reversed())
}
}