diff --git a/core/java/src/net/i2p/app/ClientApp.java b/core/java/src/net/i2p/app/ClientApp.java
new file mode 100644
index 000000000..8900630bc
--- /dev/null
+++ b/core/java/src/net/i2p/app/ClientApp.java
@@ -0,0 +1,58 @@
+package net.i2p.app;
+
+import net.i2p.I2PAppContext;
+
+/**
+ * If a class started via clients.config implements this interface,
+ * it will be used to manage the client, instead of starting with main()
+ *
+ * Clients implementing this interface MUST provide the following constructor:
+ *
+ * public MyClientApp(I2PAppContext context, ClientAppManager listener, String[] args) {...}
+ *
+ * All parameters are non-null.
+ * This constructor is for instantiation only.
+ * Do not take a long time. Do not block. Never start threads or processes in it.
+ * The ClientAppState of the returned object must be INITIALIZED,
+ * or else throw something.
+ * The startup() method will be called next.
+ *
+ * Never ever hold a static reference to the context or anything derived from it.
+ *
+ * @since 0.9.4
+ */
+public interface ClientApp {
+
+ /**
+ * Do not take a long time. Do not block. Start threads here if necessary.
+ * Client must call ClientAppManager.notify() at least once within this
+ * method to change the state from INITIALIZED to something else.
+ * Will not be called multiple times on the same object.
+ */
+ public void startup() throws Throwable;
+
+ /**
+ * Do not take a long time. Do not block. Use a thread if necessary.
+ * If previously running, client must call ClientAppManager.notify() at least once within this
+ * method to change the state to STOPPING or STOPPED.
+ * May be called multiple times on the same object, in any state.
+ */
+ public void shutdown(String[] args) throws Throwable;
+
+ /**
+ * The current state of the ClientApp.
+ */
+ public ClientAppState getState();
+
+ /**
+ * The generic name of the ClientApp, used for registration,
+ * e.g. "console". Do not translate.
+ */
+ public String getName();
+
+ /**
+ * The dislplay name of the ClientApp, used in user interfaces.
+ * The app must translate.
+ */
+ public String getDisplayName();
+}
diff --git a/core/java/src/net/i2p/app/ClientAppManager.java b/core/java/src/net/i2p/app/ClientAppManager.java
new file mode 100644
index 000000000..e246254b1
--- /dev/null
+++ b/core/java/src/net/i2p/app/ClientAppManager.java
@@ -0,0 +1,51 @@
+package net.i2p.app;
+
+/**
+ * Notify the router of events, and provide methods for
+ * client apps to find each other.
+ *
+ * @since 0.9.4
+ */
+public interface ClientAppManager {
+
+ /**
+ * Must be called on all state transitions except
+ * from UNINITIALIZED to INITIALIZED.
+ *
+ * @param app non-null
+ * @param state non-null
+ * @param message may be null
+ * @param e may be null
+ */
+ public void notify(ClientApp app, ClientAppState state, String message, Exception e);
+
+ /**
+ * Register with the manager under the given name,
+ * so that other clients may find it.
+ * Only required for apps used by other apps.
+ *
+ * @param app non-null
+ * @param name non-null
+ * @return true if successful, false if duplicate name
+ */
+ public boolean register(ClientApp app);
+
+ /**
+ * Unregister with the manager. Name must be the same as that from register().
+ * Only required for apps used by other apps.
+ *
+ * @param app non-null
+ * @param name non-null
+ */
+ public void unregister(ClientApp app);
+
+ /**
+ * Get a registered app.
+ * Only used for apps finding other apps.
+ *
+ * @param app non-null
+ * @param name non-null
+ * @return client app or null
+ */
+ public ClientApp getRegisteredApp(String name);
+}
diff --git a/core/java/src/net/i2p/app/ClientAppState.java b/core/java/src/net/i2p/app/ClientAppState.java
new file mode 100644
index 000000000..e4563aabf
--- /dev/null
+++ b/core/java/src/net/i2p/app/ClientAppState.java
@@ -0,0 +1,25 @@
+package net.i2p.app;
+
+/**
+ * Status of a client application.
+ * ClientAppManager.notify() must be called on all state transitions except
+ * from UNINITIALIZED to INITIALIZED.
+ *
+ * @since 0.9.4
+ */
+public enum ClientAppState {
+ /** initial value */
+ UNINITIALIZED,
+ /** after constructor is complete */
+ INITIALIZED,
+ STARTING,
+ START_FAILED,
+ RUNNING,
+ STOPPING,
+ /** stopped normally */
+ STOPPED,
+ /** stopped abnormally */
+ CRASHED,
+ /** forked as a new process, status unknown from now on */
+ FORKED
+}
diff --git a/core/java/src/net/i2p/app/package.html b/core/java/src/net/i2p/app/package.html
new file mode 100644
index 000000000..1be4e8331
--- /dev/null
+++ b/core/java/src/net/i2p/app/package.html
@@ -0,0 +1,18 @@
+
+
+
+Interfaces for classes to be started and stopped via clients.config.
+Classes implementing the ClientApp interface will be controlled with
+the that interface instead of being started with main().
+
+
+The benefits for clients using this interface:
+
+- Get the current context via the constructor
+
- Complete life cycle management by the router
+
- Avoid the need for static references
+
- Ability to find other clients without using static references
+
+
+
+
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 421539ce3..aff11b052 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -18,6 +18,7 @@ import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.router.peermanager.PeerManagerFacadeImpl;
import net.i2p.router.peermanager.ProfileManagerImpl;
import net.i2p.router.peermanager.ProfileOrganizer;
+import net.i2p.router.startup.RouterAppManager;
import net.i2p.router.transport.CommSystemFacadeImpl;
import net.i2p.router.transport.FIFOBandwidthLimiter;
import net.i2p.router.transport.OutboundMessageRegistry;
@@ -58,14 +59,22 @@ public class RouterContext extends I2PAppContext {
private MessageValidator _messageValidator;
//private MessageStateMonitor _messageStateMonitor;
private RouterThrottle _throttle;
+ private RouterAppManager _appManager;
private final Set _finalShutdownTasks;
// split up big lock on this to avoid deadlocks
+ private volatile boolean _initialized;
private final Object _lock1 = new Object(), _lock2 = new Object();
private static final List _contexts = new CopyOnWriteArrayList();
+ /**
+ * Caller MUST call initAll() after instantiation.
+ */
public RouterContext(Router router) { this(router, null); }
+ /**
+ * Caller MUST call initAll() after instantiation.
+ */
public RouterContext(Router router, Properties envProps) {
super(filterProps(envProps));
_router = router;
@@ -141,7 +150,9 @@ public class RouterContext extends I2PAppContext {
}
- public void initAll() {
+ public synchronized void initAll() {
+ if (_initialized)
+ throw new IllegalStateException();
if (getBooleanProperty("i2p.dummyClientFacade"))
System.err.println("i2p.dummyClientFacade currently unsupported");
_clientManagerFacade = new ClientManagerFacadeImpl(this);
@@ -182,6 +193,8 @@ public class RouterContext extends I2PAppContext {
_messageValidator = new MessageValidator(this);
_throttle = new RouterThrottleImpl(this);
//_throttle = new RouterDoSThrottle(this);
+ _appManager = new RouterAppManager(this);
+ _initialized = true;
}
/**
@@ -495,4 +508,13 @@ public class RouterContext extends I2PAppContext {
public InternalClientManager internalClientManager() {
return _clientManagerFacade;
}
+
+ /**
+ * The RouterAppManager.
+ * @return the manager
+ * @since 0.9.4
+ */
+ public RouterAppManager clientAppManager() {
+ return _appManager;
+ }
}
diff --git a/router/java/src/net/i2p/router/app/RouterApp.java b/router/java/src/net/i2p/router/app/RouterApp.java
new file mode 100644
index 000000000..84cf68562
--- /dev/null
+++ b/router/java/src/net/i2p/router/app/RouterApp.java
@@ -0,0 +1,24 @@
+package net.i2p.router.app;
+
+import net.i2p.app.ClientApp;
+
+/**
+ * If a class started via clients.config implements this interface,
+ * it will be used to manage the client, instead of starting with main()
+ *
+ * Clients implementing this interface MUST provide the following constructor:
+ *
+ * public MyClientApp(RouterContext context, ClientAppManager listener, String[] args) {...}
+ *
+ * All parameters are non-null.
+ * This constructor is for instantiation only.
+ * Do not take a long time. Do not block. Never start threads or processes in it.
+ * The ClientAppState of the returned object must be INITIALIZED,
+ * or else throw something.
+ * The startup() method will be called next.
+ *
+ * Never ever hold a static reference to the context or anything derived from it.
+ *
+ * @since 0.9.4
+ */
+public interface RouterApp extends ClientApp {}
diff --git a/router/java/src/net/i2p/router/app/package.html b/router/java/src/net/i2p/router/app/package.html
new file mode 100644
index 000000000..e35eea22f
--- /dev/null
+++ b/router/java/src/net/i2p/router/app/package.html
@@ -0,0 +1,18 @@
+
+
+
+Interface for classes to be started and stopped via clients.config.
+Classes implementing the RouterApp interface will be controlled with
+the that interface instead of being started with main().
+
+
+The benefits for clients using this interface:
+
+- Get the current context via the constructor
+
- Complete life cycle management by the router
+
- Avoid the need for static references
+
- Ability to find other clients without using static references
+
+
+
+
diff --git a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
index 3f3f9d5e6..9f0ce8c82 100644
--- a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
+++ b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
@@ -1,12 +1,17 @@
package net.i2p.router.startup;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
+import net.i2p.I2PAppContext;
+import net.i2p.app.ClientApp;
+import net.i2p.app.ClientAppManager;
import net.i2p.router.JobImpl;
import net.i2p.router.RouterContext;
+import net.i2p.router.app.RouterApp;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
@@ -24,6 +29,7 @@ public class LoadClientAppsJob extends JobImpl {
super(ctx);
_log = ctx.logManager().getLog(LoadClientAppsJob.class);
}
+
public void runJob() {
synchronized (LoadClientAppsJob.class) {
if (_loaded) return;
@@ -42,7 +48,7 @@ public class LoadClientAppsJob extends JobImpl {
String argVal[] = parseArgs(app.args);
if (app.delay <= 0) {
// run this guy now
- runClient(app.className, app.clientName, argVal, _log);
+ runClient(app.className, app.clientName, argVal, getContext(), _log);
} else {
// wait before firing it up
getContext().jobQueue().addJob(new DelayedRunClient(getContext(), app.className, app.clientName, argVal, app.delay));
@@ -73,9 +79,11 @@ public class LoadClientAppsJob extends JobImpl {
_cl = cl;
getTiming().setStartAfter(getContext().clock().now() + delay);
}
+
public String getName() { return "Delayed client job"; }
+
public void runJob() {
- runClient(_className, _clientName, _args, _log, _threadGroup, _cl);
+ runClient(_className, _clientName, _args, getContext(), _log, _threadGroup, _cl);
}
}
@@ -179,8 +187,8 @@ public class LoadClientAppsJob extends JobImpl {
* @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, null);
+ public static void runClient(String className, String clientName, String args[], RouterContext ctx, Log log) {
+ runClient(className, clientName, args, ctx, log, null, null);
}
/**
@@ -192,15 +200,15 @@ public class LoadClientAppsJob extends JobImpl {
* @param cl can be null
* @since 0.7.13
*/
- public static void runClient(String className, String clientName, String args[], Log log,
+ public static void runClient(String className, String clientName, String args[], RouterContext ctx, 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, cl));
+ t = new I2PThread(threadGroup, new RunApp(className, clientName, args, ctx, log, cl));
else
- t = new I2PThread(new RunApp(className, clientName, args, log, cl));
+ t = new I2PThread(new RunApp(className, clientName, args, ctx, log, cl));
if (clientName == null)
clientName = className + " client";
t.setName(clientName);
@@ -214,16 +222,18 @@ public class LoadClientAppsJob extends JobImpl {
private final String _className;
private final String _appName;
private final String _args[];
+ private final RouterContext _ctx;
private final Log _log;
private final ClassLoader _cl;
- public RunApp(String className, String appName, String args[], Log log, ClassLoader cl) {
+ public RunApp(String className, String appName, String args[], RouterContext ctx, Log log, ClassLoader cl) {
_className = className;
_appName = appName;
if (args == null)
_args = new String[0];
else
_args = args;
+ _ctx = ctx;
_log = log;
if (cl == null)
_cl = ClassLoader.getSystemClassLoader();
@@ -234,14 +244,53 @@ public class LoadClientAppsJob extends JobImpl {
public void run() {
try {
Class cls = Class.forName(_className, true, _cl);
- Method method = cls.getMethod("main", new Class[] { String[].class });
- method.invoke(cls, new Object[] { _args });
+ if (isRouterApp(cls)) {
+ Constructor con = cls.getConstructor(RouterContext.class, ClientAppManager.class, String[].class);
+ RouterAppManager mgr = _ctx.clientAppManager();
+ Object[] conArgs = new Object[] {_ctx, _ctx.clientAppManager(), _args};
+ RouterApp app = (RouterApp) con.newInstance(conArgs);
+ mgr.addAndStart(app);
+ } else if (isClientApp(cls)) {
+ Constructor con = cls.getConstructor(I2PAppContext.class, ClientAppManager.class, String[].class);
+ RouterAppManager mgr = _ctx.clientAppManager();
+ Object[] conArgs = new Object[] {_ctx, _ctx.clientAppManager(), _args};
+ ClientApp app = (ClientApp) con.newInstance(conArgs);
+ mgr.addAndStart(app);
+ } else {
+ Method method = cls.getMethod("main", new Class[] { String[].class });
+ method.invoke(cls, new Object[] { _args });
+ }
} catch (Throwable t) {
_log.log(Log.CRIT, "Error starting up the client class " + _className, t);
}
if (_log.shouldLog(Log.INFO))
_log.info("Done running client application " + _appName);
}
+
+ private static boolean isRouterApp(Class cls) {
+ return isInterface(cls, RouterApp.class);
+ }
+
+ private static boolean isClientApp(Class cls) {
+ return isInterface(cls, ClientApp.class);
+ }
+
+ private static boolean isInterface(Class cls, Class intfc) {
+ try {
+ Class[] intfcs = cls.getInterfaces();
+ for (int i = 0; i < intfcs.length; i++) {
+ if (intfcs[i] == intfc)
+ return true;
+ }
+ } catch (Throwable t) {}
+ return false;
+ }
+
+
+
+
+
+
}
public String getName() { return "Load up any client applications"; }
diff --git a/router/java/src/net/i2p/router/startup/RouterAppManager.java b/router/java/src/net/i2p/router/startup/RouterAppManager.java
new file mode 100644
index 000000000..45f27fefb
--- /dev/null
+++ b/router/java/src/net/i2p/router/startup/RouterAppManager.java
@@ -0,0 +1,131 @@
+package net.i2p.router.startup;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.i2p.app.*;
+import static net.i2p.app.ClientAppState.*;
+import net.i2p.router.RouterContext;
+import net.i2p.util.ConcurrentHashSet;
+import net.i2p.util.Log;
+
+/**
+ * Notify the router of events, and provide methods for
+ * client apps to find each other.
+ *
+ * @since 0.9.4
+ */
+public class RouterAppManager implements ClientAppManager {
+
+ private final RouterContext _context;
+ private final Log _log;
+ private final Set _clients;
+ private final ConcurrentHashMap _registered;
+
+ public RouterAppManager(RouterContext ctx) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(RouterAppManager.class);
+ _clients = new ConcurrentHashSet(16);
+ _registered = new ConcurrentHashMap(8);
+ }
+
+ public void addAndStart(ClientApp app) {
+ _clients.add(app);
+ try {
+ app.startup();
+ } catch (Throwable t) {
+ _clients.remove(app);
+ _log.error("Client " + app + " failed to start");
+ }
+ }
+
+ // ClientAppManager methods
+
+ /**
+ * Must be called on all state transitions except
+ * from UNINITIALIZED to INITIALIZED.
+ *
+ * @param app non-null
+ * @param state non-null
+ * @param message may be null
+ * @param e may be null
+ */
+ public void notify(ClientApp app, ClientAppState state, String message, Exception e) {
+ switch(state) {
+ case UNINITIALIZED:
+ case INITIALIZED:
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Client " + app.getDisplayName() + " called notify for" + state);
+ break;
+
+ case STARTING:
+ case RUNNING:
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Client " + app.getDisplayName() + " called notify for" + state);
+ break;
+
+ case FORKED:
+ case STOPPING:
+ case STOPPED:
+ _clients.remove(app);
+ _registered.remove(app.getName(), app);
+ if (message == null)
+ message = "";
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Client " + app.getDisplayName() + " called notify for" + state +
+ ' ' + message, e);
+ break;
+
+ case CRASHED:
+ case START_FAILED:
+ _clients.remove(app);
+ _registered.remove(app.getName(), app);
+ if (message == null)
+ message = "";
+ _log.log(Log.CRIT, "Client " + app.getDisplayName() + ' ' + state +
+ ' ' + message, e);
+ break;
+ }
+ }
+
+ /**
+ * Register with the manager under the given name,
+ * so that other clients may find it.
+ * Only required for apps used by other apps.
+ *
+ * @param app non-null
+ * @param name non-null
+ * @return true if successful, false if duplicate name
+ */
+ public boolean register(ClientApp app) {
+ if (!_clients.contains(app))
+ return false;
+ // TODO if old app in there is not running and != this app, allow replacement
+ return _registered.putIfAbsent(app.getName(), app) == null;
+ }
+
+ /**
+ * Unregister with the manager. Name must be the same as that from register().
+ * Only required for apps used by other apps.
+ *
+ * @param app non-null
+ * @param name non-null
+ */
+ public void unregister(ClientApp app) {
+ _registered.remove(app.getName(), app);
+ }
+
+ /**
+ * Get a registered app.
+ * Only used for apps finding other apps.
+ * Do not hold a static reference.
+ * If you only need to find a port, use the PortMapper instead.
+ *
+ * @param app non-null
+ * @param name non-null
+ * @return client app or null
+ */
+ public ClientApp getRegisteredApp(String name) {
+ return _registered.get(name);
+ }
+}