Package freenet.pluginmanager

Source Code of freenet.pluginmanager.PluginManager

/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.pluginmanager;

import static java.util.concurrent.TimeUnit.MINUTES;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.JarException;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipException;

import org.tanukisoftware.wrapper.WrapperManager;

import freenet.client.HighLevelSimpleClient;
import freenet.clients.fcp.ClientPut;
import freenet.clients.http.QueueToadlet;
import freenet.clients.http.PageMaker.THEME;
import freenet.clients.http.Toadlet;
import freenet.config.Config;
import freenet.config.InvalidConfigValueException;
import freenet.config.NodeNeedRestartException;
import freenet.config.SubConfig;
import freenet.crypt.SHA256;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.l10n.BaseL10n.LANGUAGE;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.RequestClient;
import freenet.node.RequestStarter;
import freenet.node.useralerts.AbstractUserAlert;
import freenet.node.useralerts.UserAlert;
import freenet.pluginmanager.OfficialPlugins.OfficialPluginDescription;
import freenet.support.HTMLNode;
import freenet.support.HexUtil;
import freenet.support.JarClassLoader;
import freenet.support.Logger;
import freenet.support.SerialExecutor;
import freenet.support.Ticker;
import freenet.support.Logger.LogLevel;
import freenet.support.api.BooleanCallback;
import freenet.support.api.HTTPRequest;
import freenet.support.api.StringArrCallback;
import freenet.support.io.Closer;
import freenet.support.io.FileUtil;
import freenet.support.io.NativeThread;

public class PluginManager {

  /*
   *
   * TODO: Synchronize
   * TODO: Synchronize
   * TODO: Synchronize
   * TODO: Synchronize
   * TODO: Synchronize
   *
   */
  private final HashMap<String, FredPlugin> toadletList;

  /* All currently starting plugins. */
  private final OfficialPlugins officialPlugins = new OfficialPlugins();
  private final Set<PluginProgress> startingPlugins = new HashSet<PluginProgress>();
  private final ArrayList<PluginInfoWrapper> pluginWrappers;
  private final HashMap<String, PluginLoadFailedUserAlert> pluginsFailedLoad;
  final Node node;
  private final NodeClientCore core;
  SubConfig pmconfig;
  private boolean logMINOR;
  private boolean logDEBUG;
  private final HighLevelSimpleClient client;

  private static PluginManager selfinstance = null;

  private THEME fproxyTheme;

  private final SerialExecutor executor;

  private boolean alwaysLoadOfficialPluginsFromCentralServer = false;

  static final short PRIO = RequestStarter.INTERACTIVE_PRIORITY_CLASS;

  public PluginManager(Node node, int lastVersion) {
    logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
    logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
    // config

    toadletList = new HashMap<String, FredPlugin>();
    pluginWrappers = new ArrayList<PluginInfoWrapper>();
    pluginsFailedLoad = new HashMap<String, PluginLoadFailedUserAlert>();
    this.node = node;
    this.core = node.clientCore;

    if(logMINOR)
      Logger.minor(this, "Starting Plugin Manager");

    if(logDEBUG)
      Logger.debug(this, "Initialize Plugin Manager config");

    client = core.makeClient(PRIO, true, false);

    // callback executor
    executor = new SerialExecutor(NativeThread.NORM_PRIORITY);
    executor.start(node.executor, "PM callback executor");

    pmconfig = new SubConfig("pluginmanager", node.config);
//    pmconfig.register("configfile", "fplugins.ini", 9, true, true, "PluginConfig.configFile", "PluginConfig.configFileLong",
//        new StringCallback() {
//      public String get() {
//        return configFile;
//      }
//
//      public void set(String val) throws InvalidConfigValueException {
//        configFile = val;
//      }
//    });
//    configFile = pmconfig.getString("configfile");
//    pmconfig.register("installdir", "fplugins", 9, true, true, "PluginConfig.installDir", "PluginConfig.installDirLong",
//        new StringCallback() {
//      public String get() {
//        return installDir;
//        //return getConfigLoadString();
//      }
//
//      public void set(String val) throws InvalidConfigValueException {
//        installDir = val;
//        //if(storeDir.equals(new File(val))) return;
//        // FIXME
//        //throw new InvalidConfigValueException(NodeL10n.getBase().getString("PluginManager.cannotSetOnceLoaded"));
//      }
//    });
//    installDir = pmconfig.getString("installdir");

    // Start plugins in the config
    pmconfig.register("loadplugin", null, 0, true, false, "PluginManager.loadedOnStartup", "PluginManager.loadedOnStartupLong",
      new StringArrCallback() {

        @Override
        public String[] get() {
          return getConfigLoadString();
        }

        @Override
        public void set(String[] val) throws InvalidConfigValueException {
          //if(storeDir.equals(new File(val))) return;
          // FIXME
          throw new InvalidConfigValueException(NodeL10n.getBase().getString("PluginManager.cannotSetOnceLoaded"));
        }

      @Override
        public boolean isReadOnly() {
          return true;
        }
      });

    toStart = pmconfig.getStringArr("loadplugin");

    if(lastVersion < 1237 && contains(toStart, "XMLLibrarian") && !contains(toStart, "Library")) {
      toStart = Arrays.copyOf(toStart, toStart.length+1);
      toStart[toStart.length-1] = "Library";
      System.err.println("Loading Library plugin, replaces XMLLibrarian, when upgrading from pre-1237");
    }

    if(contains(toStart, "KeyExplorer")) {
      for(int i=0;i<toStart.length;i++) {
        if("KeyExplorer".equals(toStart[i]))
          toStart[i] = "KeyUtils";
      }
      System.err.println("KeyExplorer plugin renamed to KeyUtils");
    }

    // This should default to false. Even though we use SSL, a wiretapper may be able to tell which
    // plugin is being loaded, and correlate that with identity creation; plus of course they can see
    // that somebody is using Freenet.
    pmconfig.register("alwaysLoadOfficialPluginsFromCentralServer", false, 0, false, false, "PluginManager.alwaysLoadPluginsFromCentralServer", "PluginManager.alwaysLoadPluginsFromCentralServerLong", new BooleanCallback() {

      @Override
      public Boolean get() {
        return alwaysLoadOfficialPluginsFromCentralServer;
      }

      @Override
      public void set(Boolean val) throws InvalidConfigValueException, NodeNeedRestartException {
        alwaysLoadOfficialPluginsFromCentralServer = val;
      }

    });

    alwaysLoadOfficialPluginsFromCentralServer = pmconfig.getBoolean("alwaysLoadOfficialPluginsFromCentralServer");
    if(node.lastVersion <= 1437)
      // Overwrite this setting, since it will have been set by the old callback and then written as it's not default.
      // FIXME remove back compatibility code.
      alwaysLoadOfficialPluginsFromCentralServer = false;

    pmconfig.finishedInitialization();

    fproxyTheme = THEME.themeFromName(node.config.get("fproxy").getString("css"));
    selfinstance = this;
  }

  private boolean contains(String[] array, String string) {
    for(String s : array)
      if(string.equals(s)) return true;
    return false;
  }

  private boolean started;
  private boolean stopping;
  private String[] toStart;

