diff --git a/.gitignore b/.gitignore index d9ac554cd..703911c04 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ gradle.properties application.yml LavalinkServer/plugins .cache/ -site/ \ No newline at end of file +site/ +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 769a345d7..d2de06457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,14 @@ Each release usually includes various fixes and improvements. The most noteworthy of these, as well as any features and breaking changes, are listed here. +## v4.0.2 +* Fixed issue where all plugins get deleted when already present (introduced in [`v4.0.1`](https://github.com/lavalink-devs/Lavalink/releases/tag/4.0.1)) +* Always include plugin info & user data when serializing (introduced in [`v4.0.1`](https://github.com/lavalink-devs/Lavalink/releases/tag/4.0.1)) +* Updated oshi to `6.4.11` + ## 4.0.1 -* Updated Lavaplayer to 2.10 -* Updated OSHI to 6.4.8 +* Updated Lavaplayer to `2.1.0` +* Updated oshi to `6.4.8` * Fix/user data missing field exception in protocol * Fix plugin manager not deleting old plugin version * Fix not being able to seek when player is paused @@ -13,7 +18,7 @@ The most noteworthy of these, as well as any features and breaking changes, are ## 4.0.0 * Lavalink now requires Java 17 or higher to run -* **Removal of all websocket messages sent by the client. Everything is now done via [REST](../api/rest.md)** +* **Removal of all websocket messages sent by the client. Everything is now done via [REST](https://lavalink.dev/api/rest.html)** * Remove default 4GB max heap allocation from docker image * Removal of all `/v3` endpoints except `/version`. All other endpoints are now under `/v4` * Reworked track loading result. For more info see [here](https://lavalink.dev/api/rest.md#track-loading-result) @@ -59,17 +64,17 @@ The most noteworthy of these, as well as any features and breaking changes, are ## 4.0.0-beta.1 * New Lavalink now requires Java 17 or higher to run -* **Removal of all websocket messages sent by the client. Everything is now done via [REST](IMPLEMENTATION.md#rest-api)** +* **Removal of all websocket messages sent by the client. Everything is now done via [REST](https://lavalink.dev/api/rest.html)** * Update to [Lavaplayer custom branch](https://github.com/Walkyst/lavaplayer-fork/tree/custom), which includes native support for artwork urls and ISRCs in the track info * Addition of full `Track` objects in following events: `TrackStartEvent`, `TrackEndEvent`, `TrackExceptionEvent`, `TrackStuckEvent` * Resuming a session now requires the `Session-Id` header instead of `Resume-Key` header -* Reworked track loading result. For more info see [here](IMPLEMENTATION.md#track-loading-result) +* Reworked track loading result. For more info see [here](https://lavalink.dev/api/rest.html#track-loading-result) * Update to the [Protocol Module](protocol) to support Kotlin/JS * Removal of all `/v3` endpoints except `/version`. All other endpoints are now under `/v4` > **Warning** > This is a beta release, and as such, may contain bugs. Please report any bugs you find to the [issue tracker](https://github.com/lavalink-devs/Lavalink/issues/new/choose). -> For more info on the changes in this release, see [here](IMPLEMENTATION.md#significant-changes-v370---v400) +> For more info on the changes in this release, see [here](https://lavalink.dev/changelog/index.html#significant-changes) > If you have any question regarding the changes in this release, please ask in the [support server](https://discord.gg/ZW4s47Ppw4) or [GitHub discussions](https://github.com/lavalink-devs/Lavalink/discussions/categories/q-a) Contributors: diff --git a/LavalinkServer/src/main/java/lavalink/server/Launcher.kt b/LavalinkServer/src/main/java/lavalink/server/Launcher.kt index 8252b9a47..28186c783 100644 --- a/LavalinkServer/src/main/java/lavalink/server/Launcher.kt +++ b/LavalinkServer/src/main/java/lavalink/server/Launcher.kt @@ -143,7 +143,7 @@ object Launcher { .properties(properties) .web(WebApplicationType.SERVLET) .bannerMode(Banner.Mode.OFF) - .resourceLoader(DefaultResourceLoader(pluginManager.classLoader)) + .resourceLoader(DefaultResourceLoader(pluginManager::class.java.classLoader)) .listeners( ApplicationListener { event: Any -> when (event) { diff --git a/LavalinkServer/src/main/java/lavalink/server/bootstrap/PluginManager.kt b/LavalinkServer/src/main/java/lavalink/server/bootstrap/PluginManager.kt index d34d61628..e422785a6 100644 --- a/LavalinkServer/src/main/java/lavalink/server/bootstrap/PluginManager.kt +++ b/LavalinkServer/src/main/java/lavalink/server/bootstrap/PluginManager.kt @@ -15,13 +15,11 @@ import java.util.jar.JarFile @SpringBootApplication class PluginManager(val config: PluginsConfig) { - companion object { private val log: Logger = LoggerFactory.getLogger(PluginManager::class.java) } final val pluginManifests: MutableList = mutableListOf() - var classLoader: ClassLoader = PluginManager::class.java.classLoader init { manageDownloads() @@ -33,53 +31,59 @@ class PluginManager(val config: PluginsConfig) { private fun manageDownloads() { if (config.plugins.isEmpty()) return + val directory = File(config.pluginsDir) directory.mkdir() - data class PluginJar(val manifest: PluginManifest, val file: File) - - val pluginJars = directory.listFiles()!!.filter { it.extension == "jar" }.map { - JarFile(it).use {jar -> - loadPluginManifests(jar).map { manifest -> PluginJar(manifest, it) } + val pluginJars = directory.listFiles()?.filter { it.extension == "jar" } + ?.flatMap { file -> + JarFile(file).use { jar -> + loadPluginManifests(jar).map { manifest -> PluginJar(manifest, file) } + } } - }.flatten() - - data class Declaration(val group: String, val name: String, val version: String, val repository: String) + ?.onEach { log.info("Found plugin '${it.manifest.name}' version ${it.manifest.version}") } + ?: return val declarations = config.plugins.map { declaration -> if (declaration.dependency == null) throw RuntimeException("Illegal dependency declaration: null") val fragments = declaration.dependency!!.split(":") if (fragments.size != 3) throw RuntimeException("Invalid dependency \"${declaration.dependency}\"") - var repository = declaration.repository - ?: if (declaration.snapshot) config.defaultPluginSnapshotRepository else config.defaultPluginRepository - repository = if (repository.endsWith("/")) repository else "$repository/" - Declaration(fragments[0], fragments[1], fragments[2], repository) + val repository = declaration.repository + ?: config.defaultPluginSnapshotRepository.takeIf { declaration.snapshot } + ?: config.defaultPluginRepository + + Declaration(fragments[0], fragments[1], fragments[2], "${repository.removeSuffix("/")}/") }.distinctBy { "${it.group}:${it.name}" } - declarations.forEach declarationLoop@{ declaration -> - var hasVersion = false - pluginJars.forEach pluginLoop@{ jar -> - if (declaration.version == jar.manifest.version && !hasVersion) { - hasVersion = true - // We already have this jar so don't redownload it - return@pluginLoop + for (declaration in declarations) { + val jars = pluginJars.filter { it.manifest.name == declaration.name } + var hasCurrentVersion = false + + for (jar in jars) { + if (jar.manifest.version == declaration.version) { + hasCurrentVersion = true + // Don't clean up the jar if it's a current version. + continue } - // Delete jar of different versions + // Delete versions of the plugin that aren't the same as declared version. if (!jar.file.delete()) throw RuntimeException("Failed to delete ${jar.file.path}") - log.info("Deleted ${jar.file.path}") + log.info("Deleted ${jar.file.path} (new version: ${declaration.version})") + } - if (hasVersion) return@declarationLoop - val url = declaration.run { "$repository${group.replace(".", "/")}/$name/$version/$name-$version.jar" } - val file = File(directory, declaration.run { "$name-$version.jar" }) - downloadJar(file, url) + if (!hasCurrentVersion) { + val url = declaration.url + val file = File(directory, declaration.canonicalJarName) + downloadJar(file, url) + } } } private fun downloadJar(output: File, url: String) { log.info("Downloading $url") + Channels.newChannel(URL(url).openStream()).use { FileOutputStream(output).channel.transferFrom(it, 0, Long.MAX_VALUE) } @@ -88,61 +92,43 @@ class PluginManager(val config: PluginsConfig) { private fun readClasspathManifests(): List { return PathMatchingResourcePatternResolver() .getResources("classpath*:lavalink-plugins/*.properties") - .map map@{ r -> - val manifest = parsePluginManifest(r.inputStream) - log.info("Found plugin '${manifest.name}' version ${manifest.version}") - return@map manifest - } + .map { parsePluginManifest(it.inputStream) } + .onEach { log.info("Found plugin '${it.name}' version ${it.version}") } } private fun loadJars(): List { - val directory = File(config.pluginsDir) - if (!directory.isDirectory) return emptyList() - val jarsToLoad = mutableListOf() - - directory.listFiles()?.forEach { file -> - if (!file.isFile) return@forEach - if (file.extension != "jar") return@forEach - jarsToLoad.add(file) - } + val directory = File(config.pluginsDir).takeIf { it.isDirectory } + ?: return emptyList() - if (jarsToLoad.isEmpty()) return emptyList() + val jarsToLoad = directory.listFiles()?.filter { it.isFile && it.extension == "jar" } + ?.takeIf { it.isNotEmpty() } + ?: return emptyList() - val cl = URLClassLoader.newInstance( + val classLoader = URLClassLoader.newInstance( jarsToLoad.map { URL("jar:file:${it.absolutePath}!/") }.toTypedArray(), javaClass.classLoader ) - classLoader = cl - - val manifests = mutableListOf() - jarsToLoad.forEach { file -> - try { - manifests.addAll(loadJar(file, cl)) - } catch (e: Exception) { - throw RuntimeException("Error loading $file", e) - } - } - - return manifests + return jarsToLoad.flatMap { loadJar(it, classLoader) } } private fun loadJar(file: File, cl: URLClassLoader): List { - var classCount = 0 val jar = JarFile(file) - var manifests: List + val manifests = loadPluginManifests(jar) + var classCount = 0 jar.use { - manifests = loadPluginManifests(jar) if (manifests.isEmpty()) { throw RuntimeException("No plugin manifest found in ${file.path}") } - val allowedPaths = manifests.map { it.path.replace(".", "/") } - jar.entries().asIterator().forEach { entry -> - if (entry.isDirectory) return@forEach - if (!entry.name.endsWith(".class")) return@forEach - if (!allowedPaths.any { entry.name.startsWith(it) }) return@forEach + val allowedPaths = manifests.map { manifest -> manifest.path.replace(".", "/") } + + for (entry in it.entries()) { + if (entry.isDirectory || + !entry.name.endsWith(".class") || + allowedPaths.none(entry.name::startsWith)) continue + cl.loadClass(entry.name.dropLast(6).replace("/", ".")) classCount++ } @@ -153,18 +139,10 @@ class PluginManager(val config: PluginsConfig) { } private fun loadPluginManifests(jar: JarFile): List { - val manifests = mutableListOf() - - jar.entries().asIterator().forEach { entry -> - if (entry.isDirectory) return@forEach - if (!entry.name.startsWith("lavalink-plugins/")) return@forEach - if (!entry.name.endsWith(".properties")) return@forEach - - val manifest = parsePluginManifest(jar.getInputStream(entry)) - log.info("Found plugin '${manifest.name}' version ${manifest.version}") - manifests.add(manifest) - } - return manifests + return jar.entries().asSequence() + .filter { !it.isDirectory && it.name.startsWith("lavalink-plugins/") && it.name.endsWith(".properties") } + .map { parsePluginManifest(jar.getInputStream(it)) } + .toList() } private fun parsePluginManifest(stream: InputStream): PluginManifest { @@ -177,4 +155,10 @@ class PluginManager(val config: PluginsConfig) { val version = props.getProperty("version") ?: throw RuntimeException("Manifest is missing 'version'") return PluginManifest(name, path, version) } -} \ No newline at end of file + + private data class PluginJar(val manifest: PluginManifest, val file: File) + private data class Declaration(val group: String, val name: String, val version: String, val repository: String) { + val canonicalJarName = "$name-$version.jar" + val url = "$repository${group.replace(".", "/")}/$name/$version/$name-$version.jar" + } +} diff --git a/docs/changelog/v4.md b/docs/changelog/v4.md index ad762f496..b18886c65 100644 --- a/docs/changelog/v4.md +++ b/docs/changelog/v4.md @@ -1,6 +1,11 @@ -## 4.0.1 -* Updated Lavaplayer to 2.10 -* Updated OSHI to 6.4.8 +## v4.0.2 +* Fixed issue where all plugins get deleted when already present (introduced in [`v4.0.1`](https://github.com/lavalink-devs/Lavalink/releases/tag/4.0.1)) +* Always include plugin info & user data when serializing (introduced in [`v4.0.1`](https://github.com/lavalink-devs/Lavalink/releases/tag/4.0.1)) +* Updated oshi to `6.4.11` + +## v4.0.1 +* Updated Lavaplayer to `2.1.0` +* Updated oshi to `6.4.8` * Fix/user data missing field exception in protocol * Fix plugin manager not deleting old plugin version * Fix not being able to seek when player is paused diff --git a/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/loadResult.kt b/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/loadResult.kt index c5c348c3c..db289f9c3 100644 --- a/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/loadResult.kt +++ b/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/loadResult.kt @@ -3,10 +3,7 @@ package dev.arbjerg.lavalink.protocol.v4 import dev.arbjerg.lavalink.protocol.v4.serialization.asPolymorphicDeserializer -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import kotlinx.serialization.* import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder @@ -133,6 +130,7 @@ data class PlaylistInfo( @Serializable data class Playlist( val info: PlaylistInfo, + @EncodeDefault val pluginInfo: JsonObject = JsonObject(emptyMap()), val tracks: List ) : LoadResult.Data { diff --git a/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/player.kt b/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/player.kt index 19c7a7917..c4e2b0c36 100644 --- a/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/player.kt +++ b/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/player.kt @@ -1,9 +1,8 @@ package dev.arbjerg.lavalink.protocol.v4 -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.Serializable +import kotlinx.serialization.* +import kotlinx.serialization.json.JsonNames import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.serializer import kotlin.jvm.JvmInline inline fun JsonObject.deserialize(): T = @@ -31,7 +30,9 @@ data class Player( data class Track( val encoded: String, val info: TrackInfo, + @EncodeDefault val pluginInfo: JsonObject = JsonObject(emptyMap()), + @EncodeDefault val userData: JsonObject = JsonObject(emptyMap()) ) : LoadResult.Data { diff --git a/settings.gradle.kts b/settings.gradle.kts index 861cb22c7..4b30af382 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -76,7 +76,7 @@ fun VersionCatalogBuilder.common() { library("logback", "ch.qos.logback", "logback-classic").version("1.4.7") library("sentry-logback", "io.sentry", "sentry-logback").version("6.22.0") - library("oshi", "com.github.oshi", "oshi-core").version("6.4.8") + library("oshi", "com.github.oshi", "oshi-core").version("6.4.11") } fun VersionCatalogBuilder.other() {