Prevent deadlocks by avoiding calling callbacks in locked blocks

This commit is contained in:
Mygod
2020-05-23 05:52:32 +08:00
parent 0cb1e7346e
commit 4336632508
5 changed files with 83 additions and 66 deletions

View File

@@ -25,18 +25,24 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
val properties = app.connectivity.getLinkProperties(network) val properties = app.connectivity.getLinkProperties(network)
val ifname = properties?.interfaceName ?: return val ifname = properties?.interfaceName ?: return
var switching = false
synchronized(this@DefaultNetworkMonitor) { synchronized(this@DefaultNetworkMonitor) {
val oldProperties = currentLinkProperties val oldProperties = currentLinkProperties
if (currentNetwork != network || ifname != oldProperties?.interfaceName) { if (currentNetwork != network || ifname != oldProperties?.interfaceName) {
callbacks.forEach { it.onLost() } // we are using the other default network now switching = true // we are using the other default network now
currentNetwork = network currentNetwork = network
} }
currentLinkProperties = properties currentLinkProperties = properties
callbacks.forEach { it.onAvailable(ifname, properties) } callbacks.toList()
}.forEach {
if (switching) it.onLost()
it.onAvailable(ifname, properties)
} }
} }
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
var losing = true
var ifname: String?
synchronized(this@DefaultNetworkMonitor) { synchronized(this@DefaultNetworkMonitor) {
if (currentNetwork == null) { if (currentNetwork == null) {
onAvailable(network) onAvailable(network)
@@ -45,30 +51,28 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
if (currentNetwork != network) return if (currentNetwork != network) return
val oldProperties = currentLinkProperties!! val oldProperties = currentLinkProperties!!
currentLinkProperties = properties currentLinkProperties = properties
val ifname = properties.interfaceName ifname = properties.interfaceName
when { when (ifname) {
ifname == null -> { null -> Timber.w("interfaceName became null: $oldProperties -> $properties")
Timber.w("interfaceName became null: $oldProperties -> $properties") oldProperties.interfaceName -> losing = false
onLost(network) else -> Timber.w(RuntimeException("interfaceName changed: $oldProperties -> $properties"))
}
ifname != oldProperties.interfaceName -> {
Timber.w(RuntimeException("interfaceName changed: $oldProperties -> $properties"))
callbacks.forEach {
it.onLost()
it.onAvailable(ifname, properties)
}
}
else -> callbacks.forEach { it.onAvailable(ifname, properties) }
} }
callbacks.toList()
}.forEach {
if (losing) {
if (ifname == null) return onLost(network)
it.onLost()
}
ifname?.let { ifname -> it.onAvailable(ifname, properties) }
} }
} }
override fun onLost(network: Network) = synchronized(this@DefaultNetworkMonitor) { override fun onLost(network: Network) = synchronized(this@DefaultNetworkMonitor) {
if (currentNetwork != network) return if (currentNetwork != network) return
callbacks.forEach { it.onLost() }
currentNetwork = null currentNetwork = null
currentLinkProperties = null currentLinkProperties = null
} callbacks.toList()
}.forEach { it.onLost() }
} }
override fun registerCallbackLocked(callback: Callback) { override fun registerCallbackLocked(callback: Callback) {

View File

@@ -27,10 +27,10 @@ abstract class FallbackUpstreamMonitor private constructor() : UpstreamMonitor()
synchronized(this) { synchronized(this) {
val old = monitor val old = monitor
val callbacks = synchronized(old) { val callbacks = synchronized(old) {
val callbacks = old.callbacks.toList() old.callbacks.toList().also {
old.callbacks.clear() old.callbacks.clear()
old.destroyLocked() old.destroyLocked()
callbacks }
} }
val new = generateMonitor() val new = generateMonitor()
monitor = new monitor = new

View File

@@ -1,21 +1,27 @@
package be.mygod.vpnhotspot.net.monitor package be.mygod.vpnhotspot.net.monitor
import android.net.LinkProperties
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class InterfaceMonitor(val iface: String) : UpstreamMonitor() { class InterfaceMonitor(val iface: String) : UpstreamMonitor() {
private fun setPresent(present: Boolean) = synchronized(this) { private fun setPresent(present: Boolean) {
val old = currentIface != null var available: Pair<String, LinkProperties>? = null
if (present == old) return synchronized(this) {
currentIface = if (present) iface else null val old = currentIface != null
if (present) Pair(iface, currentLinkProperties) else null if (present == old) return
}.let { pair -> currentIface = if (present) iface else null
if (pair != null) { if (present) available = iface to (currentLinkProperties ?: return)
val (iface, lp) = pair callbacks.toList()
lp ?: return }.forEach {
callbacks.forEach { it.onAvailable(iface, lp) } @Suppress("NAME_SHADOWING")
} else callbacks.forEach { it.onLost() } val available = available
if (available != null) {
val (iface, lp) = available
it.onAvailable(iface, lp)
} else it.onLost()
}
} }
private var registered = false private var registered = false

View File

@@ -8,8 +8,6 @@ import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.ConcurrentHashMap
abstract class UpstreamMonitor { abstract class UpstreamMonitor {
companion object : SharedPreferences.OnSharedPreferenceChangeListener { companion object : SharedPreferences.OnSharedPreferenceChangeListener {
@@ -40,11 +38,10 @@ abstract class UpstreamMonitor {
synchronized(this) { synchronized(this) {
val old = monitor val old = monitor
val (active, callbacks) = synchronized(old) { val (active, callbacks) = synchronized(old) {
val active = old.currentIface != null (old.currentIface != null) to old.callbacks.toList().also {
val callbacks = old.callbacks.toList() old.callbacks.clear()
old.callbacks.clear() old.destroyLocked()
old.destroyLocked() }
Pair(active, callbacks)
} }
val new = generateMonitor() val new = generateMonitor()
monitor = new monitor = new
@@ -78,7 +75,7 @@ abstract class UpstreamMonitor {
} }
} }
val callbacks = Collections.newSetFromMap(ConcurrentHashMap<Callback, Boolean>()) val callbacks = mutableSetOf<Callback>()
protected abstract val currentLinkProperties: LinkProperties? protected abstract val currentLinkProperties: LinkProperties?
open val currentIface: String? get() = currentLinkProperties?.interfaceName open val currentIface: String? get() = currentLinkProperties?.interfaceName
protected abstract fun registerCallbackLocked(callback: Callback) protected abstract fun registerCallbackLocked(callback: Callback)

View File

@@ -23,17 +23,23 @@ object VpnMonitor : UpstreamMonitor() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
val properties = app.connectivity.getLinkProperties(network) val properties = app.connectivity.getLinkProperties(network)
val ifname = properties?.interfaceName ?: return val ifname = properties?.interfaceName ?: return
var switching = false
synchronized(this@VpnMonitor) { synchronized(this@VpnMonitor) {
val oldProperties = available.put(network, properties) val oldProperties = available.put(network, properties)
if (currentNetwork != network || ifname != oldProperties?.interfaceName) { if (currentNetwork != network || ifname != oldProperties?.interfaceName) {
if (currentNetwork != null) callbacks.forEach { it.onLost() } if (currentNetwork != null) switching = true
currentNetwork = network currentNetwork = network
} }
callbacks.forEach { it.onAvailable(ifname, properties) } callbacks.toList()
}.forEach {
if (switching) it.onLost()
it.onAvailable(ifname, properties)
} }
} }
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
var losing = true
var ifname: String?
synchronized(this@VpnMonitor) { synchronized(this@VpnMonitor) {
if (currentNetwork == null) { if (currentNetwork == null) {
onAvailable(network) onAvailable(network)
@@ -41,33 +47,37 @@ object VpnMonitor : UpstreamMonitor() {
} }
if (currentNetwork != network) return if (currentNetwork != network) return
val oldProperties = available.put(network, properties)!! val oldProperties = available.put(network, properties)!!
val ifname = properties.interfaceName ifname = properties.interfaceName
when { when (ifname) {
ifname == null -> { null -> Timber.w("interfaceName became null: $oldProperties -> $properties")
Timber.w("interfaceName became null: $oldProperties -> $properties") oldProperties.interfaceName -> losing = false
onLost(network) else -> Timber.w(RuntimeException("interfaceName changed: $oldProperties -> $properties"))
}
ifname != oldProperties.interfaceName -> {
Timber.w("interfaceName changed: $oldProperties -> $properties")
callbacks.forEach {
it.onLost()
it.onAvailable(ifname, properties)
}
}
else -> callbacks.forEach { it.onAvailable(ifname, properties) }
} }
callbacks.toList()
}.forEach {
if (losing) {
if (ifname == null) return onLost(network)
it.onLost()
}
ifname?.let { ifname -> it.onAvailable(ifname, properties) }
} }
} }
override fun onLost(network: Network) = synchronized(this@VpnMonitor) { override fun onLost(network: Network) {
if (available.remove(network) == null || currentNetwork != network) return var newProperties: LinkProperties? = null
callbacks.forEach { it.onLost() } synchronized(this@VpnMonitor) {
if (available.isNotEmpty()) { if (available.remove(network) == null || currentNetwork != network) return
val next = available.entries.first() if (available.isNotEmpty()) {
currentNetwork = next.key val next = available.entries.first()
Timber.d("Switching to ${next.value.interfaceName} as VPN interface") currentNetwork = next.key
callbacks.forEach { it.onAvailable(next.value.interfaceName!!, next.value) } Timber.d("Switching to ${next.value.interfaceName} as VPN interface")
} else currentNetwork = null newProperties = next.value
} else currentNetwork = null
callbacks.toList()
}.forEach {
it.onLost()
newProperties?.let { prop -> it.onAvailable(prop.interfaceName!!, prop) }
}
} }
} }