diff --git a/Sponge/build.gradle b/Sponge/build.gradle index a0300fd06..203cf013c 100644 --- a/Sponge/build.gradle +++ b/Sponge/build.gradle @@ -17,6 +17,7 @@ apply plugin: 'com.github.johnrengelman.shadow' dependencies { compile project(':Core') compile 'org.spongepowered:spongeapi:4.0.3' + compile 'org.mcstats.sponge:metrics:R8-SNAPSHOT' } sourceCompatibility = 1.8 @@ -31,6 +32,10 @@ repositories { name = "Sponge" url = "https://repo.spongepowered.org/maven" } + maven { + name = "Sponge Metrics" + url = "http://repo.mcstats.org/content/repositories/releases/" + } } minecraft { version = "1.8.9-11.15.1.1742" @@ -49,7 +54,9 @@ processResources { shadowJar { dependencies { include(dependency(':Core')) + include(dependency('org.mcstats.sponge:metrics:R8-SNAPSHOT')) } + relocate 'org.mcstats', 'com.plotsquared.stats' archiveName = "${parent.name}-${project.name}-${parent.version}.jar" destinationDir = file '../target' } diff --git a/Sponge/src/main/java/com/plotsquared/sponge/SpongeMain.java b/Sponge/src/main/java/com/plotsquared/sponge/SpongeMain.java index 12e36ead1..6ee5fbb86 100644 --- a/Sponge/src/main/java/com/plotsquared/sponge/SpongeMain.java +++ b/Sponge/src/main/java/com/plotsquared/sponge/SpongeMain.java @@ -41,7 +41,6 @@ import com.plotsquared.sponge.util.SpongeEconHandler; import com.plotsquared.sponge.util.SpongeEventUtil; import com.plotsquared.sponge.util.SpongeHybridUtils; import com.plotsquared.sponge.util.SpongeInventoryUtil; -import com.plotsquared.sponge.util.SpongeMetrics; import com.plotsquared.sponge.util.SpongeSchematicHandler; import com.plotsquared.sponge.util.SpongeSetupUtils; import com.plotsquared.sponge.util.SpongeTaskManager; @@ -52,6 +51,7 @@ import com.plotsquared.sponge.util.block.SlowQueue; import com.plotsquared.sponge.uuid.SpongeLowerOfflineUUIDWrapper; import com.plotsquared.sponge.uuid.SpongeOnlineUUIDWrapper; import com.plotsquared.sponge.uuid.SpongeUUIDHandler; +import org.mcstats.Metrics; import org.slf4j.Logger; import org.spongepowered.api.Game; import org.spongepowered.api.Server; @@ -70,6 +70,7 @@ import org.spongepowered.api.world.gen.WorldGenerator; import org.spongepowered.api.world.gen.WorldGeneratorModifier; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -303,10 +304,10 @@ public class SpongeMain implements IPlotMain { @Override public void startMetrics() { try { - final SpongeMetrics metrics = new SpongeMetrics(game, (PluginContainer) this); + Metrics metrics = new Metrics(game, (PluginContainer) this); metrics.start(); log(C.PREFIX.s() + "&6Metrics enabled."); - } catch (final Exception e) { + } catch (IOException e) { log(C.PREFIX.s() + "&cFailed to load up metrics."); } } diff --git a/Sponge/src/main/java/com/plotsquared/sponge/util/SpongeMetrics.java b/Sponge/src/main/java/com/plotsquared/sponge/util/SpongeMetrics.java deleted file mode 100644 index 0aa09932b..000000000 --- a/Sponge/src/main/java/com/plotsquared/sponge/util/SpongeMetrics.java +++ /dev/null @@ -1,518 +0,0 @@ -package com.plotsquared.sponge.util; - -/* - * Copyright 2011-2013 Tyler Blair. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are those of the - * authors and contributors and should not be interpreted as representing official policies, - * either expressed or implied, of anybody else. - */ - -import com.google.inject.Inject; -import com.intellectualcrafters.plot.PS; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.zip.GZIPOutputStream; -import ninja.leaping.configurate.commented.CommentedConfigurationNode; -import ninja.leaping.configurate.hocon.HoconConfigurationLoader; -import ninja.leaping.configurate.loader.ConfigurationLoader; -import org.spongepowered.api.Game; -import org.spongepowered.api.plugin.PluginContainer; -import org.spongepowered.api.scheduler.Task; - -public class SpongeMetrics { - - /** - * 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 game data is being sent for - */ - private final Game game; - - /** - * The plugin this metrics submits for - */ - private final PluginContainer plugin; - /** - * Lock for synchronization - */ - private final Object optOutLock = new Object(); - /** - * The plugin configuration file - */ - private CommentedConfigurationNode config; - /** - * The configuration loader - */ - private ConfigurationLoader configurationLoader; - /** - * The plugin configuration file - */ - private File configurationFile; - /** - * Unique server id - */ - private String guid; - /** - * Debug mode - */ - private boolean debug; - /** - * The scheduled task - */ - private volatile Task task = null; - - @Inject - public SpongeMetrics(final Game game, final PluginContainer plugin) throws IOException { - if (plugin == null) { - throw new IllegalArgumentException("Plugin cannot be null"); - } - - this.game = game; - this.plugin = plugin; - - loadConfiguration(); - } - - /** - * GZip compress a string of bytes - * - * @param input - * @return - */ - 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 java.io.UnsupportedEncodingException - */ - private static void appendJSONPair(final StringBuilder json, final String key, final String value) throws UnsupportedEncodingException { - 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 - */ - 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"); - } - - /** - * Loads the configuration - */ - private void loadConfiguration() { - configurationFile = getConfigFile(); - configurationLoader = HoconConfigurationLoader.builder().setFile(configurationFile).build(); - - try { - if (!configurationFile.exists()) { - configurationFile.createNewFile(); - config = configurationLoader.load(); - - config.setComment("This contains settings for MCStats: http://mcstats.org"); - config.getNode("mcstats.guid").setValue(UUID.randomUUID().toString()); - config.getNode("mcstats.opt-out").setValue(false); - config.getNode("mcstats.debug").setValue(false); - - configurationLoader.save(config); - } else { - config = configurationLoader.load(); - } - - guid = config.getNode("mcstats.guid").getString(); - debug = config.getNode("mcstats.debug").getBoolean(); - } catch (final IOException e) { - e.printStackTrace(); - } - } - - /** - * 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() { - synchronized (optOutLock) { - // Did we opt out? - if (isOptOut()) { - return false; - } - - // Is metrics already running? - if (task != null) { - return true; - } - - // Begin hitting the server with glorious data - final Task.Builder builder = game.getScheduler().createTaskBuilder(); - builder.async().interval(PING_INTERVAL, TimeUnit.MINUTES).execute(new Runnable() { - - private boolean firstPost = true; - - @Override - public void run() { - try { - // 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() && (task != null)) { - task.cancel(); - task = null; - } - } - - // We use the inverse of firstPost because if it is the first time we are posting, - // it is not a interval ping, so it evaluates to FALSE - // Each time thereafter it will evaluate to TRUE, i.e PING! - 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) { - PS.debug("[Metrics] " + e.getMessage()); - } - } - } - }); - return true; - } - } - - /** - * Has the server owner denied plugin metrics? - * - * @return true if metrics should be opted out of it - */ - public boolean isOptOut() { - synchronized (optOutLock) { - loadConfiguration(); - - return config.getNode("mcstats.opt-out").getBoolean(); - } - } - - /** - * 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()) { - config.getNode("mcstats.opt-out").setValue(false); - configurationLoader.save(config); - } - - // 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 { - // 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()) { - config.getNode("mcstats.opt-out").setValue(true); - configurationLoader.save(config); - } - - // 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() { - // TODO configDir - final File configFolder = new File("config"); - - return new File(configFolder, "PluginMetrics.conf"); - } - - /** - * Generic method that posts a plugin to the metrics website - * - */ - private void postPlugin(final boolean isPing) throws IOException { - // Server software specific section - final String pluginName = plugin.getName(); - final boolean onlineMode = game.getServer().getOnlineMode(); // TRUE if online mode is enabled - final String pluginVersion = plugin.getVersion().get(); - // TODO no visible way to get MC version at the moment - // TODO added by game.getPlatform().getMinecraftVersion() -- impl in 2.1 - final String serverVersion = String.format("%s %s", "Sponge", game.getPlatform().getMinecraftVersion()); - final int playersOnline = game.getServer().getOnlinePlayers().size(); - - // 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"); - } - - // 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 - final OutputStream os = connection.getOutputStream(); - os.write(compressed); - os.flush(); - - // Now read the response - final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - String response = reader.readLine(); - - // close resources - os.close(); - reader.close(); - - 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); - } - } - - /** - * 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 (final Exception e) { - return false; - } - } - -}