diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf21202 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.gradle +build +run diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4ed701d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + java + id("net.minecrell.plugin-yml.bukkit") version "0.5.1" + id("com.github.johnrengelman.shadow") version "7.1.1" + id("xyz.jpenilla.run-paper") version "1.0.6" +} + +group = "co.technove" +version = "1.0.0" + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(16)) +} + +repositories { + mavenCentral() + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + maven("https://jitpack.io") +} + +dependencies { + compileOnly("org.jetbrains:annotations:16.0.2") + compileOnly("org.spigotmc:spigot-api:1.18.1-R0.1-SNAPSHOT") + + implementation("com.github.TECHNOVE:Flare:2c4a2114a0") + implementation("com.github.oshi:oshi-core:5.8.6") +} + +bukkit { + main = "co.technove.flareplugin.FlarePlugin" + apiVersion = "1.16" + authors = listOf("PaulBGD") + version = rootProject.version as String + + commands { + register("flare") { + description = "Flare profiling command" + aliases = listOf("profiler", "sampler") + permission = "flareplugin.command" + usage = "/flare" + } + } +} + +tasks { + build { + dependsOn(shadowJar) + } + + compileJava { + options.encoding = Charsets.UTF_8.name() + options.release.set(16) + } + + javadoc { + options.encoding = Charsets.UTF_8.name() + } + + processResources { + filteringCharset = Charsets.UTF_8.name() + } + + runServer { + minecraftVersion("1.16.5") + } + + shadowJar { + classifier = "" + } + +} + +tasks.create("relocateShadowJar") { + target = tasks["shadowJar"] as com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + prefix = "co.technove.flareplugin.lib" +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2880ba --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..bffb747 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://papermc.io/repo/repository/maven-public/") + } +} + +rootProject.name = "FlarePlugin" diff --git a/src/main/java/co/technove/flareplugin/CustomCategories.java b/src/main/java/co/technove/flareplugin/CustomCategories.java new file mode 100644 index 0000000..8065615 --- /dev/null +++ b/src/main/java/co/technove/flareplugin/CustomCategories.java @@ -0,0 +1,7 @@ +package co.technove.flareplugin; + +import co.technove.flare.live.category.GraphCategory; + +public class CustomCategories { + public static final GraphCategory MC_PERF = new GraphCategory("MC Performance"); +} diff --git a/src/main/java/co/technove/flareplugin/FlareCommand.java b/src/main/java/co/technove/flareplugin/FlareCommand.java new file mode 100644 index 0000000..821e03d --- /dev/null +++ b/src/main/java/co/technove/flareplugin/FlareCommand.java @@ -0,0 +1,79 @@ +package co.technove.flareplugin; + +import co.technove.flare.exceptions.UserReportableException; +import co.technove.flare.internal.profiling.ProfileType; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Level; + +public class FlareCommand implements CommandExecutor { + + private final FlarePlugin plugin; + + public FlareCommand(@NotNull final FlarePlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(@NotNull final CommandSender sender, @NotNull final Command command, @NotNull final String label, @NotNull final String[] args) { + if (!command.testPermission(sender)) { + return true; + } + + if (this.plugin.getFlareURI().getScheme() == null) { + sender.sendMessage(ChatColor.RED + "Invalid URL for Flare, check your config."); + return true; + } + + if (this.plugin.getAccessToken().length() == 0) { + sender.sendMessage(ChatColor.RED + "Invalid token for Flare, check your config."); + return true; + } + + final ProfilingManager profilingManager = this.plugin.getProfilingManager(); + + if (profilingManager.isProfiling()) { + if (profilingManager.stop()) { + if (!(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(ChatColor.GREEN + "Profiling has been stopped."); + } + } else { + sender.sendMessage(ChatColor.GREEN + "Profiling has already been stopped."); + } + } else { + ProfileType profileType = ProfileType.ITIMER; + if (args.length > 0) { + try { + profileType = ProfileType.valueOf(args[0].toUpperCase()); + } catch (Exception e) { + sender.sendMessage(ChatColor.RED + "Invalid profile type " + args[0] + "!"); + } + } + final ProfileType finalProfileType = profileType; + Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { + try { + if (profilingManager.start(finalProfileType)) { + if (!(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(ChatColor.GREEN + "Flare has been started: " + profilingManager.getProfilingUri()); + sender.sendMessage(ChatColor.GREEN + " Run /" + label + " to stop the Flare."); + } + } else { + sender.sendMessage(ChatColor.RED + "Flare has already been started!"); + } + } catch (UserReportableException e) { + sender.sendMessage("Flare failed to start: " + e.getUserError()); + if (e.getCause() != null) { + this.plugin.getLogger().log(Level.WARNING, "Flare failed to start", e); + } + } + }); + } + return true; + } +} diff --git a/src/main/java/co/technove/flareplugin/FlarePlugin.java b/src/main/java/co/technove/flareplugin/FlarePlugin.java new file mode 100644 index 0000000..0c04127 --- /dev/null +++ b/src/main/java/co/technove/flareplugin/FlarePlugin.java @@ -0,0 +1,67 @@ +package co.technove.flareplugin; + +import co.technove.flare.FlareInitializer; +import co.technove.flare.internal.profiling.InitializationException; +import co.technove.flareplugin.utils.NMSHelper; +import co.technove.flareplugin.utils.PluginLookup; +import org.bukkit.plugin.java.JavaPlugin; + +import java.net.URI; +import java.util.List; +import java.util.logging.Level; + +public class FlarePlugin extends JavaPlugin { + + private ProfilingManager profilingManager; + private PluginLookup pluginLookup; + + @Override + public void onEnable() { + this.saveDefaultConfig(); + + try { + NMSHelper.initialize(); + } catch (ReflectiveOperationException e) { + this.getLogger().log(Level.WARNING, "Failed to initialize NMS, you may not be running Spigot.", e); + } + + try { + final List warnings = FlareInitializer.initialize(); + this.getLogger().log(Level.WARNING, "Warnings while initializing Flare: " + String.join(", ", warnings)); + } catch (InitializationException e) { + this.getLogger().log(Level.SEVERE, "Failed to initialize Flare", e); + this.getServer().getPluginManager().disablePlugin(this); + return; + } + + this.profilingManager = new ProfilingManager(this); + + this.pluginLookup = new PluginLookup(); + this.getServer().getPluginManager().registerEvents(this.pluginLookup, this); + + this.getCommand("flare").setExecutor(new FlareCommand(this)); + } + + @Override + public void onDisable() { + if (this.profilingManager.isProfiling()) { + this.profilingManager.stop(); + } + } + + public URI getFlareURI() { + return URI.create(this.getConfig().getString("flare.url", "")); + } + + public String getAccessToken() { + return this.getConfig().getString("flare.token"); + } + + public ProfilingManager getProfilingManager() { + return profilingManager; + } + + public PluginLookup getPluginLookup() { + return pluginLookup; + } +} diff --git a/src/main/java/co/technove/flareplugin/ProfilingManager.java b/src/main/java/co/technove/flareplugin/ProfilingManager.java new file mode 100644 index 0000000..aa0e1d7 --- /dev/null +++ b/src/main/java/co/technove/flareplugin/ProfilingManager.java @@ -0,0 +1,146 @@ +package co.technove.flareplugin; + +import co.technove.flare.Flare; +import co.technove.flare.FlareAuth; +import co.technove.flare.FlareBuilder; +import co.technove.flare.exceptions.UserReportableException; +import co.technove.flare.internal.profiling.ProfileType; +import co.technove.flareplugin.collectors.GCEventCollector; +import co.technove.flareplugin.collectors.StatCollector; +import co.technove.flareplugin.collectors.TPSCollector; +import co.technove.flareplugin.utils.ServerConfigurations; +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.VirtualMemory; +import oshi.software.os.OperatingSystem; + +import java.io.IOException; +import java.net.URI; +import java.util.Objects; +import java.util.logging.Level; + +// yuck +public class ProfilingManager { + + private final FlarePlugin plugin; + + private @Nullable Flare currentFlare; + private @Nullable BukkitTask currentTask = null; + + public ProfilingManager(@NotNull final FlarePlugin plugin) { + this.plugin = plugin; + } + + public synchronized boolean isProfiling() { + return currentFlare != null && currentFlare.isRunning(); + } + + public synchronized String getProfilingUri() { + return Objects.requireNonNull(currentFlare).getURI().map(URI::toString).orElse("Flare is not running"); + } + + public synchronized boolean start(ProfileType profileType) throws UserReportableException { + if (currentFlare != null && !currentFlare.isRunning()) { + currentFlare = null; // errored out + } + if (isProfiling()) { + return false; + } + if (Bukkit.isPrimaryThread()) { + throw new UserReportableException("Profiles should be started off-thread"); + } + + try { + OperatingSystem os = new SystemInfo().getOperatingSystem(); + + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hardware = systemInfo.getHardware(); + + CentralProcessor processor = hardware.getProcessor(); + CentralProcessor.ProcessorIdentifier processorIdentifier = processor.getProcessorIdentifier(); + + GlobalMemory memory = hardware.getMemory(); + VirtualMemory virtualMemory = memory.getVirtualMemory(); + + FlareBuilder builder = new FlareBuilder() + .withProfileType(profileType) + .withMemoryProfiling(true) + .withAuth(FlareAuth.fromTokenAndUrl(this.plugin.getAccessToken(), this.plugin.getFlareURI())) + + .withFiles(ServerConfigurations.getCleanCopies()) + .withVersion("Primary Version", Bukkit.getVersion()) + .withVersion("Bukkit Version", Bukkit.getBukkitVersion()) + + .withGraphCategories(CustomCategories.MC_PERF) + .withCollectors(new TPSCollector(), new GCEventCollector(), new StatCollector()) + .withClassIdentifier(this.plugin.getPluginLookup()::getPluginForClass) + + .withHardware(new FlareBuilder.HardwareBuilder() + .setCoreCount(processor.getPhysicalProcessorCount()) + .setThreadCount(processor.getLogicalProcessorCount()) + .setCpuModel(processorIdentifier.getName()) + .setCpuFrequency(processor.getMaxFreq()) + + .setTotalMemory(memory.getTotal()) + .setTotalSwap(virtualMemory.getSwapTotal()) + .setTotalVirtual(virtualMemory.getVirtualMax()) + ) + + .withOperatingSystem(new FlareBuilder.OperatingSystemBuilder() + .setManufacturer(os.getManufacturer()) + .setFamily(os.getFamily()) + .setVersion(os.getVersionInfo().toString()) + .setBitness(os.getBitness()) + ); + + currentFlare = builder.build(); + } catch (IOException e) { + this.plugin.getLogger().log(Level.WARNING, "Failed to read configuration files:", e); + throw new UserReportableException("Failed to load configuration files, check logs for further details."); + } + + try { + currentFlare.start(); + } catch (IllegalStateException e) { + this.plugin.getLogger().log(Level.WARNING, "Error starting Flare:", e); + throw new UserReportableException("Failed to start Flare, check logs for further details."); + } + + currentTask = Bukkit.getScheduler().runTaskLater(plugin, this::stop, 20 * 60 * 15); + this.plugin.getLogger().log(Level.INFO, "Flare has been started: " + getProfilingUri()); + return true; + } + + public synchronized boolean stop() { + if (!isProfiling()) { + return false; + } + if (!currentFlare.isRunning()) { + currentFlare = null; + return true; + } + this.plugin.getLogger().log(Level.INFO, "Flare has been stopped: " + getProfilingUri()); + try { + currentFlare.stop(); + } catch (IllegalStateException e) { + this.plugin.getLogger().log(Level.WARNING, "Error occurred stopping Flare", e); + } + currentFlare = null; + + try { + currentTask.cancel(); + } catch (Throwable t) { + this.plugin.getLogger().log(Level.WARNING, "Error occurred stopping Flare", t); + } + + currentTask = null; + return true; + } + +} diff --git a/src/main/java/co/technove/flareplugin/collectors/GCEventCollector.java b/src/main/java/co/technove/flareplugin/collectors/GCEventCollector.java new file mode 100644 index 0000000..ce78a8a --- /dev/null +++ b/src/main/java/co/technove/flareplugin/collectors/GCEventCollector.java @@ -0,0 +1,65 @@ +package co.technove.flareplugin.collectors; + +import co.technove.flare.Flare; +import co.technove.flare.live.CollectorData; +import co.technove.flare.live.EventCollector; +import co.technove.flare.live.LiveEvent; +import co.technove.flare.live.category.GraphCategory; +import co.technove.flare.live.formatter.DataFormatter; +import com.google.common.collect.ImmutableMap; +import com.sun.management.GarbageCollectionNotificationInfo; + +import javax.management.ListenerNotFoundException; +import javax.management.Notification; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import javax.management.openmbean.CompositeData; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; + +public class GCEventCollector extends EventCollector implements NotificationListener { + + private static final CollectorData MINOR_GC = new CollectorData("builtin:gc:minor", "Minor GC", "A small pause in the program to allow Garbage Collection to run.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM); + private static final CollectorData MAJOR_GC = new CollectorData("builtin:gc:major", "Major GC", "A large pause in the program to allow Garbage Collection to run.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM); + private static final CollectorData UNKNOWN_GC = new CollectorData("builtin:gc:generic", "Major GC", "A run of the Garbage Collection.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM); + + public GCEventCollector() { + super(MINOR_GC, MAJOR_GC, UNKNOWN_GC); + } + + private static CollectorData fromString(String string) { + if (string.endsWith("minor GC")) { + return MINOR_GC; + } else if (string.endsWith("major GC")) { + return MAJOR_GC; + } + return UNKNOWN_GC; + } + + @Override + public void start(Flare flare) { + for (GarbageCollectorMXBean garbageCollectorBean : ManagementFactory.getGarbageCollectorMXBeans()) { + NotificationEmitter notificationEmitter = (NotificationEmitter) garbageCollectorBean; + notificationEmitter.addNotificationListener(this, null, null); + } + } + + @Override + public void stop(Flare flare) { + for (GarbageCollectorMXBean garbageCollectorBean : ManagementFactory.getGarbageCollectorMXBeans()) { + NotificationEmitter notificationEmitter = (NotificationEmitter) garbageCollectorBean; + try { + notificationEmitter.removeNotificationListener(this); + } catch (ListenerNotFoundException e) { + } + } + } + + @Override + public void handleNotification(Notification notification, Object o) { + if (notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) { + GarbageCollectionNotificationInfo gcInfo = GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData()); + reportEvent(new LiveEvent(fromString(gcInfo.getGcAction()), System.currentTimeMillis(), (int) gcInfo.getGcInfo().getDuration(), ImmutableMap.of())); + } + } +} diff --git a/src/main/java/co/technove/flareplugin/collectors/StatCollector.java b/src/main/java/co/technove/flareplugin/collectors/StatCollector.java new file mode 100644 index 0000000..4583462 --- /dev/null +++ b/src/main/java/co/technove/flareplugin/collectors/StatCollector.java @@ -0,0 +1,41 @@ +package co.technove.flareplugin.collectors; + +import co.technove.flare.live.CollectorData; +import co.technove.flare.live.LiveCollector; +import co.technove.flare.live.category.GraphCategory; +import co.technove.flare.live.formatter.DataFormatter; +import com.sun.management.OperatingSystemMXBean; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; + +import java.lang.management.ManagementFactory; +import java.time.Duration; + +public class StatCollector extends LiveCollector { + + private static final CollectorData CPU = new CollectorData("builtin:stat:cpu", "CPU Load", "The total amount of CPU usage across all cores.", DataFormatter.PERCENT, GraphCategory.SYSTEM); + private static final CollectorData CPU_PROCESS = new CollectorData("builtin:stat:cpu_process", "Process CPU", "The amount of CPU being used by this process.", DataFormatter.PERCENT, GraphCategory.SYSTEM); + private static final CollectorData MEMORY = new CollectorData("builtin:stat:memory_used", "Memory", "The amount of memory being used currently.", DataFormatter.BYTES, GraphCategory.SYSTEM); + private static final CollectorData MEMORY_TOTAL = new CollectorData("builtin:stat:memory_total", "Memory Total", "The total amount of memory allocated.", DataFormatter.BYTES, GraphCategory.SYSTEM); + + private final OperatingSystemMXBean bean; + private final CentralProcessor processor; + + public StatCollector() { + super(CPU, CPU_PROCESS, MEMORY, MEMORY_TOTAL); + this.interval = Duration.ofSeconds(5); + + this.bean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + this.processor = new SystemInfo().getHardware().getProcessor(); + } + + @Override + public void run() { + Runtime runtime = Runtime.getRuntime(); + + this.report(CPU, this.processor.getSystemLoadAverage(1)[0] / 100); // percentage + this.report(CPU_PROCESS, this.bean.getProcessCpuLoad()); + this.report(MEMORY, runtime.totalMemory() - runtime.freeMemory()); + this.report(MEMORY_TOTAL, runtime.totalMemory()); + } +} diff --git a/src/main/java/co/technove/flareplugin/collectors/TPSCollector.java b/src/main/java/co/technove/flareplugin/collectors/TPSCollector.java new file mode 100644 index 0000000..62241de --- /dev/null +++ b/src/main/java/co/technove/flareplugin/collectors/TPSCollector.java @@ -0,0 +1,36 @@ +package co.technove.flareplugin.collectors; + +import co.technove.flare.live.CollectorData; +import co.technove.flare.live.LiveCollector; +import co.technove.flare.live.formatter.SuffixFormatter; +import co.technove.flareplugin.CustomCategories; +import co.technove.flareplugin.utils.NMSHelper; + +import java.time.Duration; +import java.util.Arrays; + +public class TPSCollector extends LiveCollector { + private static final CollectorData TPS = new CollectorData("airplane:tps", "TPS", "Ticks per second, or how fast the server updates. For a smooth server this should be a constant 20TPS.", SuffixFormatter.of("TPS"), CustomCategories.MC_PERF); + private static final CollectorData MSPT = new CollectorData("airplane:mspt", "MSPT", "Milliseconds per tick, which can show how well your server is performing. This value should always be under 50mspt.", SuffixFormatter.of("mspt"), CustomCategories.MC_PERF); + + public TPSCollector() { + super(TPS, MSPT); + + this.interval = Duration.ofSeconds(5); + } + + @Override + public void run() { + final long[] times = NMSHelper.getTickTimes5s(); + final double[] tps = NMSHelper.getTps(); + + if (times.length == 0 && tps.length == 0) { + return; + } + + final double mspt = ((double) Arrays.stream(times).sum() / (double) times.length) * 1.0E-6D; + + this.report(TPS, Math.min(20D, Math.round(tps[0] * 100d) / 100d)); + this.report(MSPT, (double) Math.round(mspt * 100d) / 100d); + } +} diff --git a/src/main/java/co/technove/flareplugin/utils/NMSHelper.java b/src/main/java/co/technove/flareplugin/utils/NMSHelper.java new file mode 100644 index 0000000..59f1f62 --- /dev/null +++ b/src/main/java/co/technove/flareplugin/utils/NMSHelper.java @@ -0,0 +1,54 @@ +package co.technove.flareplugin.utils; + +import org.bukkit.Bukkit; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class NMSHelper { + + private static Object server; + private static Field recentTps; + + private static Object tickTimes5s; + private static Method getTimes; + + public static void initialize() throws ReflectiveOperationException { + Class serverClass; + try { + serverClass = Class.forName("net.minecraft.server.MinecraftServer"); + } catch (ClassNotFoundException e) { + final String ver = Bukkit.getServer().getClass().getPackageName().split("\\.")[3]; + if (!ver.startsWith("v")) { + throw e; + } + + serverClass = Class.forName("net.minecraft.server." + ver + ".MinecraftServer"); + } + final Method getServer = serverClass.getMethod("getServer"); + + server = getServer.invoke(null); + recentTps = serverClass.getField("recentTps"); + + tickTimes5s = serverClass.getField("tickTimes5s").get(server); + getTimes = tickTimes5s.getClass().getMethod("getTimes"); + } + + public static long[] getTickTimes5s() { + try { + return (long[]) getTimes.invoke(tickTimes5s); + } catch (final IllegalAccessException | InvocationTargetException | NullPointerException e) { + return new long[0]; + } + } + + public static double[] getTps() { + try { + return (double[]) recentTps.get(server); + } catch (final IllegalAccessException | NullPointerException e) { + return new double[0]; + } + } + +} diff --git a/src/main/java/co/technove/flareplugin/utils/PluginLookup.java b/src/main/java/co/technove/flareplugin/utils/PluginLookup.java new file mode 100644 index 0000000..0bc6593 --- /dev/null +++ b/src/main/java/co/technove/flareplugin/utils/PluginLookup.java @@ -0,0 +1,67 @@ +package co.technove.flareplugin.utils; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +public class PluginLookup implements Listener { + private final Cache pluginNameCache = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.MINUTES) + .maximumSize(1024) + .build(); + + private final Map classLoaderToPlugin = new ConcurrentHashMap<>(); + + @EventHandler(priority = EventPriority.MONITOR) + public void onPluginLoad(final PluginEnableEvent event) { + classLoaderToPlugin.put(event.getPlugin().getClass().getClassLoader(), event.getPlugin()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPluginDisable(final PluginDisableEvent event) { + while (classLoaderToPlugin.values().remove(event.getPlugin())); + } + + public @NotNull Optional getPluginForClass(@NotNull final String name) { + if (name.startsWith("net.minecraft") || name.startsWith("java.") || name.startsWith("com.mojang") || + name.startsWith("com.google") || name.startsWith("it.unimi") || name.startsWith("sun")) { + return Optional.empty(); + } + + final String existing = this.pluginNameCache.getIfPresent(name); + if (existing != null) { + return Optional.ofNullable(existing.isEmpty() ? null : existing); + } + + + final Class loadedClass; + try { + loadedClass = Class.forName(name); + } catch (ClassNotFoundException e) { + this.pluginNameCache.put(name, ""); + return Optional.empty(); + } + + if (loadedClass.getClassLoader() == null) { + this.pluginNameCache.put(name, ""); + return Optional.empty(); + } + + final Plugin plugin = this.classLoaderToPlugin.get(loadedClass.getClassLoader()); + final String pluginName = plugin == null ? "" : plugin.getName(); + + this.pluginNameCache.put(name, pluginName); + return Optional.ofNullable(pluginName.isEmpty() ? null : pluginName); + } +} diff --git a/src/main/java/co/technove/flareplugin/utils/ServerConfigurations.java b/src/main/java/co/technove/flareplugin/utils/ServerConfigurations.java new file mode 100644 index 0000000..b1370f1 --- /dev/null +++ b/src/main/java/co/technove/flareplugin/utils/ServerConfigurations.java @@ -0,0 +1,91 @@ +package co.technove.flareplugin.utils; + +import com.google.common.io.Files; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +public class ServerConfigurations { + + public static final String[] configurationFiles = new String[]{ + "server.properties", + "bukkit.yml", + "spigot.yml", + "paper.yml", + "airplane.yml", + "purpur.yml" + }; + + public static @NotNull Map getCleanCopies() throws IOException { + final Map files = new HashMap<>(configurationFiles.length); + for (final String file : configurationFiles) { + final String cleanCopy = getCleanCopy(file); + if (cleanCopy == null) { + continue; + } + files.put(file, cleanCopy); + } + return files; + } + + public static @Nullable String getCleanCopy(@NotNull final String configName) throws IOException { + final File file = new File(configName); + if (!file.exists()) { + return null; + } + + final List hiddenConfigs = TimingsHelper.getHiddenConfigs(); + + if (hiddenConfigs == null) { + return null; + } + + switch (Files.getFileExtension(configName)) { + case "properties": { + final Properties properties = new Properties(); + try (final FileInputStream inputStream = new FileInputStream(file)) { + properties.load(inputStream); + } + for (final String hiddenConfig : hiddenConfigs) { + properties.remove(hiddenConfig); + } + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + properties.store(outputStream, ""); + return Arrays.stream(outputStream.toString() + .split("\n")) + .filter(line -> !line.startsWith("#")) + .collect(Collectors.joining("\n")); + } + case "yml": { + final YamlConfiguration configuration = new YamlConfiguration(); + try { + configuration.load(file); + } catch (final InvalidConfigurationException e) { + throw new IOException(e); + } + configuration.options().header(null); + for (final String key : configuration.getKeys(true)) { + if (hiddenConfigs.contains(key)) { + configuration.set(key, null); + } + } + return configuration.saveToString(); + } + default: + throw new IllegalArgumentException("Bad file type " + configName); + } + } + +} diff --git a/src/main/java/co/technove/flareplugin/utils/TimingsHelper.java b/src/main/java/co/technove/flareplugin/utils/TimingsHelper.java new file mode 100644 index 0000000..ecfbe2a --- /dev/null +++ b/src/main/java/co/technove/flareplugin/utils/TimingsHelper.java @@ -0,0 +1,37 @@ +package co.technove.flareplugin.utils; + +import co.technove.flareplugin.FlarePlugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.logging.Level; + +public class TimingsHelper { + + private static boolean failed = false; + private static List hiddenConfigs; + + public synchronized static @Nullable List getHiddenConfigs() { + if (failed) { + return null; + } + + if (hiddenConfigs == null) { + try { + final Class clazz = Class.forName("co.aikar.timings.TimingsManager"); + final Field hiddenConfigs = clazz.getField("hiddenConfigs"); + List list = (List) hiddenConfigs.get(null); + + TimingsHelper.hiddenConfigs = list; + } catch (Throwable t) { + JavaPlugin.getPlugin(FlarePlugin.class).getLogger().log(Level.WARNING, "Failed to find hidden configs, will not send config files.", t); + failed = true; + return null; + } + } + + return hiddenConfigs; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..a522ff7 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,3 @@ +flare: + url: https://flare.airplane.gg + token: ""