Skip to content

Commit 1e09248

Browse files
author
ClashFX Team
committed
fix: add 10s traffic watchdog to recover from half-dead websocket
Starscream 3.1.1 does not auto-ping, and macOS TCP keep-alive only fires after ~2 hours. A half-dead connection (NAT timeout, mihomo deadlock, carrier RST without TCP FIN) never raises a disconnect callback, so the speed indicator silently freezes on the last received values even with retry logic in place. Arm a 10s one-shot timer on every traffic message and on every successful traffic connect; if the timer fires it means no data has arrived in 10s and we force resetTrafficStreamApi(), which tears down the zombie socket and reconnects. Cancel the timer when entering direct-API mode (no websocket in use).
1 parent a7ff626 commit 1e09248

1 file changed

Lines changed: 35 additions & 0 deletions

File tree

ClashFX/General/ApiRequest.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ class ApiRequest {
7070
private var loggingWebSocketRetryDelay: TimeInterval = 1
7171
private var trafficWebSocketRetryTimer: Timer?
7272
private var loggingWebSocketRetryTimer: Timer?
73+
private var trafficWatchdogTimer: Timer?
7374
private static let maxRetryDelaySeconds: TimeInterval = 64
75+
private static let trafficWatchdogTimeoutSeconds: TimeInterval = 10
7476

7577
private var alamoFireManager: Session
7678

@@ -516,6 +518,7 @@ extension ApiRequest {
516518

517519
private func requestTrafficInfo() {
518520
if ApiRequest.useDirectApi() {
521+
cancelTrafficWatchdog()
519522
trafficWebSocket?.disconnect(forceTimeout: 0.5)
520523
return
521524
}
@@ -559,6 +562,7 @@ extension ApiRequest: WebSocketDelegate {
559562
guard let webSocket = socket as? WebSocket else { return }
560563
if webSocket == trafficWebSocket {
561564
trafficWebSocketRetryDelay = 1
565+
armTrafficWatchdog()
562566
Logger.log("trafficWebSocket did Connect", level: .debug)
563567
} else {
564568
loggingWebSocketRetryDelay = 1
@@ -607,10 +611,41 @@ extension ApiRequest: WebSocketDelegate {
607611
loggingWebSocketRetryDelay = min(loggingWebSocketRetryDelay * 2, Self.maxRetryDelaySeconds)
608612
}
609613

614+
private func armTrafficWatchdog() {
615+
let arm: () -> Void = { [weak self] in
616+
guard let self = self else { return }
617+
self.trafficWatchdogTimer?.invalidate()
618+
self.trafficWatchdogTimer = Timer.scheduledTimer(
619+
withTimeInterval: Self.trafficWatchdogTimeoutSeconds, repeats: false
620+
) { [weak self] _ in
621+
Logger.log("trafficWebSocket watchdog: no data for \(Self.trafficWatchdogTimeoutSeconds)s, forcing reset", level: .warning)
622+
self?.resetTrafficStreamApi()
623+
}
624+
}
625+
if Thread.isMainThread {
626+
arm()
627+
} else {
628+
DispatchQueue.main.async(execute: arm)
629+
}
630+
}
631+
632+
private func cancelTrafficWatchdog() {
633+
let cancel: () -> Void = { [weak self] in
634+
self?.trafficWatchdogTimer?.invalidate()
635+
self?.trafficWatchdogTimer = nil
636+
}
637+
if Thread.isMainThread {
638+
cancel()
639+
} else {
640+
DispatchQueue.main.async(execute: cancel)
641+
}
642+
}
643+
610644
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
611645
guard let webSocket = socket as? WebSocket else { return }
612646
let json = JSON(parseJSON: text)
613647
if webSocket == trafficWebSocket {
648+
armTrafficWatchdog()
614649
delegate?.didUpdateTraffic(up: json["up"].intValue, down: json["down"].intValue)
615650
} else {
616651
delegate?.didGetLog(log: json["payload"].stringValue, level: json["type"].string ?? "info")

0 commit comments

Comments
 (0)