mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
fix(plugin): reload Zipline service on same-version replace
- Add `generation` counter to serialized state so DataStore emits on every addPlugin (same-version replace produces identical JSON) - Run all Zipline JNI calls (close, service.close) on ziplineDispatcher thread — wrong thread causes segfaults - Stop ALL services in flatMapLatest finally block instead of only deselected ones (leaked zombie services) - Wrap finally block in NonCancellable + try-catch per service
This commit is contained in:
parent
f05f0727b6
commit
f012defd79
@ -125,6 +125,7 @@ open class ZiplinePluginService(
|
||||
}
|
||||
private val lifecycleMutex = Mutex()
|
||||
private var ziplineLoader: ZiplineLoader
|
||||
private var ziplineInstance: Zipline? = null
|
||||
private val serviceRegistry = mutableMapOf<KClass<*>, ZiplineService>()
|
||||
|
||||
init {
|
||||
@ -271,6 +272,7 @@ open class ZiplinePluginService(
|
||||
return
|
||||
}
|
||||
|
||||
logger.d { "[$applicationName] start(): loading plugin from $manifestUrl" }
|
||||
withContext(ziplineDispatcher.dispatcher) {
|
||||
trace("start(): inside zipline dispatcher before loadOnce")
|
||||
val result = ziplineLoader.loadOnce(
|
||||
@ -280,6 +282,8 @@ open class ZiplinePluginService(
|
||||
)
|
||||
when (result) {
|
||||
is LoadResult.Success -> {
|
||||
logger.d { "[$applicationName] start(): loadOnce succeeded, consuming services" }
|
||||
ziplineInstance = result.zipline
|
||||
// Now we consume the initializer
|
||||
val initializer = result.zipline.take<Initializer>(Initializer_SERVICE_NAME)
|
||||
// Bind host services before initialization, so plugins can use them in their initializer
|
||||
@ -310,10 +314,19 @@ open class ZiplinePluginService(
|
||||
override suspend fun stop() {
|
||||
lifecycleMutex.withLock {
|
||||
trace("stop(): entered")
|
||||
for (service in serviceRegistry.values) {
|
||||
trace("stop(): closing service ${service::class.simpleName}")
|
||||
service.close()
|
||||
withContext(ziplineDispatcher.dispatcher) {
|
||||
ziplineInstance?.close()
|
||||
ziplineInstance = null
|
||||
for (service in serviceRegistry.values) {
|
||||
try {
|
||||
trace("stop(): closing service ${service::class.simpleName}")
|
||||
service.close()
|
||||
} catch (_: Exception) {
|
||||
trace("stop(): error closing service ${service::class.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
serviceRegistry.clear()
|
||||
scope.cancel()
|
||||
loggedInStateFlow.value = false
|
||||
ziplineDispatcher.close()
|
||||
|
||||
@ -34,6 +34,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
@ -147,9 +148,13 @@ class PluginManager(
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val ziplineServices = state
|
||||
.filterIsInstance<PluginManagerStates.Data>()
|
||||
.map { it.selectedPlugins }
|
||||
.map { it.selectedPlugins to it.generation }
|
||||
// Paired with generation so same-version replaces (structurally equal
|
||||
// PluginEntry) still trigger a restart. Without it the DataStore skips
|
||||
// the emission (identical JSON) and the flow stays alive with stale code.
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { plugins ->
|
||||
.flatMapLatest { (plugins, gen) ->
|
||||
logger.d { "ziplineServices: flow restarting with generation=$gen, selectedPlugins=${plugins.keys}" }
|
||||
flow {
|
||||
val ziplineServices: MutableMap<PluginAbility, PluginService?> =
|
||||
mutableMapOf()
|
||||
@ -166,6 +171,7 @@ class PluginManager(
|
||||
ziplineServicesByPluginID[plugin.id] = service
|
||||
service.start()
|
||||
} else {
|
||||
logger.d { "ziplineServices: creating new ZiplinePluginService for ${plugin.name} (${plugin.id})" }
|
||||
val service = ZiplinePluginService(
|
||||
applicationName = plugin.name,
|
||||
manifestUrl = "http://localhost?path=${(pluginsDir / plugin.id.toPath() / "manifest.zipline.json")}",
|
||||
@ -180,12 +186,16 @@ class PluginManager(
|
||||
// Keep the flow alive until the next plugin is selected.
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
// Stop services that are no longer selected
|
||||
val selectedPluginIDs = plugins.values.map { it.id }.toSet()
|
||||
ziplineServicesByPluginID.forEach { (pluginID, service) ->
|
||||
if (!selectedPluginIDs.contains(pluginID)) {
|
||||
service.stop()
|
||||
withContext(NonCancellable) {
|
||||
logger.d { "ziplineServices: finally block stopping ${ziplineServicesByPluginID.size} services" }
|
||||
ziplineServicesByPluginID.forEach { (_, service) ->
|
||||
try {
|
||||
service.stop()
|
||||
} catch (_: Exception) {
|
||||
logger.e { "ziplineServices: error stopping service" }
|
||||
}
|
||||
}
|
||||
logger.d { "ziplineServices: all services stopped" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -495,7 +505,12 @@ class PluginManager(
|
||||
currentState.selectedPlugins.mapValues { (_, selectedPlugin) ->
|
||||
if (selectedPlugin.id == plugin.id) plugin else selectedPlugin
|
||||
}
|
||||
val newState = PluginManagerStates.Data(updatedPlugins, updatedSelectedPlugins)
|
||||
val newState = PluginManagerStates.Data(
|
||||
plugins = updatedPlugins,
|
||||
selectedPlugins = updatedSelectedPlugins,
|
||||
generation = currentState.generation + 1
|
||||
)
|
||||
logger.d { "addPlugin: bumped generation to ${newState.generation} for plugin ${plugin.id}" }
|
||||
updatePluginsState(newState)
|
||||
}
|
||||
}
|
||||
@ -510,7 +525,11 @@ class PluginManager(
|
||||
val updatedPlugins = currentState.plugins.filterNot { it.id == plugin.id }
|
||||
val updatedSelectedPlugins =
|
||||
currentState.selectedPlugins.filterValues { it.id != plugin.id }
|
||||
val newState = PluginManagerStates.Data(updatedPlugins, updatedSelectedPlugins)
|
||||
val newState = PluginManagerStates.Data(
|
||||
plugins = updatedPlugins,
|
||||
selectedPlugins = updatedSelectedPlugins,
|
||||
generation = currentState.generation
|
||||
)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val pluginDir = pluginsDir / plugin.id.toPath()
|
||||
|
||||
@ -55,7 +55,12 @@ sealed class PluginManagerStates {
|
||||
@Serializable
|
||||
data class Data(
|
||||
val plugins: List<PluginEntry>,
|
||||
val selectedPlugins: Map<PluginAbility, PluginEntry> = emptyMap()
|
||||
val selectedPlugins: Map<PluginAbility, PluginEntry> = emptyMap(),
|
||||
// Bumped on every addPlugin so the DataStore always detects a structural
|
||||
// change — without it, same-version replaces produce identical JSON and
|
||||
// the DataStore's internal distinctUntilChanged blocks the state emission,
|
||||
// leaving the ziplineServices flow stuck with the old code.
|
||||
val generation: Long = 0L
|
||||
) : PluginManagerStates()
|
||||
|
||||
data object Loading : PluginManagerStates()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user