diff --git a/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/misc/BindingExtensions.kt b/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/misc/BindingExtensions.kt new file mode 100644 index 000000000..5868c908d --- /dev/null +++ b/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/misc/BindingExtensions.kt @@ -0,0 +1,148 @@ +/* + * Anvil - AnvilPowered + * Copyright (C) 2020-2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package org.anvilpowered.anvil.api.misc + +import com.google.common.reflect.TypeToken +import com.google.inject.Key +import com.google.inject.Provider +import com.google.inject.TypeLiteral +import org.anvilpowered.anvil.api.Anvil +import org.anvilpowered.anvil.api.datastore.DBComponent + +@Suppress("unchecked", "UnstableApiUsage") +interface BindingExtensions { + + /** + * Full binding method for a component + * + * A typical example of usage of this method: + * + * be.bind( + * + *   new TypeToken>(getClass()) { + * }, + * + *   new TypeToken>(getClass()) { + * }, + * + *   new TypeToken, Datastore>>(getClass()) { + * }, + * + *   new TypeToken>(getClass()) { // final implementation + * }, + * + *   Names.named("mongodb") + * + * ); + */ + fun , From2 : DBComponent<*, *>, From3 : From1, Target : From1> bind( + from1: TypeToken, + from2: TypeToken, + from3: TypeToken, + target: TypeToken, + componentAnnotation: Annotation, + ) + + /** + * Binding method for a component + * + * + * A typical example of usage of this method: + * + * be.bind( + * + *   new TypeToken>(getClass()) { + * }, + * + *   new TypeToken>(getClass()) { + * }, + * + *   new TypeToken(getClass()) { // final implementation + * }, + *   Names.named("mongodb") + * + * ); + */ + fun , From2 : From1, Target : From1> bind( + from1: TypeToken, + from2: TypeToken, + target: TypeToken, + componentAnnotation: Annotation, + ) + + fun bind( + from: TypeToken, + target: TypeToken, + annotation: Annotation, + ) + + fun bind( + from: TypeToken, + target: TypeToken, + annotation: Class, + ) + + fun bind( + from: TypeToken, + target: TypeToken, + ) + + /** + * Binds the mongodb [DataStoreContext] + * + * Using this method is the same as invoking: + * + * bind(new TypeLiteral>() { + * }).to(MongoContext.class); + */ + fun withMongoDB() + + /** + * Binds the xodus [DataStoreContext] + * + * Using this method is the same as invoking: + * + * binder.bind(new TypeLiteral>() { + * }).to(XodusContext.class); + * + */ + fun withXodus() + + companion object { + fun getTypeLiteral(typeToken: TypeToken): TypeLiteral { + return TypeLiteral.get(typeToken.type) as TypeLiteral + } + + fun getKey(typeToken: TypeToken): Key? { + return Key.get(getTypeLiteral(typeToken)) + } + + fun asInternalProvider(clazz: Class): Provider { + return Provider { Anvil.getEnvironment().injector.getInstance(clazz) } + } + + fun asInternalProvider(typeLiteral: TypeLiteral): Provider { + return Anvil.getEnvironment().injector.getProvider(Key.get(typeLiteral)) + } + + fun asInternalProvider(typeToken: TypeToken): Provider { + return Anvil.getEnvironment().injector.getProvider(getKey(typeToken)) + } + } +} diff --git a/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt b/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt index 0bf49e49d..72a8c853b 100644 --- a/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt +++ b/anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt @@ -38,4 +38,6 @@ interface PluginInfo : Named { val organizationName: String val buildDate: String + + val metricIds: Map } diff --git a/anvil-bungee/build.gradle b/anvil-bungee/build.gradle index a771bac64..25e2c46a9 100644 --- a/anvil-bungee/build.gradle +++ b/anvil-bungee/build.gradle @@ -14,6 +14,7 @@ dependencies { } implementation bungee + implementation bstats implementation configurate_hocon implementation javasisst implementation(kotlin_reflect + ":" + kotlin_version) @@ -50,6 +51,7 @@ shadowJar { include dependency(apache_commons) include dependency(aopalliance) include dependency(bson) + include dependency(bstats) include dependency(configurate_core) include dependency(configurate_hocon) include dependency(guice) diff --git a/anvil-bungee/src/main/kotlin/org/anvilpowered/anvil/bungee/metric/BungeeMetricService.kt b/anvil-bungee/src/main/kotlin/org/anvilpowered/anvil/bungee/metric/BungeeMetricService.kt new file mode 100644 index 000000000..aec035916 --- /dev/null +++ b/anvil-bungee/src/main/kotlin/org/anvilpowered/anvil/bungee/metric/BungeeMetricService.kt @@ -0,0 +1,265 @@ +/* + * Anvil - AnvilPowered + * Copyright (C) 2020-2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package org.anvilpowered.anvil.bungee.metric + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.inject.Inject +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import java.io.File +import java.io.FileReader +import java.io.FileWriter +import java.io.IOException +import java.io.InputStreamReader +import java.lang.reflect.InvocationTargetException +import java.net.URL +import java.nio.charset.StandardCharsets +import java.util.UUID +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPOutputStream +import javax.net.ssl.HttpsURLConnection +import net.md_5.bungee.api.ProxyServer +import net.md_5.bungee.api.plugin.Plugin +import net.md_5.bungee.config.Configuration +import net.md_5.bungee.config.ConfigurationProvider +import net.md_5.bungee.config.YamlConfiguration +import org.anvilpowered.anvil.api.Environment +import org.anvilpowered.anvil.common.metric.MetricService +import org.slf4j.Logger + +class BungeeMetricService @Inject constructor( + private val logger: Logger +) : MetricService { + + private lateinit var environment: Environment + private var serviceId = 0 + private var enabled = false + private var serverUUID: String? = null + private var logFailedRequests = false + private var logSentData = false + private var logResponseStatusText = false + private val knownMetricsInstances: MutableList = ArrayList() + + override fun initialize(env: Environment) { + val bungeeId: Int? = env.pluginInfo.metricIds["bungeecord"] + requireNotNull(bungeeId) { "Could not find a valid Metrics Id for BungeeCord. Please check your PluginInfo" } + initialize(env, bungeeId) + } + + private fun initialize(environment: Environment, serviceId: Int) { + this.environment = environment + this.serviceId = serviceId + try { + loadConfig() + } catch (e: IOException) { + logger.error("Failed to load bStats config!", e) + return + } + + if (!enabled) { + return + } + + val usedMetricsClass = getFirstBStatsClass() ?: return + if (usedMetricsClass == javaClass) { + linkMetrics(this) + startSubmitting() + } else { + val logMsg = "Failed to link to first metrics class ${usedMetricsClass.name}" + try { + usedMetricsClass.getMethod("linkMetrics", Any::class.java).invoke(null, this) + } catch (e: NoSuchMethodException) { + if (logFailedRequests) { + logger.error(logMsg, e) + } + } catch (e: IllegalAccessException) { + if (logFailedRequests) { + logger.error(logMsg, e) + } + } catch (e: InvocationTargetException) { + if (logFailedRequests) { + logger.error(logMsg, e) + } + } + } + } + + private fun linkMetrics(metrics: Any) = knownMetricsInstances.add(metrics) + + private fun startSubmitting() { + val initialDelay = (1000 * 60 * (3 + Math.random() * 3)).toLong() + val secondDelay = (1000 * 60 * (Math.random() * 30)).toLong() + val plugin = environment.plugin as Plugin + ProxyServer.getInstance().scheduler.schedule(plugin, { submitData() }, initialDelay, TimeUnit.MILLISECONDS) + ProxyServer.getInstance().scheduler.schedule( + plugin, { submitData() }, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS + ) + } + + private fun getServerData(): JsonObject { + val proxyInstance = ProxyServer.getInstance() + val data = JsonObject() + data.addProperty("serverUUID", serverUUID) + data.addProperty("playerAmount", proxyInstance.onlineCount) + data.addProperty("managedServers", proxyInstance.servers.size) + data.addProperty("onlineMode", if (proxyInstance.config.isOnlineMode) 1 else 0) + data.addProperty("bungeecordVersion", proxyInstance.version) + data.addProperty("javaVersion", System.getProperty("java.version")) + data.addProperty("osName", System.getProperty("os.name")) + data.addProperty("osArch", System.getProperty("os.arch")) + data.addProperty("osVersion", System.getProperty("os.version")) + data.addProperty("coreCount", Runtime.getRuntime().availableProcessors()) + return data + } + + private fun submitData() { + val data: JsonObject = getServerData() + val pluginData = JsonArray() + for (metrics in knownMetricsInstances) { + try { + val plugin = metrics.javaClass.getMethod("getPluginData").invoke(metrics) + if (plugin is JsonObject) { + pluginData.add(plugin) + } + } catch (ignored: Exception) { + } + } + data.add("plugins", pluginData) + try { + sendData(data) + } catch (e: Exception) { + if (logFailedRequests) { + logger.error("Could not submit plugin stats!", e) + } + } + } + + @Throws(IOException::class) + private fun loadConfig() { + val bStatsFolder = File("plugins/bStats") + check(bStatsFolder.mkdirs()) { "Could not create the config directory for bStats!" } + val configFile = File(bStatsFolder, "config.yml") + check(configFile.mkdirs()) { "Could not create the config file for bStats!" } + + writeFile( + configFile, + "#bStats collects some data for plugin authors like how many servers are using their plugins.", + "#To honor their work, you should not disable it.", + "#This has nearly no effect on the server performance!", + "#Check out https://bStats.org/ to learn more :)", + "enabled: true", + "serverUuid: \"" + UUID.randomUUID() + "\"", + "logFailedRequests: false", + "logSentData: false", + "logResponseStatusText: false" + ) + + val configuration: Configuration = ConfigurationProvider.getProvider(YamlConfiguration::class.java).load(configFile) + + enabled = configuration.getBoolean("enabled", true) + serverUUID = configuration.getString("serverUuid") + logFailedRequests = configuration.getBoolean("logFailedRequests", false) + logSentData = configuration.getBoolean("logSentData", false) + logResponseStatusText = configuration.getBoolean("logResponseStatusText", false) + } + + private fun getFirstBStatsClass(): Class<*>? { + val bStatsFolder = File("plugins/bStats") + bStatsFolder.mkdirs() + check(bStatsFolder.mkdirs()) { "Could not create the bStats config folder!" } + val tempFile = File(bStatsFolder, "temp.txt") + return try { + val className = readFile(tempFile) + if (className != null) { + try { + return Class.forName(className) + } catch (ignored: ClassNotFoundException) { + } + } + writeFile(tempFile, javaClass.name) + javaClass + } catch (e: IOException) { + if (logFailedRequests) { + logger.error("Failed to get first bStats class!", e) + } + null + } + } + + @Throws(IOException::class) + private fun readFile(file: File): String? { + return if (!file.exists()) { + null + } else { + BufferedReader(FileReader(file)).use { it.readLine() } + } + } + + @Throws(IOException::class) + private fun writeFile(file: File, vararg lines: String) { + BufferedWriter(FileWriter(file)).use { + for (line in lines) { + it.write(line) + it.newLine() + } + } + } + + private fun sendData(data: JsonObject?) { + requireNotNull(data) { "Data cannot be null" } + if (logSentData) { + logger.info("Sending data to bStats: $data") + } + val connection: HttpsURLConnection = URL("https://bStats.org/submitData/bungeecord").openConnection() as HttpsURLConnection + val compressedData = compress(data.toString()) + + connection.requestMethod = "POST" + connection.addRequestProperty("Accept", "application/json") + connection.addRequestProperty("Connection", "close") + connection.addRequestProperty("Content-Encoding", "gzip") + connection.addRequestProperty("Content-Length", compressedData!!.size.toString()) + connection.setRequestProperty("Content-Type", "application/json") + connection.setRequestProperty("User-Agent", "MC-Server/1") + + connection.doOutput = true + DataOutputStream(connection.outputStream).use { outputStream -> outputStream.write(compressedData) } + val builder = StringBuilder() + BufferedReader(InputStreamReader(connection.inputStream)).use { bufferedReader -> + var line: String? + while (bufferedReader.readLine().also { line = it } != null) { + builder.append(line) + } + } + if (logResponseStatusText) { + logger.info("Sent data to bStats and received response: $builder") + } + } + + private fun compress(str: String?): ByteArray? { + if (str == null) { + return null + } + val outputStream = ByteArrayOutputStream() + GZIPOutputStream(outputStream).use { gzip -> gzip.write(str.toByteArray(StandardCharsets.UTF_8)) } + return outputStream.toByteArray() + } +} diff --git a/anvil-common/build.gradle b/anvil-common/build.gradle index 2d2900cf5..93ec9ff74 100644 --- a/anvil-common/build.gradle +++ b/anvil-common/build.gradle @@ -8,6 +8,7 @@ plugins { dependencies { api(project(':anvil-api')) + api(bstats) api("net.kyori:adventure-text-serializer-legacy:4.5.0") api("net.kyori:adventure-text-serializer-plain:4.5.0") api(configurate_hocon) diff --git a/anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java b/anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java index c0c42ccf9..3f7e044fb 100644 --- a/anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java +++ b/anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java @@ -21,6 +21,7 @@ import com.google.common.base.Preconditions; import com.google.common.reflect.TypeToken; import com.google.inject.AbstractModule; +import com.google.inject.Binding; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; @@ -32,6 +33,7 @@ import org.anvilpowered.anvil.api.registry.Registry; import org.anvilpowered.anvil.api.registry.RegistryScope; import org.anvilpowered.anvil.common.PlatformImpl; +import org.anvilpowered.anvil.common.metric.MetricService; import org.anvilpowered.anvil.common.module.PlatformModule; import org.checkerframework.checker.nullness.qual.Nullable; @@ -132,6 +134,11 @@ protected void configure() { } ServiceManagerImpl.environmentManager .registerEnvironment(environment, environment.getPlugin()); + Binding metricBinding = + Anvil.environment.getInjector().getExistingBinding(Key.get(MetricService.class)); + if (metricBinding != null) { + metricBinding.getProvider().get().initialize(environment); + } for (Map.Entry, Consumer> entry : environment.getEarlyServices().entrySet()) { ((Consumer) entry.getValue()) diff --git a/anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java b/anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java index 37ffaf4fe..814f31e17 100644 --- a/anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java +++ b/anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java @@ -18,11 +18,16 @@ package org.anvilpowered.anvil.common.plugin; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Inject; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.anvilpowered.anvil.api.plugin.PluginInfo; +import org.anvilpowered.anvil.api.util.TextService; -public class AnvilPluginInfo implements PluginInfo { +import java.util.Map; + +public class AnvilPluginInfo implements PluginInfo { public static final String id = "anvil"; public static final String name = "Anvil"; public static final String version = "$modVersion"; @@ -36,6 +41,7 @@ public class AnvilPluginInfo implements PluginInfo { .append(Component.text(name, NamedTextColor.AQUA)) .append(Component.text("] ", NamedTextColor.BLUE)) .build(); + public static final Map metricId = ImmutableMap.of(); @Override public String getId() { @@ -81,4 +87,9 @@ public String getBuildDate() { public Component getPrefix() { return pluginPrefix; } + + @Override + public Map getMetricIds() { + return metricId; + } } diff --git a/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/metric/MetricService.kt b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/metric/MetricService.kt new file mode 100644 index 000000000..2c80e8b6d --- /dev/null +++ b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/metric/MetricService.kt @@ -0,0 +1,30 @@ +/* + * Anvil - AnvilPowered + * Copyright (C) 2020-2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package org.anvilpowered.anvil.common.metric + +import org.anvilpowered.anvil.api.Environment + +interface MetricService { + + /** + * Initializes metrics through bStats for the specified [Environment] + */ + fun initialize(env: Environment) + +} diff --git a/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/misc/CommonBindingExtensions.kt b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/misc/CommonBindingExtensions.kt new file mode 100644 index 000000000..39c1de1f5 --- /dev/null +++ b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/misc/CommonBindingExtensions.kt @@ -0,0 +1,97 @@ +/* + * Anvil - AnvilPowered + * Copyright (C) 2020-2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package org.anvilpowered.anvil.common.misc + +import com.google.common.reflect.TypeToken +import com.google.inject.Binder +import com.google.inject.TypeLiteral +import dev.morphia.Datastore +import jetbrains.exodus.entitystore.EntityId +import jetbrains.exodus.entitystore.PersistentEntityStore +import org.anvilpowered.anvil.api.datastore.DBComponent +import org.anvilpowered.anvil.api.datastore.DataStoreContext +import org.anvilpowered.anvil.api.datastore.MongoContext +import org.anvilpowered.anvil.api.datastore.XodusContext +import org.anvilpowered.anvil.api.misc.BindingExtensions +import org.bson.types.ObjectId + +@Suppress("unchecked", "UnstableApiUsage") +class CommonBindingExtensions(val binder: Binder) : BindingExtensions { + + override fun , From2 : DBComponent<*, *>, From3 : From1, Target : From1> bind( + from1: TypeToken, + from2: TypeToken, + from3: TypeToken, + target: TypeToken, + componentAnnotation: Annotation, + ) { + binder.bind(TypeLiteral.get(from2.type) as TypeLiteral) + .annotatedWith(componentAnnotation) + .to(BindingExtensions.getTypeLiteral(target)) + binder.bind(TypeLiteral.get(from3.type) as TypeLiteral) + .to(BindingExtensions.getTypeLiteral(target)) + } + + override fun , From2 : From1, Target : From1> bind( + from1: TypeToken, + from2: TypeToken, + target: TypeToken, + componentAnnotation: Annotation, + ) { + bind(from1, from1, from2, target, componentAnnotation) + } + + override fun bind( + from: TypeToken, + target: TypeToken, + annotation: Annotation, + ) { + binder.bind(BindingExtensions.getTypeLiteral(from)) + .annotatedWith(annotation) + .to(BindingExtensions.getTypeLiteral(target)) + } + + override fun bind( + from: TypeToken, + target: TypeToken, + annotation: Class, + ) { + binder.bind(BindingExtensions.getTypeLiteral(from)) + .annotatedWith(annotation) + .to(BindingExtensions.getTypeLiteral(target)) + } + + override fun bind( + from: TypeToken, + target: TypeToken, + ) { + binder.bind(BindingExtensions.getTypeLiteral(from)) + .to(BindingExtensions.getTypeLiteral(target)) + } + + override fun withMongoDB() { + binder.bind(object : TypeLiteral>() {}) + .to(MongoContext::class.java) + } + + override fun withXodus() { + binder.bind(object : TypeLiteral>() {}) + .to(XodusContext::class.java) + } +} diff --git a/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/plugin/FallbackPluginInfo.kt b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/plugin/FallbackPluginInfo.kt index 00348462e..144d8113e 100644 --- a/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/plugin/FallbackPluginInfo.kt +++ b/anvil-common/src/main/kotlin/org/anvilpowered/anvil/common/plugin/FallbackPluginInfo.kt @@ -18,6 +18,7 @@ package org.anvilpowered.anvil.common.plugin +import com.google.common.collect.ImmutableMap import com.google.inject.Inject import net.kyori.adventure.text.Component import org.anvilpowered.anvil.api.Environment @@ -32,6 +33,7 @@ class FallbackPluginInfo : PluginInfo { val authors = arrayOf("author") const val organizationName = "organizationName" const val buildDate = "last night" + val metricIds: Map = ImmutableMap.of("fake", 0) } @Inject @@ -53,4 +55,5 @@ class FallbackPluginInfo : PluginInfo { override val organizationName: String = Companion.organizationName override val buildDate: String = Companion.buildDate override val prefix: Component = pluginPrefix + override val metricIds: Map = Companion.metricIds } diff --git a/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/metric/SpigotMetricService.kt b/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/metric/SpigotMetricService.kt new file mode 100644 index 000000000..af0ce8120 --- /dev/null +++ b/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/metric/SpigotMetricService.kt @@ -0,0 +1,115 @@ +/* + * Anvil - AnvilPowered + * Copyright (C) 2020-2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package org.anvilpowered.anvil.spigot.metric + +import com.google.inject.Inject +import java.io.File +import java.io.IOException +import java.util.UUID +import org.anvilpowered.anvil.api.Environment +import org.anvilpowered.anvil.api.util.UserService +import org.anvilpowered.anvil.common.metric.MetricService +import org.bstats.MetricsBase +import org.bstats.json.JsonObjectBuilder +import org.bukkit.Bukkit +import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import org.slf4j.Logger + +class SpigotMetricService @Inject constructor( + private val logger: Logger, + private val userService: UserService +) : MetricService { + + private lateinit var environment: Environment + private var metricsBase: MetricsBase? = null + + override fun initialize(env: Environment) { + val serviceId: Int? = env.pluginInfo.metricIds["spigot"] + checkNotNull(serviceId) { "Could not find a valid Metrics Id for Spigot. Please check your PluginInfo" } + initialize(env, serviceId) + } + + private fun initialize(environment: Environment, serviceId: Int) { + this.environment = environment + val bStatsFolder = File("plugins/bStats") + val configFile = File(bStatsFolder, "config.yml") + if (!bStatsFolder.exists()) { + require(configFile.mkdirs()) { "Could not create the bStats config!" } + } + val config: YamlConfiguration = YamlConfiguration.loadConfiguration(configFile) + if (!config.isSet("serverUuid")) { + config.addDefault("enabled", true) + config.addDefault("serverUuid", UUID.randomUUID().toString()) + config.addDefault("logFailedRequests", false) + config.addDefault("logSentData", false) + config.addDefault("logResponseStatusText", false) + + config.options().header( + """ + bStats collects some data for plugin authors like how many servers are using their plugins.\n + To honor their work, you should not disable it.\n + This has nearly no effect on the server performance!\n + Check out https://bStats.org/ to learn more :) + """.trimIndent() + ).copyDefaults(true) + try { + config.save(configFile) + } catch (ignored: IOException) { + } + } + + val enabled: Boolean = config.getBoolean("enabled", true) + val serverUUID: String = config.getString("serverUuid")!! + val logErrors: Boolean = config.getBoolean("logFailedRequests", false) + val logSentData: Boolean = config.getBoolean("logSentData", false) + val logResponseStatusText: Boolean = config.getBoolean("logResponseStatusText", false) + metricsBase = MetricsBase( + "bukkit", + serverUUID, + serviceId, + enabled, + { appendPlatformData(it) }, + { appendServiceData(it) }, + { Bukkit.getScheduler().runTask(environment.plugin as Plugin, it) }, + { (environment.plugin as Plugin).isEnabled }, + { message: String, error: Throwable -> logger.error(message, error) }, + { logger.info(it) }, + logErrors, + logSentData, + logResponseStatusText + ) + } + + private fun appendPlatformData(builder: JsonObjectBuilder) { + builder.appendField("playerAmount", getPlayerAmount()) + builder.appendField("onlineMode", if (Bukkit.getOnlineMode()) 1 else 0) + builder.appendField("bukkitVersion", Bukkit.getVersion()) + builder.appendField("bukkitName", Bukkit.getName()) + builder.appendField("javaVersion", System.getProperty("java.version")) + builder.appendField("osName", System.getProperty("os.name")) + builder.appendField("osArch", System.getProperty("os.arch")) + builder.appendField("osVersion", System.getProperty("os.version")) + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()) + } + + private fun appendServiceData(builder: JsonObjectBuilder) = builder.appendField("pluginVersion", environment.pluginInfo.version) + private fun getPlayerAmount(): Int = userService.onlinePlayers.size +} diff --git a/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/module/ApiSpigotModule.kt b/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/module/ApiSpigotModule.kt index 4e0eaa3a9..6d6e5c2fe 100644 --- a/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/module/ApiSpigotModule.kt +++ b/anvil-spigot/src/main/kotlin/org/anvilpowered/anvil/spigot/module/ApiSpigotModule.kt @@ -27,6 +27,7 @@ import org.anvilpowered.anvil.api.util.TextService import org.anvilpowered.anvil.api.util.UserService import org.anvilpowered.anvil.common.PlatformImpl import org.anvilpowered.anvil.common.entity.EntityUtils +import org.anvilpowered.anvil.common.metric.MetricService import org.anvilpowered.anvil.common.module.JavaUtilLoggingAdapter import org.anvilpowered.anvil.common.module.PlatformModule import org.anvilpowered.anvil.common.util.CommonTextService @@ -34,6 +35,7 @@ import org.anvilpowered.anvil.common.util.SendTextService import org.anvilpowered.anvil.spigot.command.SpigotCommandExecuteService import org.anvilpowered.anvil.spigot.command.SpigotSimpleCommandService import org.anvilpowered.anvil.spigot.entity.SpigotEntityUtils +import org.anvilpowered.anvil.spigot.metric.SpigotMetricService import org.anvilpowered.anvil.spigot.server.SpigotLocationService import org.anvilpowered.anvil.spigot.util.SpigotKickService import org.anvilpowered.anvil.spigot.util.SpigotPermissionService @@ -58,6 +60,7 @@ class ApiSpigotModule : PlatformModule( bind(KickService::class.java).to(SpigotKickService::class.java) bind(EntityUtils::class.java).to(SpigotEntityUtils::class.java) bind(LocationService::class.java).to(SpigotLocationService::class.java) + bind(MetricService::class.java).to(SpigotMetricService::class.java) bind(PermissionService::class.java).to(SpigotPermissionService::class.java) bind(object : TypeLiteral>() {}).to(SpigotSendTextService::class.java) bind(object : TypeLiteral>() {}).to(object : TypeLiteral>() {}) diff --git a/anvil-sponge/anvil-sponge-7/src/main/kotlin/org/anvilpowered/anvil/sponge7/metric/Sponge7MetricService.kt b/anvil-sponge/anvil-sponge-7/src/main/kotlin/org/anvilpowered/anvil/sponge7/metric/Sponge7MetricService.kt new file mode 100644 index 000000000..d7b7d1705 --- /dev/null +++ b/anvil-sponge/anvil-sponge-7/src/main/kotlin/org/anvilpowered/anvil/sponge7/metric/Sponge7MetricService.kt @@ -0,0 +1,145 @@ +/* + * Anvil - AnvilPowered + * Copyright (C) 2020-2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package org.anvilpowered.anvil.sponge7.metric + +import com.google.inject.Inject +import java.io.File +import java.io.IOException +import java.nio.file.Path +import java.util.UUID +import ninja.leaping.configurate.commented.CommentedConfigurationNode +import ninja.leaping.configurate.hocon.HoconConfigurationLoader +import org.anvilpowered.anvil.api.Environment +import org.anvilpowered.anvil.common.metric.MetricService +import org.bstats.MetricsBase +import org.bstats.json.JsonObjectBuilder +import org.slf4j.Logger +import org.spongepowered.api.Platform +import org.spongepowered.api.Sponge +import org.spongepowered.api.plugin.PluginContainer +import org.spongepowered.api.scheduler.Scheduler +import org.spongepowered.api.scheduler.Task + +class Sponge7MetricService @Inject constructor( + private val logger: Logger +) : MetricService { + + private lateinit var plugin: PluginContainer + private lateinit var configDir: Path + private var serviceId = 0 + private lateinit var metricsBase: MetricsBase + private var serverUUID: String? = null + private var logErrors = false + private var logSentData = false + private var logResponseStatusText = false + + override fun initialize(env: Environment) { + val serviceId: Int? = env.pluginInfo.metricIds["sponge"] + checkNotNull(serviceId) { "Could not find a valid Metrics Id for Sponge. Please check your PluginInfo" } + + this.serviceId = serviceId + this.configDir = Sponge.getConfigManager().getSharedConfig(env.plugin).configPath + try { + loadConfig() + } catch (e: IOException) { + logger.warn("Failed to load bStats config!", e) + return + } + this.plugin = env.injector.getInstance(PluginContainer::class.java) + metricsBase = MetricsBase( + "sponge", + serverUUID, + serviceId, + Sponge.getMetricsConfigManager().getCollectionState(plugin).asBoolean(), + { builder: JsonObjectBuilder -> appendPlatformData(builder) }, + { builder: JsonObjectBuilder -> appendServiceData(builder) }, + { task: Runnable -> + val scheduler: Scheduler = Sponge.getScheduler() + val taskBuilder: Task.Builder = scheduler.createTaskBuilder() + taskBuilder.execute(task).submit(plugin) + }, + { true }, + logger::warn, + logger::info, + logErrors, + logSentData, + logResponseStatusText + ) + val builder = StringBuilder() + builder.append("Plugin ").append(plugin.name).append(" is using bStats Metrics ") + if (Sponge.getMetricsConfigManager().getCollectionState(plugin).asBoolean()) { + builder.append(" and is allowed to send data.") + } else { + builder.append(" but currently has data sending disabled.").append(System.lineSeparator()) + builder.append("To change the enabled/disabled state of any bStats use in a plugin, visit the Sponge config!") + } + logger.info(builder.toString()) + } + + private fun loadConfig() { + val configPath: File = configDir.resolve("bStats").toFile() + if (!configPath.exists()) { + if (!configPath.mkdirs()) { + logger.error("Could not create the bStats directory!") + } + } + + val configFile = File(configPath, "config.conf") + val configurationLoader: HoconConfigurationLoader = + HoconConfigurationLoader.builder().setFile(configFile).build() + + if (!configFile.exists()) { + require(configFile.mkdirs()) { "Could not create the bStats config!" } + } + + val node: CommentedConfigurationNode = configurationLoader.load() + node.getNode("serverUuid").value = UUID.randomUUID().toString() + node.getNode("logFailedRequests").value = false + node.getNode("logSentData").value = false + node.getNode("logResponseStatusText").value = false + node.getNode("serverUuid").setComment( + "bStats collects some data for plugin authors like how many servers are using their plugins.\n" + + "To control whether this is enabled or disabled, see the Sponge configuration file.\n" + + "Check out https://bStats.org/ to learn more :)" + ) + node.getNode("configVersion").value = 2 + configurationLoader.save(node) + + serverUUID = node.getNode("serverUuid").string + logErrors = node.getNode("logFailedRequests").getBoolean(false) + logSentData = node.getNode("logSentData").getBoolean(false) + logResponseStatusText = node.getNode("logResponseStatusText").getBoolean(false) + } + + private fun appendPlatformData(builder: JsonObjectBuilder) { + builder.appendField("playerAmount", Sponge.getServer().onlinePlayers.size) + builder.appendField("onlineMode", if (Sponge.getServer().onlineMode) 1 else 0) + builder.appendField("minecraftVersion", Sponge.getGame().platform.minecraftVersion.name) + builder.appendField("spongeImplementation", Sponge.getPlatform().getContainer(Platform.Component.IMPLEMENTATION).name) + builder.appendField("javaVersion", System.getProperty("java.version")) + builder.appendField("osName", System.getProperty("os.name")) + builder.appendField("osArch", System.getProperty("os.arch")) + builder.appendField("osVersion", System.getProperty("os.version")) + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()) + } + + private fun appendServiceData(builder: JsonObjectBuilder) { + builder.appendField("pluginVersion", plugin.version.orElse("unknown")) + } +} diff --git a/anvil-sponge/build.gradle b/anvil-sponge/build.gradle index 5a136b397..fdeffceef 100644 --- a/anvil-sponge/build.gradle +++ b/anvil-sponge/build.gradle @@ -43,9 +43,11 @@ shadowJar { exclude("META-INF/versions/**") relocate("org.apache.commons", "relocated.apache") + relocate("org.bstats", "relocated.org.bstats") relocate("ninja.leaping", "relocated") include dependency(apache_commons) include dependency(bson) + include dependency(bstats) include dependency(configurate_core) include dependency(configurate_hocon) include dependency(javasisst) diff --git a/anvil-velocity/build.gradle b/anvil-velocity/build.gradle index 9439a75d7..726d48e8e 100644 --- a/anvil-velocity/build.gradle +++ b/anvil-velocity/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation project(':Anvil:anvil-common') } + implementation bstats implementation javasisst implementation(kotlin_reflect + ":" + kotlin_version) implementation(kotlin_stdlib + ":" + kotlin_version) @@ -45,8 +46,10 @@ shadowJar { include project(':Anvil:anvil-common') } + relocate("org.bstats", "relocated.org.bstats") include dependency(apache_commons) include dependency(bson) + include dependency(bstats) include dependency(javasisst) include dependency(jedis) include dependency(kotlin_reflect) diff --git a/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/metric/VelocityMetricService.kt b/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/metric/VelocityMetricService.kt new file mode 100644 index 000000000..ce3449317 --- /dev/null +++ b/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/metric/VelocityMetricService.kt @@ -0,0 +1,186 @@ +/* + * Anvil - AnvilPowered + * Copyright (C) 2020-2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package org.anvilpowered.anvil.velocity.metric + +import com.google.inject.Inject +import com.velocitypowered.api.proxy.ProxyServer +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.File +import java.io.FileReader +import java.io.FileWriter +import java.io.IOException +import java.nio.file.Path +import java.nio.file.Paths +import java.util.Optional +import java.util.UUID +import java.util.regex.Pattern +import java.util.stream.Collectors +import org.anvilpowered.anvil.api.Environment +import org.anvilpowered.anvil.api.Platform +import org.anvilpowered.anvil.api.server.LocationService +import org.anvilpowered.anvil.common.metric.MetricService +import org.bstats.MetricsBase +import org.bstats.json.JsonObjectBuilder +import org.slf4j.Logger + +class VelocityMetricService @Inject constructor( + private val logger: Logger, + private val platform: Platform, + private val proxyServer: ProxyServer, + private val locationService: LocationService, +) : MetricService { + + private lateinit var metricsBase: MetricsBase + private var serverUUID: String? = null + private var enabled: Boolean = false + private var logErrors: Boolean = false + private var logSentData: Boolean = false + private var logResponseStatusText: Boolean = false + private lateinit var dataDirectory: Path + private lateinit var environment: Environment + + override fun initialize(env: Environment) { + val serviceId: Int? = env.pluginInfo.metricIds["velocity"] + requireNotNull(serviceId) { "Could not find a valid Metrics Id for Velocity. Please check your PluginInfo" } + initialize(env, Paths.get("plugins/bStats"), serviceId) + } + + private fun initialize(environment: Environment, dataPath: Path, serviceId: Int) { + this.dataDirectory = dataPath + this.environment = environment + try { + setupConfig(true) + } catch (e: IOException) { + logger.error("Failed to create bStats config", e) + } + metricsBase = MetricsBase( + platform.name, + serverUUID, + serviceId, + enabled, + this::appendPlatformData, + this::appendServiceData, + { task -> proxyServer.scheduler.buildTask(environment.plugin, task).schedule() }, + { true }, + logger::warn, + logger::info, + logErrors, + logSentData, + logResponseStatusText + ) + } + + private fun setupConfig(recreateWhenMalformed: Boolean) { + val configDir = dataDirectory.parent.resolve("bStats").toFile() + if (!configDir.exists()) { + require(configDir.mkdirs()) {"Could not create the bStats config!"} + } + val configFile = File(configDir, "config.txt") + if (!configFile.exists()) { + writeConfig(configFile) + } + + val lines: List = readFile(configFile) + + enabled = getConfigValue("enabled", lines).map { anObject: String -> "true" == anObject }.orElse(true) + serverUUID = getConfigValue("server-uuid", lines).orElse(null) + logErrors = getConfigValue("log-errors", lines).map { anObject: String -> "true" == anObject }.orElse(false) + logSentData = getConfigValue("log-sent-data", lines).map { anObject: String -> "true" == anObject }.orElse(false) + logResponseStatusText = getConfigValue("log-response-status-text", lines) + .map { anObject: String -> "true" == anObject }.orElse(false) + + if (serverUUID == null) { + if (recreateWhenMalformed) { + logger.info("Found malformed bStats config file. Re-creating it...") + if (!configFile.delete()) { + logger.error("Could not delete the bStats config!") + return + } + setupConfig(false) + } else { + logger.error("Failed to re-create malformed bStats config file") + return + } + } + } + + private fun writeConfig(file: File) { + val configContent: MutableList = ArrayList() + configContent.add("# bStats collects some basic information for plugin authors, like how many people use") + configContent.add("# their plugin and their total player count. It's recommend to keep bStats enabled, but") + configContent.add("# if you're not comfortable with this, you can turn this setting off. There is no") + configContent.add("# performance penalty associated with having metrics enabled, and data sent to bStats") + configContent.add("# can't identify your server.") + configContent.add("enabled=true") + configContent.add("server-uuid=" + UUID.randomUUID().toString()) + configContent.add("log-errors=false") + configContent.add("log-sent-data=false") + configContent.add("log-response-status-text=false") + writeFile(file, configContent) + } + + private fun getConfigValue(key: String, lines: List): Optional { + return lines.stream() + .filter { it.startsWith("$key=") } + .map { it.replaceFirst(Pattern.quote("$key=").toRegex(), "") } + .findFirst() + } + + private fun readFile(file: File): List { + if (!file.exists()) { + return emptyList() + } + FileReader(file).use { BufferedReader(it).use { o -> return o.lines().collect(Collectors.toList()) } } + } + + private fun writeFile(file: File, lines: List) { + if (!file.exists()) { + if (!file.createNewFile()) { + logger.error("Could not create the config file for bStats!") + return + } + } + FileWriter(file).use { + BufferedWriter(it).use { o -> + for (line in lines) { + o.write(line) + o.newLine() + } + } + } + } + + private fun appendPlatformData(builder: JsonObjectBuilder) { + builder.appendField("playerAmount", proxyServer.playerCount) + builder.appendField("managedServers", locationService.getServers().size) + builder.appendField("onlineMode", if (proxyServer.configuration.isOnlineMode) 1 else 0) + builder.appendField("velocityVersionVersion", proxyServer.version.version) + builder.appendField("velocityVersionName", proxyServer.version.name) + builder.appendField("velocityVersionVendor", proxyServer.version.vendor) + builder.appendField("javaVersion", System.getProperty("java.version")) + builder.appendField("osName", System.getProperty("os.name")) + builder.appendField("osArch", System.getProperty("os.arch")) + builder.appendField("osVersion", System.getProperty("os.version")) + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()) + } + + private fun appendServiceData(builder: JsonObjectBuilder) = + builder.appendField("pluginVersion", environment.pluginInfo.version) +} diff --git a/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/module/ApiVelocityModule.kt b/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/module/ApiVelocityModule.kt index a0e3e0963..86414a6cf 100644 --- a/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/module/ApiVelocityModule.kt +++ b/anvil-velocity/src/main/kotlin/org/anvilpowered/anvil/velocity/module/ApiVelocityModule.kt @@ -31,11 +31,13 @@ import org.anvilpowered.anvil.api.util.TextService import org.anvilpowered.anvil.api.util.UserService import org.anvilpowered.anvil.common.PlatformImpl import org.anvilpowered.anvil.common.command.CommonCallbackCommand +import org.anvilpowered.anvil.common.metric.MetricService import org.anvilpowered.anvil.common.module.PlatformModule import org.anvilpowered.anvil.common.util.CommonTextService import org.anvilpowered.anvil.common.util.SendTextService import org.anvilpowered.anvil.velocity.command.VelocityCommandExecuteService import org.anvilpowered.anvil.velocity.command.VelocitySimpleCommandService +import org.anvilpowered.anvil.velocity.metric.VelocityMetricService import org.anvilpowered.anvil.velocity.server.VelocityLocationService import org.anvilpowered.anvil.velocity.util.VelocityKickService import org.anvilpowered.anvil.velocity.util.VelocityPermissionService @@ -57,6 +59,7 @@ class ApiVelocityModule : PlatformModule( bind>().to() bind().to() bind().to() + bind().to() bind().to() bind>().to>() bind>().to>() diff --git a/gradle.properties b/gradle.properties index 70fd21b0a..f57556bae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ aopalliance=aopalliance:aopalliance:1.0 apache_commons=org.apache.commons:commons-pool2:2.6.2 bson=org.mongodb:bson:3.12.0 +bstats=org.bstats:bstats-base:2.1.0 bungee=net.md-5:bungeecord-api:1.15-SNAPSHOT configurate_core=org.spongepowered:configurate-core:3.7.2 configurate_hocon=org.spongepowered:configurate-hocon:3.7.2