diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index bb1a47d0..fc7496a7 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -208,7 +208,7 @@ public class Network: Module { self.settingsView.usageResetCallback = { [weak self] in self?.setUsageReset() } - self.settingsView.ICMPHostCallback = { [weak self] isDisabled in + self.settingsView.connectivityHostCallback = { [weak self] isDisabled in if isDisabled { self?.popupView.resetConnectivityView() self?.connectivityCallback(Network_Connectivity(status: false)) diff --git a/Modules/Net/readers.swift b/Modules/Net/readers.swift index d3b07cf1..c1e665fe 100644 --- a/Modules/Net/readers.swift +++ b/Modules/Net/readers.swift @@ -696,13 +696,26 @@ internal class ConnectivityReader: Reader { private let identifier = UInt16.random(in: 0.. { } override func read() { - guard !self.host.isEmpty else { - if self.socket != nil { - self.closeConn() + if self.connectivityMode == .http { + self.httpCheck() + } else { + guard !self.ICMPHost.isEmpty else { + if self.socket != nil { + self.closeConn() + } + return } + + self.icmpCheck() + } + + 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) + } + } + + private func httpCheck() { + guard !self.isPinging else { return } + self.isPinging = true + + let urlString = self.HTTPHost.hasPrefix("http://") || self.HTTPHost.hasPrefix("https://") ? self.HTTPHost : "https://\(self.HTTPHost)" + guard let url = URL(string: urlString) else { + self.status = false + self.isPinging = false return } + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: self.timeout) + request.httpMethod = "HEAD" + + let startTime = DispatchTime.now() + let task = URLSession.shared.dataTask(with: request) { _, response, error in + let endTime = DispatchTime.now() + let elapsed = Double(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000 + + self.latency = elapsed + if let prev = self.previousLatency { + let d = abs(elapsed - prev) + if self.jitter == nil { + self.jitter = d + } else { + self.jitter! += (d - self.jitter!) / 16.0 + } + } + self.previousLatency = elapsed + + if let http = response as? HTTPURLResponse { + self.status = (200...399).contains(http.statusCode) && error == nil + } else { + self.status = false + } + self.isPinging = false + } + task.resume() + } + + private func icmpCheck() { if self.socket == nil { self.prepare() } - if self.lastHost != self.host { + if self.lastHost != self.ICMPHost { self.addr = self.resolve() } @@ -813,17 +885,6 @@ internal class ConnectivityReader: Reader { 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() { @@ -966,9 +1027,9 @@ internal class ConnectivityReader: Reader { } private func resolve() -> Data? { - self.lastHost = self.host + self.lastHost = self.ICMPHost var streamError = CFStreamError() - let cfhost = CFHostCreateWithName(nil, self.host as CFString).takeRetainedValue() + let cfhost = CFHostCreateWithName(nil, self.ICMPHost as CFString).takeRetainedValue() let status = CFHostStartInfoResolution(cfhost, .addresses, &streamError) guard status else { return nil } var success: DarwinBoolean = false diff --git a/Modules/Net/settings.swift b/Modules/Net/settings.swift index 15e4bc12..406ef779 100644 --- a/Modules/Net/settings.swift +++ b/Modules/Net/settings.swift @@ -65,8 +65,10 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { private var widgetActivationThresholdState: Bool = false private var widgetActivationThreshold: Int = 0 private var widgetActivationThresholdSize: SizeUnit = .MB - private var ICMPHost: String = "1.1.1.1" - private var updateICMPIntervalValue: Int = 1 + private var connectivityICMPHost: String = "1.1.1.1" + private var connectivityHTTPHost: String = "https://google.com" + private var updateConnectivityIntervalValue: Int = 1 + private var connectivityMode: ConnectivityReader.ConnectivityMode = .icmp private var publicIPState: Bool = true private var publicIPRefreshInterval: String = "never" private var baseValue: String = "byte" @@ -75,7 +77,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { public var callback: (() -> Void) = {} public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {} public var usageResetCallback: (() -> Void) = {} - public var ICMPHostCallback: ((_ newState: Bool) -> Void) = { _ in } + public var connectivityHostCallback: ((_ newState: Bool) -> Void) = { _ in } public var setInterval: ((_ value: Int) -> Void) = {_ in } public var publicIPRefreshIntervalCallback: (() -> Void) = {} @@ -94,6 +96,8 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { return false } + private var connectivityHostField: NSTextField? = nil + public init(_ module: ModuleType) { self.title = module.stringValue self.numberOfProcesses = Store.shared.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses) @@ -103,8 +107,10 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { self.widgetActivationThresholdState = Store.shared.bool(key: "\(self.title)_widgetActivationThresholdState", defaultValue: self.widgetActivationThresholdState) self.widgetActivationThreshold = Store.shared.int(key: "\(self.title)_widgetActivationThreshold", defaultValue: self.widgetActivationThreshold) self.widgetActivationThresholdSize = SizeUnit.fromString(Store.shared.string(key: "\(self.title)_widgetActivationThresholdSize", defaultValue: self.widgetActivationThresholdSize.key)) - self.ICMPHost = Store.shared.string(key: "\(self.title)_ICMPHost", defaultValue: self.ICMPHost) - self.updateICMPIntervalValue = Store.shared.int(key: "\(self.title)_updateICMPInterval", defaultValue: self.updateICMPIntervalValue) + self.connectivityICMPHost = Store.shared.string(key: "\(self.title)_ICMPHost", defaultValue: self.connectivityICMPHost) + self.connectivityHTTPHost = Store.shared.string(key: "\(self.title)_HTTPHost", defaultValue: self.connectivityHTTPHost) + self.updateConnectivityIntervalValue = Store.shared.int(key: "\(self.title)_updateICMPInterval", defaultValue: self.updateConnectivityIntervalValue) + self.connectivityMode = ConnectivityReader.ConnectivityMode(rawValue: Store.shared.string(key: "\(self.title)_connectivityMode", defaultValue: "icmp")) ?? .icmp self.publicIPState = Store.shared.bool(key: "\(self.title)_publicIP", defaultValue: self.publicIPState) self.publicIPRefreshInterval = Store.shared.string(key: "\(self.title)_publicIPRefreshInterval", defaultValue: self.publicIPRefreshInterval) self.baseValue = Store.shared.string(key: "\(self.title)_base", defaultValue: self.baseValue) @@ -214,28 +220,29 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { self.addArrangedSubview(self.widgetThresholdSection!) self.widgetThresholdSection?.setRowVisibility(1, newState: self.widgetActivationThresholdState) - let valueField: NSTextField = NSTextField() - valueField.widthAnchor.constraint(equalToConstant: 250).isActive = true - valueField.font = NSFont.systemFont(ofSize: 12, weight: .regular) - valueField.textColor = .textColor - valueField.isEditable = true - valueField.isSelectable = true - valueField.usesSingleLineMode = true - valueField.maximumNumberOfLines = 1 - valueField.focusRingType = .none - valueField.stringValue = self.ICMPHost - valueField.delegate = self - valueField.placeholderString = localizedString("Leave empty to disable the check") + var connectivityHost = self.connectivityICMPHost + if self.connectivityMode == .http { + connectivityHost = self.connectivityHTTPHost + } - let ICMPField = self.inputField(id: "ICMP", value: self.ICMPHost, placeholder: localizedString("Leave empty to disable the check")) + let ICMPField = self.inputField(id: "ICMP", value: connectivityHost, placeholder: localizedString("Leave empty to disable the check")) + self.connectivityHostField = ICMPField self.addArrangedSubview(PreferencesSection([ - PreferencesRow(localizedString("Connectivity host (ICMP)"), component: ICMPField) { + PreferencesRow(localizedString("Reader type"), component: selectView( + action: #selector(self.changeConnectivityMode), + items: [ + KeyValue_t(key: "icmp", value: "ICMP"), + KeyValue_t(key: "http", value: "HTTP") + ], + selected: self.connectivityMode.rawValue + )), + PreferencesRow(localizedString("Connectivity host"), component: ICMPField) { NSWorkspace.shared.open(URL(string: "https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol")!) }, PreferencesRow(localizedString("Update interval"), component: selectView( action: #selector(self.changeICMPUpdateInterval), items: ReaderUpdateIntervals, - selected: "\(self.updateICMPIntervalValue)" + selected: "\(self.updateConnectivityIntervalValue)" )) ])) @@ -249,7 +256,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { } } - private func inputField(id: String, value: String, placeholder: String) -> NSView { + private func inputField(id: String, value: String, placeholder: String) -> NSTextField { let field: NSTextField = NSTextField() field.identifier = NSUserInterfaceItemIdentifier(id) field.widthAnchor.constraint(equalToConstant: 250).isActive = true @@ -322,9 +329,15 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { func controlTextDidChange(_ notification: Notification) { if let field = notification.object as? NSTextField { if field.identifier == NSUserInterfaceItemIdentifier("ICMP") { - self.ICMPHost = field.stringValue - Store.shared.set(key: "\(self.title)_ICMPHost", value: self.ICMPHost) - self.ICMPHostCallback(self.ICMPHost.isEmpty) + if self.connectivityMode == .http { + self.connectivityHTTPHost = field.stringValue + Store.shared.set(key: "\(self.title)_HTTPHost", value: self.connectivityHTTPHost) + self.connectivityHostCallback(self.connectivityHTTPHost.isEmpty) + } else { + self.connectivityICMPHost = field.stringValue + Store.shared.set(key: "\(self.title)_ICMPHost", value: self.connectivityICMPHost) + self.connectivityHostCallback(self.connectivityICMPHost.isEmpty) + } } else if field.identifier == NSUserInterfaceItemIdentifier("text") { self.textValue = field.stringValue Store.shared.set(key: "\(self.title)_textWidgetValue", value: self.textValue) @@ -333,7 +346,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { } @objc private func changeICMPUpdateInterval(_ sender: NSMenuItem) { guard let key = sender.representedObject as? String, let value = Int(key) else { return } - self.updateICMPIntervalValue = value + self.updateConnectivityIntervalValue = value Store.shared.set(key: "\(self.title)_updateICMPInterval", value: value) self.setInterval(value) } @@ -354,4 +367,13 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { self.baseValue = key Store.shared.set(key: "\(self.title)_base", value: self.baseValue) } + @objc private func changeConnectivityMode(_ sender: NSMenuItem) { + guard let key = sender.representedObject as? String else { return } + self.connectivityMode = ConnectivityReader.ConnectivityMode(rawValue: key) ?? .icmp + Store.shared.set(key: "\(self.title)_connectivityMode", value: self.connectivityMode.rawValue) + self.connectivityHostField?.stringValue = self.connectivityICMPHost + if self.connectivityMode == .http { + self.connectivityHostField?.stringValue = self.connectivityHTTPHost + } + } }