package com.comphenix.xp.metrics; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.lang.NullArgumentException; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; import org.apache.commons.lang.exception.ExceptionUtils; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.configuration.Configuration; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.permissions.Permissible; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import com.google.common.collect.Lists; /** * Mod: <a href= * "http://dev.bukkit.org/server-mods/experiencemod/" * >ExperienceMod</a> * * @author V10lator * @version 1.0-Comphenix */ public class AutoUpdate implements Runnable, Listener { /* * Configuration: * * delay = The delay this class checks for new updates. This time is in * ticks (1 tick = 1/20 second). ymlPrefix = A prefix added to the version * string from your plugin.yml. ymlSuffix = A suffix added to the version * string from your plugin.yml. bukkitdevPrefix = A prefix added to the * version string fetched from bukkitDev. bukkitdevSuffix = A suffix added * to the version string fetched from bukkitDev. bukitdevSlug = The * bukkitDev Slug. Leave empty for autodetection (uses * plugin.getName().toLowerCase()). COLOR_INFO = The default text color. * COLOR_OK = The text color for positive messages. COLOR_ERROR = The text * color for error messages. */ private long delay = 216000L; private final String ymlPrefix = ""; private final String ymlSuffix = ""; private final String bukkitdevPrefix = ""; private final String bukkitdevSuffix = ""; private String bukkitdevSlug = ""; private final ChatColor COLOR_INFO = ChatColor.BLUE; private final ChatColor COLOR_OK = ChatColor.GREEN; private final ChatColor COLOR_ERROR = ChatColor.RED; private boolean debug; // No need to dump these values private final static String AUTO_UPDATE_SETTING = "auto update"; private final static String SUPPORT_URL = "http://dev.bukkit.org/server-mods/experiencemod/"; /* * End of configuration. * * !!! Don't change anything below if you don't know what you are doing !!! * * WARNING: If you change anything below you loose support. Also you have to * replace every * "http://forums.bukkit.org/threads/autoupdate-update-your-plugins.84421/" * with a link to your plugin and change the version to something unique * (like adding -<yourName>). */ private final String version = "1.0-Comphenix"; private final Plugin plugin; private final String bukget; private final String bukgetFallback; private int pid = -1; private final String av; @SuppressWarnings("unused") private Configuration config; boolean enabled = false; private final AtomicBoolean lock = new AtomicBoolean(false); private boolean needUpdate = false; private boolean updatePending = false; private String updateURL; private String updateVersion; private String pluginURL; private String type; /** * This will use your main configuration (config.yml). Use this in * onEnable(). * * @param plugin The instance of your plugins main class. * @throws FileNotFoundException - configuration file could not be found. */ public AutoUpdate(Plugin plugin) throws FileNotFoundException { this(plugin, plugin.getConfig()); } /** * This will use a custom configuration. Use this in onEnable(). * * @param plugin The instance of your plugins main class. * @param config The configuration to use. * @throws FileNotFoundException - configuration file could not be found. */ public AutoUpdate(Plugin plugin, Configuration config) throws FileNotFoundException { if (plugin == null) throw new NullArgumentException("Plugin can not be null"); this.plugin = plugin; av = ymlPrefix + plugin.getDescription().getVersion() + ymlSuffix; if (bukkitdevSlug == null || bukkitdevSlug.equals("")) bukkitdevSlug = plugin.getName(); bukkitdevSlug = bukkitdevSlug.toLowerCase(); bukget = "http://bukget.v10lator.de/" + bukkitdevSlug; bukgetFallback = "http://bukget.org/api/plugin/" + bukkitdevSlug + "/latest"; if (delay < 72000L) { plugin.getLogger() .info("[AutoUpdate] delay < 72000 ticks not supported. Setting delay to 72000."); delay = 72000L; } setConfig(config); plugin.getServer().getPluginManager().registerEvents(this, plugin); } public boolean isDebug() { return debug; } /** * Use this to enable/disable debugging mode at runtime. * @param debug True if you want to enable it, false otherwise. */ public void setDebug(boolean debug) { this.debug = debug; } /** * Use this to restart the main task. This is useful after * scheduler.cancelTasks(plugin); for example. */ public boolean restartMainTask() { try { ResetTask rt = new ResetTask(enabled); rt.setPid(plugin.getServer().getScheduler() .scheduleSyncRepeatingTask(plugin, rt, 0L, 1L)); return enabled; } catch (Throwable t) { printStackTraceSync(t, false); return false; } } private boolean checkState(boolean newState, boolean restart) { if (enabled != newState) { enabled = newState; plugin.getLogger().info( "[AutoUpdate] v" + version + (enabled ? " enabled" : " disabled") + "!"); if (restart) return restartMainTask(); } return enabled; } private class ResetTask implements Runnable { private int pid; private final boolean restart; private ResetTask(boolean restart) { this.restart = restart; } private void setPid(int pid) { this.pid = pid; } @SuppressWarnings("deprecation") public void run() { try { if (!lock.compareAndSet(false, true)) return; BukkitScheduler bs = plugin.getServer().getScheduler(); if (bs.isQueued(AutoUpdate.this.pid) || bs.isCurrentlyRunning(AutoUpdate.this.pid)) bs.cancelTask(AutoUpdate.this.pid); if (restart) AutoUpdate.this.pid = bs.scheduleAsyncRepeatingTask(plugin, AutoUpdate.this, 5L, delay); else AutoUpdate.this.pid = -1; lock.set(false); bs.cancelTask(pid); } catch (Throwable t) { printStackTraceSync(t, false); } } } /** * This will overwrite the pre-saved configuration. use this after * reloadConfig(), for example. This will use your main configuration * (config.yml). This will call {@link #restartMainTask()} internally. * * @throws FileNotFoundException */ public void resetConfig() throws FileNotFoundException { setConfig(plugin.getConfig()); } /** * This will overwrite the pre-saved configuration. use this after * config.load(file), for example. This will use a custom configuration. * This will call {@link #restartMainTask()} internally. * * @param config The new configuration to use. * @throws FileNotFoundException */ public void setConfig(Configuration config) throws FileNotFoundException { if (config == null) throw new FileNotFoundException("Config can not be null"); try { while (!lock.compareAndSet(false, true)) continue; // This blocks the main thread... this.config = config; if (!config.isSet(AUTO_UPDATE_SETTING)) config.set(AUTO_UPDATE_SETTING, true); checkState(config.getBoolean(AUTO_UPDATE_SETTING), true); lock.set(false); } catch (Throwable t) { printStackTraceSync(t, false); } } /** * This is internal stuff. Don't call this directly! */ public void run() { if (!plugin.isEnabled()) { plugin.getServer().getScheduler().cancelTask(pid); return; } try { while (!lock.compareAndSet(false, true)) { try { Thread.sleep(1L); } catch (InterruptedException e) { } continue; } try { InputStreamReader ir; try { URL url = new URL(bukget); ir = new InputStreamReader(url.openStream()); } catch (Exception e) { URL url = new URL(bukgetFallback); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.connect(); int res = con.getResponseCode(); if (res != 200) { if (debug) { plugin.getServer().getScheduler().scheduleSyncDelayedTask( plugin, new SyncMessageDelayer( null, new String[] { "[AutoUpdate] WARNING: Bukget returned " + res }) ); } lock.set(false); return; } ir = new InputStreamReader(con.getInputStream()); } String nv; try { JSONParser parser = new JSONParser(); Object result = parser.parse(ir); if (!(result instanceof JSONObject)) { ir.close(); throw new Exception("No data recieved."); } JSONObject jo = (JSONObject) result; JSONArray ja = (JSONArray) jo.get("versions"); pluginURL = (String) jo.get("bukkitdev_link"); jo = (JSONObject) ja.get(0); nv = bukkitdevPrefix + jo.get("name") + bukkitdevSuffix; // Quit if we've updated before, or we have a more recent version if ((updateVersion != null && updateVersion.equals(nv)) || new Version(av).compareTo(new Version(nv)) >= 0) { plugin.getLogger().info("[AutoUpdate] No new version detected."); plugin.getLogger().info("[AutoUpdate] Version online: " + nv); lock.set(false); return; } updateURL = (String) jo.get("dl_link"); updateVersion = nv; type = (String) jo.get("type"); needUpdate = true; ir.close(); } catch (ParseException e) { lock.set(false); printStackTraceSync(e, true); ir.close(); return; } final String[] out = new String[] { "[" + plugin.getName() + "] New " + type + " available!", "If you want to update from " + av + " to " + updateVersion + " use /update " + plugin.getName(), "See " + pluginURL + " for more information." }; plugin.getServer() .getScheduler() .scheduleSyncDelayedTask(plugin, new SyncMessageDelayer(null, out)); plugin.getServer().getScheduler() .scheduleSyncDelayedTask(plugin, new Runnable() { public void run() { String[] rout = new String[3]; for (int i = 0; i < 3; i++) rout[i] = COLOR_INFO + out[i]; for (Player p : plugin.getServer() .getOnlinePlayers()) if (hasPermission(p, "autoupdate.announce")) p.sendMessage(rout); } }); } catch (Exception e) { printStackTraceSync(e, true); } lock.set(false); } catch (Throwable t) { printStackTraceSync(t, false); } } /** * This is internal stuff. Don't call this directly! */ @EventHandler(priority = EventPriority.MONITOR) public void adminJoin(PlayerJoinEvent event) { try { if (!enabled || !lock.compareAndSet(false, true)) return; Player p = event.getPlayer(); String[] out; if (needUpdate) { if (hasPermission(p, "autoupdate.announce")) { out = new String[] { COLOR_INFO + "[" + plugin.getName() + "] New " + type + " available!", COLOR_INFO + "If you want to update from " + av + " to " + updateVersion + " use /update " + plugin.getName(), COLOR_INFO + "See " + pluginURL + " for more information." }; } else out = null; } else if (updatePending) { if (hasPermission(p, "autoupdate.announce")) { out = new String[] { COLOR_INFO + "Please restart the server to finish the update of " + plugin.getName(), COLOR_INFO + "See " + pluginURL + " for more information." }; } else out = null; } else out = null; lock.set(false); if (out != null) plugin.getServer() .getScheduler() .scheduleSyncDelayedTask(plugin, new SyncMessageDelayer(p.getName(), out)); } catch (Throwable t) { printStackTraceSync(t, false); } } private class SyncMessageDelayer implements Runnable { private final String player; private final String prefix; private final List<String> msgs; private SyncMessageDelayer(String player, String[] msgs) { this(player, "", Lists.newArrayList(msgs)); } private SyncMessageDelayer(String player, String prefix, List<String> list) { this.player = player; this.prefix = prefix; this.msgs = list; } public void run() { try { CommandSender cs; if (player != null) cs = plugin.getServer().getPlayerExact(player); else cs = plugin.getServer().getConsoleSender(); if (cs != null) for (String msg : msgs) if (msg != null) cs.sendMessage(prefix + msg); } catch (Throwable t) { printStackTraceSync(t, false); } } } // Find a better way for dynamic command handling /** * This is internal stuff. Don't call this directly! */ @EventHandler(ignoreCancelled = false) public void updateCmd(PlayerCommandPreprocessEvent event) { try { String[] split = event.getMessage().split(" "); if (!split[0].equalsIgnoreCase("/update")) return; event.setCancelled(true); if (split.length > 1 && !plugin.getName().equalsIgnoreCase(split[1])) return; updatePlugin(event.getPlayer()); } catch (Throwable t) { printStackTraceSync(t, false); } } /** * Called by a player or the console to initiate updates. * <p> * Note that the return value only indicates that the update request was accepted and the download * process is scheduled to be executed. The update itself may still fail. * * @param sender - the player or console that is attempting to initiate an update. * @return TRUE if the update was initiated and the download process has begun, FALSE otherwise. */ public boolean updatePlugin(CommandSender sender) { if (enabled && needUpdate) { return update(sender); } else { return false; } } @SuppressWarnings("deprecation") private boolean update(CommandSender sender) { if (!hasPermission(sender, "autoupdate.update." + plugin.getName())) { sender.sendMessage(COLOR_ERROR + plugin.getName() + ": You are not allowed to update me!"); return false; } final BukkitScheduler bs = plugin.getServer().getScheduler(); final String pn = sender instanceof Player ? ((Player) sender) .getName() : null; bs.scheduleAsyncDelayedTask(plugin, new Runnable() { public void run() { try { while (!lock.compareAndSet(false, true)) { try { Thread.sleep(1L); } catch (InterruptedException e) { } continue; } String out; try { File to = new File(plugin.getServer() .getUpdateFolderFile(), updateURL.substring( updateURL.lastIndexOf('/') + 1, updateURL.length())); File tmp = new File(to.getAbsolutePath() + ".au"); if (!tmp.exists()) { plugin.getServer().getUpdateFolderFile().mkdirs(); tmp.createNewFile(); } URL url = new URL(updateURL); InputStream is = url.openStream(); OutputStream os = new FileOutputStream(tmp); byte[] buffer = new byte[4096]; int fetched; while ((fetched = is.read(buffer)) != -1) os.write(buffer, 0, fetched); is.close(); os.flush(); os.close(); if (to.exists()) to.delete(); if (tmp.renameTo(to)) { out = COLOR_OK + plugin.getName() + " ready! Restart server to finish the update."; needUpdate = false; updatePending = true; updateURL = type = null; } else { out = COLOR_ERROR + plugin.getName() + " failed to update!"; if (tmp.exists()) tmp.delete(); if (to.exists()) to.delete(); } } catch (Exception e) { out = COLOR_ERROR + plugin.getName() + " failed to update!"; printStackTraceSync(e, true); } bs.scheduleSyncDelayedTask(plugin, new SyncMessageDelayer( pn, new String[] { out })); lock.set(false); } catch (Throwable t) { printStackTraceSync(t, false); } } }); // The task was successfully created. It may still fail, though. return true; } @SuppressWarnings("deprecation") private void printStackTraceSync(Throwable t, boolean expected) { BukkitScheduler bs = plugin.getServer().getScheduler(); try { List<String> lines = new ArrayList<String>(); String prefix = " "; lines.add(String.format("[" + plugin.getName() + "] [AutoUpdate]: ")); lines.add("Internal error!"); lines.add(""); lines.add("If this bug hasn't been reported please open a ticket at " + SUPPORT_URL); lines.add("Include the following into your bug report:"); lines.add(" ======= SNIP HERE ======="); addMultiString(lines, ExceptionUtils.getFullStackTrace(t)); lines.add(" ======= DUMP ======="); addMultiString(lines, ToStringBuilder. reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE). replace(ChatColor.COLOR_CHAR, '&')); if (!expected) { lines.add("DISABLING UPDATER!\n"); lines.add(""); } lines.add(" ======= SNIP HERE ======="); lines.add(""); bs.scheduleSyncDelayedTask(plugin, new SyncMessageDelayer(null, prefix, lines)); } catch (Throwable e) // This prevents endless loops. { e.printStackTrace(); } if (!expected) { bs.cancelTask(pid); bs.scheduleAsyncDelayedTask(plugin, new Runnable() { public void run() { while (!lock.compareAndSet(false, true)) { try { Thread.sleep(1L); } catch (InterruptedException e) { } } pid = -1; config = null; needUpdate = updatePending = false; updateURL = updateVersion = pluginURL = type = null; } }); } } private void addMultiString(List<String> lines, String text) { for (String line : text.split("\\r?\\n")) lines.add(line); } private boolean hasPermission(Permissible player, String node) { if (player.isPermissionSet(node)) return player.hasPermission(node); while (node.contains(".")) { node = node.substring(0, node.lastIndexOf(".")); if (player.isPermissionSet(node)) return player.hasPermission(node); if (player.isPermissionSet(node + ".*")) return player.hasPermission(node + ".*"); } if (player.isPermissionSet("*")) return player.hasPermission("*"); return player.isOp(); } }