  public void start(Config config) {
    if(toStart != null)
      for(final String name : toStart) {
          core.getExecutor().execute(new Runnable() {

                    @Override
                    public void run() {
                        startPluginAuto(name, false);
                    }
             
          });
      }
    synchronized(pluginWrappers) {
      started = true;
      toStart = null;
    }
  }

  public void stop(long maxWaitTime) {
    // Stop loading plugins.
    ArrayList<PluginProgress> matches = new ArrayList<PluginProgress>();
    synchronized(this) {
      stopping = true;
      for(Iterator<PluginProgress> i = startingPlugins.iterator();i.hasNext();) {
        PluginProgress progress = i.next();
        matches.add(progress);
        i.remove();
      }
    }
    for(PluginProgress progress : matches) {
      progress.kill();
    }
    // Stop already loaded plugins.
    ArrayList<PluginInfoWrapper> wrappers;
    synchronized(pluginWrappers) {
      wrappers = new ArrayList<PluginInfoWrapper>(pluginWrappers);
    }
    for(PluginInfoWrapper pi : wrappers)
      pi.startShutdownPlugin(this, false);
    long now = System.currentTimeMillis();
    long deadline = now + maxWaitTime;
    while(true) {
      int delta = (int) (deadline - now);
      if(delta <= 0) {
        String list = pluginList(wrappers);
        Logger.error(this, "Plugins still shutting down at timeout:\n"+list);
        System.err.println("Plugins still shutting down at timeout:\n"+list);
      } else {
        for(Iterator<PluginInfoWrapper> it = wrappers.listIterator();it.hasNext();) {
          PluginInfoWrapper pi = it.next();
          System.out.println("Waiting for plugin to finish shutting down: "+pi.getFilename());
          if(pi.finishShutdownPlugin(this, delta, false)) {
            it.remove();
          }
        }
        if(wrappers.isEmpty()) {
          Logger.normal(this, "All plugins unloaded");
          System.out.println("All plugins unloaded");
          return;
        }
        String list = pluginList(wrappers);
        Logger.error(this, "Plugins still shutting down:\n"+list);
        System.err.println("Plugins still shutting down:\n"+list);
      }
    }
  }

  private static String pluginList(ArrayList<PluginInfoWrapper> wrappers) {
    StringBuffer sb = new StringBuffer();
    for(PluginInfoWrapper pi : wrappers) {
      sb.append(pi.getFilename());
      sb.append('\n');
    }
    return sb.toString();
  }

  private String[] getConfigLoadString() {
    ArrayList<String> v = new ArrayList<String>();

    synchronized(pluginWrappers) {
      if(!started) return toStart;
      for(PluginInfoWrapper pi : pluginWrappers) {
        v.add(pi.getFilename());
      }
      for(String s : pluginsFailedLoad.keySet()) {
        v.add(s);
      }
    }

    return v.toArray(new String[v.size()]);
  }

  /**
   * Returns a set of all currently starting plugins.
   *
   * @return All currently starting plugins
   */
  public Set<PluginProgress> getStartingPlugins() {
    synchronized(startingPlugins) {
      return new HashSet<PluginProgress>(startingPlugins);
    }
  }
  // try to guess around...
  public PluginInfoWrapper startPluginAuto(final String pluginname, boolean store) {

    OfficialPluginDescription desc;
    if((desc = isOfficialPlugin(pluginname)) != null) {
      return startPluginOfficial(pluginname, store, desc, false, false);
    }

    try {
      new FreenetURI(pluginname); // test for MalformedURLException
      return startPluginFreenet(pluginname, store);
    } catch(MalformedURLException e) {
      // not a freenet key
    }

    File[] roots = File.listRoots();
    for(File f : roots) {
      if(pluginname.startsWith(f.getName()) && new File(pluginname).exists()) {
        return startPluginFile(pluginname, store);
      }
    }

    return startPluginURL(pluginname, store);
  }

  public PluginInfoWrapper startPluginOfficial(final String pluginname, boolean store, boolean force, boolean forceHTTPS) {
    return startPluginOfficial(pluginname, store, officialPlugins.get(pluginname), force, forceHTTPS);
  }

  public PluginInfoWrapper startPluginOfficial(final String pluginname, boolean store, OfficialPluginDescription desc, boolean force, boolean forceHTTPS) {
    if((alwaysLoadOfficialPluginsFromCentralServer && !force)|| force && forceHTTPS) {
      return realStartPlugin(new PluginDownLoaderOfficialHTTPS(), pluginname, store, false);
    } else {
      return realStartPlugin(new PluginDownLoaderOfficialFreenet(client, node, false), pluginname, store, false);
    }
  }

  public PluginInfoWrapper startPluginFile(final String filename, boolean store) {
    return realStartPlugin(new PluginDownLoaderFile(), filename, store, false);
  }

  public PluginInfoWrapper startPluginURL(final String filename, boolean store) {
    return realStartPlugin(new PluginDownLoaderURL(), filename, store, false);
  }

  public PluginInfoWrapper startPluginFreenet(final String filename, boolean store) {
    return realStartPlugin(new PluginDownLoaderFreenet(client, node, false), filename, store, false);
  }

