* Plugins:

- Set classpath for specific client only, not for the whole JVM
      - Use ConfigDir() not AppDir()
This commit is contained in:
zzz
2010-05-05 19:34:03 +00:00
parent f3576e54c6
commit a8db6b007f
6 changed files with 133 additions and 27 deletions

View File

@ -2,6 +2,7 @@ package net.i2p.router.web;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.ClassLoader;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
@ -48,6 +49,7 @@ public class PluginStarter implements Runnable {
"midnight" }; "midnight" };
private static Map<String, ThreadGroup> pluginThreadGroups = new ConcurrentHashMap<String, ThreadGroup>(); // one thread group per plugin (map key=plugin name) private static Map<String, ThreadGroup> pluginThreadGroups = new ConcurrentHashMap<String, ThreadGroup>(); // one thread group per plugin (map key=plugin name)
private static Map<String, Collection<Job>> pluginJobs = new ConcurrentHashMap<String, Collection<Job>>(); private static Map<String, Collection<Job>> pluginJobs = new ConcurrentHashMap<String, Collection<Job>>();
private static Map<String, ClassLoader> _clCache = new ConcurrentHashMap();
public PluginStarter(RouterContext ctx) { public PluginStarter(RouterContext ctx) {
_context = ctx; _context = ctx;
@ -87,7 +89,7 @@ public class PluginStarter implements Runnable {
*/ */
static boolean startPlugin(RouterContext ctx, String appName) throws Exception { static boolean startPlugin(RouterContext ctx, String appName) throws Exception {
Log log = ctx.logManager().getLog(PluginStarter.class); 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())) { if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
log.error("Cannot start nonexistent plugin: " + appName); log.error("Cannot start nonexistent plugin: " + appName);
return false; return false;
@ -195,7 +197,7 @@ public class PluginStarter implements Runnable {
*/ */
static boolean stopPlugin(RouterContext ctx, String appName) throws Exception { static boolean stopPlugin(RouterContext ctx, String appName) throws Exception {
Log log = ctx.logManager().getLog(PluginStarter.class); 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())) { if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
log.error("Cannot stop nonexistent plugin: " + appName); log.error("Cannot stop nonexistent plugin: " + appName);
return false; return false;
@ -244,7 +246,7 @@ public class PluginStarter implements Runnable {
/** @return true on success - caller should call stopPlugin() first */ /** @return true on success - caller should call stopPlugin() first */
static boolean deletePlugin(RouterContext ctx, String appName) throws Exception { static boolean deletePlugin(RouterContext ctx, String appName) throws Exception {
Log log = ctx.logManager().getLog(PluginStarter.class); 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())) { if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
log.error("Cannot delete nonexistent plugin: " + appName); log.error("Cannot delete nonexistent plugin: " + appName);
return false; return false;
@ -287,7 +289,7 @@ public class PluginStarter implements Runnable {
/** plugin.config */ /** plugin.config */
public static Properties pluginProperties(I2PAppContext ctx, String appName) { 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(); Properties rv = new Properties();
try { try {
DataHelper.loadProps(rv, cfgFile); DataHelper.loadProps(rv, cfgFile);
@ -322,7 +324,7 @@ public class PluginStarter implements Runnable {
*/ */
public static List<String> getPlugins() { public static List<String> getPlugins() {
List<String> rv = new ArrayList(); List<String> 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(); File[] files = pluginDir.listFiles();
if (files == null) if (files == null)
return rv; return rv;
@ -405,6 +407,8 @@ public class PluginStarter implements Runnable {
argVal[i] = argVal[i].replace("$PLUGIN", pluginDir.getAbsolutePath()); argVal[i] = argVal[i].replace("$PLUGIN", pluginDir.getAbsolutePath());
} }
} }
ClassLoader cl = null;
if (app.classpath != null) { if (app.classpath != null) {
String cp = new String(app.classpath); String cp = new String(app.classpath);
if (cp.indexOf("$") >= 0) { if (cp.indexOf("$") >= 0) {
@ -412,22 +416,41 @@ public class PluginStarter implements Runnable {
cp = cp.replace("$CONFIG", ctx.getConfigDir().getAbsolutePath()); cp = cp.replace("$CONFIG", ctx.getConfigDir().getAbsolutePath());
cp = cp.replace("$PLUGIN", pluginDir.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")) { if (app.delay < 0 && action.equals("start")) {
// this will throw exceptions // 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")) { } else if (app.delay == 0 || !action.equals("start")) {
// quick check, will throw ClassNotFoundException on error // quick check, will throw ClassNotFoundException on error
LoadClientAppsJob.testClient(app.className); LoadClientAppsJob.testClient(app.className, cl);
// run this guy now // run this guy now
LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log, pluginThreadGroup); LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log, pluginThreadGroup, cl);
} else { } else {
// quick check, will throw ClassNotFoundException on error // quick check, will throw ClassNotFoundException on error
LoadClientAppsJob.testClient(app.className); LoadClientAppsJob.testClient(app.className, cl);
// wait before firing it up // 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); ctx.jobQueue().addJob(job);
pluginJobs.get(pluginName).add(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. * but I don't see how to make it magically get used for everything.
* So add this to the whole JVM's classpath. * So add this to the whole JVM's classpath.
*/ */
/******
private static void addToClasspath(String classpath, String clientName, Log log) { private static void addToClasspath(String classpath, String clientName, Log log) {
StringTokenizer tok = new StringTokenizer(classpath, ","); StringTokenizer tok = new StringTokenizer(classpath, ",");
while (tok.hasMoreTokens()) { 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<URL> 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/ * http://jimlife.wordpress.com/2007/12/19/java-adding-new-classpath-at-runtime/

View File

@ -150,7 +150,7 @@ public class PluginUpdateHandler extends UpdateHandler {
public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
updateStatus("<b>" + _("Plugin downloaded") + "</b>"); updateStatus("<b>" + _("Plugin downloaded") + "</b>");
File f = new File(_updateFile); 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())) { if ((!appDir.exists()) && (!appDir.mkdir())) {
f.delete(); f.delete();
statusDone("<b>" + _("Cannot create plugin directory {0}", appDir.getAbsolutePath()) + "</b>"); statusDone("<b>" + _("Cannot create plugin directory {0}", appDir.getAbsolutePath()) + "</b>");

View File

@ -54,7 +54,7 @@ public class WebAppConfiguration implements WebApplicationContext.Configuration
I2PAppContext i2pContext = I2PAppContext.getGlobalContext(); I2PAppContext i2pContext = I2PAppContext.getGlobalContext();
File libDir = new File(i2pContext.getBaseDir(), "lib"); File libDir = new File(i2pContext.getBaseDir(), "lib");
// FIXME this only works if war is the same name as the plugin // 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); PluginUpdateHandler.PLUGIN_DIR + ctxPath);
File dir = libDir; File dir = libDir;

View File

@ -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 2010-05-02 zzz
* ByteCache: * ByteCache:
- Add a per-cache stat - Add a per-cache stat

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */ /** deprecated */
public final static String ID = "Monotone"; public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION; public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 2; public final static long BUILD = 3;
/** for example "-test" */ /** for example "-test" */
public final static String EXTRA = ""; public final static String EXTRA = "";

View File

@ -30,7 +30,7 @@ public class LoadClientAppsJob extends JobImpl {
_loaded = true; _loaded = true;
} }
List apps = ClientAppConfig.getClientApps(getContext()); 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"); _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"); System.err.println("Warning - No client apps or router console configured - we are just a router");
return; return;
@ -56,23 +56,26 @@ public class LoadClientAppsJob extends JobImpl {
private String _args[]; private String _args[];
private Log _log; private Log _log;
private ThreadGroup _threadGroup; private ThreadGroup _threadGroup;
private ClassLoader _cl;
public DelayedRunClient(RouterContext enclosingContext, String className, String clientName, String args[], long delay) { 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); super(enclosingContext);
_className = className; _className = className;
_clientName = clientName; _clientName = clientName;
_args = args; _args = args;
_log = enclosingContext.logManager().getLog(LoadClientAppsJob.class); _log = enclosingContext.logManager().getLog(LoadClientAppsJob.class);
_threadGroup = threadGroup; _threadGroup = threadGroup;
_cl = cl;
getTiming().setStartAfter(getContext().clock().now() + delay); getTiming().setStartAfter(getContext().clock().now() + delay);
} }
public String getName() { return "Delayed client job"; } public String getName() { return "Delayed client job"; }
public void runJob() { 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, * to propagate an error back to the user,
* since runClient() runs in a separate thread. * since runClient() runs in a separate thread.
* *
* @param cl can be null
* @since 0.7.13 * @since 0.7.13
*/ */
public static void testClient(String className) throws ClassNotFoundException { public static void testClient(String className, ClassLoader cl) throws ClassNotFoundException {
Class.forName(className); if (cl == null)
cl = ClassLoader.getSystemClassLoader();
Class.forName(className, false, cl);
} }
/** /**
* Run client in this thread. * 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 * @throws just about anything, caller would be wise to catch Throwable
* @since 0.7.13 * @since 0.7.13
*/ */
public static void runClientInline(String className, String clientName, String args[], Log log) throws Exception { 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)) if (log.shouldLog(Log.INFO))
log.info("Loading up the client application " + clientName + ": " + className + " " + Arrays.toString(args)); log.info("Loading up the client application " + clientName + ": " + className + " " + Arrays.toString(args));
if (args == null) if (args == null)
args = new String[0]; 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 method = cls.getMethod("main", new Class[] { String[].class });
method.invoke(cls, new Object[] { args }); method.invoke(cls, new Object[] { args });
} }
/** /**
* Run client in a new thread. * 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) { 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. * 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)) if (log.shouldLog(Log.INFO))
log.info("Loading up the client application " + clientName + ": " + className + " " + Arrays.toString(args)); log.info("Loading up the client application " + clientName + ": " + className + " " + Arrays.toString(args));
I2PThread t; I2PThread t;
if (threadGroup != null) 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 else
t = new I2PThread(new RunApp(className, clientName, args, log)); t = new I2PThread(new RunApp(className, clientName, args, log, cl));
if (clientName == null) if (clientName == null)
clientName = className + " client"; clientName = className + " client";
t.setName(clientName); t.setName(clientName);
t.setDaemon(true); t.setDaemon(true);
if (cl != null)
t.setContextClassLoader(cl);
t.start(); t.start();
} }
@ -181,7 +215,9 @@ public class LoadClientAppsJob extends JobImpl {
private String _appName; private String _appName;
private String _args[]; private String _args[];
private Log _log; 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; _className = className;
_appName = appName; _appName = appName;
if (args == null) if (args == null)
@ -189,10 +225,15 @@ public class LoadClientAppsJob extends JobImpl {
else else
_args = args; _args = args;
_log = log; _log = log;
if (cl == null)
_cl = ClassLoader.getSystemClassLoader();
else
_cl = cl;
} }
public void run() { public void run() {
try { try {
Class cls = Class.forName(_className); Class cls = Class.forName(_className, true, _cl);
Method method = cls.getMethod("main", new Class[] { String[].class }); Method method = cls.getMethod("main", new Class[] { String[].class });
method.invoke(cls, new Object[] { _args }); method.invoke(cls, new Object[] { _args });
} catch (Throwable t) { } catch (Throwable t) {