// // $Id$ // // Narya library - tools for developing networked games // Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/narya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.presents.server; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.io.IOException; import java.net.InetAddress; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; import com.samskivert.util.Interval; import com.samskivert.util.Lifecycle; import com.samskivert.util.ObserverList; import com.samskivert.util.StringUtil; import com.threerings.util.Name; import com.threerings.presents.annotation.EventThread; import com.threerings.presents.data.ClientObject; import com.threerings.presents.net.AuthRequest; import com.threerings.presents.net.AuthResponse; import com.threerings.presents.net.Credentials; import com.threerings.presents.server.net.AuthingConnection; import com.threerings.presents.server.net.Connection; import static com.threerings.presents.Log.log; /** * The client manager is responsible for managing the clients (surprise, surprise) which are * slightly more than just connections. Clients persist in the absence of connections in case a * user goes bye bye unintentionally and wants to reconnect and continue their session. * *

The client manager operates with thread safety because it is called both from the conmgr * thread (to notify of connections showing up or going away) and from the dobjmgr thread (when * clients are given the boot for application-defined reasons). */ @Singleton public class ClientManager implements ClientResolutionListener, ReportManager.Reporter, Lifecycle.Component { /** * Used by {@link ClientManager#applyToClient}. */ public static interface ClientOp { /** * Called with the resolved client object. */ void apply (ClientObject clobj); /** * Called if the client resolution fails. */ void resolutionFailed (Exception e); } /** * Used by entites that wish to track when clients initiate and end sessions on this server. */ public static interface ClientObserver { /** * Called when a client has authenticated and been resolved and has started their session. */ void clientSessionDidStart (PresentsSession session); /** * Called when a client has logged off or been forcibly logged off due to inactivity and * has thus ended their session. */ void clientSessionDidEnd (PresentsSession session); } /** * Constructs a client manager that will interact with the supplied connection manager. */ @Inject public ClientManager (ReportManager repmgr, Lifecycle cycle) { repmgr.registerReporter(this); cycle.addComponent(this); } /** * Configures the injector we'll use to resolve dependencies for {@link PresentsSession} * instances. */ public void setInjector (Injector injector) { _injector = injector; } /** * Configures the client manager with a factory for creating {@link PresentsSession} and {@link * ClientResolver} classes for authenticated client connections. */ public void setSessionFactory (SessionFactory factory) { _factory = factory; } /** * Returns the {@link SessionFactory} currently in use. */ public SessionFactory getSessionFactory () { return _factory; } /** * Return the number of client resolutions are currently happening. */ public int getOutstandingResolutionCount () { return _penders.size(); } /** * Returns the number of client sessions (some may be disconnected). */ public int getClientCount () { synchronized (_usermap) { return _usermap.size(); } } /** * Returns all sessions logged in from the given IP in the form returned from * {@link InetAddress#getAddress()}. */ public List getSessionsForAddress (byte[] addr) { List sessions = Lists.newArrayListWithExpectedSize(1); synchronized (_usermap) { for (PresentsSession session : _usermap.values()) { InetAddress sessionAddr = session.getInetAddress(); if (sessionAddr == null) { continue; } if (Arrays.equals(addr, sessionAddr.getAddress())) { sessions.add(session); } } } return sessions; } /** * Returns the number of connected clients. */ public int getConnectionCount () { return _conmap.size(); } /** * Returns an iterable over all active client objects. */ public Iterable clientObjects () { return _objmap.values(); } /** * Enumerates all active client objects. */ public Iterator enumerateClientObjects () { return _objmap.values().iterator(); } /** * Registers an observer that will be notified when clients start and end their sessions. */ public void addClientObserver (ClientObserver observer) { _clobservers.add(observer); } /** * Removes an observer previously registered with {@link #addClientObserver}. */ public void removeClientObserver (ClientObserver observer) { _clobservers.remove(observer); } /** * Returns the client instance that manages the client session for the specified authentication * username or null if that client is not currently connected to the server. */ public PresentsSession getClient (Name authUsername) { synchronized (_usermap) { return _usermap.get(authUsername); } } /** * Returns the client object associated with the specified username. This will return null * unless the client object is resolved for some reason (like they are logged on). */ public ClientObject getClientObject (Name username) { return _objmap.get(username); } /** * Resolves the specified client, applies the supplied client operation to them and releases * the client. */ public void applyToClient (Name username, ClientOp clop) { resolveClientObject(username, new ClientOpResolver(clop)); } /** * Requests that the client object for the specified user be resolved. Note: this * must be paired with a call to {@link #releaseClientObject} when the caller is * finished with the client object. */ public synchronized void resolveClientObject ( final Name username, final ClientResolutionListener listener) { // look to see if the client object is already resolved final ClientObject clobj = _objmap.get(username); if (clobj != null) { // report that the client is resolved on the dobjmgr thread to provide equivalent // behavior to the case where we actually have to do the resolution clobj.reference(); _omgr.postRunnable(new Runnable() { public void run () { listener.clientResolved(username, clobj); } }); return; } // look to see if it's currently being resolved ClientResolver clr = _penders.get(username); if (clr != null) { // throw this guy onto the bandwagon clr.addResolutionListener(listener); return; } try { // create a client resolver instance which will create our client object, populate it // and notify the listeners clr = _injector.getInstance(_factory.getClientResolverClass(username)); clr.init(username); clr.addResolutionListener(this); clr.addResolutionListener(listener); _penders.put(username, clr); // create and register our client object and give it back to the client resolver; we // need to do this on the dobjmgr thread since we're registering an object final ClientResolver fclr = clr; _omgr.postRunnable(new Runnable() { public void run () { ClientObject clobj = fclr.createClientObject(); clobj.setLocal(ClientLocal.class, fclr.createLocalAttribute()); clobj.setPermissionPolicy(fclr.createPermissionPolicy()); fclr.objectAvailable(_omgr.registerObject(clobj)); } }); } catch (Exception e) { // let the listener know that we're hosed listener.resolutionFailed(username, e); } } /** * Releases a client object that was obtained via a call to {@link #resolveClientObject}. If * this caller is the last reference, the object will be flushed and destroyed. */ public void releaseClientObject (Name username) { ClientObject clobj = _objmap.get(username); if (clobj == null) { log.info("Requested to release unmapped client object", "username", username); return; } // decrement the reference count and stop here if there are remaining references if (clobj.release()) { return; } log.debug("Destroying client " + clobj.who() + "."); // we're all clear to go; remove the mapping _objmap.remove(username); // and destroy the object itself _omgr.destroyObject(clobj.getOid()); } // from interface Lifecycle.Component public void init () { // start up an interval that will check for and flush expired clients _flushClients = new Interval(_omgr) { @Override public void expired () { flushClients(); } }; _flushClients.schedule(CLIENT_FLUSH_INTERVAL, true); } // from interface Lifecycle.Component public void shutdown () { log.info("Client manager shutting down", "ccount", _usermap.size()); _flushClients.cancel(); // inform all of our clients that they are being shut down synchronized (_usermap) { for (PresentsSession pc : _usermap.values()) { try { pc.shutdown(); } catch (Exception e) { log.warning("Client choked in shutdown()", "client", StringUtil.safeToString(pc), e); } } } } /** * Renames a currently connected client from oldname to newname. * * @return true if the client was found and renamed. */ protected boolean renameClientObject (Name oldname, Name newname) { ClientObject clobj = _objmap.remove(oldname); if (clobj == null) { log.warning("Requested to rename unmapped client object", "username", oldname, new Exception()); return false; } _objmap.put(newname, clobj); return true; } // documentation inherited from interface ClientResolutionListener public synchronized void clientResolved (Name username, ClientObject clobj) { // because we added ourselves as a client resolution listener, the client object reference // count was increased, but we're not a real resolver (who would have to call // releaseClientObject() to release their reference), so we release our reference // immediately clobj.release(); // stuff the object into the mapping table _objmap.put(username, clobj); // and remove the resolution listener _penders.remove(username); } // documentation inherited from interface ClientResolutionListener public synchronized void resolutionFailed (Name username, Exception reason) { // clear out their pending record _penders.remove(username); } /** * Called by the connection manager to let us know when a new connection has been established. */ public synchronized void connectionEstablished ( Connection conn, AuthRequest req, AuthResponse rsp) { Credentials creds = req.getCredentials(); Name username = creds.getUsername(); String type = username.getClass().getSimpleName(); // see if a client is already registered with these credentials PresentsSession client = getClient(username); if (client != null) { log.info("Resuming session", "type", type, "who", username, "conn", conn); client.resumeSession(req, conn); } else { log.info("Session initiated", "type", type, "who", username, "conn", conn); // create a new client and stick'em in the table client = _injector.getInstance(_factory.getSessionClass(req)); client.startSession(req, conn, rsp.authdata); // map their client instance synchronized (_usermap) { _usermap.put(username, client); } } // map this connection to this client _conmap.put(conn, client); } /** * Called by the connection manager to let us know when a connection has failed. */ public synchronized void connectionFailed (Connection conn, IOException fault) { // remove the client from the connection map PresentsSession client = _conmap.remove(conn); if (client != null) { log.info("Unmapped failed client", "client", client, "conn", conn, "fault", fault); // let the client know the connection went away client.wasUnmapped(); // and let the client know things went haywire client.connectionFailed(fault); } else if (!(conn instanceof AuthingConnection)) { log.info("Unmapped connection failed?", "conn", conn, "fault", fault, new Exception()); } } /** * Called by the connection manager to let us know when a connection has been closed. */ public synchronized void connectionClosed (Connection conn) { // remove the client from the connection map PresentsSession client = _conmap.remove(conn); if (client != null) { log.debug("Unmapped client", "client", client, "conn", conn); // let the client know the connection went away client.wasUnmapped(); } else { log.info("Closed unmapped connection '" + conn + "'. " + "Client probably not yet authenticated."); } } // documentation inherited from interface ReportManager.Reporter public void appendReport (StringBuilder report, long now, long sinceLast, boolean reset) { report.append("* presents.ClientManager:\n"); report.append("- Sessions: "); synchronized (_usermap) { report.append(_usermap.size()).append(" total, "); } report.append(_conmap.size()).append(" connected, "); report.append(_penders.size()).append(" pending\n"); report.append("- Mapped users: ").append(_objmap.size()).append("\n"); } /** * Called by PresentsSession when it has started its session. */ @EventThread protected void clientSessionDidStart (final PresentsSession client) { // let the observers know _clobservers.apply(new ObserverList.ObserverOp() { public boolean apply (ClientObserver observer) { observer.clientSessionDidStart(client); return true; } }); } /** * Called by PresentsSession when it has ended its session. */ @EventThread protected void clientSessionDidEnd (final PresentsSession session) { // notify the observers that the session is ended _clobservers.apply(new ObserverList.ObserverOp() { public boolean apply (ClientObserver observer) { observer.clientSessionDidEnd(session); return true; } }); } /** * Called by PresentsSession to let us know that we can clear it entirely out of the system. */ protected void clearSession (PresentsSession session) { // remove the client from the username map Name username = session.getCredentials().getUsername(); PresentsSession rc; synchronized (_usermap) { rc = _usermap.remove(username); } // sanity check just because we can if (rc == null) { log.info("Cleared session: unregistered!", "session", session); } else if (rc != session) { log.info("Cleared session: multiple!", "s1", rc, "s2", session); } else { log.info("Cleared session", "session", session); } } /** * Called once per minute to check for clients that have been disconnected too long and * forcibly end their sessions. */ protected void flushClients () { List victims = Lists.newArrayList(); long now = System.currentTimeMillis(); // first build a list of our victims synchronized (_usermap) { for (PresentsSession client : _usermap.values()) { if (client.checkExpired(now)) { victims.add(client); } } } // now end their sessions for (PresentsSession client : victims) { try { log.info("Client expired, ending session", "session", client, "dtime", (now-client.getNetworkStamp()) + "ms]."); client.endSession(); } catch (Exception e) { log.warning("Choke while flushing client", "victim", client, e); } } } /** Used by {@link ClientManager#applyToClient}. */ protected class ClientOpResolver implements ClientResolutionListener { public ClientOpResolver (ClientOp clop) { _clop = clop; } public void clientResolved (Name username, ClientObject clobj) { try { _clop.apply(clobj); } catch (Exception e) { log.warning("Client op failed", "username", username, "clop", _clop, e); } finally { releaseClientObject(username); } } public void resolutionFailed (Name username, Exception reason) { _clop.resolutionFailed(reason); } protected ClientOp _clop; } /** Used to resolve dependencies in {@link PresentsSession} instances that we create. */ protected Injector _injector; /** A mapping from auth username to client instances. */ protected Map _usermap = Maps.newHashMap(); /** A mapping from connections to client instances. */ protected Map _conmap = Maps.newHashMap(); /** A mapping from usernames to client object instances. */ protected Map _objmap = Maps.newHashMap(); /** A mapping of pending client resolvers. */ protected Map _penders = Maps.newHashMap(); /** Lets us know what sort of client classes to use. */ protected SessionFactory _factory = SessionFactory.DEFAULT; /** Tracks registered {@link ClientObserver}s. */ protected ObserverList _clobservers = ObserverList.newSafeInOrder(); /** Interval to flush expired clients. */ protected Interval _flushClients; // our injected dependencies @Inject protected PresentsDObjectMgr _omgr; /** The frequency with which we check for expired clients. */ protected static final long CLIENT_FLUSH_INTERVAL = 60 * 1000L; }