mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-13 15:54:10 +09:00
990 lines
37 KiB
Swift
990 lines
37 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 Kit
|
||
import SystemConfiguration
|
||
import CoreWLAN
|
||
|
||
struct ipResponse: Decodable {
|
||
var ip: String
|
||
var country: String
|
||
var cc: String
|
||
}
|
||
|
||
// swiftlint:disable control_statement
|
||
extension CWPHYMode: @retroactive CustomStringConvertible {
|
||
public var description: String {
|
||
switch(self) {
|
||
case .mode11a: return "802.11a"
|
||
case .mode11ac: return "802.11ac"
|
||
case .mode11b: return "802.11b"
|
||
case .mode11g: return "802.11g"
|
||
case .mode11n: return "802.11n"
|
||
case .mode11ax: return "802.11ax"
|
||
case .modeNone: return "none"
|
||
@unknown default: return "unknown"
|
||
}
|
||
}
|
||
}
|
||
|
||
extension CWInterfaceMode: @retroactive CustomStringConvertible {
|
||
public var description: String {
|
||
switch(self) {
|
||
case .hostAP: return "AP"
|
||
case .IBSS: return "Adhoc"
|
||
case .station: return "Station"
|
||
case .none: return "none"
|
||
@unknown default: return "unknown"
|
||
}
|
||
}
|
||
}
|
||
|
||
extension CWSecurity: @retroactive CustomStringConvertible {
|
||
public var description: String {
|
||
switch(self) {
|
||
case .none: return "none"
|
||
case .WEP: return "WEP"
|
||
case .wpaPersonal: return "WPA Personal"
|
||
case .wpaPersonalMixed: return "WPA Personal Mixed"
|
||
case .wpa2Personal: return "WPA2 Personal"
|
||
case .personal: return "Personal"
|
||
case .dynamicWEP: return "Dynamic WEP"
|
||
case .wpaEnterprise: return "WPA Enterprise"
|
||
case .wpaEnterpriseMixed: return "WPA Enterprise Mixed"
|
||
case .wpa2Enterprise: return "WPA2 Enterprise"
|
||
case .enterprise: return "Enterprise"
|
||
case .unknown: return "unknown"
|
||
case .wpa3Personal: return "WPA3 Personal"
|
||
case .wpa3Enterprise: return "WPA3 Enterprise"
|
||
case .wpa3Transition: return "WPA3 Transition"
|
||
default: return "unknown"
|
||
}
|
||
}
|
||
}
|
||
|
||
extension CWChannelBand: @retroactive CustomStringConvertible {
|
||
public var description: String {
|
||
switch(self) {
|
||
case .band2GHz: return "2 GHz"
|
||
case .band5GHz: return "5 GHz"
|
||
case .band6GHz: return "6 GHz"
|
||
case .bandUnknown: return "unknown"
|
||
@unknown default: return "unknown"
|
||
}
|
||
}
|
||
}
|
||
|
||
extension CWChannelWidth: @retroactive CustomStringConvertible {
|
||
public var description: String {
|
||
switch(self) {
|
||
case .width20MHz: return "20 MHz"
|
||
case .width40MHz: return "40 MHz"
|
||
case .width80MHz: return "80 MHz"
|
||
case .width160MHz: return "160 MHz"
|
||
case .widthUnknown: return "unknown"
|
||
@unknown default: return "unknown"
|
||
}
|
||
}
|
||
}
|
||
// swiftlint:enable control_statement
|
||
|
||
extension CWChannel {
|
||
override public var description: String {
|
||
return "\(channelNumber) (\(channelBand), \(channelWidth))"
|
||
}
|
||
}
|
||
|
||
internal class UsageReader: Reader<Network_Usage>, CWEventDelegate {
|
||
private var reachability: Reachability = Reachability(start: true)
|
||
private let variablesQueue = DispatchQueue(label: "eu.exelban.NetworkUsageReader")
|
||
private var _usage: Network_Usage = Network_Usage()
|
||
public var usage: Network_Usage {
|
||
get { self.variablesQueue.sync { self._usage } }
|
||
set { self.variablesQueue.sync { self._usage = newValue } }
|
||
}
|
||
|
||
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 { Store.shared.string(key: "Network_interface", defaultValue: self.primaryInterface) }
|
||
set { Store.shared.set(key: "Network_interface", value: newValue) }
|
||
}
|
||
|
||
private var reader: String {
|
||
get { Store.shared.string(key: "Network_reader", defaultValue: "interface") }
|
||
}
|
||
|
||
private var vpnConnection: Bool {
|
||
if let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as? [String: Any], let scopes = settings["__SCOPED__"] as? [String: Any] {
|
||
return !scopes.filter({ $0.key.contains("tap") || $0.key.contains("tun") || $0.key.contains("ppp") || $0.key.contains("ipsec") || $0.key.contains("ipsec0")}).isEmpty
|
||
}
|
||
return false
|
||
}
|
||
|
||
private var VPNMode: Bool {
|
||
get { Store.shared.bool(key: "Network_VPNMode", defaultValue: false) }
|
||
}
|
||
private var publicIPState: Bool {
|
||
get { Store.shared.bool(key: "Network_publicIP", defaultValue: true) }
|
||
}
|
||
|
||
private let wifiClient = CWWiFiClient.shared()
|
||
|
||
public override func setup() {
|
||
self.reachability.reachable = {
|
||
if self.active {
|
||
self.getPublicIP()
|
||
self.getDetails()
|
||
self.getWiFiDetails()
|
||
}
|
||
}
|
||
self.reachability.unreachable = {
|
||
if self.active {
|
||
self.getWiFiDetails()
|
||
self.usage.reset()
|
||
self.callback(self.usage)
|
||
}
|
||
}
|
||
|
||
NotificationCenter.default.addObserver(self, selector: #selector(refreshPublicIP), name: .refreshPublicIP, object: nil)
|
||
NotificationCenter.default.addObserver(self, selector: #selector(resetTotalNetworkUsage), name: .resetTotalNetworkUsage, object: nil)
|
||
|
||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1) {
|
||
if self.active {
|
||
self.getPublicIP()
|
||
self.getDetails()
|
||
}
|
||
}
|
||
|
||
if let usage = self.value {
|
||
self.usage = usage
|
||
self.usage.bandwidth = Bandwidth()
|
||
}
|
||
|
||
self.wifiClient.delegate = self
|
||
self.startListeningForWifiEvents()
|
||
}
|
||
|
||
public override func terminate() {
|
||
self.reachability.stop()
|
||
self.stopListeningForWifiEvents()
|
||
}
|
||
|
||
public override func read() {
|
||
self.getDetails()
|
||
|
||
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.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.total.upload += self.usage.bandwidth.upload
|
||
self.usage.total.download += self.usage.bandwidth.download
|
||
|
||
self.usage.status = self.reachability.isReachable
|
||
|
||
if self.vpnConnection && self.VPNMode {
|
||
self.usage.bandwidth.upload /= 2
|
||
self.usage.bandwidth.download /= 2
|
||
}
|
||
|
||
self.callback(self.usage)
|
||
|
||
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 Bandwidth()
|
||
}
|
||
|
||
var pointer = interfaceAddresses
|
||
while pointer != nil {
|
||
defer { pointer = pointer?.pointee.ifa_next }
|
||
guard let pointer = pointer else { break }
|
||
|
||
if String(cString: pointer.pointee.ifa_name) != self.interfaceID {
|
||
continue
|
||
}
|
||
self.usage.interface?.status = (pointer.pointee.ifa_flags & UInt32(IFF_UP)) != 0
|
||
|
||
if let raw = pointer.pointee.ifa_data {
|
||
let dataPtr = raw.assumingMemoryBound(to: if_data.self)
|
||
let ifData = dataPtr.pointee
|
||
let baud = UInt64(ifData.ifi_baudrate)
|
||
if baud > 0 {
|
||
self.usage.interface?.transmitRate = Double(baud) / 1_000_000.0
|
||
}
|
||
}
|
||
|
||
self.getLocalIP(pointer)
|
||
|
||
if let info = self.getBytesInfo(pointer) {
|
||
totalUpload += info.upload
|
||
totalDownload += info.download
|
||
}
|
||
}
|
||
freeifaddrs(interfaceAddresses)
|
||
|
||
return Bandwidth(upload: totalUpload, download: totalDownload)
|
||
}
|
||
|
||
private func readProcessBandwidth() -> Bandwidth {
|
||
let task = Process()
|
||
task.launchPath = "/usr/bin/nettop"
|
||
task.arguments = ["-P", "-L", "1", "-n", "-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"]
|
||
task.environment = [
|
||
"NSUnbufferedIO": "YES",
|
||
"LC_ALL": "en_US.UTF-8"
|
||
]
|
||
|
||
let inputPipe = Pipe()
|
||
let outputPipe = Pipe()
|
||
let errorPipe = Pipe()
|
||
|
||
defer {
|
||
inputPipe.fileHandleForWriting.closeFile()
|
||
outputPipe.fileHandleForReading.closeFile()
|
||
errorPipe.fileHandleForReading.closeFile()
|
||
}
|
||
|
||
task.standardInput = inputPipe
|
||
task.standardOutput = outputPipe
|
||
task.standardError = errorPipe
|
||
|
||
do {
|
||
try task.run()
|
||
} catch let err {
|
||
error("read bandwidth from processes: \(err)", log: self.log)
|
||
return Bandwidth()
|
||
}
|
||
|
||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||
let output = String(data: outputData, encoding: .utf8)
|
||
_ = String(data: errorData, encoding: .utf8)
|
||
guard let output, !output.isEmpty else { return Bandwidth() }
|
||
|
||
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 Bandwidth(upload: totalUpload, download: totalDownload)
|
||
}
|
||
|
||
public func getDetails() {
|
||
guard self.interfaceID != "" else { return }
|
||
|
||
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 prefs = SCPreferencesCreate(nil, "Stats" as CFString, nil), let services = SCNetworkServiceCopyAll(prefs) as? [SCNetworkService] {
|
||
for service in services {
|
||
if let interface = SCNetworkServiceGetInterface(service), let name = SCNetworkInterfaceGetBSDName(interface), name as String == self.interfaceID,
|
||
let serviceID = SCNetworkServiceGetServiceID(service) {
|
||
let key = "State:/Network/Service/\(serviceID)/DNS" as CFString
|
||
if let settings = SCDynamicStoreCopyValue(nil, key) as? [String: Any] {
|
||
self.usage.dns = settings["ServerAddresses"] as? [String] ?? []
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
guard self.usage.interface != nil else { return }
|
||
|
||
if self.usage.wifiDetails.ssid != nil && (self.usage.wifiDetails.ssid == "" || self.usage.wifiDetails.ssid == "<redacted>") {
|
||
self.usage.wifiDetails.ssid = nil
|
||
}
|
||
|
||
if self.usage.connectionType == .wifi && self.usage.wifiDetails.ssid == nil || self.usage.wifiDetails.ssid == "" {
|
||
self.getWiFiDetails()
|
||
}
|
||
}
|
||
|
||
private func getWiFiDetails() {
|
||
if let interface = CWWiFiClient.shared().interface(withName: self.interfaceID) {
|
||
if let ssid = interface.ssid() {
|
||
self.usage.wifiDetails.ssid = ssid
|
||
} else if let cfg = interface.configuration(),
|
||
let set = (cfg.value(forKey: "networkProfiles") as? NSOrderedSet),
|
||
let first = set.firstObject as? CWNetworkProfile,
|
||
let raw = first.ssid, !raw.isEmpty {
|
||
self.usage.wifiDetails.ssid = raw.replacingOccurrences(of: "’", with: "'").replacingOccurrences(of: "‘", with: "'").trimmingCharacters(in: .whitespacesAndNewlines)
|
||
}
|
||
if let bssid = interface.bssid() {
|
||
self.usage.wifiDetails.bssid = bssid
|
||
}
|
||
if let cc = interface.countryCode() {
|
||
self.usage.wifiDetails.countryCode = cc
|
||
}
|
||
|
||
self.usage.wifiDetails.RSSI = interface.rssiValue()
|
||
self.usage.wifiDetails.noise = interface.noiseMeasurement()
|
||
|
||
self.usage.wifiDetails.standard = interface.activePHYMode().description
|
||
self.usage.wifiDetails.mode = interface.interfaceMode().description
|
||
self.usage.wifiDetails.security = interface.security().description
|
||
|
||
if let ch = interface.wlanChannel() {
|
||
self.usage.wifiDetails.channel = ch.description
|
||
|
||
self.usage.wifiDetails.channelBand = ch.channelBand.description
|
||
self.usage.wifiDetails.channelWidth = ch.channelWidth.description
|
||
self.usage.wifiDetails.channelNumber = ch.channelNumber.description
|
||
}
|
||
}
|
||
|
||
if self.usage.wifiDetails.ssid == nil || self.usage.wifiDetails.ssid == "" {
|
||
guard let res = process(path: "/usr/sbin/system_profiler", arguments: ["SPAirPortDataType", "-json"]) else {
|
||
return
|
||
}
|
||
do {
|
||
if let json = try JSONSerialization.jsonObject(with: Data(res.utf8), options: []) as? [String: Any] {
|
||
if let arr = json["SPAirPortDataType"] as? [[String: Any]],
|
||
let airport = arr.first(where: { $0["spairport_airport_interfaces"] != nil }),
|
||
let interfaces = airport["spairport_airport_interfaces"] as? [[String: Any]],
|
||
let interface = interfaces.first(where: { $0["_name"] as? String == self.interfaceID }),
|
||
let obj = interface["spairport_current_network_information"] as? [String: Any] {
|
||
|
||
self.usage.wifiDetails.ssid = obj["_name"] as? String
|
||
self.usage.wifiDetails.countryCode = obj["spairport_network_country_code"] as? String
|
||
self.usage.wifiDetails.standard = obj["spairport_network_phymode"] as? String
|
||
}
|
||
}
|
||
} catch let err as NSError {
|
||
error("error to parse system_profiler SPAirPortDataType: \(err.localizedDescription)")
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
private func getLocalIP(_ pointer: UnsafeMutablePointer<ifaddrs>) {
|
||
var addr = pointer.pointee.ifa_addr.pointee
|
||
guard addr.sa_family == UInt8(AF_INET) || addr.sa_family == UInt8(AF_INET6) else { return}
|
||
|
||
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)
|
||
|
||
let ipStr = String(cString: ip)
|
||
if addr.sa_family == UInt8(AF_INET) && !ipStr.isEmpty {
|
||
self.usage.laddr.v4 = ipStr
|
||
} else if addr.sa_family == UInt8(AF_INET6) && !ipStr.isEmpty {
|
||
self.usage.laddr.v6 = ipStr
|
||
}
|
||
}
|
||
|
||
private func getPublicIP() {
|
||
guard self.publicIPState else { return }
|
||
|
||
struct Addr_s: Decodable {
|
||
let ipv4: String?
|
||
let ipv6: String?
|
||
let country: String?
|
||
}
|
||
|
||
DispatchQueue.global(qos: .userInitiated).async {
|
||
let response = syncShell("curl -s -4 https://api.mac-stats.com/ip")
|
||
if !response.isEmpty, let data = response.data(using: .utf8),
|
||
let addr = try? JSONDecoder().decode(Addr_s.self, from: data) {
|
||
if let ip = addr.ipv4, self.isIPv4(ip) {
|
||
self.usage.raddr.v4 = ip
|
||
}
|
||
if let countryCode = addr.country {
|
||
self.usage.raddr.countryCode = countryCode
|
||
}
|
||
}
|
||
}
|
||
DispatchQueue.global(qos: .userInitiated).async {
|
||
let response = syncShell("curl -s -6 https://api.mac-stats.com/ip")
|
||
if !response.isEmpty, let data = response.data(using: .utf8),
|
||
let addr = try? JSONDecoder().decode(Addr_s.self, from: data) {
|
||
if let ip = addr.ipv6, !self.isIPv4(ip) {
|
||
self.usage.raddr.v6 = ip
|
||
}
|
||
if let countryCode = addr.country {
|
||
self.usage.raddr.countryCode = countryCode
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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))
|
||
}
|
||
|
||
private func isIPv4(_ ip: String) -> Bool {
|
||
let arr = ip.split(separator: ".").compactMap{ Int($0) }
|
||
return arr.count == 4 && arr.filter{ $0 >= 0 && $0 < 256}.count == 4
|
||
}
|
||
|
||
@objc func refreshPublicIP() {
|
||
self.usage.raddr.v4 = nil
|
||
self.usage.raddr.v6 = nil
|
||
|
||
DispatchQueue.global(qos: .background).async {
|
||
self.getPublicIP()
|
||
}
|
||
}
|
||
|
||
@objc func resetTotalNetworkUsage() {
|
||
self.usage.total = Bandwidth()
|
||
self.save(self.usage)
|
||
}
|
||
|
||
private func startListeningForWifiEvents() {
|
||
do {
|
||
try self.wifiClient.startMonitoringEvent(with: .ssidDidChange)
|
||
} catch let err as NSError {
|
||
error("failed to start monitoring Wi-Fi events: \(err.localizedDescription)")
|
||
}
|
||
}
|
||
|
||
private func stopListeningForWifiEvents() {
|
||
do {
|
||
try self.wifiClient.stopMonitoringEvent(with: .ssidDidChange)
|
||
} catch let err as NSError {
|
||
error("failed to stop monitoring Wi-Fi events: \(err.localizedDescription)")
|
||
}
|
||
}
|
||
|
||
public func ssidDidChangeForWiFiInterface(withName interfaceName: String) {
|
||
self.getWiFiDetails()
|
||
}
|
||
|
||
private func isInterfaceUp(_ ifName: String) -> Bool {
|
||
var addrs: UnsafeMutablePointer<ifaddrs>? = nil
|
||
guard getifaddrs(&addrs) == 0, let first = addrs else { return false }
|
||
defer { freeifaddrs(addrs) }
|
||
|
||
var ptr = first
|
||
while true {
|
||
let name = String(cString: ptr.pointee.ifa_name)
|
||
if name == ifName {
|
||
return (ptr.pointee.ifa_flags & UInt32(IFF_UP)) != 0
|
||
}
|
||
if let next = ptr.pointee.ifa_next {
|
||
ptr = next
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
|
||
public class ProcessReader: Reader<[Network_Process]> {
|
||
private let title: String = "Network"
|
||
private var previous: [Network_Process] = []
|
||
|
||
private var numberOfProcesses: Int {
|
||
get {
|
||
return Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
|
||
}
|
||
}
|
||
|
||
public override func setup() {
|
||
self.popup = true
|
||
}
|
||
|
||
public override func read() {
|
||
if self.numberOfProcesses == 0 {
|
||
return
|
||
}
|
||
|
||
let task = Process()
|
||
task.launchPath = "/usr/bin/nettop"
|
||
task.arguments = ["-P", "-L", "1", "-n", "-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"]
|
||
task.environment = [
|
||
"NSUnbufferedIO": "YES",
|
||
"LC_ALL": "en_US.UTF-8"
|
||
]
|
||
|
||
let inputPipe = Pipe()
|
||
let outputPipe = Pipe()
|
||
let errorPipe = Pipe()
|
||
|
||
defer {
|
||
inputPipe.fileHandleForWriting.closeFile()
|
||
outputPipe.fileHandleForReading.closeFile()
|
||
errorPipe.fileHandleForReading.closeFile()
|
||
}
|
||
|
||
task.standardInput = inputPipe
|
||
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(data: outputData, encoding: .utf8)
|
||
_ = String(data: errorData, encoding: .utf8)
|
||
guard let output, !output.isEmpty else { 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 = Int(pid) ?? 0
|
||
}
|
||
if let app = NSRunningApplication(processIdentifier: pid_t(process.pid) ) {
|
||
process.name = app.localizedName ?? nameArray.dropLast().joined(separator: ".")
|
||
} else {
|
||
process.name = nameArray.dropLast().joined(separator: ".")
|
||
}
|
||
|
||
if process.name == "" {
|
||
process.name = "\(process.pid)"
|
||
}
|
||
|
||
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.isEmpty {
|
||
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(pid: p.pid, name: p.name, time: time, download: download, upload: upload))
|
||
}
|
||
}
|
||
self.previous = list
|
||
}
|
||
|
||
processes.sort {
|
||
let firstMax = max($0.download, $0.upload)
|
||
let secondMax = max($1.download, $1.upload)
|
||
let firstMin = min($0.download, $0.upload)
|
||
let secondMin = min($1.download, $1.upload)
|
||
|
||
if firstMax == secondMax && firstMin == secondMin { // download and upload values are the same, sort by time
|
||
return $0.time < $1.time
|
||
} else if firstMax == secondMax && firstMin != secondMin { // max values are the same, min not. Sort by min values
|
||
return firstMin < secondMin
|
||
}
|
||
return firstMax < secondMax // max values are not the same, sort by max value
|
||
}
|
||
|
||
self.callback(processes.suffix(self.numberOfProcesses).reversed())
|
||
}
|
||
}
|
||
|
||
internal class ConnectivityReaderWrapper {
|
||
weak var reader: ConnectivityReader?
|
||
|
||
init(_ reader: ConnectivityReader) {
|
||
self.reader = reader
|
||
}
|
||
}
|
||
|
||
// inspired by https://github.com/samiyr/SwiftyPing
|
||
internal class ConnectivityReader: Reader<Network_Connectivity> {
|
||
private let variablesQueue = DispatchQueue(label: "eu.exelban.ConnectivityReaderQueue")
|
||
|
||
private let identifier = UInt16.random(in: 0..<UInt16.max)
|
||
private var fingerprint: UUID = UUID()
|
||
|
||
private var host: String {
|
||
Store.shared.string(key: "Network_ICMPHost", defaultValue: "1.1.1.1")
|
||
}
|
||
private var lastHost: String = ""
|
||
private var addr: Data? = nil
|
||
private let timeout: TimeInterval = 5
|
||
|
||
private var socket: CFSocket?
|
||
private var socketSource: CFRunLoopSource?
|
||
|
||
private var wrapper: Network_Connectivity = Network_Connectivity(status: false)
|
||
|
||
private var _status: Bool? = nil
|
||
private var status: Bool? {
|
||
get { self.variablesQueue.sync { self._status } }
|
||
set { self.variablesQueue.sync { self._status = newValue } }
|
||
}
|
||
|
||
private var _timeoutTimer: Timer?
|
||
private var timeoutTimer: Timer? {
|
||
get { self.variablesQueue.sync { self._timeoutTimer } }
|
||
set { self.variablesQueue.sync { self._timeoutTimer = newValue } }
|
||
}
|
||
|
||
private var _isPinging: Bool = false
|
||
private var isPinging: Bool {
|
||
get { self.variablesQueue.sync { self._isPinging } }
|
||
set { self.variablesQueue.sync { self._isPinging = newValue } }
|
||
}
|
||
|
||
private var _latency: Double? = nil
|
||
private var latency: Double? {
|
||
get { self.variablesQueue.sync { self._latency } }
|
||
set { self.variablesQueue.sync { self._latency = newValue } }
|
||
}
|
||
|
||
private var _previousLatency: Double? = nil
|
||
private var previousLatency: Double? {
|
||
get { self.variablesQueue.sync { self._previousLatency } }
|
||
set { self.variablesQueue.sync { self._previousLatency = newValue } }
|
||
}
|
||
|
||
private var _jitter: Double? = nil
|
||
private var jitter: Double? {
|
||
get { self.variablesQueue.sync { self._jitter } }
|
||
set { self.variablesQueue.sync { self._jitter = newValue } }
|
||
}
|
||
|
||
var start: DispatchTime? = nil
|
||
|
||
private struct ICMPHeader {
|
||
public var type: UInt8
|
||
public var code: UInt8
|
||
public var checksum: UInt16
|
||
public var identifier: UInt16
|
||
public var sequenceNumber: UInt16
|
||
public var payload: uuid_t
|
||
}
|
||
|
||
private struct IPHeader {
|
||
public var versionAndHeaderLength: UInt8
|
||
public var differentiatedServices: UInt8
|
||
public var totalLength: UInt16
|
||
public var identification: UInt16
|
||
public var flagsAndFragmentOffset: UInt16
|
||
public var timeToLive: UInt8
|
||
public var `protocol`: UInt8
|
||
public var headerChecksum: UInt16
|
||
public var sourceAddress: (UInt8, UInt8, UInt8, UInt8)
|
||
public var destinationAddress: (UInt8, UInt8, UInt8, UInt8)
|
||
}
|
||
|
||
override func setup() {
|
||
self.setInterval(Store.shared.int(key: "Network_updateICMPInterval", defaultValue: 1))
|
||
self.prepare()
|
||
}
|
||
|
||
deinit {
|
||
self.closeConn()
|
||
}
|
||
|
||
private func prepare() {
|
||
DispatchQueue.global(qos: .background).async {
|
||
self.addr = self.resolve()
|
||
self.openConn()
|
||
self.read()
|
||
}
|
||
}
|
||
|
||
override func read() {
|
||
guard !self.host.isEmpty else {
|
||
if self.socket != nil {
|
||
self.closeConn()
|
||
}
|
||
return
|
||
}
|
||
|
||
if self.socket == nil {
|
||
self.prepare()
|
||
}
|
||
|
||
if self.lastHost != self.host {
|
||
self.addr = self.resolve()
|
||
}
|
||
|
||
guard !self.isPinging && self.active, let socket = self.socket, let addr = self.addr, let data = self.request() else { return }
|
||
self.isPinging = true
|
||
|
||
let timer = Timer(timeInterval: self.timeout, target: self, selector: #selector(self.timeoutCallback), userInfo: nil, repeats: false)
|
||
RunLoop.main.add(timer, forMode: .common)
|
||
self.timeoutTimer = timer
|
||
self.start = DispatchTime.now()
|
||
|
||
let error = CFSocketSendData(socket, addr as CFData, data as CFData, self.timeout)
|
||
if error != .success {
|
||
self.socketCallback(data: nil, error: error)
|
||
}
|
||
|
||
if let v = self.status {
|
||
self.wrapper.status = v
|
||
if let l = self.latency {
|
||
self.wrapper.latency = l
|
||
}
|
||
if let j = self.jitter {
|
||
self.wrapper.jitter = j
|
||
}
|
||
self.callback(self.wrapper)
|
||
}
|
||
}
|
||
|
||
@objc private func timeoutCallback() {
|
||
self.status = false
|
||
self.isPinging = false
|
||
}
|
||
|
||
private func socketCallback(data: Data? = nil, error: CFSocketError? = nil) {
|
||
guard let data = data, validateResponse(data) else { return }
|
||
let end = DispatchTime.now()
|
||
|
||
self.latency = Double(end.uptimeNanoseconds - (self.start?.uptimeNanoseconds ?? 0)) / 1_000_000
|
||
|
||
if let prev = self.previousLatency {
|
||
let d = abs((self.latency ?? 0) - prev)
|
||
if self.jitter == nil {
|
||
self.jitter = d
|
||
} else {
|
||
self.jitter! += (d - self.jitter!) / 16.0
|
||
}
|
||
}
|
||
self.previousLatency = self.latency
|
||
|
||
self.status = error == nil
|
||
self.isPinging = false
|
||
self.timeoutTimer?.invalidate()
|
||
self.timeoutTimer = nil
|
||
}
|
||
|
||
// MARK: - helpers
|
||
|
||
private func validateResponse(_ data: Data) -> Bool {
|
||
guard data.count >= MemoryLayout<ICMPHeader>.size + MemoryLayout<IPHeader>.size,
|
||
let headerOffset = icmpHeaderOffset(of: data) else { return false }
|
||
|
||
let payloadSize = data.count - headerOffset - MemoryLayout<ICMPHeader>.size
|
||
let icmpHeader = data.withUnsafeBytes({ $0.load(fromByteOffset: headerOffset, as: ICMPHeader.self) })
|
||
let payload = data.subdata(in: (data.count - payloadSize)..<data.count)
|
||
let uuid = UUID(uuid: icmpHeader.payload)
|
||
|
||
guard uuid == self.fingerprint else { return false }
|
||
guard icmpHeader.checksum == computeChecksum(header: icmpHeader, additionalPayload: [UInt8](payload)) else { return false }
|
||
guard icmpHeader.type == 0 else { return false }
|
||
guard icmpHeader.code == 0 else { return false }
|
||
|
||
return true
|
||
}
|
||
|
||
private func request() -> Data? {
|
||
var header = ICMPHeader(
|
||
type: 8,
|
||
code: 0,
|
||
checksum: 0,
|
||
identifier: CFSwapInt16HostToBig(self.identifier),
|
||
sequenceNumber: CFSwapInt16HostToBig(0),
|
||
payload: self.fingerprint.uuid
|
||
)
|
||
|
||
let delta = MemoryLayout<uuid_t>.size - MemoryLayout<uuid_t>.size
|
||
var additional = [UInt8]()
|
||
if delta > 0 {
|
||
additional = (0..<delta).map { _ in UInt8.random(in: UInt8.min...UInt8.max) }
|
||
}
|
||
|
||
guard let checksum = computeChecksum(header: header, additionalPayload: additional) else { return nil }
|
||
header.checksum = checksum
|
||
|
||
return Data(bytes: &header, count: MemoryLayout<ICMPHeader>.size) + Data(additional)
|
||
}
|
||
|
||
private func computeChecksum(header: ICMPHeader, additionalPayload: [UInt8]) -> UInt16? {
|
||
let typecode = Data([header.type, header.code]).withUnsafeBytes { $0.load(as: UInt16.self) }
|
||
var sum = UInt64(typecode) + UInt64(header.identifier) + UInt64(header.sequenceNumber)
|
||
let payload = convert(payload: header.payload) + additionalPayload
|
||
guard payload.count % 2 == 0 else { return nil }
|
||
|
||
var i = 0
|
||
while i < payload.count {
|
||
guard payload.indices.contains(i + 1) else { return nil }
|
||
sum += Data([payload[i], payload[i + 1]]).withUnsafeBytes { UInt64($0.load(as: UInt16.self)) }
|
||
i += 2
|
||
}
|
||
while sum >> 16 != 0 {
|
||
sum = (sum & 0xffff) + (sum >> 16)
|
||
}
|
||
guard sum < UInt16.max else { return nil }
|
||
|
||
return ~UInt16(sum)
|
||
}
|
||
|
||
private func convert(payload: uuid_t) -> [UInt8] {
|
||
let p = payload
|
||
return [p.0, p.1, p.2, p.3, p.4, p.5, p.6, p.7, p.8, p.9, p.10, p.11, p.12, p.13, p.14, p.15].map { UInt8($0) }
|
||
}
|
||
|
||
private func icmpHeaderOffset(of packet: Data) -> Int? {
|
||
if packet.count >= MemoryLayout<IPHeader>.size + MemoryLayout<ICMPHeader>.size {
|
||
let ipHeader = packet.withUnsafeBytes({ $0.load(as: IPHeader.self) })
|
||
if ipHeader.versionAndHeaderLength & 0xF0 == 0x40 && ipHeader.protocol == IPPROTO_ICMP {
|
||
let headerLength = Int(ipHeader.versionAndHeaderLength) & 0x0F * MemoryLayout<UInt32>.size
|
||
if packet.count >= headerLength + MemoryLayout<ICMPHeader>.size {
|
||
return headerLength
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func openConn() {
|
||
let info = ConnectivityReaderWrapper(self)
|
||
let unmanagedSocketInfo = Unmanaged.passRetained(info)
|
||
var context = CFSocketContext(version: 0, info: unmanagedSocketInfo.toOpaque(), retain: nil, release: nil, copyDescription: nil)
|
||
self.socket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, CFSocketCallBackType.dataCallBack.rawValue, { _, callBackType, _, data, info in
|
||
guard let info = info, let data = data else { return }
|
||
if (callBackType as CFSocketCallBackType) == CFSocketCallBackType.dataCallBack {
|
||
let cfdata = Unmanaged<CFData>.fromOpaque(data).takeUnretainedValue()
|
||
let wrapper = Unmanaged<ConnectivityReaderWrapper>.fromOpaque(info).takeUnretainedValue()
|
||
wrapper.reader?.socketCallback(data: cfdata as Data)
|
||
}
|
||
}, &context)
|
||
let handle = CFSocketGetNative(self.socket)
|
||
var value: Int32 = 1
|
||
let err = setsockopt(handle, SOL_SOCKET, SO_NOSIGPIPE, &value, socklen_t(MemoryLayout.size(ofValue: value)))
|
||
guard err == 0 else { return }
|
||
self.socketSource = CFSocketCreateRunLoopSource(nil, self.socket, 0)
|
||
CFRunLoopAddSource(CFRunLoopGetMain(), self.socketSource, .commonModes)
|
||
}
|
||
|
||
private func closeConn() {
|
||
if let source = self.socketSource {
|
||
CFRunLoopSourceInvalidate(source)
|
||
self.socketSource = nil
|
||
}
|
||
if let socket = self.socket {
|
||
CFSocketInvalidate(socket)
|
||
self.socket = nil
|
||
}
|
||
self.timeoutTimer?.invalidate()
|
||
self.timeoutTimer = nil
|
||
}
|
||
|
||
private func resolve() -> Data? {
|
||
self.lastHost = self.host
|
||
var streamError = CFStreamError()
|
||
let cfhost = CFHostCreateWithName(nil, self.host as CFString).takeRetainedValue()
|
||
let status = CFHostStartInfoResolution(cfhost, .addresses, &streamError)
|
||
guard status else { return nil }
|
||
var success: DarwinBoolean = false
|
||
guard let addresses = CFHostGetAddressing(cfhost, &success)?.takeUnretainedValue() as? [Data] else {
|
||
return nil
|
||
}
|
||
var data: Data?
|
||
for address in addresses {
|
||
let addrin = address.socketAddress
|
||
if address.count >= MemoryLayout<sockaddr>.size && addrin.sa_family == UInt8(AF_INET) {
|
||
data = address
|
||
break
|
||
}
|
||
}
|
||
guard let data = data, !data.isEmpty else { return nil }
|
||
return data
|
||
}
|
||
}
|