mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 12:02:14 +00:00
Trust surviving tunnels, skip doomed cellular reconnects, log hold health
Field testing on device contradicted the assumption behind the interface-change rebuild: connections established over LocalDevVPN's loopback SURVIVE a Wi-Fi<->cellular handoff (an active location-sim session kept working across the switch). Tearing the tunnel state down on interface change therefore destroyed a working tunnel and, on cellular — where a fresh connect is refused because nothing listens on port 49152 without a local network interface — replaced it with a failure. Keep connected tunnels alone. For the same reason, skip the automatic reconnect cycle when the path is cellular-only: three retries with backoff that are refused every time, on every path flap, forever. Manual attempts still go through start() directly. Also count resend results in LocationSimulationKeepAlive and log transitions — first failure, recovery, and a heartbeat every ~5 minutes while failing — so the Logs tab shows whether a held location is actually being applied instead of failing silently. Counter resets on hold/release so stale counts can't fake a recovery message. Not verified on device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ef78da3c05
commit
29eb183861
2 changed files with 61 additions and 15 deletions
|
|
@ -39,6 +39,7 @@ final class LocationSimulationKeepAlive {
|
|||
private var coordinate: CLLocationCoordinate2D?
|
||||
private var timer: DispatchSourceTimer?
|
||||
private var isHolding = false
|
||||
private var consecutiveResendFailures = 0
|
||||
|
||||
/// Main-thread only, mirroring `DebugKeepAliveLease`.
|
||||
private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
||||
|
|
@ -65,6 +66,9 @@ final class LocationSimulationKeepAlive {
|
|||
stateLock.lock()
|
||||
let isFirstHold = coordinate == nil
|
||||
coordinate = newCoordinate
|
||||
if isFirstHold {
|
||||
consecutiveResendFailures = 0
|
||||
}
|
||||
stateLock.unlock()
|
||||
|
||||
persist(newCoordinate)
|
||||
|
|
@ -126,6 +130,7 @@ final class LocationSimulationKeepAlive {
|
|||
coordinate = nil
|
||||
timer?.cancel()
|
||||
timer = nil
|
||||
consecutiveResendFailures = 0
|
||||
stateLock.unlock()
|
||||
|
||||
persist(nil)
|
||||
|
|
@ -170,13 +175,40 @@ final class LocationSimulationKeepAlive {
|
|||
// set fails, so a dropped tunnel (e.g. a Wi-Fi<->cellular handoff) heals
|
||||
// on the next tick. A failure here is expected and transient; the next
|
||||
// resend retries.
|
||||
LocationSimulationCommandQueue.shared.async {
|
||||
_ = simulate_location(
|
||||
LocationSimulationCommandQueue.shared.async { [weak self] in
|
||||
let code = simulate_location(
|
||||
DeviceConnectionContext.targetIPAddress,
|
||||
target.latitude,
|
||||
target.longitude,
|
||||
PairingFileStore.prepareURL().path
|
||||
)
|
||||
self?.noteResendResult(code)
|
||||
}
|
||||
}
|
||||
|
||||
/// Surfaces hold health in the log without spamming it: the first failure,
|
||||
/// the recovery, and a heartbeat roughly every five minutes while failing.
|
||||
private func noteResendResult(_ code: Int32) {
|
||||
stateLock.lock()
|
||||
let previousFailures = consecutiveResendFailures
|
||||
consecutiveResendFailures = code == 0 ? 0 : previousFailures + 1
|
||||
let failures = consecutiveResendFailures
|
||||
stateLock.unlock()
|
||||
|
||||
if code == 0 {
|
||||
if previousFailures > 0 {
|
||||
LogManager.shared.addInfoLog(
|
||||
"Simulated location resend recovered after \(previousFailures) failed attempt(s)"
|
||||
)
|
||||
}
|
||||
} else if failures == 1 {
|
||||
LogManager.shared.addWarningLog(
|
||||
"Simulated location resend failed (error \(code)); retrying every \(Int(Self.resendInterval))s"
|
||||
)
|
||||
} else if failures % 75 == 0 {
|
||||
LogManager.shared.addWarningLog(
|
||||
"Simulated location resend still failing after \(failures) attempts (error \(code)) — the held location is NOT being applied"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -127,23 +127,37 @@ final class TunnelManager: ObservableObject {
|
|||
|
||||
let previous = lastInterfaceName
|
||||
lastInterfaceName = status.interfaceName
|
||||
let interfaceChanged = previous != nil && previous != status.interfaceName
|
||||
|
||||
if !isConnected {
|
||||
LogManager.shared.addInfoLog("Network path changed (\(status.description)); connecting tunnel")
|
||||
start(showErrorUI: false)
|
||||
// A tunnel established earlier survives a Wi-Fi↔cellular handoff: the
|
||||
// connection rides LocalDevVPN's loopback, so the physical interface
|
||||
// underneath it doesn't matter (verified on device — an active session
|
||||
// kept working across the switch). Rebuilding here would tear down a
|
||||
// live tunnel and, on cellular, fail to replace it.
|
||||
guard !isConnected else {
|
||||
if interfaceChanged {
|
||||
LogManager.shared.addInfoLog(
|
||||
"Interface changed (\(previous ?? "?") → \(status.interfaceName)); keeping existing tunnel"
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// isConnected only means "the last connect succeeded" — nothing in the app
|
||||
// notices a tunnel that died quietly, which is exactly what a handoff does.
|
||||
// So don't trust it: if the physical interface actually changed, treat the
|
||||
// old tunnel as dead and rebuild. startTunnel() is semaphore-guarded and
|
||||
// recreates its adapter, so reconnecting a live tunnel is safe.
|
||||
guard let previous, previous != status.interfaceName else { return }
|
||||
LogManager.shared.addInfoLog(
|
||||
"Interface changed (\(previous) → \(status.interfaceName)); rebuilding tunnel"
|
||||
)
|
||||
markDisconnected()
|
||||
// A fresh connect needs the on-device service listening on port 49152,
|
||||
// and iOS only binds it when a real local network interface exists.
|
||||
// Cellular-only refuses the connect outright, so an automatic retry
|
||||
// cycle just burns battery. Manual attempts (the user tapping) still go
|
||||
// through start() directly.
|
||||
guard status.interface != .cellular else {
|
||||
if interfaceChanged {
|
||||
LogManager.shared.addInfoLog(
|
||||
"On cellular without Wi-Fi; deferring tunnel reconnect until a local network is available"
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
LogManager.shared.addInfoLog("Network path changed (\(status.description)); reconnecting tunnel")
|
||||
start(showErrorUI: false)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue