From fef2bc0cf2b9290661ca0bc171589aac81278609 Mon Sep 17 00:00:00 2001 From: Jesse Boyd Date: Tue, 8 Mar 2016 16:31:40 +1100 Subject: [PATCH] Restore metrics --- .../com/plotsquared/bukkit/BukkitMain.java | 73 +- .../com/plotsquared/bukkit/util/Metrics.java | 622 ++++++++++++++++++ 2 files changed, 631 insertions(+), 64 deletions(-) create mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/util/Metrics.java diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitMain.java b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitMain.java index 6a65e807e..cf531d256 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitMain.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitMain.java @@ -12,73 +12,23 @@ import com.intellectualcrafters.plot.generator.GeneratorWrapper; import com.intellectualcrafters.plot.generator.HybridGen; import com.intellectualcrafters.plot.generator.HybridUtils; import com.intellectualcrafters.plot.generator.IndependentPlotGenerator; -import com.intellectualcrafters.plot.object.Plot; -import com.intellectualcrafters.plot.object.PlotArea; -import com.intellectualcrafters.plot.object.PlotPlayer; -import com.intellectualcrafters.plot.object.RunnableVal; -import com.intellectualcrafters.plot.object.SetupObject; -import com.intellectualcrafters.plot.util.AbstractTitle; -import com.intellectualcrafters.plot.util.ChatManager; -import com.intellectualcrafters.plot.util.ChunkManager; -import com.intellectualcrafters.plot.util.ConsoleColors; -import com.intellectualcrafters.plot.util.EconHandler; -import com.intellectualcrafters.plot.util.EventUtil; -import com.intellectualcrafters.plot.util.InventoryUtil; -import com.intellectualcrafters.plot.util.MainUtil; -import com.intellectualcrafters.plot.util.PlotQueue; -import com.intellectualcrafters.plot.util.SchematicHandler; -import com.intellectualcrafters.plot.util.SetupUtils; -import com.intellectualcrafters.plot.util.StringMan; -import com.intellectualcrafters.plot.util.TaskManager; -import com.intellectualcrafters.plot.util.UUIDHandler; -import com.intellectualcrafters.plot.util.UUIDHandlerImplementation; -import com.intellectualcrafters.plot.util.WorldUtil; +import com.intellectualcrafters.plot.object.*; +import com.intellectualcrafters.plot.util.*; import com.intellectualcrafters.plot.uuid.UUIDWrapper; import com.plotsquared.bukkit.commands.DebugUUID; import com.plotsquared.bukkit.database.plotme.ClassicPlotMeConnector; import com.plotsquared.bukkit.database.plotme.LikePlotMeConverter; import com.plotsquared.bukkit.database.plotme.PlotMeConnector_017; import com.plotsquared.bukkit.generator.BukkitPlotGenerator; -import com.plotsquared.bukkit.listeners.ChunkListener; -import com.plotsquared.bukkit.listeners.ForceFieldListener; -import com.plotsquared.bukkit.listeners.PlayerEvents; -import com.plotsquared.bukkit.listeners.PlayerEvents_1_8; -import com.plotsquared.bukkit.listeners.PlayerEvents_1_8_3; -import com.plotsquared.bukkit.listeners.PlotPlusListener; -import com.plotsquared.bukkit.listeners.WorldEvents; +import com.plotsquared.bukkit.listeners.*; import com.plotsquared.bukkit.listeners.worldedit.WEListener; import com.plotsquared.bukkit.titles.DefaultTitle_19; -import com.plotsquared.bukkit.util.BukkitChatManager; -import com.plotsquared.bukkit.util.BukkitChunkManager; -import com.plotsquared.bukkit.util.BukkitCommand; -import com.plotsquared.bukkit.util.BukkitEconHandler; -import com.plotsquared.bukkit.util.BukkitEventUtil; -import com.plotsquared.bukkit.util.BukkitHybridUtils; -import com.plotsquared.bukkit.util.BukkitInventoryUtil; -import com.plotsquared.bukkit.util.BukkitPlainChatManager; -import com.plotsquared.bukkit.util.BukkitSchematicHandler; -import com.plotsquared.bukkit.util.BukkitSetupUtils; -import com.plotsquared.bukkit.util.BukkitTaskManager; -import com.plotsquared.bukkit.util.BukkitUtil; -import com.plotsquared.bukkit.util.SendChunk; -import com.plotsquared.bukkit.util.SetGenCB; -import com.plotsquared.bukkit.util.block.FastQueue_1_7; -import com.plotsquared.bukkit.util.block.FastQueue_1_8; -import com.plotsquared.bukkit.util.block.FastQueue_1_8_3; -import com.plotsquared.bukkit.util.block.FastQueue_1_9; -import com.plotsquared.bukkit.util.block.SlowQueue; -import com.plotsquared.bukkit.uuid.DefaultUUIDWrapper; -import com.plotsquared.bukkit.uuid.FileUUIDHandler; -import com.plotsquared.bukkit.uuid.LowerOfflineUUIDWrapper; -import com.plotsquared.bukkit.uuid.OfflineUUIDWrapper; -import com.plotsquared.bukkit.uuid.SQLUUIDHandler; +import com.plotsquared.bukkit.util.*; +import com.plotsquared.bukkit.util.block.*; +import com.plotsquared.bukkit.uuid.*; import com.sk89q.worldedit.bukkit.WorldEditPlugin; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; +import org.bukkit.*; import org.bukkit.Location; -import org.bukkit.OfflinePlayer; -import org.bukkit.Server; -import org.bukkit.World; import org.bukkit.command.PluginCommand; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -87,17 +37,12 @@ import org.bukkit.generator.ChunkGenerator; import org.bukkit.metadata.MetadataValue; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; -import org.mcstats.Metrics; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; +import java.util.*; public class BukkitMain extends JavaPlugin implements Listener, IPlotMain { @@ -577,7 +522,7 @@ public class BukkitMain extends JavaPlugin implements Listener, IPlotMain { @Override public void startMetrics() { try { - final org.mcstats.Metrics metrics = new Metrics(this); + Metrics metrics = new Metrics(this); metrics.start(); log(C.PREFIX.s() + "&6Metrics enabled."); } catch (IOException e) { diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/util/Metrics.java b/Bukkit/src/main/java/com/plotsquared/bukkit/util/Metrics.java new file mode 100644 index 000000000..7048e0384 --- /dev/null +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/util/Metrics.java @@ -0,0 +1,622 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// PlotSquared - A plot manager and world generator for the Bukkit API / +// Copyright (c) 2014 IntellectualSites/IntellectualCrafters / +// / +// This program is free software; you can redistribute it and/or modify / +// it under the terms of the GNU 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 General Public License for more details. / +// / +// You should have received a copy of the GNU General Public License / +// along with this program; if not, write to the Free Software Foundation, / +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA / +// / +// You can contact us via: support@intellectualsites.com / +//////////////////////////////////////////////////////////////////////////////////////////////////// +package com.plotsquared.bukkit.util; + +import com.intellectualcrafters.configuration.file.YamlConfiguration; +import com.intellectualcrafters.plot.PS; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.scheduler.BukkitTask; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.*; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + +public class Metrics { + + /** + * The current revision number + */ + private final static int REVISION = 7; + /** + * The base url of the metrics domain + */ + private static final String BASE_URL = "http://report.mcstats.org"; + /** + * The url used to report a server's status + */ + private static final String REPORT_URL = "/plugin/%s"; + /** + * Interval of time to ping (in minutes) + */ + private static final int PING_INTERVAL = 15; + /** + * The plugin this metrics submits for + */ + private final Plugin plugin; + /** + * All of the custom graphs to submit to metrics + */ + private final Set graphs = Collections.synchronizedSet(new HashSet()); + /** + * The plugin configuration file + */ + private final YamlConfiguration configuration; + /** + * The plugin configuration file + */ + private final File configurationFile; + /** + * Unique server id + */ + private final String guid; + /** + * Debug mode + */ + private final boolean debug; + /** + * The scheduled task + */ + private volatile BukkitTask task = null; + + public Metrics(final Plugin plugin) throws IOException { + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null"); + } + this.plugin = plugin; + // load the config + configurationFile = getConfigFile(); + configuration = YamlConfiguration.loadConfiguration(configurationFile); + // add some defaults + configuration.addDefault("opt-out", false); + configuration.addDefault("guid", UUID.randomUUID().toString()); + configuration.addDefault("debug", false); + // Do we need to create the file? + if (configuration.get("guid", null) == null) { + configuration.options().header("http://mcstats.org").copyDefaults(true); + configuration.save(configurationFile); + } + // Load the guid then + guid = configuration.getString("guid"); + debug = configuration.getBoolean("debug", false); + } + + /** + * GZip compress a string of bytes + * + * @param input + * + * @return byte[] the file as a byte array + */ + public static byte[] gzip(final String input) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzos = null; + try { + gzos = new GZIPOutputStream(baos); + gzos.write(input.getBytes("UTF-8")); + } catch (final IOException e) { + e.printStackTrace(); + } finally { + if (gzos != null) { + try { + gzos.close(); + } catch (final IOException ignore) { + } + } + } + return baos.toByteArray(); + } + + /** + * Appends a json encoded key/value pair to the given string builder. + * + * @param json + * @param key + * @param value + * + * @throws UnsupportedEncodingException + */ + private static void appendJSONPair(final StringBuilder json, final String key, final String value) { + boolean isValueNumeric = false; + try { + if (value.equals("0") || !value.endsWith("0")) { + Double.parseDouble(value); + isValueNumeric = true; + } + } catch (final NumberFormatException e) { + isValueNumeric = false; + } + if (json.charAt(json.length() - 1) != '{') { + json.append(','); + } + json.append(escapeJSON(key)); + json.append(':'); + if (isValueNumeric) { + json.append(value); + } else { + json.append(escapeJSON(value)); + } + } + + /** + * Escape a string to create a valid JSON string + * + * @param text + * + * @return String + */ + private static String escapeJSON(final String text) { + final StringBuilder builder = new StringBuilder(); + builder.append('"'); + for (int index = 0; index < text.length(); index++) { + final char chr = text.charAt(index); + switch (chr) { + case '"': + case '\\': + builder.append('\\'); + builder.append(chr); + break; + case '\b': + builder.append("\\b"); + break; + case '\t': + builder.append("\\t"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + default: + if (chr < ' ') { + final String t = "000" + Integer.toHexString(chr); + builder.append("\\u" + t.substring(t.length() - 4)); + } else { + builder.append(chr); + } + break; + } + } + builder.append('"'); + return builder.toString(); + } + + /** + * Encode text as UTF-8 + * + * @param text the text to encode + * + * @return the encoded text, as UTF-8 + */ + private static String urlEncode(final String text) throws UnsupportedEncodingException { + return URLEncoder.encode(text, "UTF-8"); + } + + /** + * Construct and create a Graph that can be used to separate specific plotters to their own graphs on the metrics + * website. Plotters can be added to the graph object returned. + * + * @param name The name of the graph + * + * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given + */ + public Graph createGraph(final String name) { + if (name == null) { + throw new IllegalArgumentException("Graph name cannot be null"); + } + // Construct the graph object + final Graph graph = new Graph(name); + // Now we can add our graph + graphs.add(graph); + // and return back + return graph; + } + + /** + * Add a Graph object to BukkitMetrics that represents data for the plugin that should be sent to the backend + * + * @param graph The name of the graph + */ + public void addGraph(final Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + graphs.add(graph); + } + + /** + * Start measuring statistics. This will immediately create an async repeating task as the plugin and send the + * initial data to the metrics backend, and then after that it will post in increments of PING_INTERVAL * 1200 + * ticks. + * + * @return True if statistics measuring is running, otherwise false. + */ + public boolean start() { + // Is metrics already running? + if (task != null) { + return true; + } + // Begin hitting the server with glorious data + task = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() { + private boolean firstPost = true; + + @Override + public void run() { + try { + postPlugin(!firstPost); + // After the first post we set firstPost to + // false + // Each post thereafter will be a ping + firstPost = false; + } catch (final IOException e) { + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage()); + } + } + } + }, 0, PING_INTERVAL * 1200); + return true; + } + + /** + * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. + * + * @throws java.io.IOException + */ + public void enable() throws IOException { + // Enable Task, if it is not running + if (task == null) { + start(); + } + } + + /** + * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task. + * + * @throws java.io.IOException + */ + public void disable() throws IOException { + // Disable Task, if it is running + if (task != null) { + task.cancel(); + task = null; + } + } + + /** + * Gets the File object of the config file that should be used to store data such as the GUID and opt-out status + * + * @return the File object for the config file + */ + public File getConfigFile() { + // I believe the easiest way to get the base folder (e.g craftbukkit set + // via -P) for plugins to use + // is to abuse the plugin object we already have + // plugin.getDataFolder() => base/plugins/PluginA/ + // pluginsFolder => base/plugins/ + // The base is not necessarily relative to the startup directory. + final File pluginsFolder = plugin.getDataFolder().getParentFile(); + // return => base/plugins/PluginMetrics/config.yml + return new File(new File(pluginsFolder, "PluginMetrics"), "config.yml"); + } + + /** + * Generic method that posts a plugin to the metrics website + */ + private void postPlugin(final boolean isPing) throws IOException { + // Server software specific section + final PluginDescriptionFile description = plugin.getDescription(); + final String pluginName = description.getName(); + final boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE + // if + // online + // mode + // is + // enabled + final String pluginVersion = description.getVersion(); + final String serverVersion = Bukkit.getVersion(); + int playersOnline = 0; + try { + if (Bukkit.class.getMethod("getOnlinePlayers", new Class[0]).getReturnType() == Collection.class) { + playersOnline = ((Collection) Bukkit.class.getMethod("getOnlinePlayers", new Class[0]).invoke(null)).size(); + } else { + playersOnline = ((Player[]) Bukkit.class.getMethod("getOnlinePlayers", new Class[0]).invoke(null)).length; + } + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) { + } + // END server software specific section -- all code below does not use + // any code outside of this class / Java + // Construct the post data + final StringBuilder json = new StringBuilder(1024); + json.append('{'); + // The plugin's description file containg all of the plugin data such as + // name, version, author, etc + appendJSONPair(json, "guid", guid); + appendJSONPair(json, "plugin_version", pluginVersion); + appendJSONPair(json, "server_version", serverVersion); + appendJSONPair(json, "players_online", Integer.toString(playersOnline)); + // New data as of R6 + final String osname = System.getProperty("os.name"); + String osarch = System.getProperty("os.arch"); + final String osversion = System.getProperty("os.version"); + final String java_version = System.getProperty("java.version"); + final int coreCount = Runtime.getRuntime().availableProcessors(); + // normalize os arch .. amd64 -> x86_64 + if (osarch.equals("amd64")) { + osarch = "x86_64"; + } + appendJSONPair(json, "osname", osname); + appendJSONPair(json, "osarch", osarch); + appendJSONPair(json, "osversion", osversion); + appendJSONPair(json, "cores", Integer.toString(coreCount)); + appendJSONPair(json, "auth_mode", onlineMode ? "1" : "0"); + appendJSONPair(json, "java_version", java_version); + // If we're pinging, append it + if (isPing) { + appendJSONPair(json, "ping", "1"); + } + if (!graphs.isEmpty()) { + synchronized (graphs) { + json.append(','); + json.append('"'); + json.append("graphs"); + json.append('"'); + json.append(':'); + json.append('{'); + boolean firstGraph = true; + for (final Graph graph : graphs) { + final StringBuilder graphJson = new StringBuilder(); + graphJson.append('{'); + for (final Plotter plotter : graph.getPlotters()) { + appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue())); + } + graphJson.append('}'); + if (!firstGraph) { + json.append(','); + } + json.append(escapeJSON(graph.getName())); + json.append(':'); + json.append(graphJson); + firstGraph = false; + } + json.append('}'); + } + } + // close json + json.append('}'); + // Create the url + final URL url = new URL(BASE_URL + String.format(REPORT_URL, urlEncode(pluginName))); + // Connect to the website + URLConnection connection; + // Mineshafter creates a socks proxy, so we can safely bypass it + // It does not reroute POST requests so we need to go around it + if (isMineshafterPresent()) { + connection = url.openConnection(Proxy.NO_PROXY); + } else { + connection = url.openConnection(); + } + final byte[] uncompressed = json.toString().getBytes(); + final byte[] compressed = gzip(json.toString()); + // Headers + connection.addRequestProperty("User-Agent", "MCStats/" + REVISION); + connection.addRequestProperty("Content-Type", "application/json"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", Integer.toString(compressed.length)); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.setDoOutput(true); + if (debug) { + PS.debug("[Metrics] Prepared request for " + pluginName + " uncompressed=" + uncompressed.length + " compressed=" + compressed.length); + } + // Write the data + String response; + try (OutputStream os = connection.getOutputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + os.write(compressed); + os.flush(); + // Now read the response + response = reader.readLine(); + } + if (response == null || response.startsWith("ERR") || response.startsWith("7")) { + if (response == null) { + response = "null"; + } else if (response.startsWith("7")) { + response = response.substring(response.startsWith("7,") ? 2 : 1); + } + throw new IOException(response); + } else { + // Is this the first update this hour? + if ("1".equals(response) || response.contains("This is your first update this hour")) { + synchronized (graphs) { + for (final Graph graph : graphs) { + for (final Plotter plotter : graph.getPlotters()) { + plotter.reset(); + } + } + } + } + } + } + + /** + * Check if mineshafter is present. If it is, we need to bypass it to send POST requests + * + * @return true if mineshafter is installed on the server + */ + private boolean isMineshafterPresent() { + try { + Class.forName("mineshafter.MineServer"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * Represents a custom graph on the website + */ + public static class Graph { + + /** + * The graph's name, alphanumeric and spaces only :) If it does not comply to the above when submitted, it is + * rejected + */ + private final String name; + /** + * The set of plotters that are contained within this graph + */ + private final Set plotters = new LinkedHashSet<>(); + + private Graph(final String name) { + this.name = name; + } + + /** + * Gets the graph's name + * + * @return the Graph's name + */ + public String getName() { + return name; + } + + /** + * Add a plotter to the graph, which will be used to plot entries + * + * @param plotter the plotter to add to the graph + */ + public void addPlotter(final Plotter plotter) { + plotters.add(plotter); + } + + /** + * Remove a plotter from the graph + * + * @param plotter the plotter to remove from the graph + */ + public void removePlotter(final Plotter plotter) { + plotters.remove(plotter); + } + + /** + * Gets an unmodifiable set of the plotter objects in the graph + * + * @return an unmodifiable {@link java.util.Set} of the plotter objects + */ + public Set getPlotters() { + return Collections.unmodifiableSet(plotters); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Graph)) { + return false; + } + final Graph graph = (Graph) object; + return graph.name.equals(name); + } + + /** + * Called when the server owner decides to opt-out of BukkitMetrics while the server is running. + */ + protected void onOptOut() { + } + } + + /** + * Interface used to collect custom data for a plugin + */ + public static abstract class Plotter { + + /** + * The plot's name + */ + private final String name; + + /** + * Construct a plotter with the default plot name + */ + public Plotter() { + this("Default"); + } + + /** + * Construct a plotter with a specific plot name + * + * @param name the name of the plotter to use, which will show up on the website + */ + public Plotter(final String name) { + this.name = name; + } + + /** + * Get the current value for the plotted point. Since this function defers to an external function it may or may + * not return immediately thus cannot be guaranteed to be thread friendly or safe. This function can be called + * from any thread so care should be taken when accessing resources that need to be synchronized. + * + * @return the current value for the point to be plotted. + */ + public abstract int getValue(); + + /** + * Get the column name for the plotted point + * + * @return the plotted point's column name + */ + public String getColumnName() { + return name; + } + + /** + * Called after the website graphs have been updated + */ + public void reset() { + } + + @Override + public int hashCode() { + return getColumnName().hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Plotter)) { + return false; + } + final Plotter plotter = (Plotter) object; + return plotter.name.equals(name) && plotter.getValue() == getValue(); + } + } +} \ No newline at end of file