diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Metrics.java b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Metrics.java index 3ee7d6e9..3ff786ff 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Metrics.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Metrics.java @@ -70,85 +70,73 @@ import java.util.UUID; import java.util.logging.Level; /** - *

- * The metrics class obtains data about a plugin and submits statistics about it to the metrics backend. - *

- *

- * Public methods provided by this class: - *

+ *

The metrics class obtains data about a plugin and submits statistics about it to the metrics backend.

+ * Public methods provided by this class:

* * Graph createGraph(String name);
- * void addCustomData(Metrics.Plotter plotter);
+ * void addCustomData(BukkitMetrics.Plotter plotter);
* void start();
*
*/ -class Metrics { +public class Metrics { /** * The current revision number */ - private final static int REVISION = 5; - + private final static int REVISION = 6; /** * The base url of the metrics domain */ private static final String BASE_URL = "http://mcstats.org"; - /** * The url used to report a server's status */ private static final String REPORT_URL = "/report/%s"; - /** - * The separator to use for custom data. This MUST NOT change unless you are hosting your own - * version of metrics and want to change it. + * The separator to use for custom data. This MUST NOT change unless you are hosting your own version of metrics and + * want to change it. */ private static final String CUSTOM_DATA_SEPARATOR = "~~"; - /** * Interval of time to ping (in minutes) */ private static final int PING_INTERVAL = 10; - /** * 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 default graph, used for addCustomData when you don't want a specific graph */ private final Graph defaultGraph = new Graph("Default"); - /** * 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; /** * Lock for synchronization */ private final Object optOutLock = new Object(); - /** - * Id of the scheduled task + * The scheduled task */ - private volatile int taskId = -1; + private volatile Scheduling.TaskWrapper task = null; public Metrics(final Plugin plugin) throws IOException { if (plugin == null) { @@ -164,6 +152,7 @@ class Metrics { // 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) { @@ -173,11 +162,12 @@ class Metrics { // Load the guid then guid = configuration.getString("guid"); + debug = configuration.getBoolean("debug", false); } /** - * 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. + * 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 @@ -198,7 +188,7 @@ class Metrics { } /** - * Add a Graph object to Metrics that represents data for the plugin that should be sent to the backend + * 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 */ @@ -228,14 +218,13 @@ class Metrics { } /** - * 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. + * 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. */ - @SuppressWarnings("deprecation") - public boolean start() { + public boolean start() { synchronized (optOutLock) { // Did we opt out? if (isOptOut()) { @@ -243,12 +232,12 @@ class Metrics { } // Is metrics already running? - if (taskId >= 0) { + if (task != null) { return true; } // Begin hitting the server with glorious data - taskId = plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(plugin, new Runnable() { + task = Scheduling.runAsynchronously(plugin, new Runnable() { private boolean firstPost = true; @@ -257,11 +246,11 @@ class Metrics { // This has to be synchronized or it can collide with the disable method. synchronized (optOutLock) { // Disable Task, if it is running and the server owner decided to opt-out - if (isOptOut() && taskId > 0) { - plugin.getServer().getScheduler().cancelTask(taskId); - taskId = -1; + if (isOptOut() && task != null) { + task.cancel(); + task = null; // Tell all plotters to stop gathering information. - for (Graph graph : graphs){ + for (Graph graph : graphs) { graph.onOptOut(); } } @@ -276,7 +265,9 @@ class Metrics { // Each post thereafter will be a ping firstPost = false; } catch (IOException e) { - Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage()); + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage()); + } } } }, 0, PING_INTERVAL * 1200); @@ -291,15 +282,19 @@ class Metrics { * @return true if metrics should be opted out of it */ public boolean isOptOut() { - synchronized(optOutLock) { + synchronized (optOutLock) { try { // Reload the metrics file configuration.load(getConfigFile()); } catch (IOException ex) { - Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + } return true; } catch (InvalidConfigurationException ex) { - Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + } return true; } return configuration.getBoolean("opt-out", false); @@ -307,30 +302,30 @@ class Metrics { } /** - * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. - * - * @throws IOException - */ + * 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 { // This has to be synchronized or it can collide with the check in the task. synchronized (optOutLock) { - // Check if the server owner has already set opt-out, if not, set it. - if (isOptOut()) { - configuration.set("opt-out", false); - configuration.save(configurationFile); - } + // Check if the server owner has already set opt-out, if not, set it. + if (isOptOut()) { + configuration.set("opt-out", false); + configuration.save(configurationFile); + } - // Enable Task, if it is not running - if (taskId < 0) { - start(); - } + // 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 IOException + * @throws java.io.IOException */ public void disable() throws IOException { // This has to be synchronized or it can collide with the check in the task. @@ -342,9 +337,9 @@ class Metrics { } // Disable Task, if it is running - if (taskId > 0) { - this.plugin.getServer().getScheduler().cancelTask(taskId); - taskId = -1; + if (task != null) { + task.cancel(); + task = null; } } } @@ -370,17 +365,45 @@ class Metrics { * Generic method that posts a plugin to the metrics website */ private void postPlugin(final boolean isPing) throws IOException { - // The plugin's description file containg all of the plugin data such as name, version, author, etc - final PluginDescriptionFile description = plugin.getDescription(); + // Server software specific section + PluginDescriptionFile description = plugin.getDescription(); + String pluginName = description.getName(); + boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE if online mode is enabled + String pluginVersion = description.getVersion(); + String serverVersion = Bukkit.getVersion(); + int playersOnline = Bukkit.getServer().getOnlinePlayers().length; + + // END server software specific section -- all code below does not use any code outside of this class / Java // Construct the post data final StringBuilder data = new StringBuilder(); + + // The plugin's description file containg all of the plugin data such as name, version, author, etc data.append(encode("guid")).append('=').append(encode(guid)); - encodeDataPair(data, "version", description.getVersion()); - encodeDataPair(data, "server", Bukkit.getVersion()); - encodeDataPair(data, "players", Integer.toString(Bukkit.getServer().getOnlinePlayers().length)); + encodeDataPair(data, "version", pluginVersion); + encodeDataPair(data, "server", serverVersion); + encodeDataPair(data, "players", Integer.toString(playersOnline)); encodeDataPair(data, "revision", String.valueOf(REVISION)); + // New data as of R6 + String osname = System.getProperty("os.name"); + String osarch = System.getProperty("os.arch"); + String osversion = System.getProperty("os.version"); + String java_version = System.getProperty("java.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + // normalize os arch .. amd64 -> x86_64 + if (osarch.equals("amd64")) { + osarch = "x86_64"; + } + + encodeDataPair(data, "osname", osname); + encodeDataPair(data, "osarch", osarch); + encodeDataPair(data, "osversion", osversion); + encodeDataPair(data, "cores", Integer.toString(coreCount)); + encodeDataPair(data, "online-mode", Boolean.toString(onlineMode)); + encodeDataPair(data, "java_version", java_version); + // If we're pinging, append it if (isPing) { encodeDataPair(data, "ping", "true"); @@ -411,7 +434,7 @@ class Metrics { } // Create the url - URL url = new URL(BASE_URL + String.format(REPORT_URL, encode(plugin.getDescription().getName()))); + URL url = new URL(BASE_URL + String.format(REPORT_URL, encode(pluginName))); // Connect to the website URLConnection connection; @@ -474,8 +497,8 @@ class Metrics { } /** - *

Encode a key/value data pair to be used in a HTTP post request. This INCLUDES a & so the first - * key/value pair MUST be included manually, e.g:

+ *

Encode a key/value data pair to be used in a HTTP post request. This INCLUDES a & so the first key/value pair + * MUST be included manually, e.g:

* * StringBuffer data = new StringBuffer(); * data.append(encode("guid")).append('=').append(encode(guid)); @@ -506,11 +529,10 @@ class Metrics { 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 + * 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 */ @@ -550,7 +572,7 @@ class Metrics { /** * Gets an unmodifiable set of the plotter objects in the graph * - * @return an unmodifiable {@link Set} of the plotter objects + * @return an unmodifiable {@link java.util.Set} of the plotter objects */ public Set getPlotters() { return Collections.unmodifiableSet(plotters); @@ -572,11 +594,10 @@ class Metrics { } /** - * Called when the server owner decides to opt-out of Metrics while the server is running. + * Called when the server owner decides to opt-out of BukkitMetrics while the server is running. */ protected void onOptOut() { } - } /** @@ -606,10 +627,9 @@ class Metrics { } /** - * 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. + * 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. */ @@ -644,6 +664,5 @@ class Metrics { final Plotter plotter = (Plotter) object; return plotter.name.equals(name) && plotter.getValue() == getValue(); } - } } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Scheduling.java b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Scheduling.java new file mode 100644 index 00000000..42e6db77 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Scheduling.java @@ -0,0 +1,81 @@ +package com.comphenix.protocol.metrics; + +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; + +/** + * Allows us to stay backwards compatible with older versions of Bukkit. + * + * @author Kristian + */ +class Scheduling { + /** + * Represents a backwards compatible Bukkit task. + */ + public static interface TaskWrapper { + /** + * Cancel the current task. + */ + public void cancel(); + } + + /** + * Schedule a given task for asynchronous execution. + * @param plugin - the owner plugin. + * @param runnable - the task to run. + * @param firstDelay - the amount of time to wait until executing the task for the first time. + * @param repeatDelay - the amount of time inbetween each execution. If less than zero, the task is only executed once. + * @return A cancel token. + */ + public static TaskWrapper runAsynchronously(final Plugin plugin, Runnable runnable, long firstDelay, long repeatDelay) { + return runAsynchronously(plugin, plugin.getServer().getScheduler(), runnable, firstDelay, repeatDelay); + } + + /** + * Schedule a given task for asynchronous execution. + * @param plugin - the owner plugin. + * @param scheduler - the current Bukkit scheduler. + * @param runnable - the task to run. + * @param firstDelay - the amount of time to wait until executing the task for the first time. + * @param repeatDelay - the amount of time inbetween each execution. If less than zero, the task is only executed once. + * @return A cancel token. + */ + public static TaskWrapper runAsynchronously(final Plugin plugin, final BukkitScheduler scheduler, Runnable runnable, long firstDelay, long repeatDelay) { + try { + @SuppressWarnings("deprecation") + final int taskID = scheduler.scheduleAsyncRepeatingTask(plugin, runnable, firstDelay, repeatDelay); + + // Return the cancellable object + return new TaskWrapper() { + @Override + public void cancel() { + scheduler.cancelTask(taskID); + } + }; + + } catch (NoSuchMethodError e) { + return tryUpdatedVersion(plugin, scheduler, runnable, firstDelay, repeatDelay); + } + } + + /** + * Attempt to do the same with the updated scheduling method. + * @param plugin - the owner plugin. + * @param scheduler - the current Bukkit scheduler. + * @param runnable - the task to run. + * @param firstDelay - the amount of time to wait until executing the task for the first time. + * @param repeatDelay - the amount of time inbetween each execution. If less than zero, the task is only executed once. + * @return A cancel token. + */ + private static TaskWrapper tryUpdatedVersion(final Plugin plugin, final BukkitScheduler scheduler, Runnable runnable, long firstDelay, long repeatDelay) { + final BukkitTask task = scheduler.runTaskTimerAsynchronously(plugin, runnable, firstDelay, repeatDelay); + + return new TaskWrapper() { + @Override + public void cancel() { + task.cancel(); + } + }; + } +}