Bit the bullet and implemented invocation service groups so that our various

MetaSOY clients (Swiftly, World, and soon Admin Dashboard) don't have to know
about a bunch of unrelated crap. Fricking complexity++, grumble.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4551 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Michael Bayne
2007-02-11 00:42:35 +00:00
parent d1ff63d62d
commit 9afcc526a0
6 changed files with 514 additions and 609 deletions
@@ -21,6 +21,8 @@
package com.threerings.presents.client; package com.threerings.presents.client;
import java.util.HashSet;
import com.samskivert.util.Interval; import com.samskivert.util.Interval;
import com.samskivert.util.ObserverList; import com.samskivert.util.ObserverList;
import com.samskivert.util.RunAnywhere; import com.samskivert.util.RunAnywhere;
@@ -36,9 +38,9 @@ import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PongResponse; import com.threerings.presents.net.PongResponse;
/** /**
* Through the client object, a connection to the system is established * Through the client object, a connection to the system is established and maintained. The client
* and maintained. The client object maintains two separate threads (a * object maintains two separate threads (a reader and a writer) by which all network traffic is
* reader and a writer) by which all network traffic is managed. * managed.
*/ */
public class Client public class Client
{ {
@@ -47,22 +49,17 @@ public class Client
public static final int[] DEFAULT_SERVER_PORTS = { 47624 }; public static final int[] DEFAULT_SERVER_PORTS = { 47624 };
/** /**
* Constructs a client object with the supplied credentials and * Constructs a client object with the supplied credentials and RunQueue. The creds will be
* RunQueue. The creds will be used to authenticate with any server to * used to authenticate with any server to which this client attempts to connect. The RunQueue
* which this client attempts to connect. The RunQueue is used to * is used to operate the distributed object event dispatch mechanism. To allow the dobj event
* operate the distributed object event dispatch mechanism. To allow * dispatch to coexist with threads like the AWT thread, the client will request that the
* the dobj event dispatch to coexist with threads like the AWT * RunQueue queue up a runnable whenever there are distributed object events that need to be
* thread, the client will request that the RunQueue queue up a * processed. The RunQueue can then queue that runnable up on the AWT thread if it is so
* runnable whenever there are distributed object events that need to * inclined to make life simpler for the rest of the application.
* be processed. The RunQueue can then queue that runnable up on the
* AWT thread if it is so inclined to make life simpler for the rest
* of the application.
* *
* @param creds the credentials to use when logging on to the server. * @param creds the credentials to use when logging on to the server. These can be null, but
* These can be null, but <code>setCredentials</code> must then be * <code>setCredentials</code> must then be called before any call to <code>logon</code>.
* called before any call to <code>logon</code>. * @param runQueue a RunQueue that can be used to process incoming events.
* @param runQueue a RunQueue that can be used to process incoming
* events.
*/ */
public Client (Credentials creds, RunQueue runQueue) public Client (Credentials creds, RunQueue runQueue)
{ {
@@ -71,10 +68,9 @@ public class Client
} }
/** /**
* Registers the supplied observer with this client. While registered * Registers the supplied observer with this client. While registered the observer will receive
* the observer will receive notifications of state changes within the * notifications of state changes within the client. The function will refuse to register an
* client. The function will refuse to register an already registered * already registered observer.
* observer.
* *
* @see ClientObserver * @see ClientObserver
* @see SessionObserver * @see SessionObserver
@@ -87,9 +83,8 @@ public class Client
} }
/** /**
* Unregisters the supplied observer. Upon return of this function, * Unregisters the supplied observer. Upon return of this function, the observer will no longer
* the observer will no longer receive notifications of state changes * receive notifications of state changes within the client.
* within the client.
*/ */
public void removeClientObserver (SessionObserver observer) public void removeClientObserver (SessionObserver observer)
{ {
@@ -105,7 +100,7 @@ public class Client
{ {
_standalone = standalone; _standalone = standalone;
} }
/** /**
* Checks whether or not this client is operating in a standalone mode. * Checks whether or not this client is operating in a standalone mode.
*/ */
@@ -113,10 +108,10 @@ public class Client
{ {
return _standalone; return _standalone;
} }
/** /**
* Configures the client to communicate with the server on the supplied * Configures the client to communicate with the server on the supplied hostname and set of
* hostname and set of ports (which will be tried in succession). * ports (which will be tried in succession).
* *
* @see #logon * @see #logon
*/ */
@@ -127,8 +122,8 @@ public class Client
} }
/** /**
* Returns the RunQueue in use by this client. This can be used to * Returns the RunQueue in use by this client. This can be used to queue up event dispatching
* queue up event dispatching stints. * stints.
*/ */
public RunQueue getRunQueue () public RunQueue getRunQueue ()
{ {
@@ -136,8 +131,7 @@ public class Client
} }
/** /**
* Returns the hostname of the server to which this client is * Returns the hostname of the server to which this client is currently configured to connect.
* currently configured to connect.
*/ */
public String getHostname () public String getHostname ()
{ {
@@ -145,8 +139,7 @@ public class Client
} }
/** /**
* Returns the port on which this client is currently configured to * Returns the port on which this client is currently configured to connect to the server.
* connect to the server.
*/ */
public int[] getPorts () public int[] getPorts ()
{ {
@@ -154,8 +147,8 @@ public class Client
} }
/** /**
* Returns the credentials with which this client is currently * Returns the credentials with which this client is currently configured to connect to the
* configured to connect to the server. * server.
*/ */
public Credentials getCredentials () public Credentials getCredentials ()
{ {
@@ -163,9 +156,8 @@ public class Client
} }
/** /**
* Sets the credentials that will be used by this client to * Sets the credentials that will be used by this client to authenticate with the server. This
* authenticate with the server. This should be done before any call * should be done before any call to <code>logon</code>.
* to <code>logon</code>.
*/ */
public void setCredentials (Credentials creds) public void setCredentials (Credentials creds)
{ {
@@ -181,9 +173,8 @@ public class Client
} }
/** /**
* Sets the version string reported to the server during * Sets the version string reported to the server during authentication. Some server
* authentication. Some server implementations may wish to refuse * implementations may wish to refuse connections by old or invalid client versions.
* connections by old or invalid client versions.
*/ */
public void setVersion (String version) public void setVersion (String version)
{ {
@@ -191,8 +182,8 @@ public class Client
} }
/** /**
* Configures the client with a custom class loader which will be used * Configures the client with a custom class loader which will be used when reading objects off
* when reading objects off of the network. * of the network.
*/ */
public void setClassLoader (ClassLoader loader) public void setClassLoader (ClassLoader loader)
{ {
@@ -203,10 +194,19 @@ public class Client
} }
/** /**
* Returns the data associated with our authentication response. Users * Marks this client as interested in the specified bootstrap services group. Any services
* of the Presents system may wish to communicate authentication * registered as bootstrap services with the supplied group name will be included in this
* related information to their client by extending and augmenting * clients bootstrap services set. This must be called before {@link #logon}.
* {@link AuthResponseData}. */
public void addBootstrapGroup (String group)
{
_bootGroups.add(group);
}
/**
* Returns the data associated with our authentication response. Users of the Presents system
* may wish to communicate authentication related information to their client by extending and
* augmenting {@link AuthResponseData}.
*/ */
public AuthResponseData getAuthResponseData () public AuthResponseData getAuthResponseData ()
{ {
@@ -214,13 +214,11 @@ public class Client
} }
/** /**
* Returns the distributed object manager associated with this * Returns the distributed object manager associated with this session. This reference is only
* session. This reference is only valid for the duration of the * valid for the duration of the session and a new reference must be obtained if the client
* session and a new reference must be obtained if the client
* disconnects and reconnects to the server. * disconnects and reconnects to the server.
* *
* @return the dobjmgr in effect or null if we have no established * @return the dobjmgr in effect or null if we have no established connection to the server.
* connection to the server.
*/ */
public DObjectManager getDObjectManager () public DObjectManager getDObjectManager ()
{ {
@@ -228,17 +226,15 @@ public class Client
} }
/** /**
* Instructs the distributed object manager associated with this * Instructs the distributed object manager associated with this client to allow objects of the
* client to allow objects of the specified class to linger around the * specified class to linger around the specified number of milliseconds after their last
* specified number of milliseconds after their last subscriber has * subscriber has been removed before the client finally removes its object proxy and flushes
* been removed before the client finally removes its object proxy and * the object. Normally, objects are flushed immediately following the removal of their last
* flushes the object. Normally, objects are flushed immediately * subscriber.
* following the removal of their last subscriber.
* *
* <p><em>Note:</em> the delay will be applied to derived classes as * <p><em>Note:</em> the delay will be applied to derived classes as well as exact
* well as exact matches. <em>Note also:</em> this method cannot be * matches. <em>Note also:</em> this method cannot be called until after the client has
* called until after the client has established a connection with the * established a connection with the server and the distributed object manager is available.
* server and the distributed object manager is available.
*/ */
public void registerFlushDelay (Class objclass, long delay) public void registerFlushDelay (Class objclass, long delay)
{ {
@@ -247,8 +243,8 @@ public class Client
} }
/** /**
* Returns the oid of the client object associated with this session. * Returns the oid of the client object associated with this session. It is only valid for the
* It is only valid for the duration of the session. * duration of the session.
*/ */
public int getClientOid () public int getClientOid ()
{ {
@@ -256,8 +252,8 @@ public class Client
} }
/** /**
* Returns a reference to the client object associated with this * Returns a reference to the client object associated with this session. It is only valid for
* session. It is only valid for the duration of the session. * the duration of the session.
*/ */
public ClientObject getClientObject () public ClientObject getClientObject ()
{ {
@@ -265,8 +261,8 @@ public class Client
} }
/** /**
* Returns the invocation director associated with this session. This * Returns the invocation director associated with this session. This reference is only valid
* reference is only valid for the duration of the session. * for the duration of the session.
*/ */
public InvocationDirector getInvocationDirector () public InvocationDirector getInvocationDirector ()
{ {
@@ -274,9 +270,9 @@ public class Client
} }
/** /**
* Returns the first bootstrap service that could be located that * Returns the first bootstrap service that could be located that implements the supplied
* implements the supplied {@link InvocationService} derivation. * {@link InvocationService} derivation. <code>null</code> is returned if no such service
* <code>null</code> is returned if no such service could be found. * could be found.
*/ */
public InvocationService getService (Class sclass) public InvocationService getService (Class sclass)
{ {
@@ -295,10 +291,9 @@ public class Client
} }
/** /**
* Like {@link #getService} except that a {@link RuntimeException} is * Like {@link #getService} except that a {@link RuntimeException} is thrown if the service is
* thrown if the service is not available. Useful to avoid redundant * not available. Useful to avoid redundant error checking when you know that the shit will hit
* error checking when you know that the shit will hit the fan if a * the fan if a particular invocation service is not available.
* particular invocation service is not available.
*/ */
public InvocationService requireService (Class sclass) public InvocationService requireService (Class sclass)
{ {
@@ -312,8 +307,7 @@ public class Client
} }
/** /**
* Returns a reference to the bootstrap data provided to this client * Returns a reference to the bootstrap data provided to this client at logon time.
* at logon time.
*/ */
public BootstrapData getBootstrapData () public BootstrapData getBootstrapData ()
{ {
@@ -321,30 +315,28 @@ public class Client
} }
/** /**
* Converts a server time stamp to a value comparable to client clock * Converts a server time stamp to a value comparable to client clock readings.
* readings.
*/ */
public long fromServerTime (long stamp) public long fromServerTime (long stamp)
{ {
// when we calcuated our time delta, we did it such that: C - S = // when we calcuated our time delta, we did it such that: C - S = dT, thus to convert
// dT, thus to convert server to client time we do: C = S + dT // server to client time we do: C = S + dT
return stamp + _serverDelta; return stamp + _serverDelta;
} }
/** /**
* Converts a client clock reading to a value comparable to a server * Converts a client clock reading to a value comparable to a server time stamp.
* time stamp.
*/ */
public long toServerTime (long stamp) public long toServerTime (long stamp)
{ {
// when we calcuated our time delta, we did it such that: C - S = // when we calcuated our time delta, we did it such that: C - S = dT, thus to convert
// dT, thus to convert server to client time we do: S = C - dT // server to client time we do: S = C - dT
return stamp - _serverDelta; return stamp - _serverDelta;
} }
/** /**
* Returns true if we are in active communication (we may not yet be logged * Returns true if we are in active communication (we may not yet be logged on, but we could be
* on, but we could be trying to log on). * trying to log on).
*/ */
public synchronized boolean isActive () public synchronized boolean isActive ()
{ {
@@ -357,17 +349,16 @@ public class Client
*/ */
public synchronized boolean isLoggedOn () public synchronized boolean isLoggedOn ()
{ {
// we're not "logged on" until we're fully done with the // we're not "logged on" until we're fully done with the procedure, meaning we have a
// procedure, meaning we have a client object reference // client object reference
return (_clobj != null); return (_clobj != null);
} }
/** /**
* Requests that this client connect and logon to the server with * Requests that this client connect and logon to the server with which it was previously
* which it was previously configured. * configured.
* *
* @return false if we're already logged on, true if a logon attempt * @return false if we're already logged on, true if a logon attempt was initiated.
* was initiated.
*/ */
public synchronized boolean logon () public synchronized boolean logon ()
{ {
@@ -376,14 +367,14 @@ public class Client
return false; return false;
} }
// otherwise create a new communicator instance and start it up. // otherwise create a new communicator instance and start it up. this will initiate the
// this will initiate the logon process // logon process
_comm = new Communicator(this); _comm = new Communicator(this);
_comm.setClassLoader(_loader); _comm.setClassLoader(_loader);
_comm.logon(); _comm.logon();
// register an interval that we'll use to keep the clock synced // register an interval that we'll use to keep the clock synced and to send pings when
// and to send pings when appropriate // appropriate
if (_tickInterval == null) { if (_tickInterval == null) {
_tickInterval = new Interval() { _tickInterval = new Interval() {
public void expired () { public void expired () {
@@ -397,28 +388,23 @@ public class Client
} }
/** /**
* Requests that the client log off of the server to which it is * Requests that the client log off of the server to which it is connected.
* connected.
* *
* @param abortable If true, the client will call * @param abortable If true, the client will call <code>clientWillDisconnect</code> on all of
* <code>clientWillDisconnect</code> on all of the client observers * the client observers and abort the logoff process if any of them return false. If false,
* and abort the logoff process if any of them return false. If false,
* <code>clientWillDisconnect</code> will not be called at all. * <code>clientWillDisconnect</code> will not be called at all.
* *
* @return true if the logoff succeeded, false if it failed due to a * @return true if the logoff succeeded, false if it failed due to a disagreeable observer.
* disagreeable observer.
*/ */
public boolean logoff (boolean abortable) public boolean logoff (boolean abortable)
{ {
// if we have no communicator, we're not logged on anyway // if we have no communicator, we're not logged on anyway
if (_comm == null) { if (_comm == null) {
Log.warning("Ignoring request to logoff because we're not " + Log.warning("Ignoring request to logoff because we're not logged on.");
"logged on.");
return true; return true;
} }
// if the request is abortable, let's run it past the observers // if the request is abortable, let's run it past the observers before we act upon it
// before we act upon it
if (abortable && notifyObservers(CLIENT_WILL_LOGOFF, null)) { if (abortable && notifyObservers(CLIENT_WILL_LOGOFF, null)) {
return false; return false;
} }
@@ -427,29 +413,25 @@ public class Client
_tickInterval.cancel(); _tickInterval.cancel();
_tickInterval = null; _tickInterval = null;
// ask the communicator to send a logoff message and disconnect // ask the communicator to send a logoff message and disconnect from the server
// from the server
_comm.logoff(); _comm.logoff();
return true; return true;
} }
/** /**
* For standalone mode, this notifies observers that the client has logged * For standalone mode, this notifies observers that the client has logged off and cleans up.
* off and cleans up.
*/ */
public void standaloneLogoff () public void standaloneLogoff ()
{ {
notifyObservers(CLIENT_DID_LOGOFF, null); notifyObservers(CLIENT_DID_LOGOFF, null);
cleanup(null); cleanup(null);
} }
/** /**
* Called by the {@link ClientDObjectMgr} when our bootstrap * Called by the {@link ClientDObjectMgr} when our bootstrap notification arrives. If the
* notification arrives. If the client and server are being run in * client and server are being run in "merged" mode in a single JVM, this is how the client is
* "merged" mode in a single JVM, this is how the client is configured * configured with the server's distributed object manager and provided with bootstrap data.
* with the server's distributed object manager and provided with
* bootstrap data.
*/ */
public void gotBootstrap (BootstrapData data, DObjectManager omgr) public void gotBootstrap (BootstrapData data, DObjectManager omgr)
{ {
@@ -465,20 +447,18 @@ public class Client
// initialize our invocation director // initialize our invocation director
_invdir.init(omgr, _cloid, this); _invdir.init(omgr, _cloid, this);
// send a few pings to the server to establish the clock offset // send a few pings to the server to establish the clock offset between this client and
// between this client and server standard time // server standard time
establishClockDelta(System.currentTimeMillis()); establishClockDelta(System.currentTimeMillis());
// we can't quite call initialization completed at this point // we can't quite call initialization completed at this point because we need for the
// because we need for the invocation director to fully initialize // invocation director to fully initialize (which requires a round trip to the server)
// (which requires a round trip to the server) before turning the // before turning the client loose to do things like request invocation services
// client loose to do things like request invocation services
} }
/** /**
* Called every five seconds; ensures that we ping the server if we * Called every five seconds; ensures that we ping the server if we haven't communicated in a
* haven't communicated in a long while and periodically resyncs the * long while and periodically resyncs the client and server clock deltas.
* client and server clock deltas.
*/ */
protected void tick () protected void tick ()
{ {
@@ -501,8 +481,8 @@ public class Client
} }
} else if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) { } else if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) {
// if we haven't sent anything over the network in a while, we // if we haven't sent anything over the network in a while, we ping the server to let
// ping the server to let it know that we're still alive // it know that we're still alive
_comm.postMessage(new PingRequest()); _comm.postMessage(new PingRequest());
} else if (now - _lastSync > CLOCK_SYNC_INTERVAL) { } else if (now - _lastSync > CLOCK_SYNC_INTERVAL) {
@@ -512,10 +492,9 @@ public class Client
} }
/** /**
* Called during initialization to initiate a sequence of ping/pong * Called during initialization to initiate a sequence of ping/pong messages which will be used
* messages which will be used to determine (with "good enough" * to determine (with "good enough" accuracy) the difference between the client clock and the
* accuracy) the difference between the client clock and the server * server clock so that we can later interpret server timestamps.
* clock so that we can later interpret server timestamps.
*/ */
protected void establishClockDelta (long now) protected void establishClockDelta (long now)
{ {
@@ -530,8 +509,8 @@ public class Client
} }
/** /**
* Called by the {@link Communicator} if it is experiencing trouble logging * Called by the {@link Communicator} if it is experiencing trouble logging on but is still
* on but is still trying fallback strategies. * trying fallback strategies.
*/ */
protected void reportLogonTribulations (final LogonException cause) protected void reportLogonTribulations (final LogonException cause)
{ {
@@ -543,8 +522,8 @@ public class Client
} }
/** /**
* Called by the invocation director when it successfully subscribes * Called by the invocation director when it successfully subscribes to the client object
* to the client object immediately following logon. * immediately following logon.
*/ */
protected void gotClientObject (ClientObject clobj) protected void gotClientObject (ClientObject clobj)
{ {
@@ -556,8 +535,7 @@ public class Client
} }
/** /**
* Called by the invocation director if it fails to subscribe to the * Called by the invocation director if it fails to subscribe to the client object after logon.
* client object after logon.
*/ */
protected void getClientObjectFailed (Exception cause) protected void getClientObjectFailed (Exception cause)
{ {
@@ -566,8 +544,7 @@ public class Client
} }
/** /**
* Called by the invocation director when it discovers that the client * Called by the invocation director when it discovers that the client object has changed.
* object has changed.
*/ */
protected void clientObjectDidChange (ClientObject clobj) protected void clientObjectDidChange (ClientObject clobj)
{ {
@@ -589,17 +566,16 @@ public class Client
} }
}; };
// we need to run immediately if this is WILL_LOGOFF or if we have // we need to run immediately if this is WILL_LOGOFF or if we have no RunQueue (which
// no RunQueue (which currently only happens in some really obscure // currently only happens in some really obscure circumstances where we're using a Client
// circumstances where we're using a Client instance on the server // instance on the server so that we can sort of pretend to be a real client)
// so that we can sort of pretend to be a real client)
if (code == CLIENT_WILL_LOGOFF || _runQueue == null) { if (code == CLIENT_WILL_LOGOFF || _runQueue == null) {
unit.run(); unit.run();
return noty.getRejected(); return noty.getRejected();
} else { } else {
// otherwise we can queue this notification up with our // otherwise we can queue this notification up with our RunQueue and ensure that it's
// RunQueue and ensure that it's run on the proper thread // run on the proper thread
_runQueue.postRunnable(unit); _runQueue.postRunnable(unit);
return false; return false;
} }
@@ -607,12 +583,10 @@ public class Client
synchronized void cleanup (final Exception logonError) synchronized void cleanup (final Exception logonError)
{ {
// we know that prior to the call to this method, the observers // we know that prior to the call to this method, the observers were notified with
// were notified with CLIENT_DID_LOGOFF; that may not have been // CLIENT_DID_LOGOFF; that may not have been invoked yet, so we don't want to clear out our
// invoked yet, so we don't want to clear out our communicator // communicator reference immediately; instead we queue up a runnable unit to do so to
// reference immediately; instead we queue up a runnable unit to // ensure that it won't happen until CLIENT_DID_LOGOFF was dispatched
// do so to ensure that it won't happen until CLIENT_DID_LOGOFF
// was dispatched
_runQueue.postRunnable(new Runnable() { _runQueue.postRunnable(new Runnable() {
public void run () { public void run () {
// clear out our references // clear out our references
@@ -625,11 +599,10 @@ public class Client
// and let our invocation director know we're logged off // and let our invocation director know we're logged off
_invdir.cleanup(); _invdir.cleanup();
// if we were cleaned up due to a failure to logon, we can // if we were cleaned up due to a failure to logon, we can report the logon error
// report the logon error now that the communicator is // now that the communicator is cleaned up; this allows a logon failure listener to
// cleaned up; this allows a logon failure listener to // immediately try another logon (hopefully with something changed like the server
// immediately try another logon (hopefully with something // or port)
// changed like the server or port)
if (logonError != null) { if (logonError != null) {
notifyObservers(CLIENT_FAILED_TO_LOGON, logonError); notifyObservers(CLIENT_FAILED_TO_LOGON, logonError);
} else { } else {
@@ -640,18 +613,15 @@ public class Client
} }
/** /**
* Called when we receive a pong packet. We may be in the process of * Called when we receive a pong packet. We may be in the process of calculating the client/
* calculating the client/server time differential, or we may have * server time differential, or we may have already done that at which point we ignore pongs.
* already done that at which point we ignore pongs.
*/ */
void gotPong (PongResponse pong) void gotPong (PongResponse pong)
{ {
// if we're not currently calculating our client/server delta, then // if we're not currently calculating our delta, then we can throw away the pong
// we can throw away the pong
if (_dcalc != null) { if (_dcalc != null) {
// we update the delta after every receipt so as to immediately // we update the delta after every receipt so as to immediately obtain an estimate of
// obtain an estimate of the clock delta and then refine it as // the clock delta and then refine it as more packets come in
// more packets come in
_dcalc.gotPong(pong); _dcalc.gotPong(pong);
_serverDelta = _dcalc.getTimeDelta(); _serverDelta = _dcalc.getTimeDelta();
} }
@@ -684,8 +654,7 @@ public class Client
case CLIENT_FAILED_TO_LOGON: case CLIENT_FAILED_TO_LOGON:
if (obs instanceof ClientObserver) { if (obs instanceof ClientObserver) {
((ClientObserver)obs).clientFailedToLogon( ((ClientObserver)obs).clientFailedToLogon(Client.this, _cause);
Client.this, _cause);
} }
break; break;
@@ -697,8 +666,7 @@ public class Client
case CLIENT_CONNECTION_FAILED: case CLIENT_CONNECTION_FAILED:
if (obs instanceof ClientObserver) { if (obs instanceof ClientObserver) {
((ClientObserver)obs).clientConnectionFailed( ((ClientObserver)obs).clientConnectionFailed(Client.this, _cause);
Client.this, _cause);
} }
break; break;
@@ -721,8 +689,7 @@ public class Client
break; break;
default: default:
throw new RuntimeException("Invalid code supplied to " + throw new RuntimeException("Invalid code supplied to notifyObservers: " + _code);
"notifyObservers: " + _code);
} }
return true; return true;
@@ -739,8 +706,7 @@ public class Client
/** The version string reported to the server at auth time. */ /** The version string reported to the server at auth time. */
protected String _version = ""; protected String _version = "";
/** An entity that gives us the ability to process events on the main /** An entity that gives us the ability to process events on the main client thread. */
* client thread. */
protected RunQueue _runQueue; protected RunQueue _runQueue;
/** The distributed object manager we're using during this session. */ /** The distributed object manager we're using during this session. */
@@ -757,7 +723,7 @@ public class Client
/** Whether or not this client is operating in a standalone mode. */ /** Whether or not this client is operating in a standalone mode. */
protected boolean _standalone; protected boolean _standalone;
/** The game server host. */ /** The game server host. */
protected String _hostname; protected String _hostname;
@@ -771,22 +737,23 @@ public class Client
/** The entity that manages our network communications. */ /** The entity that manages our network communications. */
protected Communicator _comm; protected Communicator _comm;
/** A custom class loader used to load objects that come in over the /** A custom class loader used to load objects that come in over the network. */
* network. */
protected ClassLoader _loader = getClass().getClassLoader(); protected ClassLoader _loader = getClass().getClassLoader();
/** General startup information provided by the server. */ /** General startup information provided by the server. */
protected BootstrapData _bstrap; protected BootstrapData _bstrap;
/** The set of bootstrap service groups this client cares about. */
protected HashSet<String> _bootGroups = new HashSet<String>();
/** Manages invocation services. */ /** Manages invocation services. */
protected InvocationDirector _invdir = new InvocationDirector(); protected InvocationDirector _invdir = new InvocationDirector();
/** The difference between the server clock and the client clock /** The difference between the server clock and the client clock (estimated immediately after
* (estimated immediately after logging on). */ * logging on). */
protected long _serverDelta; protected long _serverDelta;
/** Used when establishing our clock delta between the client and /** Used when establishing our clock delta between the client and server. */
* server. */
protected DeltaCalculator _dcalc; protected DeltaCalculator _dcalc;
/** The last time at which we synced our clock with the server. */ /** The last time at which we synced our clock with the server. */
@@ -54,9 +54,8 @@ import com.threerings.presents.net.LogoffRequest;
import com.threerings.presents.net.UpstreamMessage; import com.threerings.presents.net.UpstreamMessage;
/** /**
* The client performs all network I/O on separate threads (one for * The client performs all network I/O on separate threads (one for reading and one for
* reading and one for writing). The communicator class encapsulates that * writing). The communicator class encapsulates that functionality.
* functionality.
* *
* <pre> * <pre>
* Logon synopsis: * Logon synopsis:
@@ -79,8 +78,7 @@ import com.threerings.presents.net.UpstreamMessage;
public class Communicator public class Communicator
{ {
/** /**
* Creates a new communicator instance which is associated with the * Creates a new communicator instance which is associated with the supplied client.
* supplied client.
*/ */
public Communicator (Client client) public Communicator (Client client)
{ {
@@ -88,8 +86,7 @@ public class Communicator
} }
/** /**
* Logs on to the server and initiates our full-duplex message * Logs on to the server and initiates our full-duplex message exchange.
* exchange.
*/ */
public void logon () public void logon ()
{ {
@@ -98,21 +95,19 @@ public class Communicator
throw new RuntimeException("Communicator already started."); throw new RuntimeException("Communicator already started.");
} }
// start up the reader thread. it will connect to the server and // start up the reader thread. it will connect to the server and start up the writer thread
// start up the writer thread if everything went successfully // if everything went successfully
_reader = new Reader(); _reader = new Reader();
_reader.start(); _reader.start();
} }
/** /**
* Delivers a logoff notification to the server and shuts down the * Delivers a logoff notification to the server and shuts down the network connection. Also
* network connection. Also causes all communication threads to * causes all communication threads to terminate.
* terminate.
*/ */
public synchronized void logoff () public synchronized void logoff ()
{ {
// if our socket is already closed, we've already taken care of // if our socket is already closed, we've already taken care of this business
// this business
if (_channel == null) { if (_channel == null) {
return; return;
} }
@@ -122,27 +117,22 @@ public class Communicator
// let our reader and writer know that it's time to go // let our reader and writer know that it's time to go
if (_reader != null) { if (_reader != null) {
// if logoff() is being called by the client as part of a // if logoff() is being called by the client as part of a normal shutdown, this will
// normal shutdown, this will cause the reader thread to be // cause the reader thread to be interrupted and shutdown gracefully. if logoff is
// interrupted and shutdown gracefully. if logoff is being // being called by the reader thread as a result of a failed socket, it won't interrupt
// called by the reader thread as a result of a failed socket, // itself as it is already shutting down gracefully. if the JVM is buggy and calling
// it won't interrupt itself as it is already shutting down // interrupt() on a thread that is blocked on a socket doesn't wake it up, then when we
// gracefully. if the JVM is buggy and calling interrupt() on // close() the socket a bit further down, we have another chance that the reader thread
// a thread that is blocked on a socket doesn't wake it up, // will wake up; this time slightly less gracefully because it will think there's a
// then when we close() the socket a bit further down, we have // network error when in fact we're just shutting down, but at least it will cleanly
// another chance that the reader thread will wake up; this // exit
// time slightly less gracefully because it will think there's
// a network error when in fact we're just shutting down, but
// at least it will cleanly exit
_reader.shutdown(); _reader.shutdown();
} }
if (_writer != null) { if (_writer != null) {
// shutting down the writer thread is simpler because we can // shutting down the writer thread is simpler because we can post a termination message
// post a termination message on the queue and be sure that it // on the queue and be sure that it will receive it. when the writer thread has
// will receive it. when the writer thread has delivered our // delivered our logoff request and exited, we will complete the logoff process by
// logoff request and exited, we will complete the logoff // closing our socket and invoking the clientDidLogoff callback
// process by closing our socket and invoking the
// clientDidLogoff callback
_writer.shutdown(); _writer.shutdown();
} }
} }
@@ -157,8 +147,8 @@ public class Communicator
} }
/** /**
* Configures this communicator with a custom class loader to be used * Configures this communicator with a custom class loader to be used when reading and writing
* when reading and writing objects over the network. * objects over the network.
*/ */
public void setClassLoader (ClassLoader loader) public void setClassLoader (ClassLoader loader)
{ {
@@ -169,10 +159,9 @@ public class Communicator
} }
/** /**
* Callback called by the reader when the authentication process * Callback called by the reader when the authentication process completes successfully. Here
* completes successfully. Here we extract the bootstrap information * we extract the bootstrap information for the client and start up the writer thread to manage
* for the client and start up the writer thread to manage the other * the other half of our bi-directional message stream.
* half of our bi-directional message stream.
*/ */
protected synchronized void logonSucceeded (AuthResponseData data) protected synchronized void logonSucceeded (AuthResponseData data)
{ {
@@ -188,22 +177,21 @@ public class Communicator
_writer = new Writer(); _writer = new Writer();
_writer.start(); _writer.start();
// fill the auth data into the client's local field so that it can // fill the auth data into the client's local field so that it can be requested by external
// be requested by external entities // entities
_client._authData = data; _client._authData = data;
// wait for the bootstrap notification before we claim that we're // wait for the bootstrap notification before we claim that we're actually logged on
// actually logged on
} }
/** /**
* Callback called by the reader or writer thread when something goes * Callback called by the reader or writer thread when something goes awry with our socket
* awry with our socket connection to the server. * connection to the server.
*/ */
protected synchronized void connectionFailed (IOException ioe) protected synchronized void connectionFailed (IOException ioe)
{ {
// make sure the socket isn't already closed down (meaning we've // make sure the socket isn't already closed down (meaning we've already dealt with the
// already dealt with the failed connection) // failed connection)
if (_channel == null) { if (_channel == null) {
return; return;
} }
@@ -219,13 +207,12 @@ public class Communicator
} }
/** /**
* Callback called by the reader if the server closes the other end of * Callback called by the reader if the server closes the other end of the connection.
* the connection.
*/ */
protected synchronized void connectionClosed () protected synchronized void connectionClosed ()
{ {
// make sure the socket isn't already closed down (meaning we've // make sure the socket isn't already closed down (meaning we've already dealt with the
// already dealt with the closed connection) // closed connection)
if (_channel == null) { if (_channel == null) {
return; return;
} }
@@ -244,8 +231,8 @@ public class Communicator
_reader = null; _reader = null;
if (_writer == null) { if (_writer == null) {
// there's no writer during authentication, so we may be // there's no writer during authentication, so we may be responsible for closing the
// responsible for closing the socket channel // socket channel
closeChannel(); closeChannel();
// let the client know when we finally go away // let the client know when we finally go away
@@ -267,9 +254,8 @@ public class Communicator
// let the client observers know that we're logged off // let the client observers know that we're logged off
_client.notifyObservers(Client.CLIENT_DID_LOGOFF, null); _client.notifyObservers(Client.CLIENT_DID_LOGOFF, null);
// now that the writer thread has gone away, we can safely close // now that the writer thread has gone away, we can safely close our socket and let the
// our socket and let the client know that the logoff process has // client know that the logoff process has completed
// completed
closeChannel(); closeChannel();
// let the client know when we finally go away // let the client know when we finally go away
@@ -279,9 +265,8 @@ public class Communicator
} }
/** /**
* Closes the socket channel that we have open to the server. Called * Closes the socket channel that we have open to the server. Called by either {@link
* by either {@link #readerDidExit} or {@link #writerDidExit} * #readerDidExit} or {@link #writerDidExit} whichever is called last.
* whichever is called last.
*/ */
protected void closeChannel () protected void closeChannel ()
{ {
@@ -319,16 +304,13 @@ public class Communicator
try { try {
ByteBuffer buffer = _fout.frameAndReturnBuffer(); ByteBuffer buffer = _fout.frameAndReturnBuffer();
if (buffer.limit() > 4096) { if (buffer.limit() > 4096) {
String txt = StringUtil.truncate( String txt = StringUtil.truncate(String.valueOf(msg), 80, "...");
String.valueOf(msg), 80, "..."); Log.info("Whoa, writin' a big one [msg=" + txt + ", size=" + buffer.limit() + "].");
Log.info("Whoa, writin' a big one [msg=" + txt +
", size=" + buffer.limit() + "].");
} }
int wrote = _channel.write(buffer); int wrote = _channel.write(buffer);
if (wrote != buffer.limit()) { if (wrote != buffer.limit()) {
Log.warning("Aiya! Couldn't write entire message [msg=" + msg + Log.warning("Aiya! Couldn't write entire message [msg=" + msg +
", size=" + buffer.limit() + ", size=" + buffer.limit() + ", wrote=" + wrote + "].");
", wrote=" + wrote + "].");
// } else { // } else {
// Log.info("Wrote " + wrote + " bytes."); // Log.info("Wrote " + wrote + " bytes.");
} }
@@ -350,8 +332,7 @@ public class Communicator
} }
/** /**
* Makes a note of the time at which we last communicated with the * Makes a note of the time at which we last communicated with the server.
* server.
*/ */
protected synchronized void updateWriteStamp () protected synchronized void updateWriteStamp ()
{ {
@@ -359,17 +340,15 @@ public class Communicator
} }
/** /**
* Reads a new message from the socket (blocking until a message has * Reads a new message from the socket (blocking until a message has arrived).
* arrived).
*/ */
protected DownstreamMessage receiveMessage () protected DownstreamMessage receiveMessage ()
throws IOException throws IOException
{ {
// read in the next message frame (readFrame() can return false // read in the next message frame (readFrame() can return false meaning it only read part
// meaning it only read part of the frame from the network, in // of the frame from the network, in which case we simply call it again because we can't do
// which case we simply call it again because we can't do anything // anything until it has a whole frame; it will throw an exception if it hits EOF or if
// until it has a whole frame; it will throw an exception if it // something goes awry)
// hits EOF or if something goes awry)
while (!_fin.readFrame(_channel)); while (!_fin.readFrame(_channel));
try { try {
@@ -386,8 +365,8 @@ public class Communicator
} }
/** /**
* Callback called by the reader thread when it has parsed a new * Callback called by the reader thread when it has parsed a new message from the socket and
* message from the socket and wishes to have it processed. * wishes to have it processed.
*/ */
protected void processMessage (DownstreamMessage msg) protected void processMessage (DownstreamMessage msg)
{ {
@@ -396,9 +375,8 @@ public class Communicator
} }
/** /**
* Cancels our preferred port saving interval. This method is called from * Cancels our preferred port saving interval. This method is called from the communication
* the communication reader thread and the interval thread and must thus be * reader thread and the interval thread and must thus be synchronized.
* synchronized.
*/ */
protected synchronized boolean clearPPI (boolean cancel) protected synchronized boolean clearPPI (boolean cancel)
{ {
@@ -414,10 +392,9 @@ public class Communicator
} }
/** /**
* The reader encapsulates the authentication and message reading * The reader encapsulates the authentication and message reading process. It calls back to the
* process. It calls back to the <code>Communicator</code> class to do * {@link Communicator} class to do things, but the general flow of the reader thread is
* things, but the general flow of the reader thread is encapsulated * encapsulated in this class.
* in this class.
*/ */
protected class Reader extends LoopingThread protected class Reader extends LoopingThread
{ {
@@ -452,8 +429,8 @@ public class Communicator
// look up the address of the target server // look up the address of the target server
InetAddress host = InetAddress.getByName(_client.getHostname()); InetAddress host = InetAddress.getByName(_client.getHostname());
// obtain the list of available ports on which to attempt our // obtain the list of available ports on which to attempt our client connection and
// client connection and determine our preferred port // determine our preferred port
String pportKey = _client.getHostname() + ".preferred_port"; String pportKey = _client.getHostname() + ".preferred_port";
int[] ports = _client.getPorts(); int[] ports = _client.getPorts();
int pport = PresentsPrefs.config.getValue(pportKey, ports[0]); int pport = PresentsPrefs.config.getValue(pportKey, ports[0]);
@@ -468,18 +445,16 @@ public class Communicator
try { try {
synchronized (Communicator.this) { synchronized (Communicator.this) {
clearPPI(true); clearPPI(true);
_prefPortInterval = _prefPortInterval = new PrefPortInterval(pportKey, port, nextPort);
new PrefPortInterval(pportKey, port, nextPort);
_channel = SocketChannel.open(addr); _channel = SocketChannel.open(addr);
_prefPortInterval.schedule(PREF_PORT_DELAY); _prefPortInterval.schedule(PREF_PORT_DELAY);
} }
break; break;
} catch (IOException ioe) { } catch (IOException ioe) {
if (ioe instanceof ConnectException && if (ioe instanceof ConnectException && ii < (ports.length-1)) {
ii < (ports.length-1)) {
_client.reportLogonTribulations( _client.reportLogonTribulations(
new LogonException( new LogonException(AuthCodes.TRYING_NEXT_PORT, true));
AuthCodes.TRYING_NEXT_PORT, true));
continue; // try the next port continue; // try the next port
} }
throw ioe; throw ioe;
@@ -488,8 +463,8 @@ public class Communicator
_channel.configureBlocking(true); _channel.configureBlocking(true);
// our messages are framed (preceded by their length), so we // our messages are framed (preceded by their length), so we use these helper streams
// use these helper streams to manage the framing // to manage the framing
_fin = new FramedInputStream(); _fin = new FramedInputStream();
_fout = new FramingOutputStream(); _fout = new FramingOutputStream();
@@ -503,8 +478,8 @@ public class Communicator
throws IOException, LogonException throws IOException, LogonException
{ {
// construct an auth request and send it // construct an auth request and send it
AuthRequest req = new AuthRequest(_client.getCredentials(), AuthRequest req = new AuthRequest(
_client.getVersion()); _client.getCredentials(), _client.getVersion(), _client._bootGroups);
sendMessage(req); sendMessage(req);
// now wait for the auth response // now wait for the auth response
@@ -513,8 +488,8 @@ public class Communicator
AuthResponseData data = rsp.getData(); AuthResponseData data = rsp.getData();
Log.debug("Got auth response: " + data); Log.debug("Got auth response: " + data);
// if the auth request failed, we want to let the communicator // if the auth request failed, we want to let the communicator know by throwing a logon
// know by throwing a logon exception // exception
if (!data.code.equals(AuthResponseData.SUCCESS)) { if (!data.code.equals(AuthResponseData.SUCCESS)) {
throw new LogonException(data.code); throw new LogonException(data.code);
} }
@@ -523,9 +498,8 @@ public class Communicator
logonSucceeded(data); logonSucceeded(data);
} }
// now that we're authenticated, we manage the reading // now that we're authenticated, we manage the reading half of things by continuously
// half of things by continuously reading messages from // reading messages from the socket and processing them
// the socket and processing them
protected void iterate () protected void iterate ()
{ {
DownstreamMessage msg = null; DownstreamMessage msg = null;
@@ -538,14 +512,12 @@ public class Communicator
processMessage(msg); processMessage(msg);
} catch (InterruptedIOException iioe) { } catch (InterruptedIOException iioe) {
// somebody set up us the bomb! we've been interrupted // somebody set up us the bomb! we've been interrupted which means that we're being
// which means that we're being shut down, so we just // shut down, so we just report it and return from iterate() like a good monkey
// report it and return from iterate() like a good monkey
Log.debug("Reader thread woken up in time to die."); Log.debug("Reader thread woken up in time to die.");
} catch (EOFException eofe) { } catch (EOFException eofe) {
// let the communicator know that our connection was // let the communicator know that our connection was closed
// closed
connectionClosed(); connectionClosed();
// and shut ourselves down // and shut ourselves down
shutdown(); shutdown();
@@ -557,8 +529,7 @@ public class Communicator
shutdown(); shutdown();
} catch (Exception e) { } catch (Exception e) {
Log.warning("Error processing message [msg=" + msg + Log.warning("Error processing message [msg=" + msg + ", error=" + e + "].");
", error=" + e + "].");
} }
} }
@@ -570,8 +541,8 @@ public class Communicator
protected void didShutdown () protected void didShutdown ()
{ {
// If we haven't recorded a preferred port yet, instead do the // if we haven't recorded a preferred port yet, instead do the failure action since we
// failure action since we didn't stay connected long enough. // didn't stay connected long enough
clearPPI(true); clearPPI(true);
// let the communicator know when we finally go away // let the communicator know when we finally go away
@@ -580,17 +551,16 @@ public class Communicator
protected void kick () protected void kick ()
{ {
// we want to interrupt the reader thread as it may be blocked // we want to interrupt the reader thread as it may be blocked listening to the socket;
// listening to the socket; this is only called if the reader // this is only called if the reader thread doesn't shut itself down
// thread doesn't shut itself down
// interrupt(); // interrupt();
} }
} }
/** /**
* The writer encapsulates the message writing process. It calls back * The writer encapsulates the message writing process. It calls back to the {@link
* to the <code>Communicator</code> class to do things, but the * Communicator} class to do things, but the general flow of the writer thread is encapsulated
* general flow of the writer thread is encapsulated in this class. * in this class.
*/ */
protected class Writer extends LoopingThread protected class Writer extends LoopingThread
{ {
@@ -599,9 +569,8 @@ public class Communicator
// fetch the next message from the queue // fetch the next message from the queue
UpstreamMessage msg = _msgq.get(); UpstreamMessage msg = _msgq.get();
// if this is a termination message, we're being // if this is a termination message, we're being requested to exit, so we want to bail
// requested to exit, so we want to bail now rather // now rather than continuing
// than continuing
if (msg instanceof TerminationMessage) { if (msg instanceof TerminationMessage) {
return; return;
} }
@@ -611,8 +580,7 @@ public class Communicator
sendMessage(msg); sendMessage(msg);
} catch (IOException ioe) { } catch (IOException ioe) {
// let the communicator know if we have any // let the communicator know if we have any problems
// problems
connectionFailed(ioe); connectionFailed(ioe);
// and bail // and bail
shutdown(); shutdown();
@@ -632,8 +600,8 @@ public class Communicator
protected void kick () protected void kick ()
{ {
// post a bogus message to the outgoing queue to ensure that // post a bogus message to the outgoing queue to ensure that the writer thread notices
// the writer thread notices that it's time to go // that it's time to go
postMessage(new TerminationMessage()); postMessage(new TerminationMessage());
} }
} }
@@ -643,12 +611,11 @@ public class Communicator
{ {
} }
/** Used to save our preferred port once we know our connection is not /** Used to save our preferred port once we know our connection is not going to be
* going to be unceremoniously closed by Windows Connection Sharing. */ * unceremoniously closed by Windows Connection Sharing. */
protected class PrefPortInterval extends Interval protected class PrefPortInterval extends Interval
{ {
public PrefPortInterval (String key, int thisPort, int nextPort) public PrefPortInterval (String key, int thisPort, int nextPort) {
{
super(); super();
_key = key; _key = key;
_thisPort = thisPort; _thisPort = thisPort;
@@ -691,8 +658,7 @@ public class Communicator
protected ClientDObjectMgr _omgr; protected ClientDObjectMgr _omgr;
protected ClassLoader _loader; protected ClassLoader _loader;
/** We use this interval to record the preferred port if it stays connected /** We use this interval to record the preferred port if it stays connected long enough. */
* long enough. */
protected PrefPortInterval _prefPortInterval; protected PrefPortInterval _prefPortInterval;
/** Time a port must remain connected before we mark it as preferred. */ /** Time a port must remain connected before we mark it as preferred. */
@@ -700,8 +666,7 @@ public class Communicator
/** Used to control low-level message logging. */ /** Used to control low-level message logging. */
protected static RuntimeAdjust.BooleanAdjust _logMessages = protected static RuntimeAdjust.BooleanAdjust _logMessages =
new RuntimeAdjust.BooleanAdjust( new RuntimeAdjust.BooleanAdjust("Toggles whether or not all sent and received low-level " +
"Toggles whether or not all sent and received low-level " + "network events are logged.", "narya.presents.log_events",
"network events are logged.", "narya.presents.log_events", PresentsPrefs.config, false);
PresentsPrefs.config, false);
} }
@@ -21,8 +21,12 @@
package com.threerings.presents.net; package com.threerings.presents.net;
import java.util.HashSet;
import java.util.TimeZone; import java.util.TimeZone;
/**
* Used to authenticate with the server.
*/
public class AuthRequest extends UpstreamMessage public class AuthRequest extends UpstreamMessage
{ {
/** /**
@@ -34,14 +38,14 @@ public class AuthRequest extends UpstreamMessage
} }
/** /**
* Constructs a auth request with the supplied credentials and client * Constructs a auth request with the supplied credentials and client version information.
* version information.
*/ */
public AuthRequest (Credentials creds, String version) public AuthRequest (Credentials creds, String version, HashSet<String> bootGroups)
{ {
_creds = creds; _creds = creds;
_version = version; _version = version;
_zone = TimeZone.getDefault().getID(); _zone = TimeZone.getDefault().getID();
_bootGroups = bootGroups.toArray(new String[bootGroups.size()]);
} }
/** /**
@@ -53,8 +57,7 @@ public class AuthRequest extends UpstreamMessage
} }
/** /**
* Returns a reference to the version information provided with this * Returns a reference to the version information provided with this request.
* request.
*/ */
public String getVersion () public String getVersion ()
{ {
@@ -69,6 +72,14 @@ public class AuthRequest extends UpstreamMessage
return TimeZone.getTimeZone(_zone); return TimeZone.getTimeZone(_zone);
} }
/**
* Returns the set of bootstrap service groups in which this client is interested.
*/
public String[] getBootGroups ()
{
return _bootGroups;
}
/** /**
* Generates a string representation of this instance. * Generates a string representation of this instance.
*/ */
@@ -86,4 +97,7 @@ public class AuthRequest extends UpstreamMessage
/** The timezone in which this client is operating. */ /** The timezone in which this client is operating. */
protected String _zone; protected String _zone;
/** The set of bootstrap service groups this client is interested in. */
protected String[] _bootGroups;
} }
@@ -24,6 +24,8 @@ package com.threerings.presents.net;
import com.threerings.io.SimpleStreamableObject; import com.threerings.io.SimpleStreamableObject;
import com.threerings.util.StreamableArrayList; import com.threerings.util.StreamableArrayList;
import com.threerings.presents.data.InvocationMarshaller;
/** /**
* A <code>BootstrapData</code> object is communicated back to the client * A <code>BootstrapData</code> object is communicated back to the client
* after authentication has succeeded and after the server is fully * after authentication has succeeded and after the server is fully
@@ -36,5 +38,5 @@ public class BootstrapData extends SimpleStreamableObject
public int clientOid; public int clientOid;
/** A list of handles to invocation services. */ /** A list of handles to invocation services. */
public StreamableArrayList services; public StreamableArrayList<InvocationMarshaller> services;
} }
@@ -40,40 +40,40 @@ import com.threerings.presents.dobj.InvocationRequestEvent;
import com.threerings.presents.dobj.ObjectAccessException; import com.threerings.presents.dobj.ObjectAccessException;
import com.threerings.presents.dobj.RootDObjectManager; import com.threerings.presents.dobj.RootDObjectManager;
import java.util.HashMap;
/** /**
* The invocation services provide client to server invocations (service * The invocation services provide client to server invocations (service requests) and server to
* requests) and server to client invocations (responses and * client invocations (responses and notifications). Via this mechanism, the client can make
* notifications). Via this mechanism, the client can make requests of the * requests of the server, be notified of its response and the server can asynchronously invoke
* server, be notified of its response and the server can asynchronously * code on the client.
* invoke code on the client.
* *
* <p> Invocations are like remote procedure calls in that they are named * <p> Invocations are like remote procedure calls in that they are named and take arguments. All
* and take arguments. All arguments must be {@link Streamable} objects, * arguments must be {@link Streamable} objects, primitive types, or String objects. All arguments
* primitive types, or String objects. All arguments are passed by value * are passed by value (by serializing and unserializing the arguments); there is no special
* (by serializing and unserializing the arguments); there is no special * facility provided for referencing non-local objects (it is assumed that the distributed object
* facility provided for referencing non-local objects (it is assumed that * facility will already be in use for any objects that should be shared).
* the distributed object facility will already be in use for any objects
* that should be shared).
* *
* <p> The server invocation manager listens for invocation requests from * <p> The server invocation manager listens for invocation requests from the client and passes
* the client and passes them on to the invocation provider registered for * them on to the invocation provider registered for the requested invocation module. It also
* the requested invocation module. It also provides a mechanism by which * provides a mechanism by which responses and asynchronous notification invocations can be
* responses and asynchronous notification invocations can be delivered to * delivered to the client.
* the client.
*/ */
public class InvocationManager public class InvocationManager
implements EventListener implements EventListener
{ {
/** The list of services that are to be provided to clients at boot /** Defines the name of the global bootstrap group. */
* time. Don't mess with this list! */ public static final String GLOBAL_BOOTSTRAP_GROUP = "global";
public StreamableArrayList<InvocationMarshaller> bootlist =
new StreamableArrayList<InvocationMarshaller>(); /** A mapping from bootstrap group to lists of services that are to be provided to clients at
* boot time. Don't mess with these lists! */
public HashMap<String,StreamableArrayList<InvocationMarshaller>> bootlists =
new HashMap<String,StreamableArrayList<InvocationMarshaller>>();
/** /**
* Constructs an invocation manager which will use the supplied * Constructs an invocation manager which will use the supplied distributed object manager to
* distributed object manager to operate its invocation * operate its invocation services. Generally only one invocation manager should be operational
* services. Generally only one invocation manager should be * in a particular system.
* operational in a particular system.
*/ */
public InvocationManager (RootDObjectManager omgr) public InvocationManager (RootDObjectManager omgr)
{ {
@@ -96,17 +96,30 @@ public class InvocationManager
} }
/** /**
* Registers the supplied invocation dispatcher, returning a * Registers the supplied invocation dispatcher, returning a marshaller that can be used to
* marshaller that can be used to send requests to the provider for * send requests to the provider for whom the dispatcher is proxying.
* whom the dispatcher is proxying.
* *
* @param dispatcher the dispatcher to be registered. * @param dispatcher the dispatcher to be registered.
* @param bootstrap if true, the service instance will be added to the * @param bootstrap if true, the service instance will be added to the list of invocation
* list of invocation service objects provided to the client in the * service objects provided to the client in the bootstrap data.
* bootstrap data.
*/ */
public InvocationMarshaller registerDispatcher ( public InvocationMarshaller registerDispatcher (
InvocationDispatcher dispatcher, boolean bootstrap) InvocationDispatcher dispatcher, boolean bootstrap)
{
return registerDispatcher(dispatcher, bootstrap ? GLOBAL_BOOTSTRAP_GROUP : null);
}
/**
* Registers the supplied invocation dispatcher, returning a marshaller that can be used to
* send requests to the provider for whom the dispatcher is proxying.
*
* @param dispatcher the dispatcher to be registered.
* @param group the bootstrap group in which this marshaller is to be registered, or null if it
* is not a bootstrap service. <em>Do not:</em> register a dispatcher with multiple boot
* groups. You must collect shared dispatchers into as fine grained a set of groups as
* necessary and have different types of clients specify the list of groups they need.
*/
public InvocationMarshaller registerDispatcher (InvocationDispatcher dispatcher, String group)
{ {
// get the next invocation code // get the next invocation code
int invCode = nextInvCode(); int invCode = nextInvCode();
@@ -119,8 +132,12 @@ public class InvocationManager
_dispatchers.put(invCode, dispatcher); _dispatchers.put(invCode, dispatcher);
// if it's a bootstrap service, slap it in the list // if it's a bootstrap service, slap it in the list
if (bootstrap) { if (group != null) {
bootlist.add(marsh); StreamableArrayList<InvocationMarshaller> list = bootlists.get(group);
if (list == null) {
bootlists.put(group, list = new StreamableArrayList<InvocationMarshaller>());
}
list.add(marsh);
} }
_recentRegServices.put(Integer.valueOf(invCode), _recentRegServices.put(Integer.valueOf(invCode),
@@ -28,12 +28,14 @@ import java.util.Iterator;
import java.util.TimeZone; import java.util.TimeZone;
import com.samskivert.util.HashIntMap; import com.samskivert.util.HashIntMap;
import com.samskivert.util.Throttle;
import com.samskivert.util.ResultListener; import com.samskivert.util.ResultListener;
import com.samskivert.util.Throttle;
import com.threerings.util.Name; import com.threerings.util.Name;
import com.threerings.util.StreamableArrayList;
import com.threerings.presents.Log; import com.threerings.presents.Log;
import com.threerings.presents.data.ClientObject; import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.dobj.DEvent; import com.threerings.presents.dobj.DEvent;
import com.threerings.presents.dobj.DObject; import com.threerings.presents.dobj.DObject;
@@ -63,18 +65,15 @@ import com.threerings.presents.server.net.Connection;
import com.threerings.presents.server.net.MessageHandler; import com.threerings.presents.server.net.MessageHandler;
/** /**
* A client object represents a client session in the server. It is * Represents a client session in the server. It is associated with a connection instance (while
* associated with a connection instance (while the client is connected) * the client is connected) and acts as the intermediary for the remote client in terms of passing
* and acts as the intermediary for the remote client in terms of passing * along events forwarded by the client, ensuring that subscriptions are maintained on behalf of
* along events forwarded by the client, ensuring that subscriptions are * the client and that events are forwarded to the client.
* maintained on behalf of the client and that events are forwarded to the
* client.
* *
* <p><em>A note on synchronization:</em> the client object is structured * <p><em>A note on synchronization:</em> the client object is structured so that its
* so that its <code>Subscriber</code> implementation (which is called * <code>Subscriber</code> implementation (which is called from the dobjmgr thread) can proceed
* from the dobjmgr thread) can proceed without synchronization. This does * without synchronization. This does not overlap with its other client duties which are called
* not overlap with its other client duties which are called from the * from the conmgr thread and therefore also need not be synchronized.
* conmgr thread and therefore also need not be synchronized.
*/ */
public class PresentsClient public class PresentsClient
implements ProxySubscriber, MessageHandler, ClientResolutionListener implements ProxySubscriber, MessageHandler, ClientResolutionListener
@@ -82,23 +81,19 @@ public class PresentsClient
/** Used by {@link #setUsername} to report success or failure. */ /** Used by {@link #setUsername} to report success or failure. */
public static interface UserChangeListener public static interface UserChangeListener
{ {
/** Called when the new client object has been resolved and the /** Called when the new client object has been resolved and the new client object reported
* new client object reported to the client, but the old one has * to the client, but the old one has not yet been destroyed. Any events delivered on this
* not yet been destroyed. Any events delivered on this callback * callback to the old client object will be delivered.
* to the old client object will be delivered.
* *
* @param rl when this method is finished with its business and * @param rl when this method is finished with its business and the old client object can
* the old client object can be destroyed, the result listener * be destroyed, the result listener should be called. */
* should be called.
* */
public void changeReported (ClientObject newObji, ResultListener rl); public void changeReported (ClientObject newObji, ResultListener rl);
/** Called when the user change is completed, the old client /** Called when the user change is completed, the old client object is destroyed and all
* object is destroyed and all updates are committed. */ * updates are committed. */
public void changeCompleted (ClientObject newObj); public void changeCompleted (ClientObject newObj);
/** Called if some failure occurs during the user change /** Called if some failure occurs during the user change process. */
* process. */
public void changeFailed (Exception cause); public void changeFailed (Exception cause);
} }
@@ -119,8 +114,8 @@ public class PresentsClient
} }
/** /**
* Returns true if this client has been disconnected for sufficiently * Returns true if this client has been disconnected for sufficiently long that its session
* long that its session should be forcibly ended. * should be forcibly ended.
*/ */
public boolean checkExpired (long now) public boolean checkExpired (long now)
{ {
@@ -128,8 +123,7 @@ public class PresentsClient
} }
/** /**
* Returns the time at which this client started their network * Returns the time at which this client started their network session.
* session.
*/ */
public long getSessionStamp () public long getSessionStamp ()
{ {
@@ -137,8 +131,7 @@ public class PresentsClient
} }
/** /**
* Returns the time at which this client most recently connected or * Returns the time at which this client most recently connected or disconnected.
* disconnected.
*/ */
public long getNetworkStamp () public long getNetworkStamp ()
{ {
@@ -154,8 +147,7 @@ public class PresentsClient
} }
/** /**
* Returns the address of the connected client or null if this client * Returns the address of the connected client or null if this client is not connected.
* is not connected.
*/ */
public InetAddress getInetAddress () public InetAddress getInetAddress ()
{ {
@@ -164,8 +156,8 @@ public class PresentsClient
} }
/** /**
* Configures this client with a custom class loader that will be used * Configures this client with a custom class loader that will be used when unserializing
* when unserializing classes from the network. * classes from the network.
*/ */
public void setClassLoader (ClassLoader loader) public void setClassLoader (ClassLoader loader)
{ {
@@ -177,38 +169,32 @@ public class PresentsClient
} }
/** /**
* <em>Danger:</em> this method is not for general consumption. This * <em>Danger:</em> this method is not for general consumption. This changes the username of
* changes the username of the client, but should only be done very * the client, but should only be done very early in a user's session, when you know that no
* early in a user's session, when you know that no one has mapped the * one has mapped the user based on their username or has in any other way made use of their
* user based on their username or has in any other way made use of * username in a way that will break. However, it should not be done <em>too</em> early in the
* their username in a way that will break. However, it should not be * session. The client must be fully resolved.
* done <em>too</em> early in the session. The client must be fully
* resolved.
* *
* <p> It exists to support systems wherein a user logs in with an * <p> It exists to support systems wherein a user logs in with an account username and then
* account username and then chooses a "screen name" by which they * chooses a "screen name" by which they will play (often from a small set of available
* will play (often from a small set of available "characters" * "characters" available per account). This will take care of remapping the username to client
* available per account). This will take care of remapping the * object mappings that were made by the Presents services when the user logs on, but anything
* username to client object mappings that were made by the Presents * else that has had its grubby mits on the username will be left to its own devices, hence the
* services when the user logs on, but anything else that has had its * care that must be exercised when using this method.
* grubby mits on the username will be left to its own devices, hence
* the care that must be exercised when using this method.
* *
* @param ucl an entity that will (optionally) be notified when the * @param ucl an entity that will (optionally) be notified when the username conversion process
* username conversion process is complete. * is complete.
*/ */
public void setUsername (Name username, final UserChangeListener ucl) public void setUsername (Name username, final UserChangeListener ucl)
{ {
ClientResolutionListener clr = new ClientResolutionListener() { ClientResolutionListener clr = new ClientResolutionListener() {
public void clientResolved (final Name username, public void clientResolved (final Name username, final ClientObject clobj)
final ClientObject clobj)
{ {
// if they old client object is gone by now, they ended // if they old client object is gone by now, they ended their session while we were
// their session while we were switching, so freak out // switching, so freak out
if (_clobj == null) { if (_clobj == null) {
Log.warning("Client disappeared before we could " + Log.warning("Client disappeared before we could complete the switch to a " +
"complete the switch to a new client " + "new client object [ousername=" + _username +
"object [ousername=" + _username +
", nusername=" + username + "]."); ", nusername=" + username + "].");
_cmgr.releaseClientObject(username); _cmgr.releaseClientObject(username);
Exception error = new Exception("Early withdrawal"); Exception error = new Exception("Early withdrawal");
@@ -216,10 +202,8 @@ public class PresentsClient
return; return;
} }
// let the client know that the rug has been yanked out // let the client know that the rug has been yanked out from under their ass
// from under their ass Object[] args = new Object[] { Integer.valueOf(clobj.getOid()) };
Object[] args = new Object[] {
Integer.valueOf(clobj.getOid()) };
_clobj.postMessage(ClientObject.CLOBJ_CHANGED, args); _clobj.postMessage(ClientObject.CLOBJ_CHANGED, args);
// call down to any derived classes // call down to any derived classes
@@ -231,7 +215,6 @@ public class PresentsClient
public void requestCompleted (Object result) { public void requestCompleted (Object result) {
finishResolved(username, clobj); finishResolved(username, clobj);
} }
public void requestFailed (Exception cause) { public void requestFailed (Exception cause) {
finishResolved(username, clobj); finishResolved(username, clobj);
} }
@@ -263,9 +246,8 @@ public class PresentsClient
} }
public void resolutionFailed (Name username, Exception reason) { public void resolutionFailed (Name username, Exception reason) {
Log.warning("Unable to resolve new client object " + Log.warning("Unable to resolve new client object [oldname=" + _username +
"[oldname=" + _username + ", newname=" + username + ", newname=" + username + ", reason=" + reason + "].");
", reason=" + reason + "].");
Log.logStackTrace(reason); Log.logStackTrace(reason);
// let our listener know we're hosed // let our listener know we're hosed
@@ -288,25 +270,21 @@ public class PresentsClient
} }
/** /**
* Forcibly terminates a client's session. This must be called from * Forcibly terminates a client's session. This must be called from the dobjmgr thread.
* the dobjmgr thread.
*/ */
public void endSession () public void endSession ()
{ {
// queue up a request for our connection to be closed (if we have // queue up a request for our connection to be closed (if we have a connection, that is)
// a connection, that is)
Connection conn = getConnection(); Connection conn = getConnection();
if (conn != null) { if (conn != null) {
// go ahead and clear out our connection now to prevent // go ahead and clear out our connection now to prevent funniness
// funniness
setConnection(null); setConnection(null);
// have the connection manager close our connection when it is // have the connection manager close our connection when it is next convenient
// next convenient
PresentsServer.conmgr.closeConnection(conn); PresentsServer.conmgr.closeConnection(conn);
} }
// if we don't have a client object, we failed to resolve in the // if we don't have a client object, we failed to resolve in the first place, in which case
// first place, in which case we have to cope as best we can // we have to cope as best we can
if (_clobj != null) { if (_clobj != null) {
// and clean up after ourselves // and clean up after ourselves
try { try {
@@ -328,17 +306,15 @@ public class PresentsClient
} }
/** /**
* This is called when the server is shut down in the middle of a * This is called when the server is shut down in the middle of a client session. In this
* client session. In this circumstance, {@link #endSession} will * circumstance, {@link #endSession} will <em>not</em> be called and so any persistent data
* <em>not</em> be called and so any persistent data that might * that might normally be flushed at the end of a client's session should likely be flushed
* normally be flushed at the end of a client's session should likely * here.
* be flushed here.
*/ */
public void shutdown () public void shutdown ()
{ {
// if the client is connected, we need to fake the computation of // if the client is connected, we need to fake the computation of their final connect time
// their final connect time because we won't be closing their // because we won't be closing their socket normally
// socket normally
if (getConnection() != null) { if (getConnection() != null) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
_connectTime += ((now - _networkStamp) / 1000); _connectTime += ((now - _networkStamp) / 1000);
@@ -346,10 +322,9 @@ public class PresentsClient
} }
/** /**
* Makes a note that this client is subscribed to this object so that * Makes a note that this client is subscribed to this object so that we can clean up after
* we can clean up after ourselves if and when the client goes * ourselves if and when the client goes away. This is called by the client internals and
* away. This is called by the client internals and needn't be called * needn't be called by code outside the client.
* by code outside the client.
*/ */
public synchronized void mapSubscrip (DObject object) public synchronized void mapSubscrip (DObject object)
{ {
@@ -357,10 +332,9 @@ public class PresentsClient
} }
/** /**
* Makes a note that this client is no longer subscribed to this * Makes a note that this client is no longer subscribed to this object. The subscription map
* object. The subscription map is used to clean up after the client * is used to clean up after the client when it goes away. This is called by the client
* when it goes away. This is called by the client internals and * internals and needn't be called by code outside the client.
* needn't be called by code outside the client.
*/ */
public synchronized void unmapSubscrip (int oid) public synchronized void unmapSubscrip (int oid)
{ {
@@ -368,8 +342,7 @@ public class PresentsClient
if (object != null) { if (object != null) {
object.removeSubscriber(this); object.removeSubscriber(this);
} else { } else {
Log.warning("Requested to unmap non-existent subscription " + Log.warning("Requested to unmap non-existent subscription [oid=" + oid + "].");
"[oid=" + oid + "].");
} }
} }
@@ -403,24 +376,23 @@ public class PresentsClient
{ {
_messagesIn++; // count 'em up! _messagesIn++; // count 'em up!
// if the client has been getting crazy with the cheeze whiz, // if the client has been getting crazy with the cheeze whiz, stick a fork in them; the
// stick a fork in them; the first time through we end our // first time through we end our session, subsequently _throttle is null and we just drop
// session, subsequently _throttle is null and we just drop any // any messages that come in until we've fully shutdown
// messages that come in until we've fully shutdown
if (_throttle == null) { if (_throttle == null) {
// Log.info("Dropping message from force-quit client " + // Log.info("Dropping message from force-quit client [conn=" + _conn +
// "[conn=" + _conn +
// ", msg=" + message + "]."); // ", msg=" + message + "].");
return; return;
} else if (_throttle.throttleOp(message.received)) { } else if (_throttle.throttleOp(message.received)) {
Log.warning("Client sent more than 100 messages in 10 seconds, " + Log.warning("Client sent more than 100 messages in 10 seconds, forcing " +
"forcing disconnect " + this + "."); "disconnect " + this + ".");
safeEndSession(); safeEndSession();
_throttle = null; _throttle = null;
} }
// we dispatch to a message dispatcher that is specialized for the // we dispatch to a message dispatcher that is specialized for the particular class of
// particular class of message that we received // message that we received
MessageDispatcher disp = _disps.get(message.getClass()); MessageDispatcher disp = _disps.get(message.getClass());
if (disp == null) { if (disp == null) {
Log.warning("No dispatcher for message [msg=" + message + "]."); Log.warning("No dispatcher for message [msg=" + message + "].");
@@ -453,8 +425,7 @@ public class PresentsClient
public void eventReceived (DEvent event) public void eventReceived (DEvent event)
{ {
if (event instanceof PresentsDObjectMgr.AccessObjectEvent) { if (event instanceof PresentsDObjectMgr.AccessObjectEvent) {
Log.warning("Ignoring event that shouldn't be forwarded " + Log.warning("Ignoring event that shouldn't be forwarded " + event + ".");
event + ".");
Thread.dumpStack(); Thread.dumpStack();
} else { } else {
postMessage(new EventNotification(event)); postMessage(new EventNotification(event));
@@ -462,10 +433,9 @@ public class PresentsClient
} }
/** /**
* Called when {@link #setUsername} has been called and the new client * Called when {@link #setUsername} has been called and the new client object is about to be
* object is about to be applied to this client. The old client object * applied to this client. The old client object will not yet have been destroyed, so any final
* will not yet have been destroyed, so any final events can be sent * events can be sent along prior to the new object being put into effect.
* along prior to the new object being put into effect.
*/ */
protected void clientObjectWillChange ( protected void clientObjectWillChange (
ClientObject oldClobj, ClientObject newClobj) ClientObject oldClobj, ClientObject newClobj)
@@ -473,16 +443,16 @@ public class PresentsClient
} }
/** /**
* Called after the new client object has been committed to this * Called after the new client object has been committed to this client due to a call to {@link
* client due to a call to {@link #setUsername}. * #setUsername}.
*/ */
protected void clientObjectDidChange (ClientObject newClobj) protected void clientObjectDidChange (ClientObject newClobj)
{ {
} }
/** /**
* Initializes this client instance with the specified username, connection * Initializes this client instance with the specified username, connection instance and client
* instance and client object and begins a client session. * object and begins a client session.
*/ */
protected void startSession ( protected void startSession (
ClientManager cmgr, AuthRequest req, Connection conn, Object authdata) ClientManager cmgr, AuthRequest req, Connection conn, Object authdata)
@@ -503,12 +473,10 @@ public class PresentsClient
} }
/** /**
* This is factored out to allow derived classes to use a different * This is factored out to allow derived classes to use a different starting username than the
* starting username than the one supplied in the user's credentials. * one supplied in the user's credentials. Generally one only wants to munge the starting
* Generally one only wants to munge the starting username if the user * username if the user will subsequently choose a "screen name" and it is desirable to avoid
* will subsequently choose a "screen name" and it is desirable to * collision between the authentication user namespace and the screen namespace.
* avoid collision between the authentication user namespace and the
* screen namespace.
*/ */
protected void assignStartingUsername () protected void assignStartingUsername ()
{ {
@@ -516,80 +484,67 @@ public class PresentsClient
} }
/** /**
* Called by the client manager when a new connection arrives that * Called by the client manager when a new connection arrives that authenticates as this
* authenticates as this already established client. This must only be * already established client. This must only be called from the congmr thread.
* called from the congmr thread.
*/ */
protected void resumeSession (Connection conn) protected void resumeSession (Connection conn)
{ {
// check to see if we've already got a connection object, in which // check to see if we've already got a connection object, in which case it's probably stale
// case it's probably stale
Connection oldconn = getConnection(); Connection oldconn = getConnection();
if (oldconn != null && !oldconn.isClosed()) { if (oldconn != null && !oldconn.isClosed()) {
Log.info("Closing stale connection [old=" + oldconn + Log.info("Closing stale connection [old=" + oldconn + ", new=" + conn + "].");
", new=" + conn + "]."); // close the old connection (which results in everything being properly unregistered)
// close the old connection (which results in everything being
// properly unregistered)
oldconn.close(); oldconn.close();
} }
// start using the new connection // start using the new connection
setConnection(conn); setConnection(conn);
// if a client connects, drops the connection and reconnects // if a client connects, drops the connection and reconnects within the span of a very
// within the span of a very short period of time, we'll find // short period of time, we'll find ourselves in resumeSession() before their client object
// ourselves in resumeSession() before their client object was // was resolved from the initial connection; in such a case, we can simply bail out here
// resolved from the initial connection; in such a case, we can // and let the original session establishment code take care of initializing this resumed
// simply bail out here and let the original session establishment // session
// code take care of initializing this resumed session
if (_clobj == null) { if (_clobj == null) {
Log.warning("Rapid-fire reconnect caused us to arrive in " + Log.warning("Rapid-fire reconnect caused us to arrive in resumeSession() before the " +
"resumeSession() before the original session " + "original session resolved its client object? " + this + ".");
"resolved its client object? " + this + ".");
return; return;
} }
// we need to get onto the distributed object thread so that we // we need to get onto the dobj thread so that we can finalize resumption of the session
// can finalize the resumption of the session.
PresentsServer.omgr.postRunnable(new Runnable() { PresentsServer.omgr.postRunnable(new Runnable() {
public void run () { public void run () {
// now that we're on the dobjmgr thread we can resume our // now that we're on the dobjmgr thread we can resume our session resumption
// session resumption
finishResumeSession(); finishResumeSession();
} }
}); });
} }
/** /**
* This is called from the dobjmgr thread to complete the session * This is called from the dobjmgr thread to complete the session resumption. We call some call
* resumption. We call some call backs and send the bootstrap info to * backs and send the bootstrap info to the client.
* the client.
*/ */
protected void finishResumeSession () protected void finishResumeSession ()
{ {
// let derived classes do any session resuming // let derived classes do any session resuming
sessionWillResume(); sessionWillResume();
// send off a bootstrap notification immediately because we've // send off a bootstrap notification immediately as we've already got our client object
// already got our client object
sendBootstrap(); sendBootstrap();
Log.info("Session resumed " + this + "."); Log.info("Session resumed " + this + ".");
} }
/** /**
* Queues up a runnable on the object manager thread where we can * Queues up a runnable on the object manager thread where we can safely end the session.
* safely end the session.
*/ */
protected void safeEndSession () protected void safeEndSession ()
{ {
PresentsServer.omgr.postRunnable(new Runnable() { PresentsServer.omgr.postRunnable(new Runnable() {
public void run () { public void run () {
if (getClientObject() == null) { if (getClientObject() == null) {
// refuse to end the session unless the client is // refuse to end the session unless the client is fully resolved
// fully resolved Log.warning("Refusing logoff from still-resolving client " + this + ".");
Log.warning("Refusing logoff request from " +
"still-resolving client " + this + ".");
} else { } else {
// end the session in a civilized manner // end the session in a civilized manner
endSession(); endSession();
@@ -599,8 +554,8 @@ public class PresentsClient
} }
/** /**
* Clears out the tracked client subscriptions. Called when the client * Clears out the tracked client subscriptions. Called when the client goes away and shouldn't
* goes away and shouldn't be called otherwise. * be called otherwise.
*/ */
protected void clearSubscrips (boolean verbose) protected void clearSubscrips (boolean verbose)
{ {
@@ -615,58 +570,50 @@ public class PresentsClient
} }
/** /**
* Called when the client session is first started. The client object * Called when the client session is first started. The client object has been created at this
* has been created at this point and after this method is executed, * point and after this method is executed, the bootstrap information will be sent to the
* the bootstrap information will be sent to the client which will * client which will trigger the start of the session. Derived classes that override this
* trigger the start of the session. Derived classes that override * method should be sure to call <code>super.sessionWillStart</code>.
* this method should be sure to call
* <code>super.sessionWillStart</code>.
* *
* <p><em>Note:</em> This function will be called on the dobjmgr * <p><em>Note:</em> This function will be called on the dobjmgr thread which means that object
* thread which means that object manipulations are OK, but client * manipulations are OK, but client instance manipulations must done carefully.
* instance manipulations must done carefully.
*/ */
protected void sessionWillStart () protected void sessionWillStart ()
{ {
} }
/** /**
* Called when the client resumes a session (after having disconnected * Called when the client resumes a session (after having disconnected and reconnected). After
* and reconnected). After this method is executed, the bootstrap * this method is executed, the bootstrap information will be sent to the client which will
* information will be sent to the client which will trigger the * trigger the resumption of the session. Derived classes that override this method should be
* resumption of the session. Derived classes that override this * sure to call <code>super.sessionWillResume</code>.
* method should be sure to call <code>super.sessionWillResume</code>.
* *
* <p><em>Note:</em> This function will be called on the dobjmgr * <p><em>Note:</em> This function will be called on the dobjmgr thread which means that object
* thread which means that object manipulations are OK, but client * manipulations are OK, but client instance manipulations must done carefully.
* instance manipulations must done carefully.
*/ */
protected void sessionWillResume () protected void sessionWillResume ()
{ {
} }
/** /**
* Called when the client session ends (either because the client * Called when the client session ends (either because the client logged off or because the
* logged off or because the server forcibly terminated the session). * server forcibly terminated the session). Derived classes that override this method should
* Derived classes that override this method should be sure to call * be sure to call <code>super.sessionDidEnd</code>.
* <code>super.sessionDidEnd</code>.
* *
* <p><em>Note:</em> This function will be called on the dobjmgr * <p><em>Note:</em> This function will be called on the dobjmgr thread which means that object
* thread which means that object manipulations are OK, but client * manipulations are OK, but client instance manipulations must done carefully.
* instance manipulations must done carefully.
*/ */
protected void sessionDidEnd () protected void sessionDidEnd ()
{ {
// clear out our subscriptions so that we don't get a complaint // clear out our subscriptions so that we don't get a complaint about inability to forward
// about inability to forward the object destroyed event we're // the object destroyed event we're about to generate
// about to generate
clearSubscrips(false); clearSubscrips(false);
} }
/** /**
* This is called once we have a handle on the client distributed * This is called once we have a handle on the client distributed object. It sends a bootstrap
* object. It sends a bootstrap notification to the client with all * notification to the client with all the information it will need to interact with the
* the information it will need to interact with the server. * server.
*/ */
protected void sendBootstrap () protected void sendBootstrap ()
{ {
@@ -681,9 +628,8 @@ public class PresentsClient
} }
/** /**
* Derived client classes can override this member to create derived * Derived client classes can override this member to create derived bootstrap data classes
* bootstrap data classes that contain extra bootstrap information, if * that contain extra bootstrap information, if desired.
* desired.
*/ */
protected BootstrapData createBootstrapData () protected BootstrapData createBootstrapData ()
{ {
@@ -691,14 +637,12 @@ public class PresentsClient
} }
/** /**
* Derived client classes can override this member to populate the * Derived client classes can override this member to populate the bootstrap data with
* bootstrap data with additional information. They should be sure to * additional information. They should be sure to call <code>super.populateBootstrapData</code>
* call <code>super.populateBootstrapData</code> before doing their * before doing their own populating, however.
* own populating, however.
* *
* <p><em>Note:</em> This function will be called on the dobjmgr * <p><em>Note:</em> This function will be called on the dobjmgr thread which means that object
* thread which means that object manipulations are OK, but client * manipulations are OK, but client instance manipulations must be done carefully.
* instance manipulations must be done carefully.
*/ */
protected void populateBootstrapData (BootstrapData data) protected void populateBootstrapData (BootstrapData data)
{ {
@@ -706,28 +650,36 @@ public class PresentsClient
data.clientOid = _clobj.getOid(); data.clientOid = _clobj.getOid();
// fill in the list of bootstrap services // fill in the list of bootstrap services
data.services = PresentsServer.invmgr.bootlist; data.services = new StreamableArrayList<InvocationMarshaller>();
StreamableArrayList<InvocationMarshaller> list =
PresentsServer.invmgr.bootlists.get(InvocationManager.GLOBAL_BOOTSTRAP_GROUP);
if (list != null) {
data.services.addAll(list);
}
for (String group : _areq.getBootGroups()) {
list = PresentsServer.invmgr.bootlists.get(group);
if (list != null) {
data.services.addAll(list);
}
}
} }
/** /**
* Called by the connection manager when this client's connection is * Called by the connection manager when this client's connection is unmapped. That may be
* unmapped. That may be because of a connection failure (in which * because of a connection failure (in which case this call will be followed up by a call to
* case this call will be followed up by a call to * <code>connectionFailed</code>) or it may be because of an orderly closing of the
* <code>connectionFailed</code>) or it may be because of an orderly * connection. In either case, the client can deal with its lack of a connection in this
* closing of the connection. In either case, the client can deal with * method. This is invoked by the conmgr thread and should behave accordingly.
* its lack of a connection in this method. This is invoked by the
* conmgr thread and should behave accordingly.
*/ */
protected void wasUnmapped () protected void wasUnmapped ()
{ {
// clear out our connection reference // clear out our connection reference
setConnection(null); setConnection(null);
// clear out our subscriptions. we need to do this on the dobjmgr // clear out our subscriptions. we need to do this on the dobjmgr thread. it is important
// thread. it is important that we do this *after* we clear out // that we do this *after* we clear out our connection reference. once the connection ref
// our connection reference. once the connection ref is null, no // is null, no more subscriptions will be processed (even those that were queued up before
// more subscriptions will be processed (even those that were // the connection went away)
// queued up before the connection went away)
PresentsServer.omgr.postRunnable(new Runnable() { PresentsServer.omgr.postRunnable(new Runnable() {
public void run () { public void run () {
sessionConnectionClosed(); sessionConnectionClosed();
@@ -736,38 +688,33 @@ public class PresentsClient
} }
/** /**
* Called on the dobjmgr thread when the connection associated with * Called on the dobjmgr thread when the connection associated with this session has been
* this session has been closed and unmapped. If the user logged off * closed and unmapped. If the user logged off before closing their connection, this will be
* before closing their connection, this will be preceded by a call to * preceded by a call to {@link #sessionDidEnd}.
* {@link #sessionDidEnd}.
*/ */
protected void sessionConnectionClosed () protected void sessionConnectionClosed ()
{ {
// clear out our dobj subscriptions in case they weren't cleared // clear out our dobj subscriptions in case they weren't cleared by a call to sessionDidEnd
// by a call to sessionDidEnd
clearSubscrips(false); clearSubscrips(false);
} }
/** /**
* Called by the connection manager when this client's connection * Called by the connection manager when this client's connection fails. This is invoked on the
* fails. This is invoked on the conmgr thread and should behave * conmgr thread and should behave accordingly.
* accordingly.
*/ */
protected void connectionFailed (IOException fault) protected void connectionFailed (IOException fault)
{ {
// nothing to do here presently. the client manager already // nothing to do here, the client manager already complained about the failed connection
// complained about the failed connection
} }
/** /**
* Sets our connection reference in a thread safe way. Also * Sets our connection reference in a thread safe way. Also establishes the back reference to
* establishes the back reference to us as the connection's message * us as the connection's message handler.
* handler.
*/ */
protected synchronized void setConnection (Connection conn) protected synchronized void setConnection (Connection conn)
{ {
// if our connection is being cleared out, record the amount of // if our connection is being cleared out, record the amount of time we were connected to
// time we were connected to our total connected time // our total connected time
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (_conn != null && conn == null) { if (_conn != null && conn == null) {
_connectTime += ((now - _networkStamp) / 1000); _connectTime += ((now - _networkStamp) / 1000);
@@ -776,8 +723,8 @@ public class PresentsClient
// keep a handle to the new connection // keep a handle to the new connection
_conn = conn; _conn = conn;
// tell the connection to pass messages on to us (if we're setting // tell the connection to pass messages on to us (if we're setting a connection rather than
// a connection rather than clearing one out) // clearing one out)
if (_conn != null) { if (_conn != null) {
_conn.setMessageHandler(this); _conn.setMessageHandler(this);
@@ -792,20 +739,19 @@ public class PresentsClient
} }
/** /**
* The connection instance must be accessed via this member function * The connection instance must be accessed via this member function because it is read from
* because it is read from both the dobjmgr and conmgr threads and is * both the dobjmgr and conmgr threads and is modified by the conmgr thread.
* modified by the conmgr thread.
* *
* @return The connection instance associated with this client or null * @return The connection instance associated with this client or null if the client is not
* if the client is not currently connected. * currently connected.
*/ */
protected synchronized Connection getConnection () protected synchronized Connection getConnection ()
{ {
return _conn; return _conn;
} }
/** Callable from non-dobjmgr thread, this queues up a runnable on the /** Callable from non-dobjmgr thread, this queues up a runnable on the dobjmgr thread to post
* dobjmgr thread to post the supplied message to this client. */ * the supplied message to this client. */
protected final void safePostMessage (final DownstreamMessage msg) protected final void safePostMessage (final DownstreamMessage msg)
{ {
PresentsServer.omgr.postRunnable(new Runnable() { PresentsServer.omgr.postRunnable(new Runnable() {
@@ -825,9 +771,8 @@ public class PresentsClient
return true; return true;
} }
// don't log dropped messages unless we're dropping a lot of them // don't log dropped messages unless we're dropping a lot of them (meaning something is
// (meaning something is still queueing messages up for this dead // still queueing messages up for this dead client even though it shouldn't be)
// client even though it shouldn't be)
if (++_messagesDropped % 50 == 0) { if (++_messagesDropped % 50 == 0) {
Log.warning("Dropping many messages? [client=" + this + Log.warning("Dropping many messages? [client=" + this +
", count=" + _messagesDropped + "]."); ", count=" + _messagesDropped + "].");
@@ -859,17 +804,15 @@ public class PresentsClient
{ {
buf.append("username=").append(_username); buf.append("username=").append(_username);
buf.append(", conn=").append(_conn); buf.append(", conn=").append(_conn);
buf.append(", cloid=").append( buf.append(", cloid=").append((_clobj == null) ? -1 : _clobj.getOid());
(_clobj == null) ? -1 : _clobj.getOid());
buf.append(", in=").append(_messagesIn); buf.append(", in=").append(_messagesIn);
buf.append(", out=").append(_messagesOut); buf.append(", out=").append(_messagesOut);
} }
/** /**
* Message dispatchers are used to dispatch each different type of * Message dispatchers are used to dispatch each different type of upstream message. We can
* upstream message. We can look the dispatcher up in a table and * look the dispatcher up in a table and invoke it through an overloaded member which is faster
* invoke it through an overloaded member which is faster (so we * (so we think) than doing a bunch of instanceofs.
* think) than doing a bunch of instanceofs.
*/ */
protected static interface MessageDispatcher protected static interface MessageDispatcher
{ {
@@ -887,8 +830,7 @@ public class PresentsClient
public void dispatch (PresentsClient client, UpstreamMessage msg) public void dispatch (PresentsClient client, UpstreamMessage msg)
{ {
SubscribeRequest req = (SubscribeRequest)msg; SubscribeRequest req = (SubscribeRequest)msg;
// Log.info("Subscribing [client=" + client + // Log.info("Subscribing [client=" + client + ", oid=" + req.getOid() + "].");
// ", oid=" + req.getOid() + "].");
// forward the subscribe request to the omgr for processing // forward the subscribe request to the omgr for processing
PresentsServer.omgr.subscribeToObject(req.getOid(), client); PresentsServer.omgr.subscribeToObject(req.getOid(), client);
@@ -911,8 +853,8 @@ public class PresentsClient
// update our subscription tracking table // update our subscription tracking table
client.unmapSubscrip(oid); client.unmapSubscrip(oid);
// post a response to the client letting them know that we // post a response to the client letting them know that we will no longer send them
// will no longer send them events regarding this object // events regarding this object
client.safePostMessage(new UnsubscribeResponse(oid)); client.safePostMessage(new UnsubscribeResponse(oid));
} }
} }
@@ -930,8 +872,7 @@ public class PresentsClient
// fill in the proper source oid // fill in the proper source oid
fevt.setSourceOid(client.getClientObject().getOid()); fevt.setSourceOid(client.getClientObject().getOid());
// Log.info("Forwarding event [client=" + client + // Log.info("Forwarding event [client=" + client + ", event=" + fevt + "].");
// ", event=" + fevt + "].");
// forward the event to the omgr for processing // forward the event to the omgr for processing
PresentsServer.omgr.postEvent(fevt); PresentsServer.omgr.postEvent(fevt);
@@ -975,16 +916,15 @@ public class PresentsClient
/** The time at which this client started their session. */ /** The time at which this client started their session. */
protected long _sessionStamp; protected long _sessionStamp;
/** The time at which this client most recently connected or /** The time at which this client most recently connected or disconnected. */
* disconnected. */
protected long _networkStamp; protected long _networkStamp;
/** The total number of seconds for which the user was connected to /** The total number of seconds for which the user was connected to the server in this
* the server in this session. */ * session. */
protected int _connectTime; protected int _connectTime;
/** Prevent the client from sending too many messages too frequently. /** Prevent the client from sending too many messages too frequently. 100 messages in 10
* 100 messages in 10 seconds and you're audi. */ * seconds and you're audi like 5000. */
protected Throttle _throttle = new Throttle(100, 10 * 1000L); protected Throttle _throttle = new Throttle(100, 10 * 1000L);
// keep these for kicks and giggles // keep these for kicks and giggles
@@ -996,8 +936,8 @@ public class PresentsClient
protected static HashMap<Class<?>,MessageDispatcher> _disps = protected static HashMap<Class<?>,MessageDispatcher> _disps =
new HashMap<Class<?>,MessageDispatcher>(); new HashMap<Class<?>,MessageDispatcher>();
/** The amount of time after disconnection a user is allowed before /** The amount of time after disconnection a user is allowed before their session is forcibly
* their session is forcibly ended. */ * ended. */
protected static final long FLUSH_TIME = 7 * 60 * 1000L; protected static final long FLUSH_TIME = 7 * 60 * 1000L;
// register our message dispatchers // register our message dispatchers