From 7b12ce102929f5a97174b3337e81cc3e13988b14 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Fri, 20 Dec 2024 00:48:57 +0100 Subject: [PATCH] Verify plugin config before starting plugin Fix issue with SLF4J in plugins --- .../administration/PluginManagement.tsx | 1 + .../components/general/PluginDetailsModal.tsx | 8 ++- .../general/PluginManagementCard.tsx | 9 +-- .../DatabasePluginStatusProvider.kt | 10 +++- .../management/GameyfinPluginManager.kt | 55 ++++++++++++++++++- plugin-api/build.gradle.kts | 6 +- .../gameyfin/plugins/steam/SteamPlugin.kt | 6 +- 7 files changed, 77 insertions(+), 18 deletions(-) diff --git a/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx b/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx index 2abf2c1..fba1bda 100644 --- a/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx +++ b/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx @@ -39,6 +39,7 @@ export default function PluginManagement() {

Notifications

+

Notification plugins not yet supported.

); } \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx b/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx index 51882c9..94584e8 100644 --- a/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx +++ b/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from "react"; import {Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@nextui-org/react"; import {toast} from "sonner"; import {Form, Formik} from "formik"; -import {PluginConfigEndpoint} from "Frontend/generated/endpoints"; +import {PluginConfigEndpoint, PluginManagementEndpoint} from "Frontend/generated/endpoints"; import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto"; import PluginConfigElement from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement"; import Input from "Frontend/components/general/Input"; @@ -12,9 +12,10 @@ interface PluginDetailsModalProps { plugin: PluginDto; isOpen: boolean; onOpenChange: () => void; + updatePlugin: (plugin: PluginDto) => void; } -export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) { +export default function PluginDetailsModal({plugin, isOpen, onOpenChange, updatePlugin}: PluginDetailsModalProps) { const [pluginConfigMeta, setPluginConfigMeta] = useState<(PluginConfigElement)[]>(); const [pluginConfig, setPluginConfig] = useState>(); @@ -32,6 +33,9 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi async function saveConfig(values: Record) { await PluginConfigEndpoint.setConfigEntries(plugin.id, values); toast.success(`Configuration for ${plugin.name} saved!`); + let updatedPlugin = await PluginManagementEndpoint.getPlugin(plugin.id); + if (updatedPlugin === undefined) return; + updatePlugin(updatedPlugin); } return ( diff --git a/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx b/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx index 59400ce..b8eb2ef 100644 --- a/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx +++ b/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx @@ -18,11 +18,7 @@ export function PluginManagementCard({plugin, updatePlugin}: { if (response === undefined) return; setConfigValid(response); }); - }, []); - - function iconColor(state: PluginState | undefined): "white" | "default" { - return "default"; - } + }, [pluginDetailsModal.isOpen]); function borderColor(state: PluginState | undefined): "success" | "warning" | "danger" | "default" { if (isDisabled(state)) return "warning"; @@ -37,7 +33,7 @@ export function PluginManagementCard({plugin, updatePlugin}: { return "success"; case PluginState.DISABLED: return "warning"; - case PluginState.STOPPED: + case PluginState.FAILED: return "danger"; default: return "default"; @@ -101,6 +97,7 @@ export function PluginManagementCard({plugin, updatePlugin}: { diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/DatabasePluginStatusProvider.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/DatabasePluginStatusProvider.kt index 9f974e4..f597ef4 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/DatabasePluginStatusProvider.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/DatabasePluginStatusProvider.kt @@ -10,8 +10,14 @@ class DatabasePluginStatusProvider( ) : PluginStatusProvider { override fun isPluginDisabled(pluginId: String): Boolean { - val pluginManagement = pluginManagementRepository.findByIdOrNull(pluginId) - return pluginManagement?.enabled != true + var pluginManagement = pluginManagementRepository.findByIdOrNull(pluginId) + + // If the plugin is unknown, persist it as enabled + if(pluginManagement == null) { + pluginManagement = pluginManagementRepository.save(PluginManagementEntry(pluginId = pluginId, enabled = true)) + } + + return pluginManagement.enabled != true } override fun disablePlugin(pluginId: String) { diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt index 2109405..a57906c 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt @@ -63,11 +63,62 @@ class GameyfinPluginManager( return pluginWrapper } + override fun startPlugin(pluginId: String?): PluginState? { + if(pluginId == null) return PluginState.FAILED + + // Validate config before starting the plugin + if (!validatePluginConfig(pluginId)) { + log.error { "Plugin $pluginId has invalid configuration" } + + val pluginWrapper = getPlugin(pluginId) + pluginWrapper.pluginState = PluginState.FAILED + this.firePluginStateEvent(PluginStateEvent(this, pluginWrapper, pluginWrapper.pluginState)); + return pluginWrapper.pluginState + } + + return super.startPlugin(pluginId) + } + + override fun startPlugins() { + for (pluginWrapper in resolvedPlugins) { + val pluginState = pluginWrapper.pluginState + if (!pluginState.isDisabled && !pluginState.isStarted) { + + // Validate config before starting the plugin + if (!validatePluginConfig(pluginWrapper.pluginId)) { + log.error { "Plugin ${pluginWrapper.pluginId} has invalid configuration" } + pluginWrapper.pluginState = PluginState.FAILED + + firePluginStateEvent(PluginStateEvent(this, pluginWrapper, pluginState)) + return + } + + try { + log.info { "${"Start plugin '{}'"} ${getPluginLabel(pluginWrapper.descriptor)}"} + pluginWrapper.plugin.start() + pluginWrapper.pluginState = PluginState.STARTED + pluginWrapper.failedException = null + startedPlugins.add(pluginWrapper) + } catch (e: LinkageError) { + pluginWrapper.pluginState = PluginState.FAILED + pluginWrapper.failedException = e + log.error { "${"Unable to start plugin '{}'"} ${getPluginLabel(pluginWrapper.descriptor)} $e"} + } catch (e: Exception) { + pluginWrapper.pluginState = PluginState.FAILED + pluginWrapper.failedException = e + log.error { "${"Unable to start plugin '{}'"} ${getPluginLabel(pluginWrapper.descriptor)} $e"} + } finally { + firePluginStateEvent(PluginStateEvent(this, pluginWrapper, pluginState)) + } + } + } + } + fun restart(pluginId: String) { val plugin = getPlugin(pluginId)?.plugin ?: return - plugin.stop() + stopPlugin(pluginId) (plugin as GameyfinPlugin).loadConfig(getConfig(pluginId)) - plugin.start() + startPlugin(pluginId) } fun validatePluginConfig(pluginId: String): Boolean { diff --git a/plugin-api/build.gradle.kts b/plugin-api/build.gradle.kts index 980066a..48a1835 100644 --- a/plugin-api/build.gradle.kts +++ b/plugin-api/build.gradle.kts @@ -10,11 +10,7 @@ repositories { dependencies { // PF4J (shared) - api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}") { - exclude(group = "org.slf4j") - } - - api("org.slf4j:slf4j-api:2.0.16") + api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}") implementation(kotlin("stdlib")) diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt index 4e2554c..99eab6b 100644 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt @@ -16,6 +16,8 @@ import kotlinx.serialization.json.* import me.xdrop.fuzzywuzzy.FuzzySearch import org.pf4j.Extension import org.pf4j.PluginWrapper +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.time.Instant @@ -66,6 +68,8 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { @Extension class SteamMetadataProvider : GameMetadataProvider { + val log: Logger = LoggerFactory.getLogger(javaClass) + val client = HttpClient(CIO) { install(ContentNegotiation) { json() @@ -89,7 +93,7 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { val searchResult: SteamSearchResult = client.get(url).body() searchResult.items } catch (e: Exception) { - println(e.message) + log.error("Failed to search Steam store: ${e.message}") emptyList() } }