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 0000000000000000000000000000000000000000..8900630bcf532e41b32bb5503b2a5cc085f29dcc --- /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 0000000000000000000000000000000000000000..e246254b17fa3fdbc3c2fbf91c81e3108c1b12ea --- /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 0000000000000000000000000000000000000000..e4563aabfd10189ad7699a3f20bd1dd9210264bd --- /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 0000000000000000000000000000000000000000..1be4e8331e53f5d65d201e5c4d331618b3680b88 --- /dev/null +++ b/core/java/src/net/i2p/app/package.html @@ -0,0 +1,18 @@ +<html> +<body> +<p> +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(). +</p> +<p> +The benefits for clients using this interface: +<ul> +<li>Get the current context via the constructor +<li>Complete life cycle management by the router +<li>Avoid the need for static references +<li>Ability to find other clients without using static references +</ul> +</p> +</body> +</html> diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 421539ce3fe1e69e42bc380e2a5412065cd87e62..aff11b0527439381e638c2a3a7195b2ec1f560be 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<Runnable> _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<RouterContext> _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 0000000000000000000000000000000000000000..84cf6856291a53ea41a632b857ebd9764d2201c4 --- /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 0000000000000000000000000000000000000000..e35eea22f6db64f2eaafa8c2ac0736474994b31d --- /dev/null +++ b/router/java/src/net/i2p/router/app/package.html @@ -0,0 +1,18 @@ +<html> +<body> +<p> +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(). +</p> +<p> +The benefits for clients using this interface: +<ul> +<li>Get the current context via the constructor +<li>Complete life cycle management by the router +<li>Avoid the need for static references +<li>Ability to find other clients without using static references +</ul> +</p> +</body> +</html> diff --git a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java index 3f3f9d5e6a2008412a648395f1fbf797e4536d16..9f0ce8c8296a9ee937d71567e6e5c1f8cbe52e56 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 0000000000000000000000000000000000000000..45f27fefbb4d704f2d312198207fcf6a314b40a6 --- /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<ClientApp> _clients; + private final ConcurrentHashMap<String, ClientApp> _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); + } +}