/* 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.concurrent.Semaphore; 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.PageMaker.THEME; import freenet.clients.http.QueueToadlet; 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.BaseL10n.LANGUAGE; import freenet.l10n.NodeL10n; import freenet.node.Node; import freenet.node.NodeClientCore; import freenet.node.RequestClient; import freenet.node.RequestClientBuilder; 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.Logger.LogLevel; import freenet.support.SerialExecutor; import freenet.support.Ticker; 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; /** Is the plugin system enabled? Set at boot time only. Mainly for simulations. */ private final boolean enabled; 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 = node.config.createSubConfig("pluginmanager"); pmconfig.register("enabled", true, 0, true, true, "PluginManager.enabled", "PluginManager.enabledLong", new BooleanCallback() { @Override public synchronized Boolean get() { return enabled; } @Override public void set(Boolean val) throws InvalidConfigValueException, NodeNeedRestartException { if(enabled != val) throw new NodeNeedRestartException(l10n("changePluginManagerEnabledInConfig")); } }); enabled = pmconfig.getBoolean("enabled"); // 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(!enabled) return; if (toStart == null) { synchronized (pluginWrappers) { started = true; return; } } final Semaphore startingPlugins = new Semaphore(0); for(final String name : toStart) { core.getExecutor().execute(new Runnable() { @Override public void run() { startPluginAuto(name, false); startingPlugins.release(); } }); } core.getExecutor().execute(new Runnable() { @Override public void run() { startingPlugins.acquireUninterruptibly(toStart.length); synchronized(pluginWrappers) { started = true; toStart = null; } } }); } public void stop(long maxWaitTime) { if(!enabled) return; // 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()); } v.addAll(pluginsFailedLoad.keySet()); } 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, desc.alwaysFetchLatestVersion); } else { return realStartPlugin(new PluginDownLoaderOfficialFreenet(client, node, false), pluginname, store, desc.alwaysFetchLatestVersion); } } 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(!enabled) throw new IllegalStateException("Plugins disabled"); 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 or filename. * * @return the PluginInfo or null if not found * @deprecated * This function was deprecated because the "or filename" part of the function specification * was NOT documented before it was deprecated. Thus it is possible that legacy callers of * the function did wrongly expect or not expect that. When removing this function, please * review the callers for correctness with regards to that.<br> * You might replace usage of this function with * {@link #getPluginInfoByClassName(String)} or {@link #getPluginInfoByFileName(String)}. */ @Deprecated 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; } /** * @param pluginClassName * The name of the main class of the plugin - that is the class which implements * {@link FredPlugin}. * @return * The {@link PluginInfoWrapper} for the plugin with the given class name, or null if no * matching plugin was found. */ public PluginInfoWrapper getPluginInfoByClassName(String pluginClassName) { synchronized(pluginWrappers) { for(PluginInfoWrapper piw : pluginWrappers) { if(piw.getPluginClassName().equals(pluginClassName)) { return piw; } } } return null; } /** * @param pluginFileName * The filename of the JAR from which the plugin was loaded. * @return * The {@link PluginInfoWrapper} for the plugin with the given file name, or null if no * matching plugin was found. */ public PluginInfoWrapper getPluginInfoByFileName(String pluginFileName) { synchronized(pluginWrappers) { for(PluginInfoWrapper piw : pluginWrappers) { if(piw.getFilename().equals(pluginFileName)) { return piw; } } } return null; } /** * look for a FCPPlugin with given classname * @param plugname * @return the plugin or null if not found * @deprecated * The {@link FredPluginFCP} API, which this returns, was deprecated to be replaced by * {@link FredPluginFCPMessageHandler.ServerSideFCPMessageHandler}. Plugin authors should * implement the new interface instead of the old, and this codepath to support plugins * which implement the old interface should be removed one day. No new code will be needed * then: The code to use the new interface already exists in its own codepath - the * equivalent function for the new API is {link #getPluginFCPServer(String)}, and it is * already being used automatically for plugins which implement it. */ @Deprecated 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; } /** * Get the {@link FredPluginFCPMessageHandler.ServerSideFCPMessageHandler} of the plugin with * the given class name. * * @param pluginClassName * See {@link #getPluginInfoByClassName(String)}. * @throws PluginNotFoundException * If the specified plugin is not loaded or does not provide an FCP server. */ public FredPluginFCPMessageHandler.ServerSideFCPMessageHandler getPluginFCPServer(String pluginClassName) throws PluginNotFoundException{ PluginInfoWrapper piw = getPluginInfoByClassName(pluginClassName); if(piw != null && piw.isFCPServerPlugin()) { return piw.getFCPServerPlugin(); } else { throw new PluginNotFoundException(pluginClassName); } } /** * 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 RequestClientBuilder().build(); 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 ‘Bombe’ Roden <bombe@freenetproject.org> * @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()); } public boolean isEnabled() { return enabled; } }