  private PluginInfoWrapper realStartPlugin(final PluginDownLoader<?> pdl, final String filename, final boolean store, boolean ignoreOld) {
    if(filename.trim().length() == 0)
      return null;
    final PluginProgress pluginProgress = new PluginProgress(filename, pdl);
    synchronized(startingPlugins) {
      startingPlugins.add(pluginProgress);
    }
    Logger.normal(this, "Loading plugin: " + filename);
    FredPlugin plug;
    PluginInfoWrapper pi = null;
    try {
      plug = loadPlugin(pdl, filename, pluginProgress, ignoreOld);
      if (plug == null)
        return null; // Already loaded
      pluginProgress.setProgress(PluginProgress.PROGRESS_STATE.STARTING);
      pi = new PluginInfoWrapper(node, plug, filename, pdl.isOfficialPluginLoader());
      PluginHandler.startPlugin(PluginManager.this, pi);
      synchronized (pluginWrappers) {
        pluginWrappers.add(pi);
        pluginsFailedLoad.remove(filename);
      }
      Logger.normal(this, "Plugin loaded: " + filename);
    } catch (PluginNotFoundException e) {
      Logger.normal(this, "Loading plugin failed (" + filename + ')', e);
      boolean stillTrying = false;
      if(pdl instanceof PluginDownLoaderOfficialFreenet) {
        PluginDownLoaderOfficialFreenet downloader = (PluginDownLoaderOfficialFreenet) pdl;
        if(!(downloader.fatalFailure() || downloader.desperate || twoCopiesInStartingPlugins(filename))) {
          // Retry forever...
          final PluginDownLoaderOfficialFreenet retry =
            new PluginDownLoaderOfficialFreenet(client, node, true);
          stillTrying = true;
          node.getTicker().queueTimedJob(new Runnable() {

            @Override
            public void run() {
              realStartPlugin(retry, filename, store, true);
            }

          }, 0);
        }
      } else if(pdl instanceof PluginDownLoaderFreenet) {
        PluginDownLoaderFreenet downloader = (PluginDownLoaderFreenet) pdl;
        if(!(downloader.fatalFailure() || downloader.desperate || twoCopiesInStartingPlugins(filename))) {
          // Retry forever...
          final PluginDownLoaderFreenet retry =
            new PluginDownLoaderFreenet(client, node, true);
          stillTrying = true;
          node.getTicker().queueTimedJob(new Runnable() {

            @Override
            public void run() {
              realStartPlugin(retry, filename, store, true);
            }

          }, 0);
        }
      }
      PluginLoadFailedUserAlert newAlert =
        new PluginLoadFailedUserAlert(filename,
            pdl instanceof PluginDownLoaderOfficialHTTPS || pdl instanceof PluginDownLoaderOfficialFreenet, pdl instanceof PluginDownLoaderOfficialFreenet, stillTrying, e);
      PluginLoadFailedUserAlert oldAlert = null;
      synchronized (pluginWrappers) {
        oldAlert = pluginsFailedLoad.put(filename, newAlert);
      }
      core.alerts.register(newAlert);
      core.alerts.unregister(oldAlert);
    } catch (UnsupportedClassVersionError e) {
      Logger.error(this, "Could not load plugin " + filename + " : " + e,
          e);
      System.err.println("Could not load plugin " + filename + " : " + e);
      e.printStackTrace();
      System.err.println("Plugin " + filename + " appears to require a later JVM");
      Logger.error(this, "Plugin " + filename + " appears to require a later JVM");
      PluginLoadFailedUserAlert newAlert =
        new PluginLoadFailedUserAlert(filename, pdl instanceof PluginDownLoaderOfficialHTTPS || pdl instanceof PluginDownLoaderOfficialFreenet, pdl instanceof PluginDownLoaderOfficialFreenet, false, l10n("pluginReqNewerJVMTitle", "name", filename));
      PluginLoadFailedUserAlert oldAlert = null;
      synchronized (pluginWrappers) {
        oldAlert = pluginsFailedLoad.put(filename, newAlert);
      }
      core.alerts.register(newAlert);
      core.alerts.unregister(oldAlert);
    } catch (Throwable e) {
      Logger.error(this, "Could not load plugin " + filename + " : " + e, e);
      System.err.println("Could not load plugin " + filename + " : " + e);
      e.printStackTrace();
      System.err.println("Plugin "+filename+" is broken, but we want to retry after next startup");
      Logger.error(this, "Plugin "+filename+" is broken, but we want to retry after next startup");
      PluginLoadFailedUserAlert newAlert =
        new PluginLoadFailedUserAlert(filename, pdl instanceof PluginDownLoaderOfficialHTTPS || pdl instanceof PluginDownLoaderOfficialFreenet, pdl instanceof PluginDownLoaderOfficialFreenet, false, e);
      PluginLoadFailedUserAlert oldAlert = null;
      synchronized (pluginWrappers) {
        oldAlert = pluginsFailedLoad.put(filename, newAlert);
      }
      core.alerts.register(newAlert);
      core.alerts.unregister(oldAlert);
    } finally {
      synchronized (startingPlugins) {
        startingPlugins.remove(pluginProgress);
      }
    }
    /* try not to destroy the config. */
    synchronized(this) {
      if (store)
        core.storeConfig();
    }
    if(pi != null)
      node.nodeUpdater.startPluginUpdater(filename);
    return pi;
  }

  private synchronized boolean twoCopiesInStartingPlugins(String filename) {
    int count = 0;
    for(PluginProgress progress : startingPlugins) {
      if(filename.equals(progress.name)) {
        count++;
        if(count == 2) return true;
      }
    }
    return false;
  }

  class PluginLoadFailedUserAlert extends AbstractUserAlert {

    final String filename;
    final String message;
    final boolean official;
    final boolean officialFromFreenet;
    final boolean stillTryingOverFreenet;

    public PluginLoadFailedUserAlert(String filename, boolean official, boolean officialFromFreenet, boolean stillTryingOverFreenet, String message) {
      this.filename = filename;
      this.official = official;
      this.message = message;
      this.officialFromFreenet = officialFromFreenet;
      this.stillTryingOverFreenet = stillTryingOverFreenet;
    }

    public PluginLoadFailedUserAlert(String filename, boolean official, boolean officialFromFreenet, boolean stillTryingOverFreenet, Throwable e) {
      this.filename = filename;
      this.official = official;
      this.stillTryingOverFreenet = stillTryingOverFreenet;
      String msg;
      if(e instanceof PluginNotFoundException)
        msg = e.getMessage();
      else
        // If it's something wierd, we need to know what it is.
        msg = e.getClass() + ": " + e.getMessage();
      if(msg == null) msg = e.toString();
      this.message = msg;
      this.officialFromFreenet = officialFromFreenet;
    }

    @Override
    public String dismissButtonText() {
      return l10n("deleteFailedPluginButton");
    }

    @Override
    public void onDismiss() {
      synchronized(pluginWrappers) {
        pluginsFailedLoad.remove(filename);
      }
      node.executor.execute(new Runnable() {

        @Override
        public void run() {
          cancelRunningLoads(filename, null);
        }

      });
    }

    @Override
    public String anchor() {
      return "pluginfailed:"+filename;
    }

