* New interface for clients started via clients.config, and a new

manager to track the lifecycle and start/stop clients on demand.
    Not hooked in to console yet, untested.
    (ticket #347)
This commit is contained in:
zzz
2012-10-13 12:45:08 +00:00
parent 6f509967bf
commit d198ae9ef1
9 changed files with 407 additions and 11 deletions

View File

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

View File

@@ -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 {}

View File

@@ -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>

View File

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

View File

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