Skip to content

Fix #68: Traffic statistics freeze after app process restart - #71

Open
aimkiray wants to merge 1 commit into
meow-rs:mainfrom
aimkiray:fix/traffic-stats-freeze-68
Open

Fix #68: Traffic statistics freeze after app process restart#71
aimkiray wants to merge 1 commit into
meow-rs:mainfrom
aimkiray:fix/traffic-stats-freeze-68

Conversation

@aimkiray

Copy link
Copy Markdown

Summary

Fixes #68

Supersedes closed PR #67, which was incompatible with main after the Compose migration (#63) and mihomo→meow rename (#57).

Problem

When the app process is killed by the system (memory pressure) while the :vpn process stays alive, traffic statistics freeze permanently. After restarting the app, the traffic chart and speed display stop updating and never recover until the VPN is manually stopped and reconnected.

Root cause

BaseService.Binder maintains two parallel data structures for bandwidth listeners:

private val callbacks = RemoteCallbackList<IMeowServiceCallback>()
private val bandwidthListeners = mutableMapOf<IBinder, Long>()

RemoteCallbackList automatically removes dead binders when a callback's process dies. However, the plain MutableMap does not — and onCallbackDied was not overridden to mirror the cleanup. After the app process is killed and restarted:

  1. The stale dead binder entry remains in bandwidthListeners
  2. startListeningForBandwidth guards on isEmpty():
if (bandwidthListeners.isEmpty() && bandwidthListeners.put(cb.asBinder(), timeout) == null) {
    looper = launch { loop() }
}
  1. Due to short-circuit evaluation, isEmpty() == false (stale entry) → put() is never called → new listener is never registered
  2. In loop(), bandwidthListeners.contains(newBinder) returns falsetrafficUpdated is never called for the new callback
  3. Permanent freeze — the looper keeps running for nobody, and the new callback never receives traffic updates

Fix

BaseService.Binder:

  • Override RemoteCallbackList.onCallbackDied to remove the dead binder from bandwidthListeners and cancel the looper when empty
  • Remove the isEmpty() guard in startListeningForBandwidth; always register the listener (bandwidthListeners[cb.asBinder()] = timeout); start the looper only if not already active

MeowConnection (defensive improvements):

  • Add bound flag and bindContext to support rebinding
  • Rebind in onServiceDisconnected via BIND_AUTO_CREATE so service reference is restored sooner after :vpn death
  • @Synchronized on connect()/disconnect()/onServiceDisconnected() for thread safety
  • bound flag prevents double-unbindService crash

Note on MeowConnection changes: In the current codebase, connect(), disconnect(), and onServiceDisconnected() all execute on the main thread (connect/disconnect from Activity lifecycle, onServiceDisconnected from system callback). @Synchronized and the bound flag are therefore defensive measures, not fixes for an observed race. The rebind creates a new :vpn process in State.Stopped (BIND_AUTO_CREATE calls onCreate+onBind but not onStartCommand), so the VPN does not auto-start — the user still needs to manually reconnect. These changes improve robustness but are not strictly bug fixes. They are included in this PR because issue #68 describes both the stale-listener bug and the missing rebind.

Testing

Device-tested on physical Android 16 (SDK 36):

Test Method Result
A1 VPN connected (tun0 up)
A2 Record PIDs (vpn=17871, app=13488)
A3 Traffic baseline: RX +2060 bytes
A4 Press HOME → am kill kills app, :vpn survives (same PID 17871)
A5 tun0 persists after app kill
A6 Reopen app (new PID=14014)
A7 Traffic stats resume: RX +3224 bytes — NOT FROZEN
A8 :vpn PID unchanged (17871) throughout

The previous PR #67 tested the wrong scenario (killing :vpn instead of the app process). This PR tests the actual bug trigger: killing the app process while :vpn survives.

Files changed

  • core/.../bg/BaseService.ktonCallbackDied override + unconditional listener registration
  • core/.../aidl/MeowConnection.kt — rebind + @Synchronized + bound flag

Two independent bugs caused traffic stats to freeze when the :vpn or
app process restarted:

1. BaseService.Binder used a plain RemoteCallbackList for callbacks but
   a separate bandwidthListeners map that was never cleaned up when a
   callback binder died. After an app-process restart, the stale entry
   kept the 1 Hz traffic looper running for nobody, and
   startListeningForBandwidth only added a listener when the map was
   empty — so the new binder was silently skipped. Override
   onCallbackDied to remove the dead entry and cancel the looper when
   empty; always (re)register the listener.

2. MeowConnection never rebound after onServiceDisconnected. When the
   :vpn process was killed (OOM, crash), the connection was lost but
   never re-established. Remember the binding context; rebind in
   onServiceDisconnected. @synchronized on connect/disconnect/
   onServiceDisconnected prevents concurrent access; disconnect()
   clears bindContext first so a racing rebind is a no-op after a
   clean unbind.

Fixes meow-rs#68
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Traffic statistics freeze after VPN process restart

1 participant