package net.i2p.router.startup; import java.io.IOException; import java.io.Writer; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; 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.Log; /** * Notify the router of events, and provide methods for * client apps to find each other. * * @since 0.9.4 */ public class RouterAppManager extends ClientAppManagerImpl { private final RouterContext _context; private final Log _log; // client to args // this assumes clients do not override equals() private final ConcurrentHashMap<ClientApp, String[]> _clients; public RouterAppManager(RouterContext ctx) { super(ctx); _context = ctx; _log = ctx.logManager().getLog(RouterAppManager.class); _clients = new ConcurrentHashMap<ClientApp, String[]>(16); ctx.addShutdownTask(new Shutdown()); } /** * @param args the args that were used to instantiate the app, non-null, may be zero-length * @return success * @throws IllegalArgumentException if already added */ public boolean addAndStart(ClientApp app, String[] args) { if (_log.shouldLog(Log.INFO)) _log.info("Client " + app.getDisplayName() + " ADDED"); String[] old = _clients.putIfAbsent(app, args); if (old != null) throw new IllegalArgumentException("already added"); try { app.startup(); return true; } catch (Throwable t) { _clients.remove(app); _log.error("Client " + app + " failed to start", t); return false; } } /** * Get the first known ClientApp with this class name and exact arguments. * Caller may then retrieve or control the state of the returned client. * A client will generally be found only if it is running or transitioning; * after it is stopped it will not be tracked by the manager. * * @param args non-null, may be zero-length * @return client app or null * @since 0.9.6 */ public ClientApp getClientApp(String className, String[] args) { for (Map.Entry<ClientApp, String[]> e : _clients.entrySet()) { if (e.getKey().getClass().getName().equals(className) && Arrays.equals(e.getValue(), args)) return e.getKey(); } return null; } // 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 */ @Override 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() + " is now " + state); break; case STARTING: case RUNNING: if (_log.shouldLog(Log.INFO)) _log.info("Client " + app.getDisplayName() + " is now " + 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() + " is now " + 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 * @return true if successful, false if duplicate name */ @Override public boolean register(ClientApp app) { if (!_clients.containsKey(app)) { // Allow registration even if we didn't start it, // useful for plugins if (_log.shouldLog(Log.INFO)) _log.info("Registering untracked client " + app.getName()); //return false; } if (_log.shouldLog(Log.INFO)) _log.info("Client " + app.getDisplayName() + " REGISTERED AS " + app.getName()); // TODO if old app in there is not running and != this app, allow replacement return super.register(app); } /// end ClientAppManager interface /** * @since 0.9.6 */ public synchronized void shutdown() { Set<ClientApp> apps = new HashSet<ClientApp>(_clients.keySet()); for (ClientApp app : apps) { ClientAppState state = app.getState(); if (state == RUNNING || state == STARTING) { try { if (_log.shouldWarn()) _log.warn("Shutting down client " + app.getDisplayName()); app.shutdown(null); } catch (Throwable t) {} } } } /** * @since 0.9.6 */ public class Shutdown implements Runnable { public void run() { shutdown(); } } /** * debug * @since 0.9.6 */ public void renderStatusHTML(Writer out) throws IOException { StringBuilder buf = new StringBuilder(1024); buf.append("<h2>App Manager</h2>"); buf.append("<h3>Tracked</h3>"); toString1(buf); buf.append("<h3>Registered</h3>"); toString2(buf); out.write(buf.toString()); } /** * debug * @since 0.9.6 */ private void toString1(StringBuilder buf) { List<String> list = new ArrayList<String>(_clients.size()); for (Map.Entry<ClientApp, String[]> entry : _clients.entrySet()) { ClientApp key = entry.getKey(); String[] val = entry.getValue(); list.add("[" + key.getName() + "] = [" + key.getClass().getName() + ' ' + Arrays.toString(val) + "] " + key.getState() + "<br>"); } Collections.sort(list); for (String e : list) { buf.append(e); } } /** * debug * @since 0.9.6 */ private void toString2(StringBuilder buf) { List<String> list = new ArrayList<String>(_registered.size()); for (Map.Entry<String, ClientApp> entry : _registered.entrySet()) { String key = entry.getKey(); ClientApp val = entry.getValue(); list.add("[" + key + "] = [" + val.getClass().getName() + "]<br>"); } Collections.sort(list); for (String e : list) { buf.append(e); } } }