Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion LavalinkServer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ dependencies {
implementation(projects.pluginApi) {
exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat")
}
implementation(libs.pf4j.spring)
implementation(libs.asm)

implementation(libs.bundles.metrics)
implementation(libs.bundles.spring) {
Expand Down Expand Up @@ -122,7 +124,6 @@ tasks {
archiveClassifier = "musl"
}


withType<BootJar> {
archiveFileName = "Lavalink.jar"

Expand Down
55 changes: 36 additions & 19 deletions LavalinkServer/src/main/java/lavalink/server/Launcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@
package lavalink.server

import com.sedmelluq.discord.lavaplayer.tools.PlayerLibrary
import lavalink.server.bootstrap.PluginManager
import lavalink.server.bootstrap.LavalinkPluginDescriptor
import lavalink.server.bootstrap.PluginComponentClassLoader
import lavalink.server.bootstrap.PluginDescriptor
import lavalink.server.bootstrap.PluginSystemImpl
import lavalink.server.info.AppInfo
import lavalink.server.info.GitRepoState
import org.pf4j.Extension
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.getBean
import org.springframework.boot.Banner
import org.springframework.boot.SpringApplication
import org.springframework.boot.WebApplicationType
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
import org.springframework.boot.context.event.ApplicationFailedEvent
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.runApplication
import org.springframework.context.ApplicationListener
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.context.annotation.ComponentScan
Expand All @@ -47,10 +52,14 @@ import java.util.*


@Suppress("SpringComponentScan")
@SpringBootApplication
@SpringBootApplication()
@ComponentScan(
value = ["\${componentScan}"],
excludeFilters = [ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = [PluginManager::class])]
excludeFilters = [
ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = [PluginSystemImpl::class]),
// These are registered manually
ComponentScan.Filter(type = FilterType.ANNOTATION, classes = [Extension::class])
]
)
class LavalinkApplication

Expand Down Expand Up @@ -101,11 +110,11 @@ object Launcher {
val defaultC = ""

var vanity = ("g . r _ _ _ _ g__ _ _\n"
+ "g /\\\\ r| | __ ___ ____ _| (_)_ __ | | __g\\ \\ \\ \\\n"
+ "g ( ( )r| |/ _` \\ \\ / / _` | | | '_ \\| |/ /g \\ \\ \\ \\\n"
+ "g \\\\/ r| | (_| |\\ V / (_| | | | | | | < g ) ) ) )\n"
+ "g ' r|_|\\__,_| \\_/ \\__,_|_|_|_| |_|_|\\_\\g / / / /\n"
+ "d =========================================g/_/_/_/d")
+ "g /\\\\ r| | __ ___ ____ _| (_)_ __ | | __g\\ \\ \\ \\\n"
+ "g ( ( )r| |/ _` \\ \\ / / _` | | | '_ \\| |/ /g \\ \\ \\ \\\n"
+ "g \\\\/ r| | (_| |\\ V / (_| | | | | | | < g ) ) ) )\n"
+ "g ' r|_|\\__,_| \\_/ \\__,_|_|_|_| |_|_|\\_\\g / / / /\n"
+ "d =========================================g/_/_/_/d")

vanity = vanity.replace("r".toRegex(), red)
vanity = vanity.replace("g".toRegex(), green)
Expand All @@ -126,32 +135,40 @@ object Launcher {
launchMain(parent, args)
}

private fun launchPluginBootstrap() = SpringApplication(PluginManager::class.java).run {
setBannerMode(Banner.Mode.OFF)
webApplicationType = WebApplicationType.NONE
run()
}
private fun launchPluginBootstrap() = runApplication<PluginSystemImpl> {
setBannerMode(Banner.Mode.OFF)
webApplicationType = WebApplicationType.NONE
}

private fun launchMain(parent: ConfigurableApplicationContext, args: Array<String>) {
val pluginManager = parent.getBean(PluginManager::class.java)
val pluginManager = parent.getBean<PluginSystemImpl>()
val properties = Properties()
properties["componentScan"] = pluginManager.pluginManifests.map { it.path }
.toMutableList().apply { add("lavalink.server") }
properties["componentScan"] = pluginManager.manager.plugins
.asSequence()
.filter { (it.descriptor as LavalinkPluginDescriptor).manifestVersion == PluginDescriptor.Version.V1 }
.map { (it.descriptor as LavalinkPluginDescriptor).path }
.toList() + "lavalink.server"

SpringApplicationBuilder()
.parent(parent)
.sources(LavalinkApplication::class.java)
.properties(properties)
.web(WebApplicationType.SERVLET)
.bannerMode(Banner.Mode.OFF)
.resourceLoader(DefaultResourceLoader(pluginManager.classLoader))
.resourceLoader(DefaultResourceLoader(PluginComponentClassLoader(pluginManager.manager)))
.listeners(
ApplicationListener { event: Any ->
when (event) {
is ApplicationEnvironmentPreparedEvent -> {
log.info(getVersionInfo())

}

is ApplicationReadyEvent -> {
pluginManager.manager.applicationContext = event.applicationContext
pluginManager.manager.startPlugins()
pluginManager.manager.injector.injectExtensions()

log.info("Lavalink is ready to accept connections.")
}

Expand All @@ -160,7 +177,7 @@ object Launcher {
}
}
}
).parent(parent)
)
.run(*args)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package lavalink.server.bootstrap

import org.pf4j.BasePluginLoader
import org.pf4j.DevelopmentPluginClasspath
import org.pf4j.PluginManager

private val developmentClasspath = DevelopmentPluginClasspath.GRADLE.addJarsDirectories("build/dependencies")

class DevelopmentPluginLoader(pluginManager: PluginManager) : BasePluginLoader(pluginManager, developmentClasspath)
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package lavalink.server.bootstrap

import lavalink.server.config.RequestHandlerMapping
import org.pf4j.ExtensionWrapper
import org.pf4j.spring.ExtensionsInjector
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.getBean
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
import org.springframework.context.annotation.AnnotationConfigRegistry
import org.springframework.context.annotation.Configuration
import org.springframework.web.bind.annotation.RestController
import kotlin.reflect.full.hasAnnotation

private val log = LoggerFactory.getLogger(LavalinkExtensionInjector::class.java)

class LavalinkExtensionInjector(pluginManager: PluginLoader, factory: AbstractAutowireCapableBeanFactory) :
ExtensionsInjector(pluginManager, factory) {

// We override this to use ExtensionWrappers instead, because the original does not respect ordinals
override fun injectExtensions() {
val pluginLoader = springPluginManager as PluginLoader
// add extensions from classpath (non plugin)
val internalExtensions =
springPluginManager.plugins.flatMap { pluginLoader.extensionFinder.find(null) }

val pluginExtensions = springPluginManager.startedPlugins.flatMap { plugin ->
log.debug("Registering extensions of the plugin '{}' as beans", plugin.pluginId)

pluginLoader.extensionFinder.find(plugin.pluginId)
}
(internalExtensions + pluginExtensions)
.sortedBy { it.ordinal }
.register()
}

fun List<ExtensionWrapper<*>>.register() = forEach { extensionWrapper ->
log.debug("Register extension '{}' as bean", extensionWrapper.descriptor.extensionClass.name)
try {
registerExtension(extensionWrapper.descriptor.extensionClass)
} catch (e: ClassNotFoundException) {
log.error(e.message, e)
}
}

override fun registerExtension(extensionClass: Class<*>) {
val extensionBeanMap = springPluginManager.applicationContext.getBeansOfType(extensionClass)
if (extensionBeanMap.isEmpty()) {
val extension = springPluginManager.getExtensionFactory().create(extensionClass)

if (extensionClass.kotlin.hasAnnotation<Configuration>()) {
(springPluginManager.applicationContext as AnnotationConfigRegistry).register(extensionClass)
}

if (extensionClass.kotlin.hasAnnotation<ConfigurationProperties>()) {
val configBinder =
springPluginManager.applicationContext.getBean<ConfigurationPropertiesBindingPostProcessor>()
configBinder.postProcessBeforeInitialization(extension, extensionClass.getName())
}

this.beanFactory.registerSingleton(extensionClass.getName(), extension)
this.beanFactory.autowireBean(extension)

if (extension::class.hasAnnotation<RestController>()) {
log.debug(
"Extension {} is annotated with @RestController, forwarding registration to request mapper",
extensionClass.getName()
)
val mapping =
springPluginManager.applicationContext.getBean<RequestHandlerMapping>("requestMappingHandlerMapping")

mapping.registerExtension(extension)
}
} else {
log.debug("Bean registeration aborted! Extension '{}' already existed as bean!", extensionClass.getName())
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package lavalink.server.bootstrap

import org.pf4j.VersionManager
import com.github.zafarkhaja.semver.Version

private val commitHashRegex = Regex("^[0-9a-fA-F]{7,40}$")

/**
* Implementation of [VersionManager] which also accepts commit hashes as versions.
*/
class FlexibleVersionManager : VersionManager {
override fun checkVersionConstraint(version: String, constraint: String): Boolean {
if (constraint == "*") return true
return if(Version.isValid(version)) {
Version.parse(version).satisfies(constraint)
} else {
return version.matches(commitHashRegex)
}
}

override fun compareVersions(v1: String, v2: String): Int {
if (v1.matches(commitHashRegex)) {
// Commit hashes cannot be compared
if (v2.matches(commitHashRegex)) return 0
// Commit hash should always win
return 1
}
// Commit hash should always win
if (v2.matches(commitHashRegex)) return -1
return Version.parse(v1).compareTo(Version.parse(v2))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package lavalink.server.bootstrap

import org.pf4j.Plugin
import org.pf4j.PluginFactory
import org.pf4j.PluginWrapper
import org.pf4j.spring.SpringPlugin
import org.pf4j.spring.SpringPluginManager
import org.pf4j.util.FileUtils
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import kotlin.io.path.useLines

class PluginClassWrapper(val context: PluginWrapper) : SpringPlugin(context) {
override fun createApplicationContext(): ApplicationContext {
val parent = (context.pluginManager as SpringPluginManager).applicationContext
return AnnotationConfigApplicationContext().apply {
this.parent = parent
classLoader = context.pluginClassLoader
FileUtils.getPath(context.pluginPath, "META-INF", "configurations.idx")
.useLines {
it.forEach { className ->
log.debug("Registering configuration {} from plugin {}", className, context.pluginId)
val clazz = context.pluginClassLoader.loadClass(className)
register(clazz)
}
}
refresh()
}
}
}

object LavalinkPluginFactory : PluginFactory {
override fun create(pluginWrapper: PluginWrapper): Plugin = PluginClassWrapper(pluginWrapper)
}
Loading