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 cf81ee022..6d0a4faa9 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 acef26f72..3633a5335 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("
" + _("Plugin") + "
" + _("Run at Startup?") + "
" + _("Start Now") + "
" + _("Description") + "
\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("
\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 000000000..006192d81
--- /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 8045c6e80..814fbff86 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 31d854aea..07d3c2ed7 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 daf3bcf87..0fcb2e2e9 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 dfa9d7c21..b4fa75381 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 54342a895..b08e7577d 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 0f86f5b61..663c56025 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() {