diff --git a/Kit/plugins/Remote.swift b/Kit/plugins/Remote.swift index dbb86ae5..2400a866 100644 --- a/Kit/plugins/Remote.swift +++ b/Kit/plugins/Remote.swift @@ -439,6 +439,11 @@ public class RemoteAuth { private var interval: Int = 5 private var repeater: Repeater? + private var lastValidationTime: Date? + private var validationAttempts: Int = 0 + private let baseCooldown: TimeInterval = 2.0 // Start with 2 seconds + private let maxCooldown: TimeInterval = 60.0 // Max 60 seconds + public init() { NotificationCenter.default.addObserver(self, selector: #selector(self.successLogin), name: .remoteLoginSuccess, object: nil) } @@ -452,6 +457,14 @@ public class RemoteAuth { completion(false) return } + + if !self.accessToken.isEmpty && !self.isTokenExpired() { + DispatchQueue.main.async { + completion(true) + } + return + } + self.validate(completion) } public func hasCredentials() -> Bool { @@ -499,6 +512,19 @@ public class RemoteAuth { return } + let now = Date() + let dynamicCooldown = min(self.baseCooldown * pow(2.0, Double(self.validationAttempts)), self.maxCooldown) + if let lastTime = self.lastValidationTime, now.timeIntervalSince(lastTime) < dynamicCooldown { + let remainingTime = dynamicCooldown - now.timeIntervalSince(lastTime) + DispatchQueue.main.asyncAfter(deadline: .now() + remainingTime) { + self.validate(completion) + } + return + } + + self.lastValidationTime = now + self.validationAttempts += 1 + var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization") @@ -511,10 +537,18 @@ public class RemoteAuth { if httpResponse.statusCode == 401 { self.refreshTokenFunc { ok in + if ok == true { + self.validationAttempts = 0 + self.lastValidationTime = nil + } completion(ok ?? false) } } else if httpResponse.statusCode == 200 { + self.validationAttempts = 0 + self.lastValidationTime = nil completion(true) + } else { + completion(false) } }.resume() } @@ -642,6 +676,28 @@ public class RemoteAuth { self.accessToken = accessToken self.refreshToken = refreshToken } + + private func isTokenExpired() -> Bool { + let parts = self.accessToken.components(separatedBy: ".") + guard parts.count == 3 else { return true } + + let payload = parts[1] + var base64 = payload + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + while base64.count % 4 != 0 { + base64 += "=" + } + + guard let data = Data(base64Encoded: base64), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let exp = json["exp"] as? TimeInterval else { + return true + } + + return Date().timeIntervalSince1970 >= exp + } } struct MQTTMessage { @@ -671,7 +727,9 @@ class MQTTManager: NSObject { private var session: URLSession? private var isConnected = false private var isDisconnected = false - private let reconnectDelay: TimeInterval = 3.0 + private var isReconnecting = false + private var reconnectAttempts = 0 + private var maxReconnectDelay: TimeInterval = 60.0 private var pingTimer: Timer? private var reachability: Reachability = Reachability(start: true) private let log: NextLog @@ -727,12 +785,29 @@ class MQTTManager: NSObject { } private func reconnect() { - guard !self.isDisconnected else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + self.reconnectDelay) { [weak self] in - if let log = self?.log { - debug("trying to reconnect MQTT after interruption", log: log) + guard !self.isDisconnected && !self.isReconnecting else { return } + + self.isReconnecting = true + + let delays: [TimeInterval] = [1, 3, 5, 10, 20, 40] + let delayIndex = min(self.reconnectAttempts, delays.count - 1) + let delay = self.reconnectAttempts >= delays.count ? self.maxReconnectDelay : delays[delayIndex] + + debug("Waiting \(delay) seconds before next MQTT reconnection attempt...", log: self.log) + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let self else { return } + + self.isReconnecting = false + + guard !self.isDisconnected && !self.isConnected else { + self.reconnectAttempts = 0 + return } - self?.connect() + + self.reconnectAttempts += 1 + debug("Attempting MQTT reconnection #\(self.reconnectAttempts)", log: self.log) + self.connect() } } @@ -929,6 +1004,8 @@ class MQTTManager: NSObject { let returnCode = data[3] if returnCode == 0 { self.isConnected = true + self.isReconnecting = false + self.reconnectAttempts = 0 self.startPingTimer() self.subscribeToControlTopics() self.sendStatus(true) diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index a2f85e1e..7b6cc1c6 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -2871,7 +2871,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 2.11.50; + MARKETING_VERSION = 2.11.51; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2908,7 +2908,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 2.11.50; + MARKETING_VERSION = 2.11.51; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Stats/Supporting Files/Info.plist b/Stats/Supporting Files/Info.plist index c84b04a7..ddc54def 100755 --- a/Stats/Supporting Files/Info.plist +++ b/Stats/Supporting Files/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 713 + 716 Description Simple macOS system monitor in your menu bar LSApplicationCategoryType diff --git a/Widgets/Supporting Files/Info.plist b/Widgets/Supporting Files/Info.plist index d8d07ea3..3614b6c1 100644 --- a/Widgets/Supporting Files/Info.plist +++ b/Widgets/Supporting Files/Info.plist @@ -11,9 +11,9 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleShortVersionString - 2.11.50 + 2.11.51 CFBundleVersion - 713 + 716 NSExtension NSExtensionPointIdentifier