From 58adccfd4afad7ce2e3c5954914610304e7dc965 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 7 Feb 2010 13:32:49 +0000 Subject: [PATCH] start of a plugin starter --- .../i2p/router/web/ConfigClientsHandler.java | 20 +-- .../i2p/router/web/ConfigClientsHelper.java | 19 ++ .../src/net/i2p/router/web/PluginStarter.java | 169 ++++++++++++++++++ .../i2p/router/web/PluginUpdateHandler.java | 36 ++-- .../i2p/router/web/RouterConsoleRunner.java | 21 ++- apps/routerconsole/jsp/configclients.jsp | 6 + .../src/net/i2p/router/RouterContext.java | 4 +- .../i2p/router/startup/ClientAppConfig.java | 21 +++ .../i2p/router/startup/LoadClientAppsJob.java | 8 +- 9 files changed, 268 insertions(+), 36 deletions(-) create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java index cf81ee0227..6d0a4faa9f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -1,7 +1,6 @@ package net.i2p.router.web; import java.io.File; -import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -13,7 +12,6 @@ import net.i2p.router.startup.ClientAppConfig; import net.i2p.router.startup.LoadClientAppsJob; import net.i2p.util.Log; -import org.mortbay.http.HttpListener; import org.mortbay.jetty.Server; /** @@ -180,16 +178,14 @@ public class ConfigClientsHandler extends FormHandler { addFormNotice(_("WebApp configuration saved successfully - restart required to take effect.")); } - // Big hack for the moment, not using properties for directory and port - // Go through all the Jetty servers, find the one serving port 7657, - // requested and add the .war to that one + /** + * Big hack for the moment, not using properties for directory and port + * Go through all the Jetty servers, find the one serving port 7657, + * requested and add the .war to that one + */ private void startWebApp(String app) { - Collection c = Server.getHttpServers(); - for (int i = 0; i < c.size(); i++) { - Server s = (Server) c.toArray()[i]; - HttpListener[] hl = s.getListeners(); - for (int j = 0; j < hl.length; j++) { - if (hl[j].getPort() == 7657) { + Server s = PluginStarter.getConsoleServer(); + if (s != null) { try { File path = new File(_context.getBaseDir(), "webapps"); path = new File(path, app + ".war"); @@ -199,8 +195,6 @@ public class ConfigClientsHandler extends FormHandler { addFormError(_("Failed to start") + ' ' + _(app) + " " + ioe + '.'); } return; - } - } } addFormError(_("Failed to find server.")); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index acef26f721..3633a53356 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -68,6 +68,25 @@ public class ConfigClientsHelper extends HelperBase { return buf.toString(); } + public String getForm3() { + StringBuilder buf = new StringBuilder(1024); + buf.append("\n"); + buf.append("\n"); + Properties props = PluginStarter.pluginProperties(); + Set keys = new TreeSet(props.keySet()); + for (Iterator iter = keys.iterator(); iter.hasNext(); ) { + String name = iter.next(); + if (name.startsWith(PluginStarter.PREFIX) && name.endsWith(PluginStarter.ENABLED)) { + String app = name.substring(PluginStarter.PREFIX.length(), name.lastIndexOf(PluginStarter.ENABLED)); + String val = props.getProperty(name); + renderForm(buf, app, app, !"addressbook".equals(app), + "true".equals(val), false, app, false, false); + } + } + buf.append("
" + _("Plugin") + "" + _("Run at Startup?") + "" + _("Start Now") + "" + _("Description") + "
\n"); + return buf.toString(); + } + /** ro trumps edit and showEditButton */ private void renderForm(StringBuilder buf, String index, String name, boolean urlify, boolean enabled, boolean ro, String desc, boolean edit, boolean showEditButton) { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java new file mode 100644 index 0000000000..006192d81c --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -0,0 +1,169 @@ +package net.i2p.router.web; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.StringTokenizer; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.router.RouterContext; +import net.i2p.router.startup.ClientAppConfig; +import net.i2p.router.startup.LoadClientAppsJob; +import net.i2p.util.Log; + +import org.mortbay.http.HttpListener; +import org.mortbay.jetty.Server; + + +/** + * Start plugins that are already installed + * + * @since 0.7.12 + * @author zzz + */ +public class PluginStarter implements Runnable { + private RouterContext _context; + static final String PREFIX = "plugin."; + static final String ENABLED = ".startOnLoad"; + + public PluginStarter(RouterContext ctx) { + _context = ctx; + } + + public void run() { + startPlugins(_context); + } + + static void startPlugins(RouterContext ctx) { + Properties props = pluginProperties(); + for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + if (name.startsWith(PluginStarter.PREFIX) && name.endsWith(PluginStarter.ENABLED)) { + if (Boolean.valueOf(props.getProperty(name)).booleanValue()) { + String app = name.substring(PluginStarter.PREFIX.length(), name.lastIndexOf(PluginStarter.ENABLED)); + try { + if (!startPlugin(ctx, app)) + System.err.println("Failed to start plugin: " + app); + } catch (Exception e) { + System.err.println("Failed to start plugin: " + app + ' ' + e); + } + } + } + } + } + + /** @return true on success */ + static boolean startPlugin(RouterContext ctx, String appName) throws Exception { + File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); + if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { + System.err.println("Cannot start nonexistent plugin: " + appName); + return false; + } + + // load and start things in clients.config + File clientConfig = new File(pluginDir, "clients.config"); + if (clientConfig.exists()) { + Properties props = new Properties(); + DataHelper.loadProps(props, clientConfig); + List clients = ClientAppConfig.getClientApps(clientConfig); + runClientApps(ctx, pluginDir, clients); + } + + // start console webapps in console/webapps + Server server = getConsoleServer(); + if (server != null) { + File consoleDir = new File(pluginDir, "console"); + Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath()); + File webappDir = new File(pluginDir, "webapps"); + String fileNames[] = webappDir.list(RouterConsoleRunner.WarFilenameFilter.instance()); + if (fileNames != null) { + for (int i = 0; i < fileNames.length; i++) { + try { + String warName = fileNames[i].substring(0, fileNames[i].lastIndexOf(".war")); + // check for duplicates in $I2P ? + String enabled = props.getProperty(PREFIX + warName + ENABLED); + if (! "false".equals(enabled)) { + String path = new File(webappDir, fileNames[i]).getCanonicalPath(); + WebAppStarter.startWebApp(ctx, server, warName, path); + } + } catch (IOException ioe) { + System.err.println("Error resolving '" + fileNames[i] + "' in '" + webappDir); + } + } + } + } + + // add translation jars in console/locale + + // add themes in console/themes + + // add summary bar link + + return true; + } + + /** this auto-adds a propery for every dir in the plugin directory */ + public static Properties pluginProperties() { + File dir = I2PAppContext.getGlobalContext().getConfigDir(); + Properties rv = new Properties(); + File cfgFile = new File(dir, "plugins.config"); + + try { + DataHelper.loadProps(rv, cfgFile); + } catch (IOException ioe) {} + + File pluginDir = new File(I2PAppContext.getGlobalContext().getAppDir(), PluginUpdateHandler.PLUGIN_DIR); + File[] files = pluginDir.listFiles(); + if (files == null) + return rv; + for (int i = 0; i < files.length; i++) { + String name = files[i].getName(); + String prop = PREFIX + name + ENABLED; + if (files[i].isDirectory() && rv.getProperty(prop) == null) + rv.setProperty(prop, "true"); + } + return rv; + } + + /** see comments in ConfigClientsHandler */ + static Server getConsoleServer() { + Collection c = Server.getHttpServers(); + for (int i = 0; i < c.size(); i++) { + Server s = (Server) c.toArray()[i]; + HttpListener[] hl = s.getListeners(); + for (int j = 0; j < hl.length; j++) { + if (hl[j].getPort() == 7657) + return s; + } + } + return null; + } + + private static void runClientApps(RouterContext ctx, File pluginDir, List apps) { + Log log = ctx.logManager().getLog(PluginStarter.class); + for(ClientAppConfig app : apps) { + if (app.disabled) + continue; + String argVal[] = LoadClientAppsJob.parseArgs(app.args); + // do this after parsing so we don't need to worry about quoting + for (int i = 0; i < argVal.length; i++) { + if (argVal[i].indexOf("$") >= 0) { + argVal[i] = argVal[i].replace("$I2P", ctx.getBaseDir().getAbsolutePath()); + argVal[i] = argVal[i].replace("$CONFIG", ctx.getConfigDir().getAbsolutePath()); + argVal[i] = argVal[i].replace("$PLUGIN", pluginDir.getAbsolutePath()); + } + } + if (app.delay == 0) { + // run this guy now + LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log); + } else { + // wait before firing it up + ctx.jobQueue().addJob(new LoadClientAppsJob.DelayedRunClient(ctx, app.className, app.clientName, argVal, app.delay)); + } + } + } +} 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 8045c6e801..814fbff86a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -20,13 +20,13 @@ import net.i2p.util.VersionComparator; /** * Download and install a plugin. * A plugin is a standard .sud file with a 40-byte signature, - * a 16-byte version (which is ignored), and a .zip file. + * a 16-byte version, and a .zip file. * Unlike for router updates, we need not have the public key * for the signature in advance. * * The zip file must have a standard directory layout, with - * a install.properties file at the top level. - * The properties file contains properties for the package name, version, + * a plugin.config file at the top level. + * The config file contains properties for the package name, version, * signing public key, and other settings. * The zip file will typically contain a webapps/ or lib/ dir, * and a webapps.config and/or clients.config file. @@ -159,7 +159,7 @@ public class PluginUpdateHandler extends UpdateHandler { updateStatus("" + _("Plugin from {0} is corrupt", url) + ""); return; } - File installProps = new File(tempDir, "install.properties"); + File installProps = new File(tempDir, "plugin.config"); Properties props = new OrderedProperties(); try { DataHelper.loadProps(props, installProps); @@ -220,6 +220,8 @@ public class PluginUpdateHandler extends UpdateHandler { return; } + // todo compare sud version with property version + String minVersion = props.getProperty("min-i2p-version"); if (minVersion != null && (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) { @@ -236,17 +238,16 @@ public class PluginUpdateHandler extends UpdateHandler { return; } - boolean isUpdate = Boolean.valueOf(props.getProperty("update")).booleanValue(); File destDir = new File(appDir, appName); if (destDir.exists()) { - if (!isUpdate) { + if (Boolean.valueOf(props.getProperty("install-only")).booleanValue()) { to.delete(); updateStatus("" + _("Downloaded plugin is not for upgrading but the plugin is already installed", url) + ""); return; } // compare previous version - File oldPropFile = new File(destDir, "install.properties"); + File oldPropFile = new File(destDir, "plugin.config"); Properties oldProps = new OrderedProperties(); try { DataHelper.loadProps(oldProps, oldPropFile); @@ -289,7 +290,7 @@ public class PluginUpdateHandler extends UpdateHandler { // check if it is running now and stop it? } else { - if (isUpdate) { + if (Boolean.valueOf(props.getProperty("update-only")).booleanValue()) { to.delete(); updateStatus("" + _("Plugin is for upgrades only, but the plugin is not installed", url) + ""); return; @@ -309,9 +310,22 @@ public class PluginUpdateHandler extends UpdateHandler { } to.delete(); - updateStatus("" + _("Plugin successfully installed in {0}", destDir.getAbsolutePath()) + ""); - - // start everything + if (Boolean.valueOf(props.getProperty("dont-start-at-install")).booleanValue()) { + if (Boolean.valueOf(props.getProperty("router-restart-required")).booleanValue()) + updateStatus("" + _("Plugin {0} successfully installed, router restart required", appName) + ""); + else + updateStatus("" + _("Plugin {0} successfully installed", appName) + ""); + } else { + // start everything + try { + if (PluginStarter.startPlugin(_context, appName)) + updateStatus("" + _("Plugin {0} started", appName) + ""); + else + updateStatus("" + _("Failed to start plugin {0}, check logs", appName) + ""); + } catch (Exception e) { + updateStatus("" + _("Failed to start plugin {0}:", appName) + ' ' + e + ""); + } + } } @Override diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java index 31d854aeae..07d3c2ed75 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -181,13 +181,17 @@ public class RouterConsoleRunner { } NewsFetcher fetcher = NewsFetcher.getInstance(I2PAppContext.getGlobalContext()); - Thread t = new I2PAppThread(fetcher, "NewsFetcher"); - t.setDaemon(true); + Thread t = new I2PAppThread(fetcher, "NewsFetcher", true); t.start(); - Thread st = new I2PAppThread(new StatSummarizer(), "StatSummarizer"); - st.setDaemon(true); - st.start(); + t = new I2PAppThread(new StatSummarizer(), "StatSummarizer", true); + t.start(); + + List contexts = RouterContext.listContexts(); + if (contexts != null) { + t = new I2PAppThread(new PluginStarter(contexts.get(0)), "PluginStarter", true); + t.start(); + } } static void initialize(WebApplicationContext context) { @@ -206,10 +210,10 @@ public class RouterConsoleRunner { } static String getPassword() { - List contexts = RouterContext.listContexts(); + List contexts = RouterContext.listContexts(); if (contexts != null) { for (int i = 0; i < contexts.size(); i++) { - RouterContext ctx = (RouterContext)contexts.get(i); + RouterContext ctx = contexts.get(i); String password = ctx.getProperty("consolePassword"); if (password != null) { password = password.trim(); @@ -267,11 +271,12 @@ public class RouterConsoleRunner { } } - private static class WarFilenameFilter implements FilenameFilter { + static class WarFilenameFilter implements FilenameFilter { private static final WarFilenameFilter _filter = new WarFilenameFilter(); public static WarFilenameFilter instance() { return _filter; } public boolean accept(File dir, String name) { return (name != null) && (name.endsWith(".war") && !name.equals(ROUTERCONSOLE + ".war")); } } + } diff --git a/apps/routerconsole/jsp/configclients.jsp b/apps/routerconsole/jsp/configclients.jsp index daf3bcf87b..0fcb2e2e97 100644 --- a/apps/routerconsole/jsp/configclients.jsp +++ b/apps/routerconsole/jsp/configclients.jsp @@ -54,6 +54,12 @@ button span.hide{ <%=intl._("All changes require restart to take effect.")%>


" /> +

<%=intl._("Plugin Configuration")%>

+ <%=intl._("The plugins listed below are started by the webConsole client and run in the same JVM as the router. They are usually web applications accessible through the router console.")%> +

+ +


+ " />

<%=intl._("Plugin Installation")%>

<%=intl._("To install a plugin, enter the URL to download the plugin from:")%>

diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index dfa9d7c210..b4fa753812 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -62,7 +62,7 @@ public class RouterContext extends I2PAppContext { private Calculator _capacityCalc; - private static List _contexts = new ArrayList(1); + private static List _contexts = new ArrayList(1); public RouterContext(Router router) { this(router, null); } public RouterContext(Router router, Properties envProps) { @@ -148,7 +148,7 @@ public class RouterContext extends I2PAppContext { * context is created or a router is shut down. * */ - public static List listContexts() { return _contexts; } + public static List listContexts() { return _contexts; } /** what router is this context working for? */ public Router router() { return _router; } diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java index 54342a8951..b08e7577d4 100644 --- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java +++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Properties; @@ -72,6 +73,26 @@ public class ClientAppConfig { */ public static List getClientApps(RouterContext ctx) { Properties clientApps = getClientAppProps(ctx); + return getClientApps(clientApps); + } + + /* + * Go through the properties, and return a List of ClientAppConfig structures + */ + public static List getClientApps(File cfgFile) { + Properties clientApps = new Properties(); + try { + DataHelper.loadProps(clientApps, cfgFile); + } catch (IOException ioe) { + return Collections.EMPTY_LIST; + } + return getClientApps(clientApps); + } + + /* + * Go through the properties, and return a List of ClientAppConfig structures + */ + private static List getClientApps(Properties clientApps) { List rv = new ArrayList(8); int i = 0; while (true) { diff --git a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java index 0f86f5b61b..663c56025b 100644 --- a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java +++ b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java @@ -48,16 +48,20 @@ public class LoadClientAppsJob extends JobImpl { } } } - private class DelayedRunClient extends JobImpl { + + public static class DelayedRunClient extends JobImpl { private String _className; private String _clientName; private String _args[]; + private Log _log; + public DelayedRunClient(RouterContext enclosingContext, String className, String clientName, String args[], long delay) { super(enclosingContext); _className = className; _clientName = clientName; _args = args; - getTiming().setStartAfter(LoadClientAppsJob.this.getContext().clock().now() + delay); + _log = enclosingContext.logManager().getLog(LoadClientAppsJob.class); + getTiming().setStartAfter(getContext().clock().now() + delay); } public String getName() { return "Delayed client job"; } public void runJob() {