start of a plugin starter

This commit is contained in:
zzz
2010-02-07 13:32:49 +00:00
parent 040f3e016e
commit 58adccfd4a
9 changed files with 268 additions and 36 deletions

View File

@@ -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");
@@ -200,8 +196,6 @@ public class ConfigClientsHandler extends FormHandler {
}
return;
}
}
}
addFormError(_("Failed to find server."));
}

View File

@@ -68,6 +68,25 @@ public class ConfigClientsHelper extends HelperBase {
return buf.toString();
}
public String getForm3() {
StringBuilder buf = new StringBuilder(1024);
buf.append("<table>\n");
buf.append("<tr><th align=\"right\">" + _("Plugin") + "</th><th>" + _("Run at Startup?") + "</th><th>" + _("Start Now") + "</th><th align=\"left\">" + _("Description") + "</th></tr>\n");
Properties props = PluginStarter.pluginProperties();
Set<String> keys = new TreeSet(props.keySet());
for (Iterator<String> 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("</table>\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) {

View File

@@ -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<ClientAppConfig> 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<ClientAppConfig> 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));
}
}
}
}

View File

@@ -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("<b>" + _("Plugin from {0} is corrupt", url) + "</b>");
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("<b>" + _("Downloaded plugin is not for upgrading but the plugin is already installed", url) + "</b>");
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("<b>" + _("Plugin is for upgrades only, but the plugin is not installed", url) + "</b>");
return;
@@ -309,9 +310,22 @@ public class PluginUpdateHandler extends UpdateHandler {
}
to.delete();
updateStatus("<b>" + _("Plugin successfully installed in {0}", destDir.getAbsolutePath()) + "</b>");
if (Boolean.valueOf(props.getProperty("dont-start-at-install")).booleanValue()) {
if (Boolean.valueOf(props.getProperty("router-restart-required")).booleanValue())
updateStatus("<b>" + _("Plugin {0} successfully installed, router restart required", appName) + "</b>");
else
updateStatus("<b>" + _("Plugin {0} successfully installed", appName) + "</b>");
} else {
// start everything
try {
if (PluginStarter.startPlugin(_context, appName))
updateStatus("<b>" + _("Plugin {0} started", appName) + "</b>");
else
updateStatus("<b>" + _("Failed to start plugin {0}, check logs", appName) + "</b>");
} catch (Exception e) {
updateStatus("<b>" + _("Failed to start plugin {0}:", appName) + ' ' + e + "</b>");
}
}
}
@Override

View File

@@ -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<RouterContext> 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<RouterContext> 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"));
}
}
}

View File

@@ -54,6 +54,12 @@ button span.hide{
<i><%=intl._("All changes require restart to take effect.")%></i>
</p><hr><div class="formaction">
<input type="submit" name="action" value="<%=intl._("Save WebApp Configuration")%>" />
</div></div><h3><a name="webapp"></a><%=intl._("Plugin Configuration")%></h3><p>
<%=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.")%>
</p><div class="wideload"><p>
<jsp:getProperty name="clientshelper" property="form3" />
</p><hr><div class="formaction">
<input type="submit" name="action" value="<%=intl._("Save Plugin Configuration")%>" />
</div></div><h3><a name="plugin"></a><%=intl._("Plugin Installation")%></h3><p>
<%=intl._("To install a plugin, enter the URL to download the plugin from:")%>
</p><div class="wideload"><p>

View File

@@ -62,7 +62,7 @@ public class RouterContext extends I2PAppContext {
private Calculator _capacityCalc;
private static List _contexts = new ArrayList(1);
private static List<RouterContext> _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<RouterContext> listContexts() { return _contexts; }
/** what router is this context working for? */
public Router router() { return _router; }

View File

@@ -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<ClientAppConfig> getClientApps(RouterContext ctx) {
Properties clientApps = getClientAppProps(ctx);
return getClientApps(clientApps);
}
/*
* Go through the properties, and return a List of ClientAppConfig structures
*/
public static List<ClientAppConfig> 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<ClientAppConfig> getClientApps(Properties clientApps) {
List<ClientAppConfig> rv = new ArrayList(8);
int i = 0;
while (true) {

View File

@@ -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() {