diff --git a/.idea/misc.xml b/.idea/misc.xml index b21d13d100..f9cee6c190 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml index f7c708e3bb..7bc838d592 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -8,8 +8,10 @@ + + - \ No newline at end of file + diff --git a/.idea/modules/plugins/kord-extensions.plugins.main.iml b/.idea/modules/plugins/kord-extensions.plugins.main.iml new file mode 100644 index 0000000000..ec6c1d1ca7 --- /dev/null +++ b/.idea/modules/plugins/kord-extensions.plugins.main.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/plugins/kord-extensions.plugins.test.iml b/.idea/modules/plugins/kord-extensions.plugins.test.iml new file mode 100644 index 0000000000..563d69839f --- /dev/null +++ b/.idea/modules/plugins/kord-extensions.plugins.test.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/annotation-processor/build.gradle.kts b/annotation-processor/build.gradle.kts index 43825c3cf1..8ee90db321 100644 --- a/annotation-processor/build.gradle.kts +++ b/annotation-processor/build.gradle.kts @@ -26,12 +26,6 @@ dokkaModule { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of("11")) - } + sourceCompatibility = JavaVersion.VERSION_13 + targetCompatibility = JavaVersion.VERSION_13 } diff --git a/buildSrc/src/main/kotlin/kordex-module.gradle.kts b/buildSrc/src/main/kotlin/kordex-module.gradle.kts index 2b053f486c..8a96f967ae 100644 --- a/buildSrc/src/main/kotlin/kordex-module.gradle.kts +++ b/buildSrc/src/main/kotlin/kordex-module.gradle.kts @@ -29,10 +29,6 @@ tasks { kotlin { explicitApi() - - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of("11")) - } } jar { @@ -43,8 +39,8 @@ tasks { rootProject.file("LICENSE").copyTo(rootProject.file("build/LICENSE-kordex"), true) tasks.withType().configureEach { - sourceCompatibility = "11" - targetCompatibility = "11" + sourceCompatibility = "13" + targetCompatibility = "13" } withType().configureEach { @@ -53,7 +49,7 @@ tasks { } kotlinOptions { - jvmTarget = "11" + jvmTarget = "13" } } } diff --git a/gradle.properties b/gradle.properties index edeb68eb82..f508d87ff9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ kotlin.incremental = true ksp.incremental = false -projectVersion = 1.5.9-SNAPSHOT +projectVersion = 1.5.10-SNAPSHOT #dokka will run out of memory with the default meta space org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 11b49d8f7c..c5255fe06c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ logback-groovy = "1.14.5" logging = "5.1.0" mongodb = "4.10.2" pf4j = "3.10.0" +semver = "1.4.2" sentry = "6.29.0" slf4j = "2.0.9" time4j-base = "5.9.3" @@ -53,12 +54,14 @@ ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } kx-coro = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kx-coro"} kx-ser = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kx-ser" } +kx-ser-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kx-ser" } linkie = { module = "me.shedaniel:linkie-core", version.ref = "linkie" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logback-groovy = { module = "io.github.virtualdogbert:logback-groovy-config", version.ref = "logback-groovy" } kotlin-logging = { module = "io.github.oshai:kotlin-logging", version.ref = "logging" } mongodb = { module = "org.mongodb:mongodb-driver-kotlin-coroutine", version.ref = "mongodb" } pf4j = { module = "org.pf4j:pf4j", version.ref = "pf4j" } +semver = { module = "io.github.z4kn4fein:semver", version.ref = "semver" } sentry = { module = "io.sentry:sentry", version.ref = "sentry" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } time4j-base = { module = "net.time4j:time4j-base", version.ref = "time4j-base" } diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts new file mode 100644 index 0000000000..8490d7cf22 --- /dev/null +++ b/plugins/build.gradle.kts @@ -0,0 +1,39 @@ +buildscript { + repositories { + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } +} + +plugins { + `kordex-module` + `published-module` + `dokka-module` + `tested-module` +} + +metadata { + name = "KordEx: Plugins" + description = "Self-contained API implementing a simple plugin system" +} + +group = "com.kotlindiscord.kord.extensions" + +dependencies { + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) + + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) + implementation(libs.kx.ser) + implementation(libs.kx.ser.json) // No ktor dep + implementation(libs.semver) + + testImplementation(libs.groovy) // For logback config + testImplementation(libs.jansi) + testImplementation(libs.junit) + testImplementation(libs.logback) + testImplementation(libs.logback.groovy) +} diff --git a/plugins/plugin-load-test/build.gradle.kts b/plugins/plugin-load-test/build.gradle.kts new file mode 100644 index 0000000000..1a80bb0251 --- /dev/null +++ b/plugins/plugin-load-test/build.gradle.kts @@ -0,0 +1,66 @@ +buildscript { + repositories { + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } +} + +plugins { + `kordex-module` + `dokka-module` + `tested-module` +} + +group = "com.kotlindiscord.kord.extensions" + +dependencies { + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) + + testImplementation(libs.bundles.logging) + testImplementation(libs.kotlin.stdlib) + testImplementation(libs.kx.ser) + testImplementation(libs.kx.ser.json) // No ktor dep + testImplementation(libs.semver) + + testImplementation(libs.groovy) // For logback config + testImplementation(libs.jansi) + testImplementation(libs.junit) + testImplementation(libs.logback) + testImplementation(libs.logback.groovy) + + // Make sure these get built before the test module + testImplementation(project(":plugins:test-plugin-core")) + testImplementation(project(":plugins:test-plugin-1")) + testImplementation(project(":plugins:test-plugin-2")) +} + +val copyTestJars = tasks.register("copyTestJars") { + val matchRegex = Regex("^.*(\\d|-SNAPSHOT)\\.jar\$") + + val root = rootProject.rootDir + val pluginDir = root.resolve("plugins/plugin-load-test/tmp/plugins") + + val testOneJar = root.resolve("plugins/test-plugin-1/build/libs") + .listFiles() + ?.first { + it.name.matches(matchRegex) + } + + val testTwoJar = root.resolve("plugins/test-plugin-2/build/libs") + .listFiles() + ?.first { + it.name.matches(matchRegex) + } + + pluginDir.mkdirs() + + from(testOneJar, testTwoJar) + into(pluginDir) +} + +tasks.test { + dependsOn(copyTestJars) +} diff --git a/plugins/plugin-load-test/src/test/kotlin/PluginJarTests.kt b/plugins/plugin-load-test/src/test/kotlin/PluginJarTests.kt new file mode 100644 index 0000000000..ad006249ab --- /dev/null +++ b/plugins/plugin-load-test/src/test/kotlin/PluginJarTests.kt @@ -0,0 +1,28 @@ +import com.kotlindiscord.kord.extensions.plugins.PluginManager +import com.kotlindiscord.kord.extensions.plugins.test.core.TestPlugin +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PluginJarTests { + private val pluginManager = PluginManager( + baseTypeReference = "TestPlugin", + pluginDirectory = "tmp/plugins" + ) + + @Test + fun `Standard plugin load`() { + pluginManager.loadAllPlugins() + + Assertions.assertEquals( + "test-one", + pluginManager.loadPlugin("test-one")?.get()?.manifest?.id + ) + + Assertions.assertEquals( + "test-two", + pluginManager.loadPlugin("test-two")?.get()?.manifest?.id + ) + } +} diff --git a/plugins/plugin-load-test/src/test/resources/junit-platform.properties b/plugins/plugin-load-test/src/test/resources/junit-platform.properties new file mode 100644 index 0000000000..580f511dad --- /dev/null +++ b/plugins/plugin-load-test/src/test/resources/junit-platform.properties @@ -0,0 +1,7 @@ +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# + +junit.jupiter.execution.parallel.enabled=true diff --git a/plugins/plugin-load-test/src/test/resources/logback.groovy b/plugins/plugin-load-test/src/test/resources/logback.groovy new file mode 100644 index 0000000000..a0c69989f4 --- /dev/null +++ b/plugins/plugin-load-test/src/test/resources/logback.groovy @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.core.joran.spi.ConsoleTarget +import ch.qos.logback.core.ConsoleAppender +import ch.qos.logback.core.FileAppender + +def environment = System.getenv("ENVIRONMENT") ?: "dev" +def defaultLevel = TRACE + +if (environment == "spam") { + logger("dev.kord.rest.DefaultGateway", TRACE) +} else { + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) +} + +appender("CONSOLE", ConsoleAppender) { + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + + withJansi = true + } + + target = ConsoleTarget.SystemOut +} + +appender("FILE", FileAppender) { + file = "output.log" + + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } +} + +root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/plugins/plugin-load-test/src/test/resources/logbackCompiler.groovy b/plugins/plugin-load-test/src/test/resources/logbackCompiler.groovy new file mode 100644 index 0000000000..a374f9af62 --- /dev/null +++ b/plugins/plugin-load-test/src/test/resources/logbackCompiler.groovy @@ -0,0 +1,458 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +importsAcceptList = [ + 'ch.qos.logback.core.testUtil.SampleConverter', + + 'ch.qos.logback.core.testUtil.StringListAppender', + 'java.lang.Object', + 'org.springframework.beans.factory.annotation.Autowired', + 'java.nio.charset.Charset.forName', + 'com.logentries.logback.LogentriesAppender', + 'grails.util.BuildSettings', + 'grails.util.Environment', + 'io.micronaut.context.env.Environment', + 'org.slf4j.MDC', + 'org.springframework.boot.logging.logback.ColorConverter', + 'org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter', + 'java.nio.charset.Charset', + 'java.nio.charset.StandardCharsets', + + 'ch.qos.logback.core.BasicStatusManager', + 'ch.qos.logback.core.ConsoleAppender', + 'ch.qos.logback.core.hook.ShutdownHook', + 'ch.qos.logback.core.hook.ShutdownHookBase', + 'ch.qos.logback.core.hook.DelayingShutdownHook', + 'ch.qos.logback.core.spi.PropertyContainer', + 'ch.qos.logback.core.spi.ContextAwareBase', + 'ch.qos.logback.core.spi.LogbackLock', + 'ch.qos.logback.core.spi.FilterAttachableImpl', + 'ch.qos.logback.core.spi.ContextAwareImpl', + 'ch.qos.logback.core.spi.ScanException', + 'ch.qos.logback.core.spi.DeferredProcessingAware', + 'ch.qos.logback.core.spi.ContextAware', + 'ch.qos.logback.core.spi.LifeCycle', + 'ch.qos.logback.core.spi.FilterReply', + 'ch.qos.logback.core.spi.PreSerializationTransformer', + 'ch.qos.logback.core.spi.AppenderAttachable', + 'ch.qos.logback.core.spi.CyclicBufferTracker', + 'ch.qos.logback.core.spi.FilterAttachable', + 'ch.qos.logback.core.spi.ComponentTracker', + 'ch.qos.logback.core.spi.AppenderAttachableImpl', + 'ch.qos.logback.core.spi.PropertyDefiner', + 'ch.qos.logback.core.spi.AbstractComponentTracker', + 'ch.qos.logback.core.property.FileExistsPropertyDefiner', + 'ch.qos.logback.core.property.ResourceExistsPropertyDefiner', + 'ch.qos.logback.core.CoreConstants', + 'ch.qos.logback.core.layout.EchoLayout', + 'ch.qos.logback.core.Appender', + 'ch.qos.logback.core.joran.JoranConfiguratorBase', + 'ch.qos.logback.core.joran.spi.ActionException', + 'ch.qos.logback.core.joran.spi.HostClassAndPropertyDouble', + 'ch.qos.logback.core.joran.spi.JoranException', + 'ch.qos.logback.core.joran.spi.NoAutoStart', + 'ch.qos.logback.core.joran.spi.EventPlayer', + 'ch.qos.logback.core.joran.spi.XMLUtil', + 'ch.qos.logback.core.joran.spi.ConsoleTarget', + 'ch.qos.logback.core.joran.spi.Interpreter', + 'ch.qos.logback.core.joran.spi.SimpleRuleStore', + 'ch.qos.logback.core.joran.spi.InterpretationContext', + 'ch.qos.logback.core.joran.spi.RuleStore', + 'ch.qos.logback.core.joran.spi.NoAutoStartUtil', + 'ch.qos.logback.core.joran.spi.ElementSelector', + 'ch.qos.logback.core.joran.spi.ConfigurationWatchList', + 'ch.qos.logback.core.joran.spi.DefaultNestedComponentRegistry', + 'ch.qos.logback.core.joran.spi.DefaultClass', + 'ch.qos.logback.core.joran.spi.ElementPath', + 'ch.qos.logback.core.joran.conditional.ThenOrElseActionBase', + 'ch.qos.logback.core.joran.conditional.Condition', + 'ch.qos.logback.core.joran.conditional.PropertyWrapperForScripts', + 'ch.qos.logback.core.joran.conditional.ThenAction', + 'ch.qos.logback.core.joran.conditional.PropertyEvalScriptBuilder', + 'ch.qos.logback.core.joran.conditional.ElseAction', + 'ch.qos.logback.core.joran.conditional.IfAction', + 'ch.qos.logback.core.joran.util.beans.BeanDescriptionCache', + 'ch.qos.logback.core.joran.util.beans.BeanDescriptionFactory', + 'ch.qos.logback.core.joran.util.beans.BeanDescription', + 'ch.qos.logback.core.joran.util.beans.BeanUtil', + 'ch.qos.logback.core.joran.util.PropertySetter', + 'ch.qos.logback.core.joran.util.ConfigurationWatchListUtil', + 'ch.qos.logback.core.joran.util.StringToObjectConverter', + 'ch.qos.logback.core.joran.GenericConfigurator', + 'ch.qos.logback.core.joran.action.ImplicitAction', + 'ch.qos.logback.core.joran.action.IncludeAction', + 'ch.qos.logback.core.joran.action.NOPAction', + 'ch.qos.logback.core.joran.action.IADataForBasicProperty', + 'ch.qos.logback.core.joran.action.TimestampAction', + 'ch.qos.logback.core.joran.action.AbstractEventEvaluatorAction', + 'ch.qos.logback.core.joran.action.ParamAction', + 'ch.qos.logback.core.joran.action.AppenderAction', + 'ch.qos.logback.core.joran.action.DefinePropertyAction', + 'ch.qos.logback.core.joran.action.StatusListenerAction', + 'ch.qos.logback.core.joran.action.ContextPropertyAction', + 'ch.qos.logback.core.joran.action.NestedComplexPropertyIA', + 'ch.qos.logback.core.joran.action.NestedBasicPropertyIA', + 'ch.qos.logback.core.joran.action.Action', + 'ch.qos.logback.core.joran.action.AppenderRefAction', + 'ch.qos.logback.core.joran.action.ActionUtil', + 'ch.qos.logback.core.joran.action.ShutdownHookAction', + 'ch.qos.logback.core.joran.action.IADataForComplexProperty', + 'ch.qos.logback.core.joran.action.ConversionRuleAction', + 'ch.qos.logback.core.joran.action.ActionConst', + 'ch.qos.logback.core.joran.action.PropertyAction', + 'ch.qos.logback.core.joran.action.NewRuleAction', + 'ch.qos.logback.core.joran.node.ComponentNode', + 'ch.qos.logback.core.joran.event.EndEvent', + 'ch.qos.logback.core.joran.event.SaxEventRecorder', + 'ch.qos.logback.core.joran.event.SaxEvent', + 'ch.qos.logback.core.joran.event.BodyEvent', + 'ch.qos.logback.core.joran.event.StartEvent', + 'ch.qos.logback.core.joran.event.InPlayListener', + 'ch.qos.logback.core.joran.event.stax.EndEvent', + 'ch.qos.logback.core.joran.event.stax.StaxEventRecorder', + 'ch.qos.logback.core.joran.event.stax.BodyEvent', + 'ch.qos.logback.core.joran.event.stax.StartEvent', + 'ch.qos.logback.core.joran.event.stax.StaxEvent', + 'ch.qos.logback.core.LogbackException', + 'ch.qos.logback.core.PropertyDefinerBase', + 'ch.qos.logback.core.helpers.CyclicBuffer', + 'ch.qos.logback.core.helpers.ThrowableToStringArray', + 'ch.qos.logback.core.helpers.Transform', + 'ch.qos.logback.core.helpers.NOPAppender', + 'ch.qos.logback.core.net.LoginAuthenticator', + 'ch.qos.logback.core.net.DefaultSocketConnector', + 'ch.qos.logback.core.net.ssl.KeyStoreFactoryBean', + 'ch.qos.logback.core.net.ssl.SSLParametersConfiguration', + 'ch.qos.logback.core.net.ssl.SSLComponent', + 'ch.qos.logback.core.net.ssl.SSLNestedComponentRegistryRules', + 'ch.qos.logback.core.net.ssl.SSLConfigurableSocket', + 'ch.qos.logback.core.net.ssl.SSLConfigurableServerSocket', + 'ch.qos.logback.core.net.ssl.SSLConfiguration', + 'ch.qos.logback.core.net.ssl.ConfigurableSSLSocketFactory', + 'ch.qos.logback.core.net.ssl.ConfigurableSSLServerSocketFactory', + 'ch.qos.logback.core.net.ssl.SecureRandomFactoryBean', + 'ch.qos.logback.core.net.ssl.SSLContextFactoryBean', + 'ch.qos.logback.core.net.ssl.SSL', + 'ch.qos.logback.core.net.ssl.SSLConfigurable', + 'ch.qos.logback.core.net.ssl.TrustManagerFactoryFactoryBean', + 'ch.qos.logback.core.net.ssl.KeyManagerFactoryFactoryBean', + 'ch.qos.logback.core.net.SMTPAppenderBase', + 'ch.qos.logback.core.net.SyslogAppenderBase', + 'ch.qos.logback.core.net.SocketConnector', + 'ch.qos.logback.core.net.SyslogOutputStream', + 'ch.qos.logback.core.net.QueueFactory', + 'ch.qos.logback.core.net.HardenedObjectInputStream', + 'ch.qos.logback.core.net.AbstractSocketAppender', + 'ch.qos.logback.core.net.AbstractSSLSocketAppender', + 'ch.qos.logback.core.net.ObjectWriterFactory', + 'ch.qos.logback.core.net.ObjectWriter', + 'ch.qos.logback.core.net.AutoFlushingObjectWriter', + 'ch.qos.logback.core.net.SyslogConstants', + 'ch.qos.logback.core.net.server.ServerRunner', + 'ch.qos.logback.core.net.server.Client', + 'ch.qos.logback.core.net.server.ServerListener', + 'ch.qos.logback.core.net.server.RemoteReceiverStreamClient', + 'ch.qos.logback.core.net.server.AbstractServerSocketAppender', + 'ch.qos.logback.core.net.server.ClientVisitor', + 'ch.qos.logback.core.net.server.RemoteReceiverClient', + 'ch.qos.logback.core.net.server.RemoteReceiverServerRunner', + 'ch.qos.logback.core.net.server.SSLServerSocketAppenderBase', + 'ch.qos.logback.core.net.server.ConcurrentServerRunner', + 'ch.qos.logback.core.net.server.ServerSocketListener', + 'ch.qos.logback.core.net.server.RemoteReceiverServerListener', + 'ch.qos.logback.core.UnsynchronizedAppenderBase', + 'ch.qos.logback.core.AsyncAppenderBase', + 'ch.qos.logback.core.util.CloseUtil', + 'ch.qos.logback.core.util.DatePatternToRegexUtil', + 'ch.qos.logback.core.util.StatusListenerConfigHelper', + 'ch.qos.logback.core.util.SystemInfo', + 'ch.qos.logback.core.util.DefaultInvocationGate', + 'ch.qos.logback.core.util.CachingDateFormatter', + 'ch.qos.logback.core.util.InterruptUtil', + 'ch.qos.logback.core.util.LocationUtil', + 'ch.qos.logback.core.util.TimeUtil', + 'ch.qos.logback.core.util.COWArrayList', + 'ch.qos.logback.core.util.Loader', + 'ch.qos.logback.core.util.CharSequenceState', + 'ch.qos.logback.core.util.StatusPrinter', + 'ch.qos.logback.core.util.Duration', + 'ch.qos.logback.core.util.ContentTypeUtil', + 'ch.qos.logback.core.util.FileUtil', + 'ch.qos.logback.core.util.DynamicClassLoadingException', + 'ch.qos.logback.core.util.InvocationGate', + 'ch.qos.logback.core.util.OptionHelper', + 'ch.qos.logback.core.util.IncompatibleClassException', + 'ch.qos.logback.core.util.ExecutorServiceUtil', + 'ch.qos.logback.core.util.StringCollectionUtil', + 'ch.qos.logback.core.util.CharSequenceToRegexMapper', + 'ch.qos.logback.core.util.FixedDelay', + 'ch.qos.logback.core.util.FileSize', + 'ch.qos.logback.core.util.DelayStrategy', + 'ch.qos.logback.core.util.EnvUtil', + 'ch.qos.logback.core.util.ContextUtil', + 'ch.qos.logback.core.util.AggregationType', + 'ch.qos.logback.core.util.PropertySetterException', + 'ch.qos.logback.core.LifeCycleManager', + 'ch.qos.logback.core.LayoutBase', + 'ch.qos.logback.core.encoder.NonClosableInputStream', + 'ch.qos.logback.core.encoder.Encoder', + 'ch.qos.logback.core.encoder.ByteArrayUtil', + 'ch.qos.logback.core.encoder.EncoderBase', + 'ch.qos.logback.core.encoder.EchoEncoder', + 'ch.qos.logback.core.encoder.LayoutWrappingEncoder', + 'ch.qos.logback.core.recovery.RecoveryCoordinator', + 'ch.qos.logback.core.recovery.ResilientOutputStreamBase', + 'ch.qos.logback.core.recovery.ResilientSyslogOutputStream', + 'ch.qos.logback.core.recovery.ResilientFileOutputStream', + 'ch.qos.logback.core.AppenderBase', + 'ch.qos.logback.core.subst.Node', + 'ch.qos.logback.core.subst.Parser', + 'ch.qos.logback.core.subst.Token', + 'ch.qos.logback.core.subst.NodeToStringTransformer', + 'ch.qos.logback.core.subst.Tokenizer', + 'ch.qos.logback.core.FileAppender', + 'ch.qos.logback.core.sift.AppenderFactory', + 'ch.qos.logback.core.sift.SiftingAppenderBase', + 'ch.qos.logback.core.sift.SiftingJoranConfiguratorBase', + 'ch.qos.logback.core.sift.AbstractDiscriminator', + 'ch.qos.logback.core.sift.Discriminator', + 'ch.qos.logback.core.sift.AbstractAppenderFactoryUsingJoran', + 'ch.qos.logback.core.sift.AppenderTracker', + 'ch.qos.logback.core.sift.DefaultDiscriminator', + 'ch.qos.logback.core.html.CssBuilder', + 'ch.qos.logback.core.html.NOPThrowableRenderer', + 'ch.qos.logback.core.html.HTMLLayoutBase', + 'ch.qos.logback.core.html.IThrowableRenderer', + 'ch.qos.logback.core.rolling.TriggeringPolicyBase', + 'ch.qos.logback.core.rolling.helper.Compressor', + 'ch.qos.logback.core.rolling.helper.PeriodicityType', + 'ch.qos.logback.core.rolling.helper.TokenConverter', + 'ch.qos.logback.core.rolling.helper.IntegerTokenConverter', + 'ch.qos.logback.core.rolling.helper.CompressionMode', + 'ch.qos.logback.core.rolling.helper.ArchiveRemover', + 'ch.qos.logback.core.rolling.helper.FileFilterUtil', + 'ch.qos.logback.core.rolling.helper.RenameUtil', + 'ch.qos.logback.core.rolling.helper.DateTokenConverter', + 'ch.qos.logback.core.rolling.helper.FileNamePattern', + 'ch.qos.logback.core.rolling.helper.RollingCalendar', + 'ch.qos.logback.core.rolling.helper.FileStoreUtil', + 'ch.qos.logback.core.rolling.helper.SizeAndTimeBasedArchiveRemover', + 'ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover', + 'ch.qos.logback.core.rolling.helper.MonoTypedConverter', + 'ch.qos.logback.core.rolling.RollingPolicyBase', + 'ch.qos.logback.core.rolling.RollingFileAppender', + 'ch.qos.logback.core.rolling.FixedWindowRollingPolicy', + 'ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase', + 'ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy', + 'ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy', + 'ch.qos.logback.core.rolling.RollingPolicy', + 'ch.qos.logback.core.rolling.TimeBasedRollingPolicy', + 'ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy', + 'ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy', + 'ch.qos.logback.core.rolling.RolloverFailure', + 'ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP', + 'ch.qos.logback.core.rolling.TriggeringPolicy', + 'ch.qos.logback.core.pattern.ReplacingCompositeConverter', + 'ch.qos.logback.core.pattern.ConverterUtil', + 'ch.qos.logback.core.pattern.parser.Compiler', + 'ch.qos.logback.core.pattern.parser.Node', + 'ch.qos.logback.core.pattern.parser.Parser', + 'ch.qos.logback.core.pattern.parser.Token', + 'ch.qos.logback.core.pattern.parser.OptionTokenizer', + 'ch.qos.logback.core.pattern.parser.TokenStream', + 'ch.qos.logback.core.pattern.parser.CompositeNode', + 'ch.qos.logback.core.pattern.parser.FormattingNode', + 'ch.qos.logback.core.pattern.parser.SimpleKeywordNode', + 'ch.qos.logback.core.pattern.Converter', + 'ch.qos.logback.core.pattern.PatternLayoutEncoderBase', + 'ch.qos.logback.core.pattern.LiteralConverter', + 'ch.qos.logback.core.pattern.PostCompileProcessor', + 'ch.qos.logback.core.pattern.util.RegularEscapeUtil', + 'ch.qos.logback.core.pattern.util.AsIsEscapeUtil', + 'ch.qos.logback.core.pattern.util.AlmostAsIsEscapeUtil', + 'ch.qos.logback.core.pattern.util.IEscapeUtil', + 'ch.qos.logback.core.pattern.util.RestrictedEscapeUtil', + 'ch.qos.logback.core.pattern.SpacePadder', + 'ch.qos.logback.core.pattern.CompositeConverter', + 'ch.qos.logback.core.pattern.PatternLayoutBase', + 'ch.qos.logback.core.pattern.DynamicConverter', + 'ch.qos.logback.core.pattern.color.YellowCompositeConverter', + 'ch.qos.logback.core.pattern.color.ANSIConstants', + 'ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter', + 'ch.qos.logback.core.pattern.color.CyanCompositeConverter', + 'ch.qos.logback.core.pattern.color.MagentaCompositeConverter', + 'ch.qos.logback.core.pattern.color.BlueCompositeConverter', + 'ch.qos.logback.core.pattern.color.BlackCompositeConverter', + 'ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase', + 'ch.qos.logback.core.pattern.color.GrayCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter', + 'ch.qos.logback.core.pattern.color.RedCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldRedCompositeConverter', + 'ch.qos.logback.core.pattern.color.GreenCompositeConverter', + 'ch.qos.logback.core.pattern.color.WhiteCompositeConverter', + 'ch.qos.logback.core.pattern.FormattingConverter', + 'ch.qos.logback.core.pattern.IdentityCompositeConverter', + 'ch.qos.logback.core.pattern.FormatInfo', + 'ch.qos.logback.core.OutputStreamAppender', + 'ch.qos.logback.core.boolex.JaninoEventEvaluatorBase', + 'ch.qos.logback.core.boolex.Matcher', + 'ch.qos.logback.core.boolex.EventEvaluatorBase', + 'ch.qos.logback.core.boolex.EvaluationException', + 'ch.qos.logback.core.boolex.EventEvaluator', + 'ch.qos.logback.core.read.CyclicBufferAppender', + 'ch.qos.logback.core.read.ListAppender', + 'ch.qos.logback.core.Context', + 'ch.qos.logback.core.ContextBase', + 'ch.qos.logback.core.status.StatusListenerAsList', + 'ch.qos.logback.core.status.StatusBase', + 'ch.qos.logback.core.status.NopStatusListener', + 'ch.qos.logback.core.status.StatusUtil', + 'ch.qos.logback.core.status.OnPrintStreamStatusListenerBase', + 'ch.qos.logback.core.status.StatusManager', + 'ch.qos.logback.core.status.ViewStatusMessagesServletBase', + 'ch.qos.logback.core.status.ErrorStatus', + 'ch.qos.logback.core.status.Status', + 'ch.qos.logback.core.status.StatusListener', + 'ch.qos.logback.core.status.InfoStatus', + 'ch.qos.logback.core.status.OnConsoleStatusListener', + 'ch.qos.logback.core.status.WarnStatus', + 'ch.qos.logback.core.status.OnErrorConsoleStatusListener', + 'ch.qos.logback.core.filter.EvaluatorFilter', + 'ch.qos.logback.core.filter.Filter', + 'ch.qos.logback.core.filter.AbstractMatcherFilter', + 'ch.qos.logback.core.Layout', + 'ch.qos.logback.classic.ViewStatusMessagesServlet', + 'ch.qos.logback.classic.ClassicConstants', + 'ch.qos.logback.classic.layout.TTLLLayout', + 'ch.qos.logback.classic.helpers.MDCInsertingServletFilter', + 'ch.qos.logback.classic.Level', + 'ch.qos.logback.classic.Level.off', + 'ch.qos.logback.classic.Level.error', + 'ch.qos.logback.classic.Level.warn', + 'ch.qos.logback.classic.Level.info', + 'ch.qos.logback.classic.Level.debug', + 'ch.qos.logback.classic.Level.trace', + 'ch.qos.logback.classic.Level.all,', + 'ch.qos.logback.classic.net.SSLSocketReceiver', + 'ch.qos.logback.classic.net.ReceiverBase', + 'ch.qos.logback.classic.net.SimpleSocketServer', + 'ch.qos.logback.classic.net.SimpleSSLSocketServer', + 'ch.qos.logback.classic.net.SocketNode', + 'ch.qos.logback.classic.net.SMTPAppender', + 'ch.qos.logback.classic.net.SocketReceiver', + 'ch.qos.logback.classic.net.SocketAcceptor', + 'ch.qos.logback.classic.net.SSLSocketAppender', + 'ch.qos.logback.classic.net.LoggingEventPreSerializationTransformer', + 'ch.qos.logback.classic.net.server.RemoteAppenderStreamClient', + 'ch.qos.logback.classic.net.server.RemoteAppenderServerListener', + 'ch.qos.logback.classic.net.server.SSLServerSocketAppender', + 'ch.qos.logback.classic.net.server.RemoteAppenderClient', + 'ch.qos.logback.classic.net.server.HardenedLoggingEventInputStream', + 'ch.qos.logback.classic.net.server.ServerSocketAppender', + 'ch.qos.logback.classic.net.server.SSLServerSocketReceiver', + 'ch.qos.logback.classic.net.server.RemoteAppenderServerRunner', + 'ch.qos.logback.classic.net.server.ServerSocketReceiver', + 'ch.qos.logback.classic.net.SocketAppender', + 'ch.qos.logback.classic.net.SyslogAppender', + 'ch.qos.logback.classic.PatternLayout', + 'ch.qos.logback.classic.util.ContextSelectorStaticBinder', + 'ch.qos.logback.classic.util.StatusViaSLF4JLoggerFactory', + 'ch.qos.logback.classic.util.JNDIUtil', + 'ch.qos.logback.classic.util.LevelToSyslogSeverity', + 'ch.qos.logback.classic.util.LoggerNameUtil', + 'ch.qos.logback.classic.util.LogbackMDCAdapter', + 'ch.qos.logback.classic.util.CopyOnInheritThreadLocal', + 'ch.qos.logback.classic.util.ContextInitializer', + 'ch.qos.logback.classic.util.EnvUtil', + 'ch.qos.logback.classic.util.DefaultNestedComponentRules', + 'ch.qos.logback.classic.AsyncAppender', + 'ch.qos.logback.classic.jul.JULHelper', + 'ch.qos.logback.classic.jul.LevelChangePropagator', + 'ch.qos.logback.classic.encoder.PatternLayoutEncoder', + 'ch.qos.logback.classic.db.names.DBNameResolver', + 'ch.qos.logback.classic.db.names.ColumnName', + 'ch.qos.logback.classic.db.names.TableName', + 'ch.qos.logback.classic.db.names.DefaultDBNameResolver', + 'ch.qos.logback.classic.db.names.SimpleDBNameResolver', + 'ch.qos.logback.classic.log4j.XMLLayout', + 'ch.qos.logback.classic.LoggerContext', + 'ch.qos.logback.classic.turbo.TurboFilter', + 'ch.qos.logback.classic.turbo.MDCFilter', + 'ch.qos.logback.classic.turbo.ReconfigureOnChangeFilter', + 'ch.qos.logback.classic.turbo.DuplicateMessageFilter', + 'ch.qos.logback.classic.turbo.MarkerFilter', + 'ch.qos.logback.classic.turbo.MDCValueLevelPair', + 'ch.qos.logback.classic.turbo.DynamicThresholdFilter', + 'ch.qos.logback.classic.turbo.MatchingFilter', + 'ch.qos.logback.classic.turbo.LRUMessageCache', + 'ch.qos.logback.classic.selector.servlet.LoggerContextFilter', + 'ch.qos.logback.classic.selector.servlet.ContextDetachingSCL', + 'ch.qos.logback.classic.selector.ContextJNDISelector', + 'ch.qos.logback.classic.selector.DefaultContextSelector', + 'ch.qos.logback.classic.selector.ContextSelector', + 'ch.qos.logback.classic.sift.MDCBasedDiscriminator', + 'ch.qos.logback.classic.sift.SiftingJoranConfigurator', + 'ch.qos.logback.classic.sift.JNDIBasedContextDiscriminator', + 'ch.qos.logback.classic.sift.AppenderFactoryUsingJoran', + 'ch.qos.logback.classic.sift.ContextBasedDiscriminator', + 'ch.qos.logback.classic.sift.SiftingAppender', + 'ch.qos.logback.classic.sift.SiftAction', + 'ch.qos.logback.classic.html.UrlCssBuilder', + 'ch.qos.logback.classic.html.HTMLLayout', + 'ch.qos.logback.classic.html.DefaultCssBuilder', + 'ch.qos.logback.classic.html.DefaultThrowableRenderer', + 'ch.qos.logback.classic.Logger', + 'ch.qos.logback.classic.pattern.ThrowableHandlingConverter', + 'ch.qos.logback.classic.pattern.ContextNameConverter', + 'ch.qos.logback.classic.pattern.LocalSequenceNumberConverter', + 'ch.qos.logback.classic.pattern.ClassOfCallerConverter', + 'ch.qos.logback.classic.pattern.PrefixCompositeConverter', + 'ch.qos.logback.classic.pattern.LineOfCallerConverter', + 'ch.qos.logback.classic.pattern.EnsureExceptionHandling', + 'ch.qos.logback.classic.pattern.TargetLengthBasedClassNameAbbreviator', + 'ch.qos.logback.classic.pattern.FileOfCallerConverter', + 'ch.qos.logback.classic.pattern.LevelConverter', + 'ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter', + 'ch.qos.logback.classic.pattern.NamedConverter', + 'ch.qos.logback.classic.pattern.ClassicConverter', + 'ch.qos.logback.classic.pattern.NopThrowableInformationConverter', + 'ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter', + 'ch.qos.logback.classic.pattern.MethodOfCallerConverter', + 'ch.qos.logback.classic.pattern.CallerDataConverter', + 'ch.qos.logback.classic.pattern.ClassNameOnlyAbbreviator', + 'ch.qos.logback.classic.pattern.MarkerConverter', + 'ch.qos.logback.classic.pattern.RelativeTimeConverter', + 'ch.qos.logback.classic.pattern.DateConverter', + 'ch.qos.logback.classic.pattern.PropertyConverter', + 'ch.qos.logback.classic.pattern.ThreadConverter', + 'ch.qos.logback.classic.pattern.LineSeparatorConverter', + 'ch.qos.logback.classic.pattern.MDCConverter', + 'ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter', + 'ch.qos.logback.classic.pattern.ThrowableProxyConverter', + 'ch.qos.logback.classic.pattern.Abbreviator', + 'ch.qos.logback.classic.pattern.Util', + 'ch.qos.logback.classic.pattern.LoggerConverter', + 'ch.qos.logback.classic.pattern.SyslogStartConverter', + 'ch.qos.logback.classic.pattern.MessageConverter', + 'ch.qos.logback.classic.gaffer.GafferUtil', + 'ch.qos.logback.classic.boolex.OnMarkerEvaluator', + 'ch.qos.logback.classic.boolex.JaninoEventEvaluator', + 'ch.qos.logback.classic.boolex.OnErrorEvaluator', + 'ch.qos.logback.classic.boolex.GEventEvaluator', + 'ch.qos.logback.classic.boolex.IEvaluator', + 'ch.qos.logback.classic.filter.ThresholdFilter', + 'ch.qos.logback.classic.filter.LevelFilter', + 'java.lang.System', + 'java.lang.System.getenv', + 'java.lang.System.getProperty', + 'java.lang.System.getenv', + 'java.util.Map.getOrDefault', + 'com.kotlindiscord.kord.extensions.utils._EnvironmentKt.envOrNull', +] diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/ParentPluginClassLoader.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/ParentPluginClassLoader.kt new file mode 100644 index 0000000000..09102517fb --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/ParentPluginClassLoader.kt @@ -0,0 +1,30 @@ +package com.kotlindiscord.kord.extensions.plugins + +public class ParentPluginClassLoader( + public val pluginsDirectory: String, +) : ClassLoader() { + private val childClassLoaders: MutableMap = mutableMapOf() + + public fun loadPluginLoader(pluginId: String, jarPath: String): Boolean { + if (pluginId in childClassLoaders) { + return false + } + + childClassLoaders[pluginId] = PluginClassLoader(jarPath, this) + + return true + } + + public fun removePluginLoader(pluginId: String): Boolean = + childClassLoaders.remove(pluginId) != null + + override fun loadClass(name: String?): Class<*> { + for (cl in childClassLoaders.values) { + try { + return cl.loadClassWithoutRecursion(name) + } catch (_: ClassNotFoundException) {} + } + + return getSystemClassLoader().loadClass(name) + } +} diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/Plugin.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/Plugin.kt new file mode 100644 index 0000000000..b2495a3e5a --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/Plugin.kt @@ -0,0 +1,25 @@ +package com.kotlindiscord.kord.extensions.plugins + +import com.kotlindiscord.kord.extensions.plugins.types.PluginManifest + +public abstract class Plugin> { + public lateinit var pluginManager: PluginManager + internal set + + public lateinit var manifest: PluginManifest + internal set + + public abstract fun load() + public abstract fun unload() + + public open fun onPluginLoaded(plugin: T) {} + public open fun onPluginUnloaded(plugin: T) {} + + public open fun internalLoad() { + load() + } + + public open fun internalUnload() { + unload() + } +} diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginClassLoader.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginClassLoader.kt new file mode 100644 index 0000000000..0abd4a1fd9 --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginClassLoader.kt @@ -0,0 +1,29 @@ +package com.kotlindiscord.kord.extensions.plugins + +import java.net.URL +import java.net.URLClassLoader + +public class PluginClassLoader( + public val jarPath: String, + private val parent: ParentPluginClassLoader +) : ClassLoader() { + private val urlClassLoader = URLClassLoader( + arrayOf(URL("file://${parent.pluginsDirectory}/$jarPath")), + null + ) + + internal fun loadClassWithoutRecursion(name: String?): Class<*> = + urlClassLoader.loadClass(name) + + override fun loadClass(name: String?): Class<*> { + return try { + urlClassLoader.loadClass(name) + } catch (e: ClassNotFoundException) { + parent.loadClass(name) + } + } + + public fun close() { + urlClassLoader.close() + } +} diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginManager.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginManager.kt new file mode 100644 index 0000000000..774b5f947d --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginManager.kt @@ -0,0 +1,219 @@ +@file:Suppress("TooGenericExceptionCaught") + +package com.kotlindiscord.kord.extensions.plugins + +import com.kotlindiscord.kord.extensions.plugins.constraints.ConstraintChecker +import com.kotlindiscord.kord.extensions.plugins.constraints.ConstraintResult +import com.kotlindiscord.kord.extensions.plugins.types.PluginManifest +import io.github.oshai.kotlinlogging.KotlinLogging +import io.github.z4kn4fein.semver.Version +import kotlinx.serialization.json.Json +import java.io.File +import java.lang.ref.WeakReference +import java.nio.file.FileSystems +import kotlin.io.path.exists +import kotlin.io.path.readText + +public class PluginManager>( + public val baseTypeReference: String, + public val extraVersions: Map = mapOf(), + public val errorOnFailedConstraint: Boolean = true, + + pluginDirectory: String = "plugins", + constraintCheckerCallback: (PluginManager<*>) -> ConstraintChecker = ::ConstraintChecker, +) { + private val logger = KotlinLogging.logger {} + private val constraintChecker = constraintCheckerCallback(this) + + public val pluginsDirectory: String = pluginDirectory.trimEnd('/', '\\') + + private val classLoader: ParentPluginClassLoader = ParentPluginClassLoader(pluginsDirectory) + private val pluginManifests: MutableMap = mutableMapOf() + + private val plugins: MutableMap = mutableMapOf() + + @Suppress("UNCHECKED_CAST") + public fun loadPlugin(pluginId: String, setup: Boolean = true, checkManifest: Boolean = true): WeakReference? { + if (pluginId in plugins) { + return WeakReference(plugins[pluginId]!!) + } + + val manifest = pluginManifests[pluginId] + ?: return null + + if (checkManifest) { + val results = constraintChecker.checkPlugin(pluginId, pluginManifests) + + if (results.isNotEmpty()) { + logConstraintResults(results) + + return null + } + } + + if (manifest.failed) { + logger.warn { "Not loading \"$pluginId\" as it failed to validate" } + + return null + } + + classLoader.loadPluginLoader(pluginId, manifest.jarPath) + + val loadedClass = try { + classLoader.loadClass(manifest.classRef).getDeclaredConstructor().newInstance() as T? + } catch (e: ClassNotFoundException) { + logger.error { "Failed to load \"$pluginId\": Class not found" } + + return null + } + + if (loadedClass == null) { + logger.error { "Class ${manifest.classRef} for \"$pluginId\" does not extend $baseTypeReference" } + + return null + } + + loadedClass.manifest = manifest + loadedClass.pluginManager = this + + plugins[pluginId] = loadedClass + + if (setup) { + try { + loadedClass.internalLoad() + } catch (e: Exception) { + logger.error(e) { "Failed to set up plugin \"$pluginId\", load function threw an exception" } + unloadPlugin(pluginId) + + return null + } + } + + logger.debug { "Loaded \"$pluginId\"" } + + return WeakReference(loadedClass) + } + + public fun unloadPlugin(pluginId: String): Boolean { + if (pluginId in plugins) { + try { + plugins[pluginId]!!.internalUnload() + } catch (e: Exception) { + logger.error(e) { "Failed to properly unload plugin \"$pluginId\", unload function threw an exception" } + } + + plugins.remove(pluginId) + classLoader.removePluginLoader(pluginId) + + return true + } + + return false + } + + public fun loadAllPlugins() { + reloadManifests() + + val pluginsToSetup = pluginManifests.keys.filter { it !in plugins } + + for (manifest in pluginManifests.values) { + try { + loadPlugin(manifest.id, setup = false, checkManifest = false) + } catch (e: Exception) { + logger.error(e) { "Failed to load plugin ${manifest.id} from ${manifest.jarPath}" } + } + } + + pluginsToSetup.forEach { + try { + plugins[it]?.internalLoad() + } catch (e: Exception) { + logger.error(e) { "Failed to set up plugin \"$it\", load function threw an exception" } + + unloadPlugin(it) + } + } + } + + public fun reloadManifests(): Boolean { + val files = File(pluginsDirectory).listFiles() + + if (files == null) { + logger.error { "Plugin directory $pluginsDirectory does not exist or is inaccessible" } + + return false + } + + val foundManifests: MutableList = mutableListOf() + + for (jarFile in files.filter { file -> file.extension == "jar" }) { + val zipFileSystem = FileSystems.newFileSystem(jarFile.toPath(), emptyMap()) + val path = zipFileSystem.getPath("kordex.plugin.json") + + if (!path.exists()) { + logger.warn { "Skipping ${jarFile.name} | JAR file does not contain a kordex.plugin.json file" } + + continue + } + + val manifest: PluginManifest = try { + Json.decodeFromString(path.readText()) + } catch (e: Exception) { + logger.error(e) { "Skipping ${jarFile.name} | Failed to load invalid manifest in kordex.plugin.json" } + + continue + } + + zipFileSystem.close() + + manifest.jarPath = jarFile.name + + pluginManifests[manifest.id] = manifest + foundManifests.add(manifest) + } + + val results = constraintChecker.checkAll(pluginManifests) + + if (results.isNotEmpty()) { + logConstraintResults(results) + + if (errorOnFailedConstraint) { + error("Failed to load plugins - check the log for more information.") + } + } + + logger.info { "Loaded ${foundManifests.size} plugin manifests" } + + return true + } + + public fun logConstraintResults(results: MutableMap>) { + val length = results.keys.maxBy { it.length }.length + + logger.error { + buildString { + appendLine("Plugin constraint errors detected:") + appendLine() + + results.forEach { (pluginId, resultList) -> + resultList.forEach { result -> + appendLine("${pluginId.padEnd(length)} | ${result.readableString}") + } + } + } + } + } + + public fun logConstraintResults(results: List) { + logger.error { + buildString { + appendLine("Plugin constraint errors detected:") + appendLine() + + results.forEach { result -> + appendLine("${result.pluginId} | ${result.readableString}") + } + } + } + } +} diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintChecker.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintChecker.kt new file mode 100644 index 0000000000..76ae8996c6 --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintChecker.kt @@ -0,0 +1,135 @@ +package com.kotlindiscord.kord.extensions.plugins.constraints + +import com.kotlindiscord.kord.extensions.plugins.PluginManager +import com.kotlindiscord.kord.extensions.plugins.types.PluginManifest +import io.github.z4kn4fein.semver.constraints.satisfiedBy + +private const val MAX_DEPTH: Int = 5 + +public open class ConstraintChecker( + protected val pluginManager: PluginManager<*>, +) { + public open fun checkAll(manifests: Map): MutableMap> { + val results: MutableMap> = mutableMapOf() + + manifests.forEach { (pluginId, _) -> + val result = checkPlugin(pluginId, manifests) + + if (result.isNotEmpty()) { + results[pluginId] = result + } + } + + return results + } + + public open fun checkPlugin( + pluginId: String, + manifests: Map, + depth: Int = 0, + alreadyExamined: MutableSet = mutableSetOf() + ): List { + val results: MutableList = mutableListOf() + + val manifest = manifests[pluginId] + ?: return emptyList() // Likely from the "extraVersions" map in the plugin manager + + alreadyExamined.add(pluginId) + + if (depth >= MAX_DEPTH) { + error("Maximum recursion depth exceeded") + } + + for ((conflictingId, constraint) in manifest.constraints.conflicts) { + val conflictingVersion = manifests[conflictingId]?.version + ?: pluginManager.extraVersions[conflictingId] + ?: continue + + if (constraint satisfiedBy conflictingVersion) { + results.add( + ConstraintResult.Conflict( + pluginId, + conflictingId, + conflictingVersion, + constraint + ) + ) + } + } + + for ((neededId, constraint) in manifest.constraints.needs) { + val neededVersion = manifests[neededId]?.version + ?: pluginManager.extraVersions[neededId] + + if (neededVersion == null) { + results.add( + ConstraintResult.Missing( + pluginId, + neededId, + constraint + ) + ) + } else if (!(constraint satisfiedBy neededVersion)) { + results.add( + ConstraintResult.WrongNeededVersion( + pluginId, + neededId, + neededVersion, + constraint + ) + ) + } else { + if (neededId in alreadyExamined) { + continue + } + + val innerResults = checkPlugin(neededId, manifests, depth + 1, alreadyExamined) + + if (innerResults.isNotEmpty()) { + results.add( + ConstraintResult.NeededPluginFailedConstraints( + pluginId, neededId, innerResults + ) + ) + } + } + } + + for ((wantedId, constraint) in manifest.constraints.wants) { + val wantedVersion = manifests[wantedId]?.version + ?: pluginManager.extraVersions[wantedId] + ?: continue + + if (!(constraint satisfiedBy wantedVersion)) { + results.add( + ConstraintResult.WrongWantedVersion( + pluginId, + wantedId, + wantedVersion, + constraint + ) + ) + } else { + if (wantedId in alreadyExamined) { + continue + } + + val innerResults = checkPlugin(wantedId, manifests, depth + 1, alreadyExamined) + + if (innerResults.isNotEmpty()) { + results.add( + ConstraintResult.WantedPluginFailedConstraints( + pluginId, wantedId, innerResults + ) + ) + } + } + } + + if (results.isNotEmpty()) { + manifests[pluginId]?.failed = true + } + + return results + } +} diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintResult.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintResult.kt new file mode 100644 index 0000000000..8191f7f8ec --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintResult.kt @@ -0,0 +1,118 @@ +package com.kotlindiscord.kord.extensions.plugins.constraints + +import io.github.z4kn4fein.semver.Version +import io.github.z4kn4fein.semver.constraints.Constraint + +private const val INDENT = "\t -> " + +public sealed class ConstraintResult( + public val pluginId: String, + public open val readableString: String, +) { + public class Conflict( + pluginId: String, + + public val conflictingPluginId: String, + public val conflictingPluginVersion: Version, + public val constraint: Constraint, + ) : ConstraintResult( + pluginId, + "Conflicts with \"$conflictingPluginId\" version $conflictingPluginVersion " + + "(constraint: $constraint)" + ) { + override fun toString(): String = + "Conflict: $pluginId -> $conflictingPluginId v$conflictingPluginVersion ($constraint)" + } + + public class Missing( + pluginId: String, + + public val neededPluginId: String, + public val constraint: Constraint, + ) : ConstraintResult( + pluginId, + "Needs \"$neededPluginId\" (constraint: $constraint), but it's missing" + ) { + override fun toString(): String = + "Missing: $pluginId -> $neededPluginId ($constraint)" + } + + public sealed class WrongVersion( + pluginId: String, + + public override val readableString: String, + ) : ConstraintResult(pluginId, readableString) { + public abstract val requiredPluginId: String + public abstract val requiredPluginVersion: Version + public abstract val constraint: Constraint + } + + public class WrongNeededVersion( + pluginId: String, + + public override val requiredPluginId: String, + public override val requiredPluginVersion: Version, + public override val constraint: Constraint, + ) : WrongVersion( + pluginId, + "Needs \"$requiredPluginId\" (constraint: $constraint), but incompatible version " + + "$requiredPluginVersion was provided" + ) { + override fun toString(): String = + "Need version: $pluginId -> $requiredPluginId $constraint, found $requiredPluginVersion" + } + + public class WrongWantedVersion( + pluginId: String, + + public override val requiredPluginId: String, + public override val requiredPluginVersion: Version, + public override val constraint: Constraint, + ) : WrongVersion( + pluginId, + "Wants \"$requiredPluginId\" (constraint: $constraint), but incompatible version " + + "$requiredPluginVersion was provided" + ) { + override fun toString(): String = + "Want version: $pluginId -> $requiredPluginId $constraint, found $requiredPluginVersion" + } + + public sealed class FailedInnerConstraints( + pluginId: String, + + public override val readableString: String, + ) : ConstraintResult(pluginId, readableString) { + public abstract val requiredPluginId: String + public abstract val innerResults: List + } + + public class NeededPluginFailedConstraints( + pluginId: String, + + public override val requiredPluginId: String, + public override val innerResults: List, + ) : FailedInnerConstraints( + pluginId, + "\"$requiredPluginId\" is required but has ${innerResults.size} failed constraints:\n" + + innerResults.joinToString("\n") { it.readableString.prependIndent(INDENT) } + ) { + override fun toString(): String = + "Needed failed constraints: $pluginId -> $requiredPluginId, ${innerResults.size} failures \n" + + innerResults.joinToString("\n") { it.toString().prependIndent(INDENT) } + } + + public class WantedPluginFailedConstraints( + pluginId: String, + + public override val requiredPluginId: String, + public override val innerResults: List, + ) : FailedInnerConstraints( + pluginId, + "\"$requiredPluginId\" is wanted but has ${innerResults.size} failed constraints:\n" + + innerResults.joinToString("\n") { it.readableString.prependIndent(INDENT) } + ) { + override fun toString(): String = + "Wanted failed constraints: $pluginId -> $requiredPluginId, ${innerResults.size} failures \n" + + innerResults.joinToString("\n") { it.toString().prependIndent(INDENT) } + } +} diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/Aliases.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/Aliases.kt new file mode 100644 index 0000000000..7e6183a9f5 --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/Aliases.kt @@ -0,0 +1,5 @@ +package com.kotlindiscord.kord.extensions.plugins.types + +import io.github.z4kn4fein.semver.constraints.Constraint + +public typealias PluginConstraint = Map diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/PluginConstraints.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/PluginConstraints.kt new file mode 100644 index 0000000000..fd42a38c64 --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/PluginConstraints.kt @@ -0,0 +1,10 @@ +package com.kotlindiscord.kord.extensions.plugins.types + +import kotlinx.serialization.Serializable + +@Serializable +public data class PluginConstraints( + val conflicts: PluginConstraint = mapOf(), + val needs: PluginConstraint = mapOf(), + val wants: PluginConstraint = mapOf(), +) diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/PluginManifest.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/PluginManifest.kt new file mode 100644 index 0000000000..876ab0c6fd --- /dev/null +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/types/PluginManifest.kt @@ -0,0 +1,28 @@ +package com.kotlindiscord.kord.extensions.plugins.types + +import io.github.z4kn4fein.semver.Version +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +@Suppress("DataClassShouldBeImmutable") +public data class PluginManifest( + @SerialName("class") + val classRef: String, + + val id: String, + + val description: String, + val license: String, + val name: String, + val version: Version, + + val constraints: PluginConstraints = PluginConstraints(), +) { + @Transient + lateinit var jarPath: String + + @Transient + public var failed: Boolean = false +} diff --git a/plugins/src/test/kotlin/ManifestFixtures.kt b/plugins/src/test/kotlin/ManifestFixtures.kt new file mode 100644 index 0000000000..45e98ed1cd --- /dev/null +++ b/plugins/src/test/kotlin/ManifestFixtures.kt @@ -0,0 +1,224 @@ +import com.kotlindiscord.kord.extensions.plugins.types.PluginConstraints +import com.kotlindiscord.kord.extensions.plugins.types.PluginManifest +import io.github.z4kn4fein.semver.constraints.toConstraint +import io.github.z4kn4fein.semver.toVersion + +object ManifestFixtures { + val good: Map = mapOf( + "one" to PluginManifest( + classRef = "a.b.OneKt", + id = "one", + description = "Test Plugin 1", + license = "MPL-2.0", + name = "Test Plugin 1", + version = "0.0.1".toVersion(), + + constraints = PluginConstraints( + needs = mapOf( + "two" to ">= 0.0.1".toConstraint(), + "fake20" to "> 1.0.0".toConstraint() + ), + + wants = mapOf( + "three" to "> 0.0.2".toConstraint() + ) + ) + ), + + "two" to PluginManifest( + classRef = "a.b.TwoKt", + id = "two", + description = "Test Plugin 2", + license = "MPL-2.0", + name = "Test Plugin 2", + version = "0.0.2".toVersion(), + + constraints = PluginConstraints( + needs = mapOf( + "one" to "*".toConstraint(), + "fake35" to "> 2.5.0".toConstraint() + ), + + wants = mapOf( + "three" to "*".toConstraint() + ) + ) + ), + + "three" to PluginManifest( + classRef = "a.b.TwoKt", + id = "two", + description = "Test Plugin 2", + license = "MPL-2.0", + name = "Test Plugin 2", + version = "0.0.3".toVersion() + ), + ) + + val missingNeeds: Map = mapOf( + "one" to PluginManifest( + classRef = "a.b.OneKt", + id = "one", + description = "Test Plugin 1", + license = "MPL-2.0", + name = "Test Plugin 1", + version = "0.0.1".toVersion(), + + constraints = PluginConstraints( + needs = mapOf( + "two" to ">= 0.0.1".toConstraint(), + "fake20" to "> 1.0.0".toConstraint(), + "three" to "> 0.0.2".toConstraint() + ), + ) + ), + + "two" to PluginManifest( + classRef = "a.b.TwoKt", + id = "two", + description = "Test Plugin 2", + license = "MPL-2.0", + name = "Test Plugin 2", + version = "0.0.2".toVersion(), + + constraints = PluginConstraints( + needs = mapOf( + "one" to "*".toConstraint(), + "fake35" to "> 2.5.0".toConstraint(), + "three" to "*".toConstraint() + ), + ) + ), + ) + val missingWants: Map = mapOf( + "one" to PluginManifest( + classRef = "a.b.OneKt", + id = "one", + description = "Test Plugin 1", + license = "MPL-2.0", + name = "Test Plugin 1", + version = "0.0.1".toVersion(), + + constraints = PluginConstraints( + wants = mapOf( + "two" to ">= 0.0.1".toConstraint(), + "fake20" to "> 1.0.0".toConstraint(), + "three" to "> 0.0.2".toConstraint() + ), + ) + ), + + "two" to PluginManifest( + classRef = "a.b.TwoKt", + id = "two", + description = "Test Plugin 2", + license = "MPL-2.0", + name = "Test Plugin 2", + version = "0.0.2".toVersion(), + + constraints = PluginConstraints( + wants = mapOf( + "one" to "*".toConstraint(), + "fake35" to "> 2.5.0".toConstraint(), + "three" to "*".toConstraint() + ), + ) + ), + ) + + val badWants: Map = mapOf( + "one" to PluginManifest( + classRef = "a.b.OneKt", + id = "one", + description = "Test Plugin 1", + license = "MPL-2.0", + name = "Test Plugin 1", + version = "0.0.1".toVersion(), + + constraints = PluginConstraints( + needs = mapOf( + "two" to ">= 0.0.1".toConstraint(), + "fake20" to "> 1.0.0".toConstraint() + ), + + wants = mapOf( + "three" to "> 0.0.4".toConstraint() + ) + ) + ), + + "two" to PluginManifest( + classRef = "a.b.TwoKt", + id = "two", + description = "Test Plugin 2", + license = "MPL-2.0", + name = "Test Plugin 2", + version = "0.0.2".toVersion(), + + constraints = PluginConstraints( + needs = mapOf( + "one" to "*".toConstraint(), + "fake35" to "> 2.5.0".toConstraint() + ), + + wants = mapOf( + "fake35" to "< 3.0.0".toConstraint() + ) + ) + ), + + "three" to PluginManifest( + classRef = "a.b.TwoKt", + id = "two", + description = "Test Plugin 2", + license = "MPL-2.0", + name = "Test Plugin 2", + version = "0.0.3".toVersion() + ), + ) + + val badNeeds: Map = mapOf( + "one" to PluginManifest( + classRef = "a.b.OneKt", + id = "one", + description = "Test Plugin 1", + license = "MPL-2.0", + name = "Test Plugin 1", + version = "0.0.1".toVersion(), + + constraints = PluginConstraints( + needs = mapOf( + "two" to ">= 0.0.1".toConstraint(), + "three" to "> 0.0.4".toConstraint(), + + "fake20" to "> 1.0.0".toConstraint() + ), + ) + ), + + "two" to PluginManifest( + classRef = "a.b.TwoKt", + id = "two", + description = "Test Plugin 2", + license = "MPL-2.0", + name = "Test Plugin 2", + version = "0.0.2".toVersion(), + + constraints = PluginConstraints( + needs = mapOf( + "one" to "*".toConstraint(), + "fake35" to "< 3.0.0".toConstraint(), + ), + ) + ), + + "three" to PluginManifest( + classRef = "a.b.TwoKt", + id = "two", + description = "Test Plugin 2", + license = "MPL-2.0", + name = "Test Plugin 2", + version = "0.0.3".toVersion() + ), + ) +} diff --git a/plugins/src/test/kotlin/tests/ConstraintTest.kt b/plugins/src/test/kotlin/tests/ConstraintTest.kt new file mode 100644 index 0000000000..418b301aaf --- /dev/null +++ b/plugins/src/test/kotlin/tests/ConstraintTest.kt @@ -0,0 +1,125 @@ +package tests + +import ManifestFixtures +import com.kotlindiscord.kord.extensions.plugins.PluginManager +import com.kotlindiscord.kord.extensions.plugins.constraints.ConstraintChecker +import com.kotlindiscord.kord.extensions.plugins.constraints.ConstraintResult +import io.github.z4kn4fein.semver.toVersion +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode +import types.FakePlugin + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ConstraintTest { + private val pluginManager = PluginManager( + "types.FakePlugin", + + extraVersions = mapOf( + "fake10" to "1.0.0".toVersion(), + "fake15" to "1.5.0".toVersion(), + "fake20" to "2.0.0".toVersion(), + "fake25" to "2.5.0".toVersion(), + "fake30" to "3.0.0".toVersion(), + "fake35" to "3.5.0".toVersion(), + ) + ) + + private val constraintChecker = ConstraintChecker(pluginManager) + + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `No problems with good constraints`() { + val results = constraintChecker.checkAll(ManifestFixtures.good) + + assert(results.isEmpty()) { + "Expected zero results, got $results" + } + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `No problems with missing wanted dependency`() { + val results = constraintChecker.checkAll(ManifestFixtures.missingWants) + + assert(results.isEmpty()) { + "Expected zero results, got $results" + } + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Problems found with bad wants`() { + val results = constraintChecker.checkAll(ManifestFixtures.badWants) + + assert(results.isNotEmpty()) { + "Expected at least one result, got zero" + } + + val badResults = results.values.flatten().filter { + it !is ConstraintResult.WrongWantedVersion && + it !is ConstraintResult.FailedInnerConstraints + } + + assert(badResults.isEmpty()) { + "Expected only wanted version/inner failures, additionally found $badResults" + } + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Problems found with bad needs`() { + val results = constraintChecker.checkAll(ManifestFixtures.badNeeds) + + assert(results.isNotEmpty()) { + "Expected at least one result, got zero" + } + + val badResults = results.values.flatten().filter { + it !is ConstraintResult.WrongNeededVersion && + it !is ConstraintResult.FailedInnerConstraints + } + + assert(badResults.isEmpty()) { + "Expected only needed version/inner failures results, additionally found $badResults" + } + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Problems found with missing needed dependency`() { + val results = constraintChecker.checkAll(ManifestFixtures.missingNeeds) + + assert(results.isNotEmpty()) { + "Expected at least one result, got zero" + } + + val badResults = results.values.flatten().filter { + it !is ConstraintResult.Missing && + it !is ConstraintResult.FailedInnerConstraints + } + + assert(badResults.isEmpty()) { + "Expected only missing/inner failures, additionally found $badResults" + } + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Recursive checking with errors`() { + val results = constraintChecker.checkPlugin("one", ManifestFixtures.badWants) + + assert(results.isNotEmpty()) { + "Expected at least one result, got zero" + } + + val goodResults = results.filter { + it is ConstraintResult.FailedInnerConstraints + } + + assert(goodResults.isNotEmpty()) { + "Expected inner failures results, but found zero; results: $results" + } + } +} diff --git a/plugins/src/test/kotlin/types/FakePlugin.kt b/plugins/src/test/kotlin/types/FakePlugin.kt new file mode 100644 index 0000000000..d3aa07d4b8 --- /dev/null +++ b/plugins/src/test/kotlin/types/FakePlugin.kt @@ -0,0 +1,13 @@ +package types + +import com.kotlindiscord.kord.extensions.plugins.Plugin + +class FakePlugin : Plugin() { + override fun load() { + TODO("Not yet implemented") + } + + override fun unload() { + TODO("Not yet implemented") + } +} diff --git a/plugins/src/test/resources/junit-platform.properties b/plugins/src/test/resources/junit-platform.properties new file mode 100644 index 0000000000..580f511dad --- /dev/null +++ b/plugins/src/test/resources/junit-platform.properties @@ -0,0 +1,7 @@ +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# + +junit.jupiter.execution.parallel.enabled=true diff --git a/plugins/src/test/resources/logback.groovy b/plugins/src/test/resources/logback.groovy new file mode 100644 index 0000000000..a0c69989f4 --- /dev/null +++ b/plugins/src/test/resources/logback.groovy @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.core.joran.spi.ConsoleTarget +import ch.qos.logback.core.ConsoleAppender +import ch.qos.logback.core.FileAppender + +def environment = System.getenv("ENVIRONMENT") ?: "dev" +def defaultLevel = TRACE + +if (environment == "spam") { + logger("dev.kord.rest.DefaultGateway", TRACE) +} else { + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) +} + +appender("CONSOLE", ConsoleAppender) { + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + + withJansi = true + } + + target = ConsoleTarget.SystemOut +} + +appender("FILE", FileAppender) { + file = "output.log" + + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } +} + +root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/plugins/src/test/resources/logbackCompiler.groovy b/plugins/src/test/resources/logbackCompiler.groovy new file mode 100644 index 0000000000..a374f9af62 --- /dev/null +++ b/plugins/src/test/resources/logbackCompiler.groovy @@ -0,0 +1,458 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +importsAcceptList = [ + 'ch.qos.logback.core.testUtil.SampleConverter', + + 'ch.qos.logback.core.testUtil.StringListAppender', + 'java.lang.Object', + 'org.springframework.beans.factory.annotation.Autowired', + 'java.nio.charset.Charset.forName', + 'com.logentries.logback.LogentriesAppender', + 'grails.util.BuildSettings', + 'grails.util.Environment', + 'io.micronaut.context.env.Environment', + 'org.slf4j.MDC', + 'org.springframework.boot.logging.logback.ColorConverter', + 'org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter', + 'java.nio.charset.Charset', + 'java.nio.charset.StandardCharsets', + + 'ch.qos.logback.core.BasicStatusManager', + 'ch.qos.logback.core.ConsoleAppender', + 'ch.qos.logback.core.hook.ShutdownHook', + 'ch.qos.logback.core.hook.ShutdownHookBase', + 'ch.qos.logback.core.hook.DelayingShutdownHook', + 'ch.qos.logback.core.spi.PropertyContainer', + 'ch.qos.logback.core.spi.ContextAwareBase', + 'ch.qos.logback.core.spi.LogbackLock', + 'ch.qos.logback.core.spi.FilterAttachableImpl', + 'ch.qos.logback.core.spi.ContextAwareImpl', + 'ch.qos.logback.core.spi.ScanException', + 'ch.qos.logback.core.spi.DeferredProcessingAware', + 'ch.qos.logback.core.spi.ContextAware', + 'ch.qos.logback.core.spi.LifeCycle', + 'ch.qos.logback.core.spi.FilterReply', + 'ch.qos.logback.core.spi.PreSerializationTransformer', + 'ch.qos.logback.core.spi.AppenderAttachable', + 'ch.qos.logback.core.spi.CyclicBufferTracker', + 'ch.qos.logback.core.spi.FilterAttachable', + 'ch.qos.logback.core.spi.ComponentTracker', + 'ch.qos.logback.core.spi.AppenderAttachableImpl', + 'ch.qos.logback.core.spi.PropertyDefiner', + 'ch.qos.logback.core.spi.AbstractComponentTracker', + 'ch.qos.logback.core.property.FileExistsPropertyDefiner', + 'ch.qos.logback.core.property.ResourceExistsPropertyDefiner', + 'ch.qos.logback.core.CoreConstants', + 'ch.qos.logback.core.layout.EchoLayout', + 'ch.qos.logback.core.Appender', + 'ch.qos.logback.core.joran.JoranConfiguratorBase', + 'ch.qos.logback.core.joran.spi.ActionException', + 'ch.qos.logback.core.joran.spi.HostClassAndPropertyDouble', + 'ch.qos.logback.core.joran.spi.JoranException', + 'ch.qos.logback.core.joran.spi.NoAutoStart', + 'ch.qos.logback.core.joran.spi.EventPlayer', + 'ch.qos.logback.core.joran.spi.XMLUtil', + 'ch.qos.logback.core.joran.spi.ConsoleTarget', + 'ch.qos.logback.core.joran.spi.Interpreter', + 'ch.qos.logback.core.joran.spi.SimpleRuleStore', + 'ch.qos.logback.core.joran.spi.InterpretationContext', + 'ch.qos.logback.core.joran.spi.RuleStore', + 'ch.qos.logback.core.joran.spi.NoAutoStartUtil', + 'ch.qos.logback.core.joran.spi.ElementSelector', + 'ch.qos.logback.core.joran.spi.ConfigurationWatchList', + 'ch.qos.logback.core.joran.spi.DefaultNestedComponentRegistry', + 'ch.qos.logback.core.joran.spi.DefaultClass', + 'ch.qos.logback.core.joran.spi.ElementPath', + 'ch.qos.logback.core.joran.conditional.ThenOrElseActionBase', + 'ch.qos.logback.core.joran.conditional.Condition', + 'ch.qos.logback.core.joran.conditional.PropertyWrapperForScripts', + 'ch.qos.logback.core.joran.conditional.ThenAction', + 'ch.qos.logback.core.joran.conditional.PropertyEvalScriptBuilder', + 'ch.qos.logback.core.joran.conditional.ElseAction', + 'ch.qos.logback.core.joran.conditional.IfAction', + 'ch.qos.logback.core.joran.util.beans.BeanDescriptionCache', + 'ch.qos.logback.core.joran.util.beans.BeanDescriptionFactory', + 'ch.qos.logback.core.joran.util.beans.BeanDescription', + 'ch.qos.logback.core.joran.util.beans.BeanUtil', + 'ch.qos.logback.core.joran.util.PropertySetter', + 'ch.qos.logback.core.joran.util.ConfigurationWatchListUtil', + 'ch.qos.logback.core.joran.util.StringToObjectConverter', + 'ch.qos.logback.core.joran.GenericConfigurator', + 'ch.qos.logback.core.joran.action.ImplicitAction', + 'ch.qos.logback.core.joran.action.IncludeAction', + 'ch.qos.logback.core.joran.action.NOPAction', + 'ch.qos.logback.core.joran.action.IADataForBasicProperty', + 'ch.qos.logback.core.joran.action.TimestampAction', + 'ch.qos.logback.core.joran.action.AbstractEventEvaluatorAction', + 'ch.qos.logback.core.joran.action.ParamAction', + 'ch.qos.logback.core.joran.action.AppenderAction', + 'ch.qos.logback.core.joran.action.DefinePropertyAction', + 'ch.qos.logback.core.joran.action.StatusListenerAction', + 'ch.qos.logback.core.joran.action.ContextPropertyAction', + 'ch.qos.logback.core.joran.action.NestedComplexPropertyIA', + 'ch.qos.logback.core.joran.action.NestedBasicPropertyIA', + 'ch.qos.logback.core.joran.action.Action', + 'ch.qos.logback.core.joran.action.AppenderRefAction', + 'ch.qos.logback.core.joran.action.ActionUtil', + 'ch.qos.logback.core.joran.action.ShutdownHookAction', + 'ch.qos.logback.core.joran.action.IADataForComplexProperty', + 'ch.qos.logback.core.joran.action.ConversionRuleAction', + 'ch.qos.logback.core.joran.action.ActionConst', + 'ch.qos.logback.core.joran.action.PropertyAction', + 'ch.qos.logback.core.joran.action.NewRuleAction', + 'ch.qos.logback.core.joran.node.ComponentNode', + 'ch.qos.logback.core.joran.event.EndEvent', + 'ch.qos.logback.core.joran.event.SaxEventRecorder', + 'ch.qos.logback.core.joran.event.SaxEvent', + 'ch.qos.logback.core.joran.event.BodyEvent', + 'ch.qos.logback.core.joran.event.StartEvent', + 'ch.qos.logback.core.joran.event.InPlayListener', + 'ch.qos.logback.core.joran.event.stax.EndEvent', + 'ch.qos.logback.core.joran.event.stax.StaxEventRecorder', + 'ch.qos.logback.core.joran.event.stax.BodyEvent', + 'ch.qos.logback.core.joran.event.stax.StartEvent', + 'ch.qos.logback.core.joran.event.stax.StaxEvent', + 'ch.qos.logback.core.LogbackException', + 'ch.qos.logback.core.PropertyDefinerBase', + 'ch.qos.logback.core.helpers.CyclicBuffer', + 'ch.qos.logback.core.helpers.ThrowableToStringArray', + 'ch.qos.logback.core.helpers.Transform', + 'ch.qos.logback.core.helpers.NOPAppender', + 'ch.qos.logback.core.net.LoginAuthenticator', + 'ch.qos.logback.core.net.DefaultSocketConnector', + 'ch.qos.logback.core.net.ssl.KeyStoreFactoryBean', + 'ch.qos.logback.core.net.ssl.SSLParametersConfiguration', + 'ch.qos.logback.core.net.ssl.SSLComponent', + 'ch.qos.logback.core.net.ssl.SSLNestedComponentRegistryRules', + 'ch.qos.logback.core.net.ssl.SSLConfigurableSocket', + 'ch.qos.logback.core.net.ssl.SSLConfigurableServerSocket', + 'ch.qos.logback.core.net.ssl.SSLConfiguration', + 'ch.qos.logback.core.net.ssl.ConfigurableSSLSocketFactory', + 'ch.qos.logback.core.net.ssl.ConfigurableSSLServerSocketFactory', + 'ch.qos.logback.core.net.ssl.SecureRandomFactoryBean', + 'ch.qos.logback.core.net.ssl.SSLContextFactoryBean', + 'ch.qos.logback.core.net.ssl.SSL', + 'ch.qos.logback.core.net.ssl.SSLConfigurable', + 'ch.qos.logback.core.net.ssl.TrustManagerFactoryFactoryBean', + 'ch.qos.logback.core.net.ssl.KeyManagerFactoryFactoryBean', + 'ch.qos.logback.core.net.SMTPAppenderBase', + 'ch.qos.logback.core.net.SyslogAppenderBase', + 'ch.qos.logback.core.net.SocketConnector', + 'ch.qos.logback.core.net.SyslogOutputStream', + 'ch.qos.logback.core.net.QueueFactory', + 'ch.qos.logback.core.net.HardenedObjectInputStream', + 'ch.qos.logback.core.net.AbstractSocketAppender', + 'ch.qos.logback.core.net.AbstractSSLSocketAppender', + 'ch.qos.logback.core.net.ObjectWriterFactory', + 'ch.qos.logback.core.net.ObjectWriter', + 'ch.qos.logback.core.net.AutoFlushingObjectWriter', + 'ch.qos.logback.core.net.SyslogConstants', + 'ch.qos.logback.core.net.server.ServerRunner', + 'ch.qos.logback.core.net.server.Client', + 'ch.qos.logback.core.net.server.ServerListener', + 'ch.qos.logback.core.net.server.RemoteReceiverStreamClient', + 'ch.qos.logback.core.net.server.AbstractServerSocketAppender', + 'ch.qos.logback.core.net.server.ClientVisitor', + 'ch.qos.logback.core.net.server.RemoteReceiverClient', + 'ch.qos.logback.core.net.server.RemoteReceiverServerRunner', + 'ch.qos.logback.core.net.server.SSLServerSocketAppenderBase', + 'ch.qos.logback.core.net.server.ConcurrentServerRunner', + 'ch.qos.logback.core.net.server.ServerSocketListener', + 'ch.qos.logback.core.net.server.RemoteReceiverServerListener', + 'ch.qos.logback.core.UnsynchronizedAppenderBase', + 'ch.qos.logback.core.AsyncAppenderBase', + 'ch.qos.logback.core.util.CloseUtil', + 'ch.qos.logback.core.util.DatePatternToRegexUtil', + 'ch.qos.logback.core.util.StatusListenerConfigHelper', + 'ch.qos.logback.core.util.SystemInfo', + 'ch.qos.logback.core.util.DefaultInvocationGate', + 'ch.qos.logback.core.util.CachingDateFormatter', + 'ch.qos.logback.core.util.InterruptUtil', + 'ch.qos.logback.core.util.LocationUtil', + 'ch.qos.logback.core.util.TimeUtil', + 'ch.qos.logback.core.util.COWArrayList', + 'ch.qos.logback.core.util.Loader', + 'ch.qos.logback.core.util.CharSequenceState', + 'ch.qos.logback.core.util.StatusPrinter', + 'ch.qos.logback.core.util.Duration', + 'ch.qos.logback.core.util.ContentTypeUtil', + 'ch.qos.logback.core.util.FileUtil', + 'ch.qos.logback.core.util.DynamicClassLoadingException', + 'ch.qos.logback.core.util.InvocationGate', + 'ch.qos.logback.core.util.OptionHelper', + 'ch.qos.logback.core.util.IncompatibleClassException', + 'ch.qos.logback.core.util.ExecutorServiceUtil', + 'ch.qos.logback.core.util.StringCollectionUtil', + 'ch.qos.logback.core.util.CharSequenceToRegexMapper', + 'ch.qos.logback.core.util.FixedDelay', + 'ch.qos.logback.core.util.FileSize', + 'ch.qos.logback.core.util.DelayStrategy', + 'ch.qos.logback.core.util.EnvUtil', + 'ch.qos.logback.core.util.ContextUtil', + 'ch.qos.logback.core.util.AggregationType', + 'ch.qos.logback.core.util.PropertySetterException', + 'ch.qos.logback.core.LifeCycleManager', + 'ch.qos.logback.core.LayoutBase', + 'ch.qos.logback.core.encoder.NonClosableInputStream', + 'ch.qos.logback.core.encoder.Encoder', + 'ch.qos.logback.core.encoder.ByteArrayUtil', + 'ch.qos.logback.core.encoder.EncoderBase', + 'ch.qos.logback.core.encoder.EchoEncoder', + 'ch.qos.logback.core.encoder.LayoutWrappingEncoder', + 'ch.qos.logback.core.recovery.RecoveryCoordinator', + 'ch.qos.logback.core.recovery.ResilientOutputStreamBase', + 'ch.qos.logback.core.recovery.ResilientSyslogOutputStream', + 'ch.qos.logback.core.recovery.ResilientFileOutputStream', + 'ch.qos.logback.core.AppenderBase', + 'ch.qos.logback.core.subst.Node', + 'ch.qos.logback.core.subst.Parser', + 'ch.qos.logback.core.subst.Token', + 'ch.qos.logback.core.subst.NodeToStringTransformer', + 'ch.qos.logback.core.subst.Tokenizer', + 'ch.qos.logback.core.FileAppender', + 'ch.qos.logback.core.sift.AppenderFactory', + 'ch.qos.logback.core.sift.SiftingAppenderBase', + 'ch.qos.logback.core.sift.SiftingJoranConfiguratorBase', + 'ch.qos.logback.core.sift.AbstractDiscriminator', + 'ch.qos.logback.core.sift.Discriminator', + 'ch.qos.logback.core.sift.AbstractAppenderFactoryUsingJoran', + 'ch.qos.logback.core.sift.AppenderTracker', + 'ch.qos.logback.core.sift.DefaultDiscriminator', + 'ch.qos.logback.core.html.CssBuilder', + 'ch.qos.logback.core.html.NOPThrowableRenderer', + 'ch.qos.logback.core.html.HTMLLayoutBase', + 'ch.qos.logback.core.html.IThrowableRenderer', + 'ch.qos.logback.core.rolling.TriggeringPolicyBase', + 'ch.qos.logback.core.rolling.helper.Compressor', + 'ch.qos.logback.core.rolling.helper.PeriodicityType', + 'ch.qos.logback.core.rolling.helper.TokenConverter', + 'ch.qos.logback.core.rolling.helper.IntegerTokenConverter', + 'ch.qos.logback.core.rolling.helper.CompressionMode', + 'ch.qos.logback.core.rolling.helper.ArchiveRemover', + 'ch.qos.logback.core.rolling.helper.FileFilterUtil', + 'ch.qos.logback.core.rolling.helper.RenameUtil', + 'ch.qos.logback.core.rolling.helper.DateTokenConverter', + 'ch.qos.logback.core.rolling.helper.FileNamePattern', + 'ch.qos.logback.core.rolling.helper.RollingCalendar', + 'ch.qos.logback.core.rolling.helper.FileStoreUtil', + 'ch.qos.logback.core.rolling.helper.SizeAndTimeBasedArchiveRemover', + 'ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover', + 'ch.qos.logback.core.rolling.helper.MonoTypedConverter', + 'ch.qos.logback.core.rolling.RollingPolicyBase', + 'ch.qos.logback.core.rolling.RollingFileAppender', + 'ch.qos.logback.core.rolling.FixedWindowRollingPolicy', + 'ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase', + 'ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy', + 'ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy', + 'ch.qos.logback.core.rolling.RollingPolicy', + 'ch.qos.logback.core.rolling.TimeBasedRollingPolicy', + 'ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy', + 'ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy', + 'ch.qos.logback.core.rolling.RolloverFailure', + 'ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP', + 'ch.qos.logback.core.rolling.TriggeringPolicy', + 'ch.qos.logback.core.pattern.ReplacingCompositeConverter', + 'ch.qos.logback.core.pattern.ConverterUtil', + 'ch.qos.logback.core.pattern.parser.Compiler', + 'ch.qos.logback.core.pattern.parser.Node', + 'ch.qos.logback.core.pattern.parser.Parser', + 'ch.qos.logback.core.pattern.parser.Token', + 'ch.qos.logback.core.pattern.parser.OptionTokenizer', + 'ch.qos.logback.core.pattern.parser.TokenStream', + 'ch.qos.logback.core.pattern.parser.CompositeNode', + 'ch.qos.logback.core.pattern.parser.FormattingNode', + 'ch.qos.logback.core.pattern.parser.SimpleKeywordNode', + 'ch.qos.logback.core.pattern.Converter', + 'ch.qos.logback.core.pattern.PatternLayoutEncoderBase', + 'ch.qos.logback.core.pattern.LiteralConverter', + 'ch.qos.logback.core.pattern.PostCompileProcessor', + 'ch.qos.logback.core.pattern.util.RegularEscapeUtil', + 'ch.qos.logback.core.pattern.util.AsIsEscapeUtil', + 'ch.qos.logback.core.pattern.util.AlmostAsIsEscapeUtil', + 'ch.qos.logback.core.pattern.util.IEscapeUtil', + 'ch.qos.logback.core.pattern.util.RestrictedEscapeUtil', + 'ch.qos.logback.core.pattern.SpacePadder', + 'ch.qos.logback.core.pattern.CompositeConverter', + 'ch.qos.logback.core.pattern.PatternLayoutBase', + 'ch.qos.logback.core.pattern.DynamicConverter', + 'ch.qos.logback.core.pattern.color.YellowCompositeConverter', + 'ch.qos.logback.core.pattern.color.ANSIConstants', + 'ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter', + 'ch.qos.logback.core.pattern.color.CyanCompositeConverter', + 'ch.qos.logback.core.pattern.color.MagentaCompositeConverter', + 'ch.qos.logback.core.pattern.color.BlueCompositeConverter', + 'ch.qos.logback.core.pattern.color.BlackCompositeConverter', + 'ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase', + 'ch.qos.logback.core.pattern.color.GrayCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter', + 'ch.qos.logback.core.pattern.color.RedCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter', + 'ch.qos.logback.core.pattern.color.BoldRedCompositeConverter', + 'ch.qos.logback.core.pattern.color.GreenCompositeConverter', + 'ch.qos.logback.core.pattern.color.WhiteCompositeConverter', + 'ch.qos.logback.core.pattern.FormattingConverter', + 'ch.qos.logback.core.pattern.IdentityCompositeConverter', + 'ch.qos.logback.core.pattern.FormatInfo', + 'ch.qos.logback.core.OutputStreamAppender', + 'ch.qos.logback.core.boolex.JaninoEventEvaluatorBase', + 'ch.qos.logback.core.boolex.Matcher', + 'ch.qos.logback.core.boolex.EventEvaluatorBase', + 'ch.qos.logback.core.boolex.EvaluationException', + 'ch.qos.logback.core.boolex.EventEvaluator', + 'ch.qos.logback.core.read.CyclicBufferAppender', + 'ch.qos.logback.core.read.ListAppender', + 'ch.qos.logback.core.Context', + 'ch.qos.logback.core.ContextBase', + 'ch.qos.logback.core.status.StatusListenerAsList', + 'ch.qos.logback.core.status.StatusBase', + 'ch.qos.logback.core.status.NopStatusListener', + 'ch.qos.logback.core.status.StatusUtil', + 'ch.qos.logback.core.status.OnPrintStreamStatusListenerBase', + 'ch.qos.logback.core.status.StatusManager', + 'ch.qos.logback.core.status.ViewStatusMessagesServletBase', + 'ch.qos.logback.core.status.ErrorStatus', + 'ch.qos.logback.core.status.Status', + 'ch.qos.logback.core.status.StatusListener', + 'ch.qos.logback.core.status.InfoStatus', + 'ch.qos.logback.core.status.OnConsoleStatusListener', + 'ch.qos.logback.core.status.WarnStatus', + 'ch.qos.logback.core.status.OnErrorConsoleStatusListener', + 'ch.qos.logback.core.filter.EvaluatorFilter', + 'ch.qos.logback.core.filter.Filter', + 'ch.qos.logback.core.filter.AbstractMatcherFilter', + 'ch.qos.logback.core.Layout', + 'ch.qos.logback.classic.ViewStatusMessagesServlet', + 'ch.qos.logback.classic.ClassicConstants', + 'ch.qos.logback.classic.layout.TTLLLayout', + 'ch.qos.logback.classic.helpers.MDCInsertingServletFilter', + 'ch.qos.logback.classic.Level', + 'ch.qos.logback.classic.Level.off', + 'ch.qos.logback.classic.Level.error', + 'ch.qos.logback.classic.Level.warn', + 'ch.qos.logback.classic.Level.info', + 'ch.qos.logback.classic.Level.debug', + 'ch.qos.logback.classic.Level.trace', + 'ch.qos.logback.classic.Level.all,', + 'ch.qos.logback.classic.net.SSLSocketReceiver', + 'ch.qos.logback.classic.net.ReceiverBase', + 'ch.qos.logback.classic.net.SimpleSocketServer', + 'ch.qos.logback.classic.net.SimpleSSLSocketServer', + 'ch.qos.logback.classic.net.SocketNode', + 'ch.qos.logback.classic.net.SMTPAppender', + 'ch.qos.logback.classic.net.SocketReceiver', + 'ch.qos.logback.classic.net.SocketAcceptor', + 'ch.qos.logback.classic.net.SSLSocketAppender', + 'ch.qos.logback.classic.net.LoggingEventPreSerializationTransformer', + 'ch.qos.logback.classic.net.server.RemoteAppenderStreamClient', + 'ch.qos.logback.classic.net.server.RemoteAppenderServerListener', + 'ch.qos.logback.classic.net.server.SSLServerSocketAppender', + 'ch.qos.logback.classic.net.server.RemoteAppenderClient', + 'ch.qos.logback.classic.net.server.HardenedLoggingEventInputStream', + 'ch.qos.logback.classic.net.server.ServerSocketAppender', + 'ch.qos.logback.classic.net.server.SSLServerSocketReceiver', + 'ch.qos.logback.classic.net.server.RemoteAppenderServerRunner', + 'ch.qos.logback.classic.net.server.ServerSocketReceiver', + 'ch.qos.logback.classic.net.SocketAppender', + 'ch.qos.logback.classic.net.SyslogAppender', + 'ch.qos.logback.classic.PatternLayout', + 'ch.qos.logback.classic.util.ContextSelectorStaticBinder', + 'ch.qos.logback.classic.util.StatusViaSLF4JLoggerFactory', + 'ch.qos.logback.classic.util.JNDIUtil', + 'ch.qos.logback.classic.util.LevelToSyslogSeverity', + 'ch.qos.logback.classic.util.LoggerNameUtil', + 'ch.qos.logback.classic.util.LogbackMDCAdapter', + 'ch.qos.logback.classic.util.CopyOnInheritThreadLocal', + 'ch.qos.logback.classic.util.ContextInitializer', + 'ch.qos.logback.classic.util.EnvUtil', + 'ch.qos.logback.classic.util.DefaultNestedComponentRules', + 'ch.qos.logback.classic.AsyncAppender', + 'ch.qos.logback.classic.jul.JULHelper', + 'ch.qos.logback.classic.jul.LevelChangePropagator', + 'ch.qos.logback.classic.encoder.PatternLayoutEncoder', + 'ch.qos.logback.classic.db.names.DBNameResolver', + 'ch.qos.logback.classic.db.names.ColumnName', + 'ch.qos.logback.classic.db.names.TableName', + 'ch.qos.logback.classic.db.names.DefaultDBNameResolver', + 'ch.qos.logback.classic.db.names.SimpleDBNameResolver', + 'ch.qos.logback.classic.log4j.XMLLayout', + 'ch.qos.logback.classic.LoggerContext', + 'ch.qos.logback.classic.turbo.TurboFilter', + 'ch.qos.logback.classic.turbo.MDCFilter', + 'ch.qos.logback.classic.turbo.ReconfigureOnChangeFilter', + 'ch.qos.logback.classic.turbo.DuplicateMessageFilter', + 'ch.qos.logback.classic.turbo.MarkerFilter', + 'ch.qos.logback.classic.turbo.MDCValueLevelPair', + 'ch.qos.logback.classic.turbo.DynamicThresholdFilter', + 'ch.qos.logback.classic.turbo.MatchingFilter', + 'ch.qos.logback.classic.turbo.LRUMessageCache', + 'ch.qos.logback.classic.selector.servlet.LoggerContextFilter', + 'ch.qos.logback.classic.selector.servlet.ContextDetachingSCL', + 'ch.qos.logback.classic.selector.ContextJNDISelector', + 'ch.qos.logback.classic.selector.DefaultContextSelector', + 'ch.qos.logback.classic.selector.ContextSelector', + 'ch.qos.logback.classic.sift.MDCBasedDiscriminator', + 'ch.qos.logback.classic.sift.SiftingJoranConfigurator', + 'ch.qos.logback.classic.sift.JNDIBasedContextDiscriminator', + 'ch.qos.logback.classic.sift.AppenderFactoryUsingJoran', + 'ch.qos.logback.classic.sift.ContextBasedDiscriminator', + 'ch.qos.logback.classic.sift.SiftingAppender', + 'ch.qos.logback.classic.sift.SiftAction', + 'ch.qos.logback.classic.html.UrlCssBuilder', + 'ch.qos.logback.classic.html.HTMLLayout', + 'ch.qos.logback.classic.html.DefaultCssBuilder', + 'ch.qos.logback.classic.html.DefaultThrowableRenderer', + 'ch.qos.logback.classic.Logger', + 'ch.qos.logback.classic.pattern.ThrowableHandlingConverter', + 'ch.qos.logback.classic.pattern.ContextNameConverter', + 'ch.qos.logback.classic.pattern.LocalSequenceNumberConverter', + 'ch.qos.logback.classic.pattern.ClassOfCallerConverter', + 'ch.qos.logback.classic.pattern.PrefixCompositeConverter', + 'ch.qos.logback.classic.pattern.LineOfCallerConverter', + 'ch.qos.logback.classic.pattern.EnsureExceptionHandling', + 'ch.qos.logback.classic.pattern.TargetLengthBasedClassNameAbbreviator', + 'ch.qos.logback.classic.pattern.FileOfCallerConverter', + 'ch.qos.logback.classic.pattern.LevelConverter', + 'ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter', + 'ch.qos.logback.classic.pattern.NamedConverter', + 'ch.qos.logback.classic.pattern.ClassicConverter', + 'ch.qos.logback.classic.pattern.NopThrowableInformationConverter', + 'ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter', + 'ch.qos.logback.classic.pattern.MethodOfCallerConverter', + 'ch.qos.logback.classic.pattern.CallerDataConverter', + 'ch.qos.logback.classic.pattern.ClassNameOnlyAbbreviator', + 'ch.qos.logback.classic.pattern.MarkerConverter', + 'ch.qos.logback.classic.pattern.RelativeTimeConverter', + 'ch.qos.logback.classic.pattern.DateConverter', + 'ch.qos.logback.classic.pattern.PropertyConverter', + 'ch.qos.logback.classic.pattern.ThreadConverter', + 'ch.qos.logback.classic.pattern.LineSeparatorConverter', + 'ch.qos.logback.classic.pattern.MDCConverter', + 'ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter', + 'ch.qos.logback.classic.pattern.ThrowableProxyConverter', + 'ch.qos.logback.classic.pattern.Abbreviator', + 'ch.qos.logback.classic.pattern.Util', + 'ch.qos.logback.classic.pattern.LoggerConverter', + 'ch.qos.logback.classic.pattern.SyslogStartConverter', + 'ch.qos.logback.classic.pattern.MessageConverter', + 'ch.qos.logback.classic.gaffer.GafferUtil', + 'ch.qos.logback.classic.boolex.OnMarkerEvaluator', + 'ch.qos.logback.classic.boolex.JaninoEventEvaluator', + 'ch.qos.logback.classic.boolex.OnErrorEvaluator', + 'ch.qos.logback.classic.boolex.GEventEvaluator', + 'ch.qos.logback.classic.boolex.IEvaluator', + 'ch.qos.logback.classic.filter.ThresholdFilter', + 'ch.qos.logback.classic.filter.LevelFilter', + 'java.lang.System', + 'java.lang.System.getenv', + 'java.lang.System.getProperty', + 'java.lang.System.getenv', + 'java.util.Map.getOrDefault', + 'com.kotlindiscord.kord.extensions.utils._EnvironmentKt.envOrNull', +] diff --git a/plugins/test-plugin-1/build.gradle.kts b/plugins/test-plugin-1/build.gradle.kts new file mode 100644 index 0000000000..4421fa26f2 --- /dev/null +++ b/plugins/test-plugin-1/build.gradle.kts @@ -0,0 +1,25 @@ +buildscript { + repositories { + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } +} + +plugins { + `kordex-module` + `dokka-module` +} + +group = "com.kotlindiscord.kord.extensions" + +dependencies { + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) + + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) + + implementation(project(":plugins:test-plugin-core")) +} diff --git a/plugins/test-plugin-1/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/one/TestPluginOne.kt b/plugins/test-plugin-1/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/one/TestPluginOne.kt new file mode 100644 index 0000000000..b95f660dad --- /dev/null +++ b/plugins/test-plugin-1/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/one/TestPluginOne.kt @@ -0,0 +1,16 @@ +package com.kotlindiscord.kord.extensions.plugins.test.one + +import com.kotlindiscord.kord.extensions.plugins.test.core.TestPlugin +import io.github.oshai.kotlinlogging.KotlinLogging + +public class TestPluginOne : TestPlugin() { + private val logger = KotlinLogging.logger { } + + override fun load() { + logger.info { "Plugin 1 loaded" } + } + + override fun unload() { + logger.info { "Plugin 1 unloaded" } + } +} diff --git a/plugins/test-plugin-1/src/main/resources/kordex.plugin.json b/plugins/test-plugin-1/src/main/resources/kordex.plugin.json new file mode 100644 index 0000000000..7e16871274 --- /dev/null +++ b/plugins/test-plugin-1/src/main/resources/kordex.plugin.json @@ -0,0 +1,13 @@ +{ + "class": "com.kotlindiscord.kord.extensions.plugins.test.one.TestPluginOne", + "id": "test-one", + "description": "The first test plugin", + "license": "MPL-2.0", + "name": "Test plugin one", + "version": "1.0.0", + "constraints": { + "needs": { + "test-two": ">= 1.0.0" + } + } +} diff --git a/plugins/test-plugin-2/build.gradle.kts b/plugins/test-plugin-2/build.gradle.kts new file mode 100644 index 0000000000..4421fa26f2 --- /dev/null +++ b/plugins/test-plugin-2/build.gradle.kts @@ -0,0 +1,25 @@ +buildscript { + repositories { + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } +} + +plugins { + `kordex-module` + `dokka-module` +} + +group = "com.kotlindiscord.kord.extensions" + +dependencies { + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) + + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) + + implementation(project(":plugins:test-plugin-core")) +} diff --git a/plugins/test-plugin-2/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/two/TestPluginTwo.kt b/plugins/test-plugin-2/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/two/TestPluginTwo.kt new file mode 100644 index 0000000000..a7a1002a67 --- /dev/null +++ b/plugins/test-plugin-2/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/two/TestPluginTwo.kt @@ -0,0 +1,16 @@ +package com.kotlindiscord.kord.extensions.plugins.test.two + +import com.kotlindiscord.kord.extensions.plugins.test.core.TestPlugin +import io.github.oshai.kotlinlogging.KotlinLogging + +public class TestPluginTwo : TestPlugin() { + private val logger = KotlinLogging.logger { } + + override fun load() { + logger.info { "Plugin 2 loaded" } + } + + override fun unload() { + logger.info { "Plugin 2 unloaded" } + } +} diff --git a/plugins/test-plugin-2/src/main/resources/kordex.plugin.json b/plugins/test-plugin-2/src/main/resources/kordex.plugin.json new file mode 100644 index 0000000000..7002c3bd58 --- /dev/null +++ b/plugins/test-plugin-2/src/main/resources/kordex.plugin.json @@ -0,0 +1,8 @@ +{ + "class": "com.kotlindiscord.kord.extensions.plugins.test.two.TestPluginTwo", + "id": "test-two", + "description": "The second test plugin", + "license": "MPL-2.0", + "name": "Test plugin two", + "version": "1.0.0" +} diff --git a/plugins/test-plugin-core/build.gradle.kts b/plugins/test-plugin-core/build.gradle.kts new file mode 100644 index 0000000000..4e4d024927 --- /dev/null +++ b/plugins/test-plugin-core/build.gradle.kts @@ -0,0 +1,25 @@ +buildscript { + repositories { + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } +} + +plugins { + `kordex-module` + `dokka-module` +} + +group = "com.kotlindiscord.kord.extensions" + +dependencies { + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) + + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) + + api(project(":plugins")) +} diff --git a/plugins/test-plugin-core/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/core/TestPlugin.kt b/plugins/test-plugin-core/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/core/TestPlugin.kt new file mode 100644 index 0000000000..a5e381798c --- /dev/null +++ b/plugins/test-plugin-core/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/test/core/TestPlugin.kt @@ -0,0 +1,20 @@ +package com.kotlindiscord.kord.extensions.plugins.test.core + +import com.kotlindiscord.kord.extensions.plugins.Plugin +import io.github.oshai.kotlinlogging.KotlinLogging + +public abstract class TestPlugin : Plugin() { + private val logger = KotlinLogging.logger {} + + override fun internalLoad() { + super.internalLoad() + + logger.info { "Plugin loaded: ${manifest.name}" } + } + + override fun internalUnload() { + super.internalUnload() + + logger.info { "Plugin unloaded: ${manifest.name}" } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e07745fc35..0840eb0969 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,5 +16,11 @@ include("modules:java-time") include("modules:time4j") include("modules:unsafe") +include("plugins") +include("plugins:plugin-load-test") +include("plugins:test-plugin-1") +include("plugins:test-plugin-2") +include("plugins:test-plugin-core") + include("test-bot") include("token-parser")