diff --git a/.gitignore b/.gitignore index 205bc24..98b27a2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.settings # netbeans +/nbbuild.xml /nbproject # we use maven! diff --git a/src/de/diddiz/LogBlock/CommandsHandler.java b/src/de/diddiz/LogBlock/CommandsHandler.java index 79f2935..e23bf7a 100644 --- a/src/de/diddiz/LogBlock/CommandsHandler.java +++ b/src/de/diddiz/LogBlock/CommandsHandler.java @@ -5,7 +5,6 @@ import static de.diddiz.LogBlock.config.Config.askClearLogAfterRollback; import static de.diddiz.LogBlock.config.Config.askClearLogs; import static de.diddiz.LogBlock.config.Config.askRedos; import static de.diddiz.LogBlock.config.Config.askRollbacks; -import static de.diddiz.LogBlock.config.Config.checkVersion; import static de.diddiz.LogBlock.config.Config.defaultTime; import static de.diddiz.LogBlock.config.Config.dumpDeletedLog; import static de.diddiz.LogBlock.config.Config.getWorldConfig; @@ -67,8 +66,6 @@ public class CommandsHandler implements CommandExecutor try { if (args.length == 0) { sender.sendMessage(ChatColor.LIGHT_PURPLE + "LogBlock v" + logblock.getDescription().getVersion() + " by DiddiZ"); - if (checkVersion) - sender.sendMessage(ChatColor.LIGHT_PURPLE + logblock.getUpdater().checkVersion()); sender.sendMessage(ChatColor.LIGHT_PURPLE + "Type /lb help for help"); } else { final String command = args[0].toLowerCase(); diff --git a/src/de/diddiz/LogBlock/LogBlock.java b/src/de/diddiz/LogBlock/LogBlock.java index cbe049f..27f6a5a 100644 --- a/src/de/diddiz/LogBlock/LogBlock.java +++ b/src/de/diddiz/LogBlock/LogBlock.java @@ -2,7 +2,6 @@ package de.diddiz.LogBlock; import static de.diddiz.LogBlock.config.Config.askRollbackAfterBan; import static de.diddiz.LogBlock.config.Config.autoClearLogDelay; -import static de.diddiz.LogBlock.config.Config.checkVersion; import static de.diddiz.LogBlock.config.Config.delayBetweenRuns; import static de.diddiz.LogBlock.config.Config.enableAutoClearLog; import static de.diddiz.LogBlock.config.Config.isLogging; @@ -17,6 +16,7 @@ import static de.diddiz.util.Utils.download; import static org.bukkit.Bukkit.getPluginManager; import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; import java.net.URL; import java.sql.Connection; import java.sql.ResultSet; @@ -86,8 +86,6 @@ public class LogBlock extends JavaPlugin try { updater = new Updater(this); Config.load(this); - if (checkVersion) - getLogger().info("[LogBlock] Version check: " + updater.checkVersion()); getLogger().info("[LogBlock] Connecting to " + user + "@" + url + "..."); pool = new MySQLConnectionPool(url, user, password); final Connection conn = getConnection(); @@ -151,6 +149,12 @@ public class LogBlock extends JavaPlugin final Permission perm = new Permission("logblock.tools." + tool.name, tool.permissionDefault); pm.addPermission(perm); } + try { + Metrics metrics = new Metrics(this); + metrics.start(); + } catch (IOException ex) { + getLogger().info("Could not start metrics: " + ex.getMessage()); + } } private void registerEvents() { diff --git a/src/de/diddiz/LogBlock/Metrics.java b/src/de/diddiz/LogBlock/Metrics.java new file mode 100644 index 0000000..e7f2ec3 --- /dev/null +++ b/src/de/diddiz/LogBlock/Metrics.java @@ -0,0 +1,347 @@ +/* + * Copyright 2011 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. + */ + +package de.diddiz.LogBlock; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +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.logging.Level; + +public class Metrics { + + /** + * The current revision number + */ + private final static int REVISION = 5; + + /** + * 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 file where guid and opt out is stored in + */ + private static final String CONFIG_FILE = "plugins/PluginMetrics/config.yml"; + + /** + * Interval of time to ping (in minutes) + */ + private final static int PING_INTERVAL = 10; + + /** + * The plugin this metrics submits for + */ + private final Plugin plugin; + + /** + * The plugin configuration file + */ + private final YamlConfiguration configuration; + + /** + * The plugin configuration file + */ + private final File configurationFile; + + /** + * Unique server id + */ + private final String guid; + + /** + * Lock for synchronization + */ + private final Object optOutLock = new Object(); + + /** + * Id of the scheduled task + */ + private volatile int taskId = -1; + + public Metrics(Plugin plugin) throws IOException { + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null"); + } + + this.plugin = plugin; + + // load the config + configurationFile = new File(CONFIG_FILE); + configuration = YamlConfiguration.loadConfiguration(configurationFile); + + // add some defaults + configuration.addDefault("opt-out", false); + configuration.addDefault("guid", UUID.randomUUID().toString()); + + // 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"); + } + + + /** + * 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 (taskId >= 0) { + return true; + } + + // Begin hitting the server with glorious data + taskId = plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(plugin, new Runnable() { + + private boolean firstPost = true; + + 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() && taskId > 0) { + plugin.getServer().getScheduler().cancelTask(taskId); + taskId = -1; + } + } + + // 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 (IOException e) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage()); + } + } + }, 0, PING_INTERVAL * 1200); + + return true; + } + } + + /** + * Has the server owner denied plugin metrics? + * + * @return + */ + public boolean isOptOut() { + synchronized(optOutLock) { + try { + // Reload the metrics file + configuration.load(CONFIG_FILE); + } catch (IOException ex) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + return true; + } catch (InvalidConfigurationException ex) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + return true; + } + return configuration.getBoolean("opt-out", false); + } + } + + /** + * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. + * + * @throws 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); + } + + // Enable Task, if it is not running + if (taskId < 0) { + start(); + } + } + } + + /** + * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task. + * + * @throws 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()) { + configuration.set("opt-out", true); + configuration.save(configurationFile); + } + + // Disable Task, if it is running + if (taskId > 0) { + this.plugin.getServer().getScheduler().cancelTask(taskId); + taskId = -1; + } + } + } + + /** + * Generic method that posts a plugin to the metrics website + */ + private void postPlugin(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(); + + // Construct the post data + final StringBuilder data = new StringBuilder(); + 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, "revision", String.valueOf(REVISION)); + + // If we're pinging, append it + if (isPing) { + encodeDataPair(data, "ping", "true"); + } + + // Create the url + URL url = new URL(BASE_URL + String.format(REPORT_URL, encode(plugin.getDescription().getName()))); + + // 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(); + } + + connection.setDoOutput(true); + + // Write the data + final OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); + writer.write(data.toString()); + writer.flush(); + + // Now read the response + final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + final String response = reader.readLine(); + + // close resources + writer.close(); + reader.close(); + + if (response == null || response.startsWith("ERR")) { + throw new IOException(response); //Throw the exception + } + //if (response.startsWith("OK")) - We should get "OK" followed by an optional description if everything goes right + } + + /** + * Check if mineshafter is present. If it is, we need to bypass it to send POST requests + * + * @return + */ + private boolean isMineshafterPresent() { + try { + Class.forName("mineshafter.MineServer"); + return true; + } catch (Exception e) { + return false; + } + } + + /** + *

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)); + * encodeDataPair(data, "version", description.getVersion()); + * + * + * @param buffer + * @param key + * @param value + * @return + */ + private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException { + buffer.append('&').append(encode(key)).append('=').append(encode(value)); + } + + /** + * Encode text as UTF-8 + * + * @param text + * @return + */ + private static String encode(String text) throws UnsupportedEncodingException { + return URLEncoder.encode(text, "UTF-8"); + } + +} \ No newline at end of file diff --git a/src/de/diddiz/LogBlock/Updater.java b/src/de/diddiz/LogBlock/Updater.java index 02da813..10fa4fb 100644 --- a/src/de/diddiz/LogBlock/Updater.java +++ b/src/de/diddiz/LogBlock/Updater.java @@ -232,12 +232,4 @@ class Updater throw new SQLException("Table " + table + " not found and failed to create"); } } - - String checkVersion() { - try { - return readURL(new URL("http://diddiz.insane-architects.net/lbuptodate.php?v=" + logblock.getDescription().getVersion())); - } catch (final Exception ex) { - return "Can't check version"; - } - } } diff --git a/src/de/diddiz/LogBlock/config/Config.java b/src/de/diddiz/LogBlock/config/Config.java index 007f774..f86cda6 100644 --- a/src/de/diddiz/LogBlock/config/Config.java +++ b/src/de/diddiz/LogBlock/config/Config.java @@ -50,7 +50,6 @@ public class Config public static int linesPerPage, linesLimit; public static boolean askRollbacks, askRedos, askClearLogs, askClearLogAfterRollback, askRollbackAfterBan; public static String banPermission; - public static boolean checkVersion; public static Set hiddenBlocks; public static Set hiddenPlayers; @@ -104,7 +103,6 @@ public class Config def.put("questioner.askClearLogAfterRollback", true); def.put("questioner.askRollbackAfterBan", false); def.put("questioner.banPermission", "mcbans.ban.local"); - def.put("updater.checkVersion", true); def.put("tools.tool.aliases", Arrays.asList("t")); def.put("tools.tool.leftClickBehavior", "NONE"); def.put("tools.tool.rightClickBehavior", "TOOL"); @@ -173,7 +171,6 @@ public class Config askClearLogAfterRollback = config.getBoolean("questioner.askClearLogAfterRollback", true); askRollbackAfterBan = config.getBoolean("questioner.askRollbackAfterBan", false); banPermission = config.getString("questioner.banPermission"); - checkVersion = config.getBoolean("updater.checkVersion", true); final List tools = new ArrayList(); final ConfigurationSection toolsSec = config.getConfigurationSection("tools"); for (final String toolName : toolsSec.getKeys(false))