1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-01-04 22:40:32 +00:00

PlayerUiList: guard list actions with mutex

The new implementation would throw `ConcurrentModificationExceptions`
when destroying the UIs. So let’s play it safe and put the list behind
a mutex.

Adds a helper class `GuardedByMutex` that can be wrapped around a
property to force all use-sites to acquire the lock before doing
anything with the data.
This commit is contained in:
Profpatsch 2024-12-26 15:04:45 +01:00
parent 5b92de4f76
commit cbade0b54d
2 changed files with 87 additions and 19 deletions

View File

@ -1,10 +1,10 @@
package org.schabi.newpipe.player.ui package org.schabi.newpipe.player.ui
import androidx.core.util.Consumer import org.schabi.newpipe.util.GuardedByMutex
import java.util.Optional import java.util.Optional
class PlayerUiList(vararg initialPlayerUis: PlayerUi) { class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
val playerUis = mutableListOf<PlayerUi>() var playerUis = GuardedByMutex(mutableListOf<PlayerUi>())
/** /**
* Creates a [PlayerUiList] starting with the provided player uis. The provided player uis * Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
@ -16,7 +16,9 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
* @param initialPlayerUis the player uis this list should start with; the order will be kept * @param initialPlayerUis the player uis this list should start with; the order will be kept
*/ */
init { init {
playerUis.addAll(listOf(*initialPlayerUis)) playerUis.runWithLockSync {
lockData.addAll(listOf(*initialPlayerUis))
}
} }
/** /**
@ -41,7 +43,9 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
} }
} }
playerUis.add(playerUi) playerUis.runWithLockSync {
lockData.add(playerUi)
}
} }
/** /**
@ -52,12 +56,24 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
* @param T the class type parameter </T> * @param T the class type parameter </T>
* */ * */
fun <T> destroyAll(playerUiType: Class<T?>) { fun <T> destroyAll(playerUiType: Class<T?>) {
for (ui in playerUis) { val toDestroy = mutableListOf<PlayerUi>()
if (playerUiType.isInstance(ui)) {
ui.destroyPlayer() // short blocking removal from class to prevent interfering from other threads
ui.destroy() playerUis.runWithLockSync {
playerUis.remove(ui) val new = mutableListOf<PlayerUi>()
for (ui in lockData) {
if (playerUiType.isInstance(ui)) {
toDestroy.add(ui)
} else {
new.add(ui)
}
} }
lockData = new
}
// then actually destroy the UIs
for (ui in toDestroy) {
ui.destroyPlayer()
ui.destroy()
} }
} }
@ -67,18 +83,19 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
* @param T the class type parameter * @param T the class type parameter
* @return the first player UI of the required type found in the list, or null * @return the first player UI of the required type found in the list, or null
</T> */ </T> */
fun <T> get(playerUiType: Class<T>): T? { fun <T> get(playerUiType: Class<T>): T? =
for (ui in playerUis) { playerUis.runWithLockSync {
if (playerUiType.isInstance(ui)) { for (ui in lockData) {
when (val r = playerUiType.cast(ui)) { if (playerUiType.isInstance(ui)) {
// try all UIs before returning null when (val r = playerUiType.cast(ui)) {
null -> continue // try all UIs before returning null
else -> return r null -> continue
else -> return@runWithLockSync r
}
} }
} }
return@runWithLockSync null
} }
return null
}
/** /**
* @param playerUiType the class of the player UI to return; * @param playerUiType the class of the player UI to return;
@ -96,7 +113,11 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
* @param consumer the consumer to call with player UIs * @param consumer the consumer to call with player UIs
*/ */
fun call(consumer: java.util.function.Consumer<PlayerUi>) { fun call(consumer: java.util.function.Consumer<PlayerUi>) {
for (ui in playerUis) { // copy the list out of the mutex before calling the consumer which might block
val new = playerUis.runWithLockSync {
lockData.toMutableList()
}
for (ui in new) {
consumer.accept(ui) consumer.accept(ui)
} }
} }

View File

@ -0,0 +1,47 @@
package org.schabi.newpipe.util
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/** Guard the given data so that it can only be accessed by locking the mutex first.
*
* Inspired by [this blog post](https://jonnyzzz.com/blog/2017/03/01/guarded-by-lock/)
* */
class GuardedByMutex<T>(
private var data: T,
private val lock: Mutex = Mutex(locked = false),
) {
/** Lock the mutex and access the data, blocking the current thread.
* @param action to run with locked mutex
* */
fun <Y> runWithLockSync(
action: MutexData<T>.() -> Y
) =
runBlocking {
lock.withLock {
MutexData(data, { d -> data = d }).action()
}
}
/** Lock the mutex and access the data, suspending the coroutine.
* @param action to run with locked mutex
* */
suspend fun <Y> runWithLock(action: MutexData<T>.() -> Y) =
lock.withLock {
MutexData(data, { d -> data = d }).action()
}
}
/** The data inside a [GuardedByMutex], which can be accessed via [lockData].
* [lockData] is a `var`, so you can `set` it as well.
* */
class MutexData<T>(data: T, val setFun: (T) -> Unit) {
/** The data inside this [GuardedByMutex] */
var lockData: T = data
set(t) {
setFun(t)
field = t
}
}