    @Override
    public HTMLNode getHTMLText() {
      HTMLNode div = new HTMLNode("div");
      HTMLNode p = div.addChild("p");
      p.addChild("#", l10n("pluginLoadingFailedWithMessage", new String[] { "name", "message" }, new String[] { filename, message }));
      if(stillTryingOverFreenet) {
        div.addChild("p", l10n("pluginLoadingFailedStillTryingOverFreenet"));
      }

      if(official) {
        p = div.addChild("p");
        if(officialFromFreenet)
          p.addChild("#", l10n("officialPluginLoadFailedSuggestTryAgainFreenet"));
        else
          p.addChild("#", l10n("officialPluginLoadFailedSuggestTryAgainHTTPS"));

        HTMLNode reloadForm = div.addChild("form", new String[] { "action", "method" }, new String[] { "/plugins/", "post" });
        reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "formPassword", node.clientCore.formPassword });
        reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "plugin-name", filename });
        reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "pluginSource", "https" });
        reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "submit-official", l10n("officialPluginLoadFailedTryAgain") });

        if(!stillTryingOverFreenet) {
          reloadForm = div.addChild("form", new String[] { "action", "method" }, new String[] { "/plugins/", "post" });
          reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "formPassword", node.clientCore.formPassword });
          reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "plugin-name", filename });
          reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "pluginSource", "freenet" });
          reloadForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "submit-official", l10n("officialPluginLoadFailedTryAgainFreenet") });
        }
      }

      return div;
    }

    @Override
    public short getPriorityClass() {
      return UserAlert.ERROR;
    }

    @Override
    public String getShortText() {
      return l10n("pluginLoadingFailedShort", "name", filename);
    }

    @Override
    public String getText() {
      return l10n("pluginLoadingFailedWithMessage", new String[] { "name", "message" }, new String[] { filename, message });
    }

    @Override
    public String getTitle() {
      return l10n("pluginLoadingFailedTitle");
    }

    @Override
    public boolean isEventNotification() {
      return false;
    }

    @Override
    public boolean isValid() {
      boolean success;
      synchronized(pluginWrappers) {
        success = pluginsFailedLoad.containsKey(filename);
      }
      if(!success) {
        core.alerts.unregister(this);
      }
      return success;
    }

    @Override
    public void isValid(boolean validity) {
    }

    @Override
    public boolean shouldUnregisterOnDismiss() {
      return true;
    }

    @Override
    public boolean userCanDismiss() {
      return true;
    }


  }

  void register(PluginInfoWrapper pi) {
    FredPlugin plug = pi.getPlugin();

    // handles FProxy? If so, register
    if(pi.isPproxyPlugin())
      registerToadlet(plug);

    if(pi.isConfigurablePlugin()) {
      // Registering the toadlet with atFront=false means that
      // the node's ConfigToadlet will clobber the plugin's
      // ConfigToadlet and the page will not be visible. So it
      // must be registered with atFront=true. This means that
      // malicious plugins could try to hijack node config
      // pages, to ill effect. Let's avoid that.
      boolean pluginIsTryingToHijackNodeConfig = false;
      for(SubConfig subconfig : node.config.getConfigs()) {
        if(pi.getPluginClassName().equals(subconfig.getPrefix())) {
          pluginIsTryingToHijackNodeConfig = true;
          break;
        }
      }
      if(pluginIsTryingToHijackNodeConfig) {
        Logger.warning(this, "The plugin loaded from "+pi.getFilename()+" is attempting to hijack a node configuration page; refusing to register its ConfigToadlet");
      } else {
        Toadlet toadlet = pi.getConfigToadlet();
        core.getToadletContainer().register(toadlet, "FProxyToadlet.categoryConfig", toadlet.path(), true, "ConfigToadlet."+pi.getPluginClassName()+".label", "ConfigToadlet."+pi.getPluginClassName()+".tooltip", true, null, (FredPluginL10n)pi.getPlugin());
      }
    }

    if(pi.isIPDetectorPlugin())
      node.ipDetector.registerIPDetectorPlugin((FredPluginIPDetector) plug);
    if(pi.isPortForwardPlugin())
      node.ipDetector.registerPortForwardPlugin((FredPluginPortForward) plug);
    if(pi.isBandwidthIndicator())
      node.ipDetector.registerBandwidthIndicatorPlugin((FredPluginBandwidthIndicator) plug);
  }

  public void cancelRunningLoads(String filename, PluginProgress exceptFor) {
    Logger.normal(this, "Cancelling loads for plugin "+filename);
    ArrayList<PluginProgress> matches = null;
    synchronized(this) {
      for(Iterator<PluginProgress> i = startingPlugins.iterator();i.hasNext();) {
        PluginProgress progress = i.next();
        if(progress == exceptFor) continue;
        if(!filename.equals(progress.name)) continue;
        if(matches == null) matches = new ArrayList<PluginProgress>();
        matches.add(progress);
        i.remove();
      }
    }
    if(matches == null) return;
    for(PluginProgress progress : matches) {
      progress.kill();
    }
  }

  /**
   * Returns the translation of the given key, prefixed by the short name of
   * the current class.
   *
   * @param key
   *            The key to fetch
   * @return The translation
   */
  static String l10n(String key) {
    return NodeL10n.getBase().getString("PluginManager." + key);
  }

  private static String l10n(String key, String pattern, String value) {
    return NodeL10n.getBase().getString("PluginManager." + key, pattern, value);
  }

  /**
   * Returns the translation of the given key, replacing each occurence of
   * <code>${<em>pattern</em>}</code> with <code>value</code>.
   *
   * @param key
   *            The key to fetch
   * @param patterns
   *            The patterns to replace
   * @param values
   *            The values to substitute
   * @return The translation
   */
  private String l10n(String key, String[] patterns, String[] values) {
    return NodeL10n.getBase().getString("PluginManager." + key, patterns, values);
  }

  private void registerToadlet(FredPlugin pl) {
    //toadletList.put(e.getStackTrace()[1].getClass().toString(), pl);
    synchronized(toadletList) {
      toadletList.put(pl.getClass().getName(), pl);
    }
    Logger.normal(this, "Added HTTP handler for /plugins/" + pl.getClass().getName() + '/');
  }

  /**
   * Remove a plugin from the plugin list.
   */
  public void removePlugin(PluginInfoWrapper pi) {
    synchronized(pluginWrappers) {
      if((!stopping) && !pluginWrappers.remove(pi))
        return;
    }
    core.storeConfig();
  }

  /**
   * Removes the cached copy of the given plugin from the plugins/ directory.
   *
   * @param pluginSpecification
   *            The plugin specification
   */
  public void removeCachedCopy(String pluginSpecification) {
    if(pluginSpecification == null) {
      // Will be null if the file for a given plugin can't be found, eg. if it has already been
      // removed. Ignore it since the file isn't there anyway
      Logger.warning(this, "Can't remove null from cache. Ignoring");
      return;
    }

    int lastSlash = pluginSpecification.lastIndexOf('/');
    String pluginFilename;
    if(lastSlash == -1)
      /* Windows, maybe? */
      lastSlash = pluginSpecification.lastIndexOf('\\');
    File pluginDirectory = node.getPluginDir();
    if(lastSlash == -1) {
      /* it's an official plugin or filename without path */
      if (pluginSpecification.toLowerCase().endsWith(".jar"))
        pluginFilename = pluginSpecification;
      else
        pluginFilename = pluginSpecification + ".jar";
    } else
      pluginFilename = pluginSpecification.substring(lastSlash + 1);
    if(logDEBUG)
      Logger.minor(this, "Delete plugin - plugname: " + pluginSpecification + " filename: " + pluginFilename, new Exception("debug"));
    File[] cachedFiles = getPreviousInstances(pluginDirectory, pluginFilename);
    for (File cachedFile : cachedFiles) {
      if (!cachedFile.delete())
        if(logMINOR) Logger.minor(this, "Can't delete file " + cachedFile);
    }
  }

  public void unregisterPluginToadlet(PluginInfoWrapper pi) {
    synchronized(toadletList) {
      try {
        toadletList.remove(pi.getPluginClassName());
        Logger.normal(this, "Removed HTTP handler for /plugins/" +
          pi.getPluginClassName() + '/', new Exception("debug"));
      } catch(Throwable ex) {
        Logger.error(this, "removing Plugin", ex);
      }
    }
  }

  public void addToadletSymlinks(PluginInfoWrapper pi) {
    synchronized(toadletList) {
      try {
        String targets[] = pi.getPluginToadletSymlinks();
        if(targets == null)
          return;

        for(String target: targets) {
          toadletList.remove(target);
          Logger.normal(this, "Removed HTTP symlink: " + target +
            " => /plugins/" + pi.getPluginClassName() + '/');
        }
      } catch(Throwable ex) {
        Logger.error(this, "removing Toadlet-link", ex);
      }
    }
  }

  public void removeToadletSymlinks(PluginInfoWrapper pi) {
    synchronized(toadletList) {
      String rm = null;
      try {
        String targets[] = pi.getPluginToadletSymlinks();
        if(targets == null)
          return;

        for(String target: targets) {
          rm = target;
          toadletList.remove(target);
          pi.removePluginToadletSymlink(target);
          Logger.normal(this, "Removed HTTP symlink: " + target +
            " => /plugins/" + pi.getPluginClassName() + '/');
        }
      } catch(Throwable ex) {
        Logger.error(this, "removing Toadlet-link: " + rm, ex);
      }
    }
  }

  public String dumpPlugins() {
    StringBuilder out = new StringBuilder();
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size(); i++) {
        PluginInfoWrapper pi = pluginWrappers.get(i);
        out.append(pi.toString());
        out.append('\n');
      }
    }
    return out.toString();
  }

  public Set<PluginInfoWrapper> getPlugins() {
    TreeSet<PluginInfoWrapper> out = new TreeSet<PluginInfoWrapper>();
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size(); i++) {
        PluginInfoWrapper pi = pluginWrappers.get(i);
        out.add(pi);
      }
    }
    return out;
  }

  /**
   * look for PluginInfo for a Plugin with given classname
   * @param plugname
   * @return the PluginInfo or null if not found
   */
  public PluginInfoWrapper getPluginInfo(String plugname) {
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size(); i++) {
        PluginInfoWrapper pi = pluginWrappers.get(i);
        if(pi.getPluginClassName().equals(plugname) || pi.getFilename().equals(plugname))
          return pi;
      }
    }
    return null;
  }

  /**
   * look for a FCPPlugin with given classname
   * @param plugname
   * @return the plugin or null if not found
   */
  public FredPluginFCP getFCPPlugin(String plugname) {
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size(); i++) {
        PluginInfoWrapper pi = pluginWrappers.get(i);
        if(pi.isFCPPlugin() && pi.getPluginClassName().equals(plugname) && !pi.isStopping())
          return (FredPluginFCP) pi.plug;
      }
    }
    return null;
  }

  /**
   * look for a Plugin with given classname
   * @param plugname
   * @return the true if not found
   */
  public boolean isPluginLoaded(String plugname) {
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size(); i++) {
        PluginInfoWrapper pi = pluginWrappers.get(i);
        if(pi.getPluginClassName().equals(plugname) || pi.getFilename().equals(plugname))
          return true;
      }
    }
    return false;
  }

  /**
   * @param plugname The plugin filename e.g. "Library" for an official plugin.
   * @return the true if not found
   */
  public boolean isPluginLoadedOrLoadingOrWantLoad(String plugname) {
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size(); i++) {
        PluginInfoWrapper pi = pluginWrappers.get(i);
        if(pi.getFilename().equals(plugname))
          return true;

      }
    }
    if(pluginsFailedLoad.containsKey(plugname)) return true;
    for(PluginProgress prog : startingPlugins)
      if(prog.name.equals(plugname)) return true;
    return false;
  }

  public String handleHTTPGet(String plugin, HTTPRequest request) throws PluginHTTPException {
    FredPlugin handler = null;
    synchronized(toadletList) {
      handler = toadletList.get(plugin);
    }
    if (!(handler instanceof FredPluginHTTP)) {
      throw new NotFoundPluginHTTPException("Plugin not loaded!", "/plugins");
    }

    ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
    ClassLoader pluginClassLoader = handler.getClass().getClassLoader();
    Thread.currentThread().setContextClassLoader(pluginClassLoader);
    try {
      return ((FredPluginHTTP) handler).handleHTTPGet(request);
    } finally {
      Thread.currentThread().setContextClassLoader(oldClassLoader);
    }
  }

  public String handleHTTPPost(String plugin, HTTPRequest request) throws PluginHTTPException {
    FredPlugin handler = null;
    synchronized(toadletList) {
      handler = toadletList.get(plugin);
    }
    if (handler == null)
      throw new NotFoundPluginHTTPException("Plugin '"+plugin+"' not found!", "/plugins");

    ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
    ClassLoader pluginClassLoader = handler.getClass().getClassLoader();
    Thread.currentThread().setContextClassLoader(pluginClassLoader);
    try {
    if(handler instanceof FredPluginHTTP)
      return ((FredPluginHTTP) handler).handleHTTPPost(request);
    } finally {
      Thread.currentThread().setContextClassLoader(oldClassLoader);
    }
    throw new NotFoundPluginHTTPException("Plugin '"+plugin+"' not found!", "/plugins");
  }

  public void killPlugin(String name, long maxWaitTime, boolean reloading) {
    PluginInfoWrapper pi = null;
    boolean found = false;
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size() && !found; i++) {
        pi = pluginWrappers.get(i);
        if(pi.getThreadName().equals(name)) {
          found = true;
          break;
        }
      }
    }
    if(found)
      pi.stopPlugin(this, maxWaitTime, reloading);
  }

  public void killPluginByFilename(String name, long maxWaitTime, boolean reloading) {
    PluginInfoWrapper pi = null;
    boolean found = false;
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size() && !found; i++) {
        pi = pluginWrappers.get(i);
        if(pi.getFilename().equals(name)) {
          found = true;
          break;
        }
      }
    }
    if(found)
      pi.stopPlugin(this, maxWaitTime, reloading);
  }

  public void killPluginByClass(String name, long maxWaitTime) {
    PluginInfoWrapper pi = null;
    boolean found = false;
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size() && !found; i++) {
        pi = pluginWrappers.get(i);
        if(pi.getPluginClassName().equals(name)) {
          found = true;
          break;
        }
      }
    }
    if(found)
      pi.stopPlugin(this, maxWaitTime, false);
  }

  public void killPlugin(FredPlugin plugin, long maxWaitTime) {
    PluginInfoWrapper pi = null;
    boolean found = false;
    synchronized(pluginWrappers) {
      for(int i = 0; i < pluginWrappers.size() && !found; i++) {
        pi = pluginWrappers.get(i);
        if(pi.plug == plugin)
          found = true;
      }
    }
    if(found)
      pi.stopPlugin(this, maxWaitTime, false);
  }

  public OfficialPluginDescription getOfficialPlugin(String name) {
    return officialPlugins.get(name);
  }

  public Collection<OfficialPluginDescription> getOfficialPlugins() {
    return officialPlugins.getAll();
  }

  /**
   * Returns a list of the names of all available official plugins. Right now
   * this list is hardcoded but in future we could retrieve this list from emu
   * or from freenet itself.
   *
   * @return A list of all available plugin names
   */
  public List<OfficialPluginDescription> findAvailablePlugins() {
    List<OfficialPluginDescription> availablePlugins = new ArrayList<OfficialPluginDescription>();
    availablePlugins.addAll(officialPlugins.getAll());
    return availablePlugins;
  }

  public OfficialPluginDescription isOfficialPlugin(String name) {
    if((name == null) || (name.trim().length() == 0))
      return null;
    List<OfficialPluginDescription> availablePlugins = findAvailablePlugins();
    for(OfficialPluginDescription desc : availablePlugins) {
      if(desc.name.equals(name))
        return desc;
    }
    return null;
  }

  /** Separate lock for plugin loading. Don't use (this) as we also use that for
   * writing the config file, and because we do a lot inside the lock below; it
   * must not be taken in any other circumstance. */
  private final Object pluginLoadSyncObject = new Object();

  /** All plugin updates are on a single request client. */
  public final RequestClient singleUpdaterRequestClient = new RequestClient() {

    @Override
    public boolean persistent() {
      return false;
    }

    @Override
    public boolean realTimeFlag() {
      return false;
    }

  };

  public File getPluginFilename(String pluginName) {
    File pluginDirectory = node.getPluginDir();
    if((pluginDirectory.exists() && !pluginDirectory.isDirectory()) || (!pluginDirectory.exists() && !pluginDirectory.mkdirs()))
      return null;
    return new File(pluginDirectory, pluginName + ".jar");
  }

  /**
   * Tries to load a plugin from the given name. If the name only contains the
   * name of a plugin it is loaded from the plugin directory, if found,
   * otherwise it's loaded from the project server. If the name contains a
   * complete url and the short file already exists in the plugin directory
   * it's loaded from the plugin directory, otherwise it's retrieved from the
   * remote server.
   * @param pdl
   *
   * @param name
   *            The specification of the plugin
   * @param ignoreOld If true, ignore any old cached copies of the plugin,
   * and download a new version anyway. This is especially important on Windows,
   * where we will not usually be able to delete the file after determining
   * that it is too old.
   * @return An instanciated object of the plugin
   * @throws PluginNotFoundException
   *             If anything goes wrong.
   */
  private FredPlugin loadPlugin(PluginDownLoader<?> pdl, String name, PluginProgress progress, boolean ignoreOld) throws PluginNotFoundException {

    pdl.setSource(name);

    /* check for plugin directory. */
    File pluginDirectory = node.getPluginDir();
    if((pluginDirectory.exists() && !pluginDirectory.isDirectory()) || (!pluginDirectory.exists() && !pluginDirectory.mkdirs())) {
      Logger.error(this, "could not create plugin directory");
      throw new PluginNotFoundException("could not create plugin directory");
    }

    /* get plugin filename. */
    String filename = pdl.getPluginName(name);
    boolean pluginIsLocal = pdl instanceof PluginDownLoaderFile;
    File pluginFile = new File(pluginDirectory, filename + "-" + System.currentTimeMillis());

    /* check for previous instances and delete them. */
    File[] filesInPluginDirectory = getPreviousInstances(pluginDirectory, filename);
    boolean first = true;
    for (File cachedFile : filesInPluginDirectory) {
      if (first && !pluginIsLocal && !ignoreOld) {
        first = false;
        pluginFile = new File(pluginDirectory, cachedFile.getName());
        continue;
      }
      first = false;
      cachedFile.delete();
    }

    boolean downloaded = false;
    /* check if file needs to be downloaded. */
    if(logMINOR)
      Logger.minor(this, "plugin file " + pluginFile.getAbsolutePath() + " exists: " + pluginFile.exists()+" downloader "+pdl+" name "+name);
    int RETRIES = 5;
    for(int i = 0; i < RETRIES; i++) {
      if(!pluginFile.exists() || pluginFile.length() == 0)
        try {
          downloaded = true;
          System.err.println("Downloading plugin "+name);
          WrapperManager.signalStarting((int) MINUTES.toMillis(5));
          File tempPluginFile = null;
          OutputStream pluginOutputStream = null;
          InputStream pluginInputStream = null;
          try {
            tempPluginFile = File.createTempFile("plugin-", ".jar", pluginDirectory);
            tempPluginFile.deleteOnExit();


            pluginOutputStream = new FileOutputStream(tempPluginFile);
            pluginInputStream = pdl.getInputStream(progress);
            byte[] buffer = new byte[1024];
            int read;
            while((read = pluginInputStream.read(buffer)) != -1) {
              pluginOutputStream.write(buffer, 0, read);
            }
            pluginOutputStream.close();
            if(tempPluginFile.length() == 0)
              throw new PluginNotFoundException("downloaded zero length file");
            if(!FileUtil.renameTo(tempPluginFile, pluginFile)) {
              Logger.error(this, "could not rename temp file to plugin file");
              throw new PluginNotFoundException("could not rename temp file to plugin file");
            }

            // try strongest first
            String testsum = null;
            String digest = pdl.getSHA256sum();
            if(digest == null) {
              digest = pdl.getSHA1sum();
            } else {
              testsum = getFileDigest(pluginFile, "SHA-256");
            }
            if(digest != null && testsum == null) {
              testsum = getFileDigest(pluginFile, "SHA-1");
            }

            if(digest != null) {
              if(!(digest.equalsIgnoreCase(testsum))) {
                Logger.error(this, "Checksum verification failed, should be " + digest + " but was " + testsum);
                throw new PluginNotFoundException("Checksum verification failed, should be " + digest + " but was " + testsum);
              }
            }

          } catch(IOException ioe1) {
            Logger.error(this, "could not load plugin", ioe1);
            if(tempPluginFile != null)
              tempPluginFile.delete();
            throw new PluginNotFoundException("could not load plugin: " + ioe1.getMessage(), ioe1);
          } finally {
            Closer.close(pluginOutputStream);
            Closer.close(pluginInputStream);
          }
        } catch(PluginNotFoundException e) {
          if(i < RETRIES - 1) {
            Logger.normal(this, "Failed to load plugin: " + e, e);
            continue;
          } else
            throw e;
        }

    cancelRunningLoads(name, progress);

    // we do quite a lot inside the lock, use a dedicated one
    synchronized (pluginLoadSyncObject) {
      /* now get the manifest file. */
      JarFile pluginJarFile = null;
      String pluginMainClassName = null;
      try {
        pluginJarFile = new JarFile(pluginFile);
        Manifest manifest = pluginJarFile.getManifest();
        if(manifest == null) {
          Logger.error(this, "could not load manifest from plugin file");
          pluginFile.delete();
          if(!downloaded) continue;
          throw new PluginNotFoundException("could not load manifest from plugin file");
        }
        Attributes mainAttributes = manifest.getMainAttributes();
        if(mainAttributes == null) {
          Logger.error(this, "manifest does not contain attributes");
          pluginFile.delete();
          if(!downloaded) continue;
          throw new PluginNotFoundException("manifest does not contain attributes");
        }
        pluginMainClassName = mainAttributes.getValue("Plugin-Main-Class");
        if(pluginMainClassName == null) {
          Logger.error(this, "manifest does not contain a Plugin-Main-Class attribute");
          pluginFile.delete();
          if(!downloaded) continue;
          throw new PluginNotFoundException("manifest does not contain a Plugin-Main-Class attribute");
        }
        if(this.isPluginLoaded(pluginMainClassName)) {
          Logger.error(this, "Plugin already loaded: "+filename);
          return null;
        }

      } catch(JarException je1) {
        Logger.error(this, "could not process jar file", je1);
        pluginFile.delete();
        if(!downloaded) continue;
        throw new PluginNotFoundException("could not process jar file", je1);
      } catch(ZipException ze1) {
        Logger.error(this, "could not process jar file", ze1);
        pluginFile.delete();
        if(!downloaded) continue;
        throw new PluginNotFoundException("could not process jar file", ze1);
      } catch(IOException ioe1) {
        Logger.error(this, "error processing jar file", ioe1);
        pluginFile.delete();
        if(!downloaded) continue;
        throw new PluginNotFoundException("error procesesing jar file", ioe1);
      } finally {
        Closer.close(pluginJarFile);
      }

      try {
        JarClassLoader jarClassLoader = new JarClassLoader(pluginFile);
        Class<?> pluginMainClass = jarClassLoader.loadClass(pluginMainClassName);
        Object object = pluginMainClass.newInstance();
        if(!(object instanceof FredPlugin)) {
          Logger.error(this, "plugin main class is not a plugin");
          pluginFile.delete();
          if(!downloaded) continue;
          throw new PluginNotFoundException("plugin main class is not a plugin");
        }

        if(pdl instanceof PluginDownLoaderOfficialHTTPS ||
            pdl instanceof PluginDownLoaderOfficialFreenet) {
          System.err.println("Loading official plugin "+name);
          // Check the version after loading it!
          // FIXME IMPORTANT Build the version into the manifest. This is actually pretty easy and just involves changing build.xml.
          // We already do similar things elsewhere.

          // Ugh, this is just as messy ... ideas???? Maybe we need to have OS
          // detection and use grep/sed on unix and find on windows???

          OfficialPluginDescription desc = officialPlugins.get(name);

          long minVer = desc.minimumVersion;
          long ver = -1;

          if(minVer != -1) {
            if(object instanceof FredPluginRealVersioned) {
              ver = ((FredPluginRealVersioned)object).getRealVersion();
            }
          }

          // FIXME l10n the PluginNotFoundException errors.
          if(ver < minVer) {
            System.err.println("Failed to load plugin "+name+" : TOO OLD: need at least version "+minVer+" but is "+ver);
            Logger.error(this, "Failed to load plugin "+name+" : TOO OLD: need at least version "+minVer+" but is "+ver);

            // At this point, the constructor has run, so it's theoretically possible that the plugin has e.g. created some threads.
            // However, it has not been able to use any of the node's services, because we haven't passed it the PluginRespirator.
            // So there is no need to call runPlugin and terminate().
            // And it doesn't matter all that much if the shutdown fails - we won't be able to delete the file on Windows anyway, we're relying on the ignoreOld logic.
            // Plus, this will not cause a leak of more than one fd per plugin, even when it has started threads.
            try {
              jarClassLoader.close();
            } catch (Throwable t) {
              Logger.error(this, "Failed to close jar classloader for plugin: "+t, t);
            }
            pluginFile.delete();
            if(!downloaded) continue;
            throw new PluginTooOldException("plugin too old: need at least version "+minVer+" but is "+ver);
          }

        }

        if(object instanceof FredPluginL10n) {
          ((FredPluginL10n)object).setLanguage(NodeL10n.getBase().getSelectedLanguage());
        }

        if(object instanceof FredPluginBaseL10n) {
          ((FredPluginBaseL10n)object).setLanguage(NodeL10n.getBase().getSelectedLanguage());
        }

        if(object instanceof FredPluginThemed) {
          ((FredPluginThemed)object).setTheme(fproxyTheme);
        }

        return (FredPlugin) object;
      } catch(IOException ioe1) {
        Logger.error(this, "could not load plugin", ioe1);
        pluginFile.delete();
        throw new PluginNotFoundException("could not load plugin", ioe1);
      } catch(ClassNotFoundException cnfe1) {
        Logger.error(this, "could not find plugin class", cnfe1);
        pluginFile.delete();
        if(!downloaded) continue;
        throw new PluginNotFoundException(
          "could not find plugin class: \"" + cnfe1.getMessage() + "\"", cnfe1);
      } catch(InstantiationException ie1) {
        Logger.error(this, "could not instantiate plugin", ie1);
        pluginFile.delete();
        if(!downloaded) continue;
        throw new PluginNotFoundException("could not instantiate plugin", ie1);
      } catch(IllegalAccessException iae1) {
        Logger.error(this, "could not access plugin main class", iae1);
        pluginFile.delete();
        throw new PluginNotFoundException("could not access plugin main class", iae1);
      } catch(NoClassDefFoundError ncdfe1) {
        Logger.error(this, "could not find class def, may a missing lib?", ncdfe1);
        pluginFile.delete();
        if(!downloaded) continue;
        throw new PluginNotFoundException("could not find class def, may a missing lib?", ncdfe1);
      } catch(Throwable t) {
        Logger.error(this, "unexpected error while plugin loading", t);
        pluginFile.delete();
        throw new PluginNotFoundException("unexpected error while plugin loading " + t, t);
      }
    }
    }
    return null;
  }

  /**
   * This returns all existing instances of cached JAR files that start with
   * the given filename followed by a dash (“-”), sorted numerically by the
   * appendix, largest (i.e. newest) first.
   *
   * @param pluginDirectory
   *            The plugin cache directory
   * @param filename
   *            The name of the JAR file
   * @return All cached instances
   */
  private File[] getPreviousInstances(File pluginDirectory, final String filename) {
    File[] cachedFiles = pluginDirectory.listFiles(new FileFilter() {

      @Override
      public boolean accept(File pathname) {
        return pathname.isFile() && pathname.getName().startsWith(filename);
      }
    });
    Arrays.sort(cachedFiles, new Comparator<File>() {

      @Override
      public int compare(File file1, File file2) {
        return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, extractTimestamp(file2.getName()) - extractTimestamp(file1.getName())));
      }

      private long extractTimestamp(String filename) {
        int lastIndexOfDash = filename.lastIndexOf(".jar-");
        if (lastIndexOfDash == -1) {
          return 0;
        }
        try {
          return Long.parseLong(filename.substring(lastIndexOfDash + 5));
        } catch (NumberFormatException nfe1) {
          return 0;
        }
      }
    });
    return cachedFiles;
  }

  private String getFileDigest(File file, String digest) throws PluginNotFoundException {
    final int BUFFERSIZE = 4096;
    MessageDigest hash = null;
    FileInputStream fis = null;
    BufferedInputStream bis = null;
    boolean wasFromDigest256Pool = false;
    String result;

    try {
      if ("SHA-256".equals(digest)) {
        hash = SHA256.getMessageDigest(); // grab digest from pool
        wasFromDigest256Pool = true;
      } else {
        hash = MessageDigest.getInstance(digest);
      }
      // We compute the hash
      // http://java.sun.com/developer/TechTips/1998/tt0915.html#tip2
      fis = new FileInputStream(file);
      bis = new BufferedInputStream(fis);
      int len = 0;
      byte[] buffer = new byte[BUFFERSIZE];
      while((len = bis.read(buffer)) > -1) {
        hash.update(buffer, 0, len);
      }
      result = HexUtil.bytesToHex(hash.digest());
      if (wasFromDigest256Pool)
        SHA256.returnMessageDigest(hash);
    } catch(Exception e) {
      throw new PluginNotFoundException("Error while computing hash '"+digest+"' of the downloaded plugin: " + e, e);
    } finally {
      Closer.close(bis);
      Closer.close(fis);
    }
    return result;
  }

  Ticker getTicker() {
    return node.getTicker();
  }

  /**
   * Tracks the progress of loading and starting a plugin.
   *
   * @author David &lsquo;Bombe&rsquo; Roden &lt;bombe@freenetproject.org&gt;
   * @version $Id$
   */
  public static class PluginProgress {

    enum PROGRESS_STATE {
      DOWNLOADING,
      STARTING
    }

    /** The starting time. */
    private long startingTime = System.currentTimeMillis();
    /** The current state. */
    private PROGRESS_STATE pluginProgress;
    /** The name by which the plugin is loaded. */
    private String name;
    /** Total. Might be bytes, might be blocks. */
    private int total;
    /** Minimum for success */
    private int minSuccessful;
    /** Current value. Same units as total. */
    private int current;
    private boolean finalisedTotal;
    private int failed;
    private int fatallyFailed;
    private final PluginDownLoader<?> loader;

    /**
     * Creates a new progress tracker for a plugin that is loaded by the
     * given name.
     *
     * @param name
     *            The name by which the plugin is loaded
     * @param pdl
     */
    PluginProgress(String name, PluginDownLoader<?> pdl) {
      this.name = name;
      pluginProgress = PROGRESS_STATE.DOWNLOADING;
      loader = pdl;
    }

    public void kill() {
      loader.tryCancel();
    }

    /**
     * Returns the number of milliseconds this plugin is already being
     * loaded.
     *
     * @return The time this plugin is already being loaded (in
     *         milliseconds)
     */
    public long getTime() {
      return System.currentTimeMillis() - startingTime;
    }

    /**
     * Returns the name by which the plugin is loaded.
     *
     * @return The name by which the plugin is loaded
     */
    public String getName() {
      return name;
    }

    /**
     * Returns the current state of the plugin start procedure.
     *
     * @return The current state of the plugin
     */
    public PROGRESS_STATE getProgress() {
      return pluginProgress;
    }

    /**
     * Sets the current state of the plugin start procedure
     *
     * @param pluginProgress
     *            The current state
     */
    void setProgress(PROGRESS_STATE state) {
      this.pluginProgress = state;
    }

    /**
     * If this object is one of the constants {@link #DOWNLOADING} or
     * {@link #STARTING}, the name of those constants will be returned,
     * otherwise a textual representation of the plugin progress is
     * returned.
     *
     * @return The name of a constant, or the plugin progress
     */
    @Override
    public String toString() {
      return "PluginProgress[name=" + name + ",startingTime=" + startingTime + ",progress=" + pluginProgress + "]";
    }

    public String toLocalisedString() {
      if(pluginProgress == PROGRESS_STATE.DOWNLOADING && total > 0)
        return NodeL10n.getBase().getString("PproxyToadlet.startingPluginStatus.downloading") + " : "+((current*100.0) / total)+"%";
      else if(pluginProgress == PROGRESS_STATE.DOWNLOADING)
        return NodeL10n.getBase().getString("PproxyToadlet.startingPluginStatus.downloading");
      else if(pluginProgress == PROGRESS_STATE.STARTING)
        return NodeL10n.getBase().getString("PproxyToadlet.startingPluginStatus.starting");
      else
        return toString();
    }

    public HTMLNode toLocalisedHTML() {
      if(pluginProgress == PROGRESS_STATE.DOWNLOADING && total > 0) {
        return QueueToadlet.createProgressCell(false, true, ClientPut.COMPRESS_STATE.WORKING, current, failed, fatallyFailed, minSuccessful, total, finalisedTotal, false);
      } else if(pluginProgress == PROGRESS_STATE.DOWNLOADING)
        return new HTMLNode("td", NodeL10n.getBase().getString("PproxyToadlet.startingPluginStatus.downloading"));
      else if(pluginProgress == PROGRESS_STATE.STARTING)
        return new HTMLNode("td", NodeL10n.getBase().getString("PproxyToadlet.startingPluginStatus.starting"));
      else
        return new HTMLNode("td", toString());
    }

    public void setDownloadProgress(int minSuccess, int current, int total, int failed, int fatallyFailed, boolean finalised) {
      this.pluginProgress = PROGRESS_STATE.DOWNLOADING;
      this.total = total;
      this.current = current;
      this.minSuccessful = minSuccess;
      this.failed = failed;
      this.fatallyFailed = fatallyFailed;
      this.finalisedTotal = finalised;
    }

    public void setDownloading() {
      this.pluginProgress = PROGRESS_STATE.DOWNLOADING;
    }
   
    public boolean isOfficialPlugin() {
      return loader.isOfficialPluginLoader();
    }

    public String getLocalisedPluginName() {
      String pluginName = getName();
      if(isOfficialPlugin()) {
        return getOfficialPluginLocalisedName(pluginName);
      } else return pluginName;
    }
  }
 
  static String getOfficialPluginLocalisedName(String pluginName) {
    return l10n("pluginName."+pluginName);
  }

  public void setFProxyTheme(final THEME cssName) {
    //if (fproxyTheme.equals(cssName)) return;
    fproxyTheme = cssName;
    synchronized(pluginWrappers) {
      for(PluginInfoWrapper pi: pluginWrappers) {
        pi.pr.getPageMaker().setTheme(cssName);
        if(pi.isThemedPlugin()) {
          final FredPluginThemed plug = (FredPluginThemed)(pi.plug);
          executor.execute(new Runnable() {
            @Override
            public void run() {
              try {
                plug.setTheme(cssName);
              } catch (Throwable t) {
                Logger.error(this, "Cought Trowable in Callback", t);
              }
            }}, "Callback");
        }
      }
    }
  }

  public static void setLanguage(LANGUAGE lang) {
    if (selfinstance == null) return;
    selfinstance.setPluginLanguage(lang);
  }

  private void setPluginLanguage(final LANGUAGE lang) {
    synchronized(pluginWrappers) {
      for(PluginInfoWrapper pi: pluginWrappers) {
        if(pi.isL10nPlugin()) {
          final FredPluginL10n plug = (FredPluginL10n)(pi.plug);
          executor.execute(new Runnable() {
            @Override
            public void run() {
              try {
                plug.setLanguage(lang);
              } catch (Throwable t) {
                Logger.error(this, "Cought Trowable in Callback", t);
              }
            }}, "Callback");
        } else if(pi.isBaseL10nPlugin()) {
          final FredPluginBaseL10n plug = (FredPluginBaseL10n)(pi.plug);
          executor.execute(new Runnable() {
            @Override
            public void run() {
              try {
                plug.setLanguage(lang);
              } catch (Throwable t) {
                Logger.error(this, "Cought Trowable in Callback", t);
              }
            }}, "Callback");
        }
      }
    }
  }

  public THEME getFProxyTheme() {
    return fproxyTheme;
  }

  public boolean loadOfficialPluginsFromWeb() {
    return alwaysLoadOfficialPluginsFromCentralServer;
  }

  public void unregisterPlugin(PluginInfoWrapper wrapper, FredPlugin plug, boolean reloading) {
    unregisterPluginToadlet(wrapper);
    if(wrapper.isConfigurablePlugin()) {
      core.getToadletContainer().unregister(wrapper.getConfigToadlet());
    }
    if(wrapper.isIPDetectorPlugin())
      node.ipDetector.unregisterIPDetectorPlugin((FredPluginIPDetector)plug);
    if(wrapper.isPortForwardPlugin())
      node.ipDetector.unregisterPortForwardPlugin((FredPluginPortForward)plug);
    if(wrapper.isBandwidthIndicator())
      node.ipDetector.unregisterBandwidthIndicatorPlugin((FredPluginBandwidthIndicator)plug);
    if(!reloading)
      node.nodeUpdater.stopPluginUpdater(wrapper.getFilename());
  }

}
TOP

Related Classes of freenet.pluginmanager.PluginManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.