// // 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 { public var store: UnsafePointer? = 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? = 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.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) -> 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) -> (upload: Int64, download: Int64)? { let addr = pointer.pointee.ifa_addr.pointee guard addr.sa_family == UInt8(AF_LINK) else { return nil } 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)) } } public class ProcessReader: Reader<[Network_Process]> { private let store: UnsafePointer 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) { 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()) } }