// // $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 Listoldname 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