From fbd086f7daebb889e43480f194e211e83781fcba Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:52:08 -0400 Subject: [PATCH 1/2] fix(net): stop blocking ConnectivityThread; offer network events non-blocking DefaultNetworkListener.Callback delivered onAvailable/onCapabilitiesChanged/onLost via runBlocking { networkActor.send(...) }, parking the framework ConnectivityThread (API 23 path) / main handler until the actor received - a stall/ANR risk during rapid network flaps. Make the actor UNLIMITED-capacity and replace the three runBlocking sends with non-blocking trySend (always enqueues for these low-rate control messages). Actor message-handling logic unchanged; get()/start()/stop() unchanged. --- .../sagernet/utils/DefaultNetworkListener.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt index f0cd8f8ca..ce7673505 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt @@ -13,8 +13,8 @@ import io.nekohasekai.sagernet.ktx.Logs import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.runBlocking import java.net.UnknownHostException object DefaultNetworkListener { @@ -31,7 +31,10 @@ object DefaultNetworkListener { class Lost(val network: Network) : NetworkMessage() } - private val networkActor = GlobalScope.actor(Dispatchers.Unconfined) { + private val networkActor = GlobalScope.actor( + Dispatchers.Unconfined, + capacity = Channel.UNLIMITED, + ) { val listeners = mutableMapOf Unit>() var network: Network? = null val pendingRequests = arrayListOf() @@ -95,15 +98,21 @@ object DefaultNetworkListener { suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key)) - // NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26 + // NB: this runs in ConnectivityThread; offer events non-blocking (trySend) so we never + // park the framework callback thread on the actor. The actor is UNLIMITED, so trySend + // always enqueues for these low-rate control messages. private object Callback : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) = runBlocking { networkActor.send(NetworkMessage.Put(network)) } + override fun onAvailable(network: Network) { + networkActor.trySend(NetworkMessage.Put(network)) + } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { // it's a good idea to refresh capabilities - runBlocking { networkActor.send(NetworkMessage.Update(network)) } + networkActor.trySend(NetworkMessage.Update(network)) } - override fun onLost(network: Network) = runBlocking { networkActor.send(NetworkMessage.Lost(network)) } + override fun onLost(network: Network) { + networkActor.trySend(NetworkMessage.Lost(network)) + } } private var fallback = false From 1de8f8bfa5891490f44674000496c0943a51f524 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:01:10 -0400 Subject: [PATCH 2/2] fix(net): log dropped network events instead of silently discarding trySend Address CodeRabbit: with UNLIMITED capacity trySend only fails if the actor channel is closed (actor coroutine died); route the three callbacks through a helper that logs a warning on failure rather than silently dropping, so a dead network actor is diagnosable. --- .../sagernet/utils/DefaultNetworkListener.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt index ce7673505..9c3eb9c08 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt @@ -99,19 +99,27 @@ object DefaultNetworkListener { suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key)) // NB: this runs in ConnectivityThread; offer events non-blocking (trySend) so we never - // park the framework callback thread on the actor. The actor is UNLIMITED, so trySend - // always enqueues for these low-rate control messages. + // park the framework callback thread on the actor. The actor is UNLIMITED, so trySend only + // fails if the actor channel is closed (actor coroutine died) - log that rather than + // silently dropping, since it means network-change handling has stopped working. + private fun offer(message: NetworkMessage) { + val result = networkActor.trySend(message) + if (result.isFailure) { + Logs.w("DefaultNetworkListener: dropped ${message.javaClass.simpleName} (actor closed?)") + } + } + private object Callback : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { - networkActor.trySend(NetworkMessage.Put(network)) + offer(NetworkMessage.Put(network)) } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { // it's a good idea to refresh capabilities - networkActor.trySend(NetworkMessage.Update(network)) + offer(NetworkMessage.Update(network)) } override fun onLost(network: Network) { - networkActor.trySend(NetworkMessage.Lost(network)) + offer(NetworkMessage.Lost(network)) } }