Skip to content

Commit 02e7b95

Browse files
committed
feat: logRateLimiter
(cherry picked from commit a9d52cd)
1 parent 64e8584 commit 02e7b95

3 files changed

Lines changed: 71 additions & 0 deletions

File tree

ClashX.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
0106179F2AF38EFA005C7877 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FEC6682AD9369C00BAD9F5 /* Command.swift */; };
11+
0114F0CF2E5B60CB007C7AAC /* LogRateLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0114F0CE2E5B60CB007C7AAC /* LogRateLimiter.swift */; };
1112
0162E74F2864B819007218A6 /* MetaTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0162E74E2864B819007218A6 /* MetaTask.swift */; };
1213
0197255A2CA15D6400C14E49 /* UserNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019725592CA15D6400C14E49 /* UserNotificationCenter.swift */; };
1314
0197255C2CA15FC600C14E49 /* NSWorkspace+openFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0197255B2CA15FC600C14E49 /* NSWorkspace+openFile.swift */; };
@@ -214,6 +215,7 @@
214215
/* End PBXCopyFilesBuildPhase section */
215216

216217
/* Begin PBXFileReference section */
218+
0114F0CE2E5B60CB007C7AAC /* LogRateLimiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRateLimiter.swift; sourceTree = "<group>"; };
217219
015F1E90288E42A50052B20A /* ClashMetaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashMetaConfig.swift; sourceTree = "<group>"; };
218220
0162E74D2864B818007218A6 /* com.metacubex.ClashX.ProxyConfigHelper-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "com.metacubex.ClashX.ProxyConfigHelper-Bridging-Header.h"; sourceTree = "<group>"; };
219221
0162E74E2864B819007218A6 /* MetaTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaTask.swift; sourceTree = "<group>"; };
@@ -597,6 +599,7 @@
597599
49B445152457CDF000B27E3E /* ClashStatusTool.swift */,
598600
49D223382A1DA5F10002FFCB /* SSIDSuspendTool.swift */,
599601
49FEC6682AD9369C00BAD9F5 /* Command.swift */,
602+
0114F0CE2E5B60CB007C7AAC /* LogRateLimiter.swift */,
600603
);
601604
path = Utils;
602605
sourceTree = "<group>";
@@ -1101,6 +1104,7 @@
11011104
01BCDB042C9ECD010028FA94 /* ClashApiDatasStorage.swift in Sources */,
11021105
01BCDB052C9ECD010028FA94 /* RulesView.swift in Sources */,
11031106
01BCDB062C9ECD010028FA94 /* RuleProviderView.swift in Sources */,
1107+
0114F0CF2E5B60CB007C7AAC /* LogRateLimiter.swift in Sources */,
11041108
01BCDB072C9ECD010028FA94 /* SidebarItem.swift in Sources */,
11051109
01BCDB092C9ECD010028FA94 /* DBProxyStorage.swift in Sources */,
11061110
01BCDB0A2C9ECD010028FA94 /* SwiftUIViewExtensions.swift in Sources */,

ClashX/General/ApiRequest.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ class ApiRequest {
9494
private var trafficWebSocketRetryTimer: Timer?
9595
private var loggingWebSocketRetryTimer: Timer?
9696
private var memoryWebSocketRetryTimer: Timer?
97+
98+
private var logRateLimiter = LogRateLimiter {
99+
let alert = NSAlert()
100+
alert.messageText = NSLocalizedString("Log system crashed.", comment: "")
101+
alert.addButton(withTitle: NSLocalizedString("Quit", comment: ""))
102+
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
103+
let response = alert.runModal()
104+
if response == .alertFirstButtonReturn {
105+
NSApplication.shared.terminate(nil)
106+
}
107+
}
97108

98109
private var alamoFireManager: Session
99110

@@ -769,6 +780,7 @@ extension ApiRequest: WebSocketDelegate {
769780
delegate?.didUpdateTraffic(up: json["up"].intValue, down: json["down"].intValue)
770781
dashboardDelegate?.didUpdateTraffic(up: json["up"].intValue, down: json["down"].intValue)
771782
case loggingWebSocket:
783+
guard logRateLimiter.processLog() else { return }
772784
delegate?.didGetLog(log: json["payload"].stringValue, level: json["type"].string ?? "info")
773785
dashboardDelegate?.didGetLog(log: json["payload"].stringValue, level: json["type"].string ?? "info")
774786
case memoryWebSocket:
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
3+
class LogRateLimiter {
4+
private let maxLogsCount: Int = 5000
5+
private let timeDuration: TimeInterval = 5.0
6+
private var logCount: Int = 0
7+
private var startTime: Date = Date()
8+
private let queue = DispatchQueue(label: "clashx.logratelimiter", qos: .utility)
9+
private var isBlocked = false
10+
11+
private let onRateLimitTriggered: () -> Void
12+
13+
init(onRateLimitTriggered: @escaping () -> Void) {
14+
self.onRateLimitTriggered = onRateLimitTriggered
15+
}
16+
17+
// Returns true if log can be processed, false if rate limited
18+
func processLog() -> Bool {
19+
return queue.sync { [weak self] in
20+
guard let self = self, !self.isBlocked else { return false }
21+
22+
let now = Date()
23+
24+
// Reset counter if more than 5 seconds passed
25+
if now.timeIntervalSince(self.startTime) >= self.timeDuration {
26+
self.startTime = now
27+
self.logCount = 0
28+
}
29+
30+
// Check if rate limit exceeded
31+
if self.logCount >= self.maxLogsCount {
32+
self.triggerRateLimit()
33+
return false
34+
}
35+
36+
self.logCount += 1
37+
return true
38+
}
39+
}
40+
41+
private func triggerRateLimit() {
42+
isBlocked = true
43+
44+
DispatchQueue.main.async { [weak self] in
45+
self?.onRateLimitTriggered()
46+
Logger.log("⚠️ Rate limit triggered: >5000 logs/5sec, paused for 1min")
47+
}
48+
49+
// Resume after 5 seconds
50+
DispatchQueue.main.asyncAfter(deadline: .now() + 60.0) { [weak self] in
51+
self?.isBlocked = false
52+
Logger.log("✅ Rate limit resumed")
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)