AutoUpdate - Update your plugins!

Discussion in 'Resources' started by V10lator, Jul 3, 2012.

Thread Status:
Not open for further replies.
  1. This is up for grabs. See here why.

    I want to present you a class I wrote.

    This class handles plugin updates. It does this by broadcasting new updates to admins and giving them a command to update a specific or all plugins.

    What makes this special?
    • Easy setup.
    • Automatic update check at BukkitDev via bukget.
    • Uses bukkits build-in update API.
    • One permission set for all plugins.
    • Standard config node for all plugins.
    • One command for all plugins.
    • Multi-threading for best performance.
    There are two permission nodes you have to care (and tell your users) about:
    • autoupdate.announce - Users with this node will be notified about new updates.
    • autoupdate.update.<yourPluginName> - Users with this node will be able to /update your plugin.
    Both nodes default to op.

    Known caveeats
    None. :)

    One simple command:
    /update <PluginName> - To update <PluginName> or all plugins if none given.

    The lazy way
    If you're a lucky winner all you have to do is to put this into your plugin and add
    1. new AutoUpdate(this);

    into your onEnable();
    Yes, that's it. But I won't guarant you that everything works like expected this way.

    The right way
    First go the lazy way, but this time put
    1. new AutoUpdate(this);

    between the codeblock that loads the config and the one that saves it, example:
    1. Configuration config = getConfig();
    2. = config.getString("foo");
    3. new AutoUpdate(this);
    4. config.set("foo", foo);
    5. saveConfig();

    If you reload the config at any time make sure you are reseting the config, too. Example:
    1. Configuration config;
    2. AutoUpdate autoUpdate;
    3. public void onEnable()
    4. {
    5. config = getConfig();
    6. = config.getString("foo");
    7. autoUpdate = new AutoUpdate(this);
    8. config.set("foo", foo);
    9. saveConfig();
    10. }
    12. public void foo()
    13. {
    14. ...
    15. reloadConfig()
    16. config = getConfig();
    17. autoUpdate.resetConfig();
    18. ...
    19. }

    If you use a custom config, give it to the calls. Example:
    1. myConfig.load(something);
    2. AutoUpdate autoUpdate = new AutoUpdate(this, myConfig);
    3. ...
    4. myConfig = somethingOther;
    5. myConfig.load(foo);
    6. autoUpdate.setConfig(myConfig);

    Important: If you cancel all your tasks restart the updater task! Example:
    1. scheduler.cancelTasks(this);
    2. autoUpdate.restartMainTask();

    The configuration is inside of the class, look:
    1. /*
    2. * Configuration:
    3. *
    4. * delay = The delay this class checks for new updates. This time is in ticks (1 tick = 1/20 second).
    5. * ymlPrefix = A prefix added to the version string from your plugin.yml.
    6. * ymlSuffix = A suffix added to the version string from your plugin.yml.
    7. * bukkitdevPrefix = A prefix added to the version string fetched from bukkitDev.
    8. * bukkitdevSuffix = A suffix added to the version string fetched from bukkitDev.
    9. * bukitdevSlug = The bukkitDev Slug. Leave empty for autodetection (uses plugin.getName().toLowerCase()).
    10. * COLOR_INFO = The default text color.
    11. * COLOR_OK = The text color for positive messages.
    12. * COLOR_ERROR = The text color for error messages.
    13. */
    14. private final long delay = 72000L;
    15. private final String ymlPrefix = "v";
    16. private final String ymlSuffix = "";
    17. private final String bukkitdevPrefix = "";
    18. private final String bukkitdevSuffix = "";
    19. private String bukkitdevSlug = "";
    20. private final ChatColor COLOR_INFO = ChatColor.PURPLE;
    21. private final ChatColor COLOR_OK = ChatColor.GREEN;
    22. private final ChatColor COLOR_ERROR = ChatColor.RED;
    23. /*
    24. * End of configuration.

    Moar docs!


    Plugins using this
    Want your plugin added to the list? Just leave a reply. :)

    • v1.4: Reflecting latest changes in the bukget API.
    • v1.3: Finally made the command usable from the console.
    • v1.2: Silenced HTTP errors. - Better lock handling. - Removed some (now that bukget reads from the jar file directly) unneeded settings. - Removed the caching server. - Switched to bukgets API v2 this breaks older versions! Updating is highly recommended!
    • v1.1: Switched to JSON.simple (suggested by Comphenix). - Added debug mode (request by iKeirNez).
    • v1.0: Fixed a bug in the bug catcher. :confused: - Better JSON handling. - Splitted into a version using JSON and one using StrippedJSON. - Fixed corrupted downloads. - Added bukget caching via a server owned by Mikeambrose3 - Added automated fallback to bukget if the caching server isn't available. - Rised min. update check time to 1h. - Fixed a locking issue that can cause a endless loop in minecrafts main thread. Updating is highly recommended!
    • v0.9: Fixed a couple of bugs.
    • v0.8: Even better permission handling.
    • v0.7: Better permission handling.
    • v0.6: Made command case insensitive. - Better file handling.
    • v0.5: Better messages. - Reduced amount of messages. - Raised default update delay from 1h to 3h. - Added minimum update delay (30 minutes).
    • v0.4: Fixed typos. - Better config management. - Added enable/disable messages.
    • v0.3: Fixed JavaDoc. - Added more config options. - Fixed error handling.
    • v0.2: Better error handling.
    Where's the class now?
    v1.0: Build-in JSON - External JSON

    Want to live on the bleeding etch? Check out [​IMG]. Pull requests welcome! :)
  2. Offline


    Unfortunately This, and bukget violate the ToU for
  3. Sleaker I don't think this breaks the ToU, quotes please?
    For legal question about bukget ask their support as I'm not related to them in any way.
  4. Offline


    curse specifically prohibits use of auto-downloaders unless expressly permitted by them or released by them, that's all.

    Other than that I think there was also a precedent for the bukkit team to deny any plugin approvals that used auto-downloaders because they could be too easily manipulated for malicious purposes.
  5. Sleaker Again: Quotes please. The only part of the Curse ToS that may be against the use of this class would be this:
    But this class uses bukget to get the information, so this does not "send more request messages to the Curse servers ..." and again: I' not responsible for bukget.
    If anything this class advertises the curse network as it broadcasts the bukkitdev link of the plugin to Admins (having the autoupdate.announce permission node).

  6. Offline


    might be okay then. I dunno. I jsut stay as far away from this kind of stuff as possible. To each his own i guess.
  7. Offline


    The use of auto updaters using anything other than as the source of the download will cause the file to not be approved.

    We are working with Curse to make this aspect easier on server administrators, and plugin writers (aka, static link to latest file and an intelligent way of determining whether the latest file is different than the one existing one the server).
    FisheyLP likes this.
  8. But as far as I see none of the things I linked here uses as source. But this one here uses to download the jar file (to get the file information and the bukkitDev link it uses bukget).

    BTW: The first files using this class where approved at bukkitDev. :)
  9. Offline


    did you ever get onCommand working? or does it still use commandPreprocess?
  10. Offline


    Seems like a lot of code.

    This is what I do:
    1. boolean is_latest_version()
    2. {
    3. DocumentBuilder dbf;
    4. try
    5. {
    6. dbf = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    7. Document doc = dbf.parse("[url][/url]");
    8. XPath xpath = XPathFactory.newInstance().newXPath();
    9. String s = ((Element) xpath.evaluate("//item[1]/title", doc,
    10. XPathConstants.NODE)).getTextContent();
    11. return (s.equalsIgnoreCase(getDescription().getVersion()));
    12. }
    13. catch (Exception e) {return true;}
    14. }

    I'm sure there's something horribly wrong with it but it works fine for me :)

    God I hate this editor! Keeps adding url tags :(
  11. Still uses the PlayerCommandPreprocessEvent. Any help is welcome. :)

    coldandtired Well, have a look at "What makes this special?" (and at the video) in the first post to see why there's that much code. ;)
  12. Offline


    Sorry, I was referring to just the checking for updates section :)
  13. Offline


    Thanks for the links, we'll clean those up. Getting the file info from and downloading the files from is allowed in an autoupdater.
  14. You're confusing me. I put a lot of work into this project and it starts spreading into plugins and even being loved by stuff, but I don't know if it's allowed or not.

    Again: it uses bukget to get informations about the latest file (it fetches<bukkitDevSlug>/latest ) and informs the user if there's a new update. If the user decides to update it downloads the file directly from I think I could change the class to get the file information directly, but then:
    1. The class would bring a lot of traffic to, possibly breaking the ToS:
    2. I can't find a page at containing all the information the class needs. The rss feed coldandtired notified about would be a good start, but the class could only take two (bukkitdev link + new version) of the 4 needed informations from it, while it had to throw a lot of useless data into the trash.

    tyzoid I had a deeper look into dynamic command registration now: It seems like you basically need to register your new command at the CraftServer.
    Creating the new command is doable with some reflection:
    1. Constructor<PluginCommand> c = PluginCommand.class.getConstructor(String.class, Plugin.class);
    2. c.setAccessible(true);
    3. PluginCommand cmd = c.newInstance("update", plugin);
    4. c.setAccessible(false);

    But to register it we need to hook into CB code and I don't want to force users of this class to figure around with CB:
    1. CraftServer cs = (CraftServer)plugin.getServer();
    2. SimpleCommandMap scm = cs.getCommandMap();
    3. scm.register("update", cmd);

  15. Offline


    The method you use is the only way we allow it. Bukget is the API provided for, but it is not meant for extensive public consumption that an auto updater would cause. This is why you may break the ToS by creating an auto updater , and why we have not encouraged their use before.

    Even with Bukget, we understand its not a fully realized API. Bukget was never meant to be utilized as extensively as it is after the community found it, and the load on that API is quite extensive. We are working with Curse to provide a better method to allow auto update plugins, with a more robust API. You cannot find a page on containing all the information because one does not exist at this time.

    To adequately do an auto updater, the program must check for a new file, understand if the file is newer than the one on the server already, get the link and stage it in the plugin/update folder for the next restart/reload. We need a better API to provide those abilities, and we will continue to work with Curse to get one.
  16. I have an important question to everybody:
    Should this class depend on the JSON library or use a inlined, stripped-down version?

    I ask this cause first I tried to provide a stipped down JSON lib for use with this project: but it didn't seem to work that easy.
    But I could easily in-line this stripped down version. That's how it looks:
    As you see it's still a lot of code, but way less than the original lib. It allows us to do what we need to do, but nothing more. Also some internals where tuned for faster execution speed (still a WIP).

    Pros for in-lining:
    • Fast, (relative) lightweigt JSON handling.
    • No dependencies.
    • Its easy to cut out the inlined version and use the default lib (just remove the in-lined code and import the then missing classes from org.json).
    • - Duplicated JSON code (needed ram = JSON code * plugins using this class).

    What do you think?
  17. Offline


    How big would each JSON string be?

    if it's nothing more than 5000 characters, I'd say go for it.
  18. TnT So bukget == bukkitdev. I (and I think others, too) Didn't know that. :)
    If that means that the Curse ToS is valid for bukget then you're right: This class may break it. But at bukget there's no information that it's associated with Curse, nor do they tell me I have to accept the Curse ToS at any point. Also this API is not designed to be watched by a web browser, so the Curse ToS doesn't make much sense here (may even be invalid).

    As you seem to be involved in bukget/bukkitdevAPI: Could you ask if it's okay to use bukget for this class till the better API is ready? As you see it tries to fetch as little data as possible and if needed I could rise the default (and the min.) update check time more future (currently: Default: every 3 h (changeable by the dev using this class), min: 30 mins).
    This question is important for me as if I'm not allowed to use bukget for this I will stop doing so, but till now nobody tried to stop me... :confused:
  19. Offline


    A possible solution to your extra traffic delemma, why not just cache the results on a seperate domain (run by you, or someone who would donate space).

    If you cache them for ~4 hours, then high-volume traffic created by this plugin would be diverted to the other server, and hence, not put much additional load on Curse's servers.
  20. Around that size:
    But was has the data to be parsed to do with the size/efficiency/... of the code doing it, especially if the stripped version is directly based on top of the org.json one? ^^

    Great idea! Will have a talk to my hoster if he would allow that. :)
  21. Offline


    Why not cache for 24 hours? We're talking about plugin updates, not heart transplants.

    V10lator Auto Updates are becoming a large concern lately, both from an API perspective, and a community safety one, hence not hearing anything about it until now. We are forced to take more interest in the process now, as more people discover this API, or attempt to create an auto updater by other means.
    V10lator likes this.
  22. Offline


    Well, the idea is that it would give reasonably recent results without causing lag.

    With 24 hours, server administrators would have to wait an entire day (or plugin authors to see if it worked) to get the update.
  23. Offline


    I was writing a plugin just like this. Ran into some issues with locked files. I could overrite the plugin in the plugins folder but (FULLY) unlocking and unloading it was the true problem. I kept pulling an IOException and could get bukkit to let go of the jar.
    But a little help I made up some methods of checking for an update.
    Show Spoiler

    public void updateCheck() throws MalformedURLException, IOException, ParseException, UnknownDependencyException, InvalidPluginException, InvalidDescriptionException{
    Plugin[] plugins = this.getServer().getPluginManager().getPlugins();
    for(Plugin plugin: plugins){
    final String name = plugin.getName();
    boolean update = updateExists(plugin);
    if(update && !plugin.equals(this) && !this.plugins.contains(plugin.getName())){
    final File actual = new File("plugins/", name+".jar");
    //Changed object
    if(actual != null){
    this.getServer().getScheduler().scheduleAsyncDelayedTask(this,new Runnable(){
    public void run() {
    PluginManager man = Bukkit.getServer().getPluginManager();
    try {
    } catch (UnknownDependencyException e) {
    // TODO Auto-generated catch block
    } catch (InvalidPluginException e) {
    // TODO Auto-generated catch block
    } catch (InvalidDescriptionException e) {
    // TODO Auto-generated catch block
    }, this.getServer().getConnectionThrottle());
    private boolean updateExists(Plugin plugin) throws IOException, ParseException, MalformedURLException{
    URL url = new URL(""+plugin.getName().toLowerCase()+"s");
    URLConnection connection = url.openConnection();
    connection.addRequestProperty("Referer","http://" + Bukkit.getServer().getIp());
    String line;
    StringBuilder builder = new StringBuilder();
    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
    while((line = reader.readLine()) != null) {
    JSONParser parser = new JSONParser();
    return false;
    Object obj = parser.parse(builder.toString());
    JSONObject jsonObject = (JSONObject) obj;
    JSONArray version = (JSONArray) jsonObject.get("versions");
    this.getLogger().log(Level.INFO, version.toString());
    if(version.size() < 1) return false;
    JSONObject latest = (JSONObject) version.get(1);
    long date = Long.parseLong((latest.get("date").toString()));
    File actual = new File("plugins/", plugin.getName());
    if(Math.abs(date - actual.lastModified()) >  172800000){
    return true;
    return false;
    public void wget(String name) throws MalformedURLException, IOException{
    BufferedInputStream in = new BufferedInputStream(new URL(""+name.toLowerCase()+"/latest/download").openStream());
    FileOutputStream fos = new FileOutputStream("plugins/"+name+".jar", false);
    BufferedOutputStream bout = new BufferedOutputStream(fos,1024);
    byte[] data = new byte[1024];
    int x=0;
  24. Offline


    You could try this code, it updates the plugin, and does not require a restart.
    Show Spoiler
    public String checkUpdate(boolean forceUpdate) {
      String link = "";
      try {
        URL rss = new URL("" + this.getDescription().getName() + "/files.rss");
        String search = "version-";
        ReadableByteChannel rbc = Channels.newChannel(rss.openStream());
        File outputFile = new File(this.getDataFolder(), this.getDescription().getName() + ".tmp");
        FileOutputStream fos = new FileOutputStream(outputFile);
        fos.getChannel().transferFrom(rbc, 0, 1 << 24);
        Scanner s = new Scanner(outputFile);
        int line = 0;
        while(s.hasNextLine()) {
          link = s.nextLine();
          if(link.contains("<link>")) {
          if(line == 2) {
            link = link.substring(link.indexOf(">") + 1);
            link = link.substring(0, link.indexOf("<"));
        String newVersion = link.substring(link.lastIndexOf(search) +, link.lastIndexOf("/")).replace("-", ".");
        String currentVersion = this.getDescription().getVersion();
        if(!newVersion.equals(currentVersion)) {
          if(forceUpdate) {
      catch (IOException e) {
      return link;
    public String downloadSite(String url) {
      String link = "";
      try {
        URL website = new URL(url);
        ReadableByteChannel rbc = Channels.newChannel(website.openStream());
        File outputFile = new File(this.getDataFolder(), this.getDescription().getName() + ".tmp");
        FileOutputStream fos = new FileOutputStream(outputFile);
        fos.getChannel().transferFrom(rbc, 0, 1 << 24);
        Scanner s = new Scanner(outputFile);
        while(s.hasNextLine()) {
          link = s.nextLine();
          if(link.contains("user-action-download")) {
            link = link.substring(link.indexOf("href"));
            link = link.substring(link.indexOf("\"") + 1, link.lastIndexOf("\""));
      catch (IOException e) {
      return link;
    public void downloadJar(String url) {
      try {
        URL website = new URL(url);
        ReadableByteChannel rbc = Channels.newChannel(website.openStream());
        File outputFile = new File("plugins", this.getDescription().getName() + ".jar");
        FileOutputStream fos = new FileOutputStream(outputFile);
        fos.getChannel().transferFrom(rbc, 0, 1 << 24);
        fos.close();"Reloading" + this.getDescription().getName() + " v" + this.getDescription().getVersion());
      catch(Exception e) {
    private void unloadPlugin(final String pluginName) throws NoSuchFieldException, IllegalAccessException {
      PluginManager manager = getServer().getPluginManager();
      SimplePluginManager spm = (SimplePluginManager) manager;
      SimpleCommandMap commandMap = null;
      List<Plugin> plugins = null;
      Map<String, Plugin> lookupNames = null;
      Map<String, Command> knownCommands = null;
      Map<Event, SortedSet<RegisteredListener>> listeners = null;
      boolean reloadlisteners = true;
      if(spm != null) {
        Field pluginsField = spm.getClass().getDeclaredField("plugins");
        plugins = (List<Plugin>) pluginsField.get(spm);
        Field lookupNamesField = spm.getClass().getDeclaredField("lookupNames");
        lookupNames = (Map<String, Plugin>) lookupNamesField.get(spm);
        try {
          Field listenersField = spm.getClass().getDeclaredField("listeners");
          listeners = (Map<Event, SortedSet<RegisteredListener>>) listenersField.get(spm);
        catch (Exception e) {
          reloadlisteners = false;
        Field commandMapField = spm.getClass().getDeclaredField("commandMap");
        commandMap = (SimpleCommandMap) commandMapField.get(spm);
        Field knownCommandsField = commandMap.getClass().getDeclaredField("knownCommands");
        knownCommands = (Map<String, Command>) knownCommandsField.get(commandMap);
      for(Plugin pl : getServer().getPluginManager().getPlugins()) {
        if(pl.getDescription().getName().equalsIgnoreCase(pluginName)) {
          if(plugins != null && plugins.contains(pl)) {
          if(lookupNames != null && lookupNames.containsKey(pluginName)) {
          if(listeners != null && reloadlisteners) {
            for(SortedSet<RegisteredListener> set : listeners.values()) {
              for(Iterator<RegisteredListener> it = set.iterator(); it.hasNext();) {
                RegisteredListener value =;
                if(value.getPlugin() == pl) {
          if(commandMap != null) {
            for(Iterator<Map.Entry<String, Command>> it = knownCommands.entrySet().iterator(); it.hasNext();) {
              Map.Entry<String, Command> entry =;
              if(entry.getValue() instanceof PluginCommand) {
                PluginCommand c = (PluginCommand) entry.getValue();
                if(c.getPlugin() == pl) {
    public void loadPlugin(final String pluginName) throws InvalidPluginException, InvalidDescriptionException {
      PluginManager manager = getServer().getPluginManager();
      Plugin plugin = manager.loadPlugin(new File("plugins", pluginName + ".jar"));
      if(plugin == null) {

    The only thing is, like V10lator, if we can't use something like Bukget, then you'll end up with a lot of useless data being downloaded.
    Deathmarine likes this.
  25. TnT Well, 24 hours seems a bit high, especially as bukget caches, too... ;)
    It's planned to set up a caching server at (URL may change later) which caches for 6 hours at the beginning. I want to fine-tune the caching time based at bandwith (to bukget) when everything is running.

    The caching is done via htaccess, php and mysql. As the page at the URL linked above isn't ready yet you can have a preview look here:
    just put the bukkitdev slug at the end, example:

    Important notice for everyone: This caching mechanism is meant to be used by AutoUpdate only! Do not use it for anything other!

    Credits for hosting the caching site goes to Mikeambrose3
    MikeA likes this.
  26. v1.0 is finally released and the caching server is up and running. Thanks again to Mikeambrose3 for the hosting.

    It is highly recommended to update to v1.0 as there are important bug fixes! All other versions are no longer supported!
  27. Okay I am using this in my plugin but the problem is that we upload our plugin in a ZIP file, do you think there would be anyway for this to unzip the zip and use the jar file in there? The name and directory of the jar file would have to be specified though because there are multiple files in the zip.

    Thanks :)
  28. iKeirNez Zip files aren't supported atm and I don't think I'll implement them as using zip files is bad anyway. Why don't you include the content of your zip in your jar file and extract it if needed?
  29. Ok I will try and figure something out.
Thread Status:
Not open for further replies.

Share This Page