diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 3c4c4fd9b..22062cb7f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -2,6 +2,7 @@ package net.i2p.router.web; import java.io.File; import java.io.IOException; +import java.lang.ClassLoader; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; @@ -48,6 +49,7 @@ public class PluginStarter implements Runnable { "midnight" }; private static Map pluginThreadGroups = new ConcurrentHashMap(); // one thread group per plugin (map key=plugin name) private static Map> pluginJobs = new ConcurrentHashMap>(); + private static Map _clCache = new ConcurrentHashMap(); public PluginStarter(RouterContext ctx) { _context = ctx; @@ -87,7 +89,7 @@ public class PluginStarter implements Runnable { */ static boolean startPlugin(RouterContext ctx, String appName) throws Exception { Log log = ctx.logManager().getLog(PluginStarter.class); - File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); + File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { log.error("Cannot start nonexistent plugin: " + appName); return false; @@ -195,7 +197,7 @@ public class PluginStarter implements Runnable { */ static boolean stopPlugin(RouterContext ctx, String appName) throws Exception { Log log = ctx.logManager().getLog(PluginStarter.class); - File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); + File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { log.error("Cannot stop nonexistent plugin: " + appName); return false; @@ -244,7 +246,7 @@ public class PluginStarter implements Runnable { /** @return true on success - caller should call stopPlugin() first */ static boolean deletePlugin(RouterContext ctx, String appName) throws Exception { Log log = ctx.logManager().getLog(PluginStarter.class); - File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); + File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { log.error("Cannot delete nonexistent plugin: " + appName); return false; @@ -287,7 +289,7 @@ public class PluginStarter implements Runnable { /** plugin.config */ public static Properties pluginProperties(I2PAppContext ctx, String appName) { - File cfgFile = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + '/' + "plugin.config"); + File cfgFile = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + '/' + "plugin.config"); Properties rv = new Properties(); try { DataHelper.loadProps(rv, cfgFile); @@ -322,7 +324,7 @@ public class PluginStarter implements Runnable { */ public static List getPlugins() { List rv = new ArrayList(); - File pluginDir = new File(I2PAppContext.getGlobalContext().getAppDir(), PluginUpdateHandler.PLUGIN_DIR); + File pluginDir = new File(I2PAppContext.getGlobalContext().getConfigDir(), PluginUpdateHandler.PLUGIN_DIR); File[] files = pluginDir.listFiles(); if (files == null) return rv; @@ -405,6 +407,8 @@ public class PluginStarter implements Runnable { argVal[i] = argVal[i].replace("$PLUGIN", pluginDir.getAbsolutePath()); } } + + ClassLoader cl = null; if (app.classpath != null) { String cp = new String(app.classpath); if (cp.indexOf("$") >= 0) { @@ -412,22 +416,41 @@ public class PluginStarter implements Runnable { cp = cp.replace("$CONFIG", ctx.getConfigDir().getAbsolutePath()); cp = cp.replace("$PLUGIN", pluginDir.getAbsolutePath()); } - addToClasspath(cp, app.clientName, log); + + // Old way - add for the whole JVM + //addToClasspath(cp, app.clientName, log); + + // New way - add only for this client + // We cache the ClassLoader we start the client with, so + // we can reuse it for stopping and uninstalling. + // If we don't, the client won't be able to find its + // static members. + String clCacheKey = pluginName + app.className + app.args; + if (!action.equals("start")) + cl = _clCache.get(clCacheKey); + if (cl == null) { + URL[] urls = classpathToURLArray(cp, app.clientName, log); + if (urls != null) { + cl = new URLClassLoader(urls, ClassLoader.getSystemClassLoader()); + if (action.equals("start")) + _clCache.put(clCacheKey, cl); + } + } } if (app.delay < 0 && action.equals("start")) { // this will throw exceptions - LoadClientAppsJob.runClientInline(app.className, app.clientName, argVal, log); + LoadClientAppsJob.runClientInline(app.className, app.clientName, argVal, log, cl); } else if (app.delay == 0 || !action.equals("start")) { // quick check, will throw ClassNotFoundException on error - LoadClientAppsJob.testClient(app.className); + LoadClientAppsJob.testClient(app.className, cl); // run this guy now - LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log, pluginThreadGroup); + LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log, pluginThreadGroup, cl); } else { // quick check, will throw ClassNotFoundException on error - LoadClientAppsJob.testClient(app.className); + LoadClientAppsJob.testClient(app.className, cl); // wait before firing it up - Job job = new LoadClientAppsJob.DelayedRunClient(ctx, app.className, app.clientName, argVal, app.delay, pluginThreadGroup); + Job job = new LoadClientAppsJob.DelayedRunClient(ctx, app.className, app.clientName, argVal, app.delay, pluginThreadGroup, cl); ctx.jobQueue().addJob(job); pluginJobs.get(pluginName).add(job); } @@ -470,6 +493,7 @@ public class PluginStarter implements Runnable { * but I don't see how to make it magically get used for everything. * So add this to the whole JVM's classpath. */ +/****** private static void addToClasspath(String classpath, String clientName, Log log) { StringTokenizer tok = new StringTokenizer(classpath, ","); while (tok.hasMoreTokens()) { @@ -488,6 +512,33 @@ public class PluginStarter implements Runnable { } } } +*****/ + + /** + * @return null if no valid elements + */ + private static URL[] classpathToURLArray(String classpath, String clientName, Log log) { + StringTokenizer tok = new StringTokenizer(classpath, ","); + List urls = new ArrayList(); + while (tok.hasMoreTokens()) { + String elem = tok.nextToken().trim(); + File f = new File(elem); + if (!f.isAbsolute()) { + log.error("Plugin client " + clientName + " classpath element is not absolute: " + f); + continue; + } + try { + urls.add(f.toURI().toURL()); + if (log.shouldLog(Log.WARN)) + log.warn("INFO: Adding plugin to classpath: " + f); + } catch (Exception e) { + log.error("Plugin client " + clientName + " bad classpath element: " + f, e); + } + } + if (urls.isEmpty()) + return null; + return urls.toArray(new URL[urls.size()]); + } /** * http://jimlife.wordpress.com/2007/12/19/java-adding-new-classpath-at-runtime/ diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index e028ee3cd..8ba470840 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -150,7 +150,7 @@ public class PluginUpdateHandler extends UpdateHandler { public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { updateStatus("" + _("Plugin downloaded") + ""); File f = new File(_updateFile); - File appDir = new File(_context.getAppDir(), PLUGIN_DIR); + File appDir = new File(_context.getConfigDir(), PLUGIN_DIR); if ((!appDir.exists()) && (!appDir.mkdir())) { f.delete(); statusDone("" + _("Cannot create plugin directory {0}", appDir.getAbsolutePath()) + ""); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java index 1cfc09b4c..0943ea417 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java @@ -54,7 +54,7 @@ public class WebAppConfiguration implements WebApplicationContext.Configuration I2PAppContext i2pContext = I2PAppContext.getGlobalContext(); File libDir = new File(i2pContext.getBaseDir(), "lib"); // FIXME this only works if war is the same name as the plugin - File pluginDir = new File(i2pContext.getAppDir(), + File pluginDir = new File(i2pContext.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + ctxPath); File dir = libDir; diff --git a/history.txt b/history.txt index 8f29bb18c..15fca9fff 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,17 @@ +2010-05-05 zzz + * build.xml: Create packed sud in release + * Console: + - Print stack trace if exception on startup + - IllegalStateException rather than NPE if no context + * EepGet: Limit max times to fail completely even if numRetries is higher + * i2psnark: Skip 'the' when sorting snarks + * I2PTunnelHTTPClient: Reject 192.168.* + * Plugins: + - Set classpath for specific client only, not for the whole JVM + - Use ConfigDir() not AppDir() + * Replace size() <= 0 with isEmpty() everywhere, ditto > 0 -> !isEmpty() + * RouterInfo: Clean up use of sortStructures() + 2010-05-02 zzz * ByteCache: - Add a per-cache stat diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 282c18b42..0725033fa 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 2; + public final static long BUILD = 3; /** for example "-test" */ public final static String EXTRA = ""; diff --git a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java index 13dae3a15..f737c452d 100644 --- a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java +++ b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java @@ -30,7 +30,7 @@ public class LoadClientAppsJob extends JobImpl { _loaded = true; } List apps = ClientAppConfig.getClientApps(getContext()); - if (apps.size() <= 0) { + if (apps.isEmpty()) { _log.error("Warning - No client apps or router console configured - we are just a router"); System.err.println("Warning - No client apps or router console configured - we are just a router"); return; @@ -56,23 +56,26 @@ public class LoadClientAppsJob extends JobImpl { private String _args[]; private Log _log; private ThreadGroup _threadGroup; + private ClassLoader _cl; public DelayedRunClient(RouterContext enclosingContext, String className, String clientName, String args[], long delay) { - this(enclosingContext, className, clientName, args, delay, null); + this(enclosingContext, className, clientName, args, delay, null, null); } - public DelayedRunClient(RouterContext enclosingContext, String className, String clientName, String args[], long delay, ThreadGroup threadGroup) { + public DelayedRunClient(RouterContext enclosingContext, String className, String clientName, String args[], + long delay, ThreadGroup threadGroup, ClassLoader cl) { super(enclosingContext); _className = className; _clientName = clientName; _args = args; _log = enclosingContext.logManager().getLog(LoadClientAppsJob.class); _threadGroup = threadGroup; + _cl = cl; getTiming().setStartAfter(getContext().clock().now() + delay); } public String getName() { return "Delayed client job"; } public void runJob() { - runClient(_className, _clientName, _args, _log, _threadGroup); + runClient(_className, _clientName, _args, _log, _threadGroup, _cl); } } @@ -129,50 +132,81 @@ public class LoadClientAppsJob extends JobImpl { * to propagate an error back to the user, * since runClient() runs in a separate thread. * + * @param cl can be null * @since 0.7.13 */ - public static void testClient(String className) throws ClassNotFoundException { - Class.forName(className); + public static void testClient(String className, ClassLoader cl) throws ClassNotFoundException { + if (cl == null) + cl = ClassLoader.getSystemClassLoader(); + Class.forName(className, false, cl); } /** * Run client in this thread. * + * @param clientName can be null + * @param args can be null * @throws just about anything, caller would be wise to catch Throwable * @since 0.7.13 */ public static void runClientInline(String className, String clientName, String args[], Log log) throws Exception { + runClientInline(className, clientName, args, log, null); + } + + /** + * Run client in this thread. + * + * @param clientName can be null + * @param args can be null + * @param cl can be null + * @throws just about anything, caller would be wise to catch Throwable + * @since 0.7.14 + */ + public static void runClientInline(String className, String clientName, String args[], + Log log, ClassLoader cl) throws Exception { if (log.shouldLog(Log.INFO)) log.info("Loading up the client application " + clientName + ": " + className + " " + Arrays.toString(args)); if (args == null) args = new String[0]; - Class cls = Class.forName(className); + Class cls = Class.forName(className, true, cl); Method method = cls.getMethod("main", new Class[] { String[].class }); method.invoke(cls, new Object[] { args }); } /** * Run client in a new thread. + * + * @param clientName can be null + * @param args can be null */ public static void runClient(String className, String clientName, String args[], Log log) { - runClient(className, clientName, args, log, null); + runClient(className, clientName, args, log, null, null); } /** * Run client in a new thread. + * + * @param clientName can be null + * @param args can be null + * @param threadGroup can be null + * @param cl can be null + * @since 0.7.13 */ - public static void runClient(String className, String clientName, String args[], Log log, ThreadGroup threadGroup) { + public static void runClient(String className, String clientName, String args[], Log log, + ThreadGroup threadGroup, ClassLoader cl) { if (log.shouldLog(Log.INFO)) log.info("Loading up the client application " + clientName + ": " + className + " " + Arrays.toString(args)); I2PThread t; if (threadGroup != null) - t = new I2PThread(threadGroup, new RunApp(className, clientName, args, log)); + t = new I2PThread(threadGroup, new RunApp(className, clientName, args, log, cl)); else - t = new I2PThread(new RunApp(className, clientName, args, log)); + t = new I2PThread(new RunApp(className, clientName, args, log, cl)); if (clientName == null) clientName = className + " client"; t.setName(clientName); t.setDaemon(true); + if (cl != null) + t.setContextClassLoader(cl); t.start(); } @@ -181,7 +215,9 @@ public class LoadClientAppsJob extends JobImpl { private String _appName; private String _args[]; private Log _log; - public RunApp(String className, String appName, String args[], Log log) { + private ClassLoader _cl; + + public RunApp(String className, String appName, String args[], Log log, ClassLoader cl) { _className = className; _appName = appName; if (args == null) @@ -189,10 +225,15 @@ public class LoadClientAppsJob extends JobImpl { else _args = args; _log = log; + if (cl == null) + _cl = ClassLoader.getSystemClassLoader(); + else + _cl = cl; } + public void run() { try { - Class cls = Class.forName(_className); + Class cls = Class.forName(_className, true, _cl); Method method = cls.getMethod("main", new Class[] { String[].class }); method.invoke(cls, new Object[] { _args }); } catch (Throwable t) {