Convert Narya (most of the way) over to a Maven Ant task based build. The

ActionScript bits remain belligerent, but the Java stuff is mostly shipshape.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6222 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Michael Bayne
2010-10-22 21:12:29 +00:00
parent 555b865bbf
commit 9d2ca42eac
434 changed files with 163 additions and 208 deletions
@@ -0,0 +1,84 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.google.inject.Inject;
import static com.threerings.presents.Log.log;
/**
* A base class that is used to wire up signal handling in one of a couple of possible ways.
*/
public abstract class AbstractSignalHandler
{
/**
* Initializes this signal handler.
*/
public boolean init ()
{
return registerHandlers();
}
/**
* Signal handler implementations should wire themselves up in the call to this method.
*
* @return true if the handlers were successfully wired up, false if they were not able to be
* wired up.
*/
protected abstract boolean registerHandlers ();
/**
* Implementations should call this method when a SIGTERM is received.
*/
protected void termReceived ()
{
log.info("Shutdown initiated by TERM signal.");
_server.queueShutdown();
}
/**
* Implementations should call this method when a SIGINT is received.
*/
protected void intReceived ()
{
log.info("Shutdown initiated by INT signal.");
_server.queueShutdown();
}
/**
* Implementations should call this method when a SIGHUP is received.
*/
protected void hupReceived ()
{
log.info(_repmgr.generateReport(ReportManager.DEFAULT_TYPE));
}
protected void usr2Received ()
{
if (_usr2receiver != null) {
_usr2receiver.received();
}
}
@Inject(optional=true) protected SignalReceiver _usr2receiver;
@Inject protected PresentsServer _server;
@Inject protected ReportManager _repmgr;
}
@@ -0,0 +1,116 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.samskivert.util.Invoker;
import com.samskivert.util.ResultListener;
import com.threerings.presents.data.AuthCodes;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.server.net.AuthingConnection;
import static com.threerings.presents.Log.log;
/**
* The authenticator is a pluggable component of the authentication framework. The base class
* handles the basic mechanics of authentication and a system would extend the base authenticator
* and add code that does the actual client authentication.
*/
public abstract class Authenticator
{
/**
* Called by the connection management code when an authenticating connection has received its
* authentication request from the client.
*/
public void authenticateConnection (Invoker invoker, final AuthingConnection conn,
final ResultListener<AuthingConnection> onComplete)
{
final AuthRequest req = conn.getAuthRequest();
final AuthResponseData rdata = createResponseData();
final AuthResponse rsp = new AuthResponse(rdata);
invoker.postUnit(new Invoker.Unit("authenticateConnection") {
@Override
public boolean invoke () {
try {
processAuthentication(conn, rsp);
if (AuthResponseData.SUCCESS.equals(rdata.code) &&
conn.getAuthName() == null) { // fail early, fail (less) often
throw new IllegalStateException("Authenticator failed to provide authname");
}
} catch (AuthException e) {
rdata.code = e.getMessage();
} catch (Exception e) {
log.warning("Error authenticating user", "areq", req, e);
rdata.code = AuthCodes.SERVER_ERROR;
}
return true;
}
@Override
public void handleResult () {
// stuff a reference to the auth response into the connection so that we have
// access to it later in the authentication process
conn.setAuthResponse(rsp);
// send the response back to the client
conn.postMessage(rsp);
// if the authentication request was granted, let the connection manager know that
// we just authed
if (AuthResponseData.SUCCESS.equals(rdata.code)) {
onComplete.requestCompleted(conn);
}
}
});
}
/**
* Create a new AuthResponseData instance to use for authenticating a connection.
*/
protected AuthResponseData createResponseData ()
{
return new AuthResponseData();
}
/**
* Process the authentication for the specified connection. The method may return after it has
* stuffed a valid response code in rsp.getData().code.
*
* @param conn The client connection.
* @param rsp The response to the client, which will already contain an AuthResponseData
* created by {@link #createResponseData}.
*/
protected abstract void processAuthentication (AuthingConnection conn, AuthResponse rsp)
throws Exception;
/** An exception that can be thrown during {@link #processAuthentication}. The results of
* {@link #getMessage} string will be filled in as the auth failure code. */
protected static class AuthException extends Exception
{
public AuthException (String code) {
super(code);
}
}
}
@@ -0,0 +1,37 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.presents.server.net.AuthingConnection;
/**
* Handles certain special kinds of authentications and passes the remainder through to the default
* authenticator.
*/
public abstract class ChainedAuthenticator extends Authenticator
{
/**
* Derived classes should implement this method and return true if the supplied connection is
* one that they should authenticate.
*/
public abstract boolean shouldHandleConnection (AuthingConnection conn);
}
@@ -0,0 +1,38 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.io.SimpleStreamableObject;
import com.threerings.presents.data.ClientObject;
/**
* Contains information about a client only tracked on the server. This is configured as a local
* attribute on the {@link ClientObject}.
*
* <p> Note: this object implements streamable so that it can be cleanly passed between servers in
* a peered environment. It is never sent to the client.
*/
public class ClientLocal extends SimpleStreamableObject
{
// nothing to track at this level
}
@@ -0,0 +1,630 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.io.IOException;
import java.net.InetAddress;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import com.samskivert.util.Lifecycle;
import com.samskivert.util.ObserverList;
import com.samskivert.util.StringUtil;
import com.threerings.util.Name;
import com.threerings.presents.annotation.EventThread;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.server.net.AuthingConnection;
import com.threerings.presents.server.net.Connection;
import static com.threerings.presents.Log.log;
/**
* The client manager is responsible for managing client sessions which are slightly more than just
* connections. Clients persist in the absence of connections in case a user goes bye bye
* unintentionally and wants to reconnect and continue their session.
*
* <p> The client manager operates with thread safety because it is called both from the conmgr
* thread (to notify of connections showing up or going away) and from the dobjmgr thread (when
* clients are given the boot for application-defined reasons).
*/
@Singleton
public class ClientManager
implements ClientResolutionListener, ReportManager.Reporter, Lifecycle.Component
{
/**
* Used by {@link ClientManager#applyToClient}.
*/
public static interface ClientOp
{
/**
* Called with the resolved client object.
*/
void apply (ClientObject clobj);
/**
* Called if the client resolution fails.
*/
void resolutionFailed (Exception e);
}
/**
* Used by entities that wish to track when clients initiate and end sessions on this server.
*/
public static interface ClientObserver
{
/**
* Called when a client has authenticated and been resolved and has started their session.
*/
void clientSessionDidStart (PresentsSession session);
/**
* Called when a client has logged off or been forcibly logged off due to inactivity and
* has thus ended their session.
*/
void clientSessionDidEnd (PresentsSession session);
}
/**
* Constructs a client manager that will interact with the supplied connection manager.
*/
@Inject public ClientManager (ReportManager repmgr, Lifecycle cycle)
{
repmgr.registerReporter(this);
cycle.addComponent(this);
}
/**
* Configures the injector we'll use to resolve dependencies for {@link PresentsSession}
* instances.
*/
public void setInjector (Injector injector)
{
_injector = injector;
}
/**
* Configures the default factory for creating {@link PresentsSession} and {@link
* ClientResolver} classes for authenticated client connections. All factories added via {@link
* #addSessionFactory} will be offered a chance to handle sessions before this factory of last
* resort.
*/
public void setDefaultSessionFactory (SessionFactory factory)
{
_factories.set(_factories.size()-1, factory);
}
/**
* Adds a session factory to the chain. This factory will be offered a chance to resolve
* sessions before passing the buck to the next factory in the chain.
*/
public void addSessionFactory (SessionFactory factory)
{
_factories.add(0, factory);
}
/**
* Return the number of client resolutions are currently happening.
*/
public int getOutstandingResolutionCount ()
{
return _penders.size();
}
/**
* Returns the number of client sessions (some may be disconnected).
*/
public int getClientCount ()
{
synchronized (_usermap) {
return _usermap.size();
}
}
/**
* Returns all sessions logged in from the given IP in the form returned from
* {@link InetAddress#getAddress()}.
*/
public List<PresentsSession> getSessionsForAddress (byte[] addr)
{
List<PresentsSession> sessions = Lists.newArrayListWithExpectedSize(1);
synchronized (_usermap) {
for (PresentsSession session : _usermap.values()) {
InetAddress sessionAddr = session.getInetAddress();
if (sessionAddr == null) {
continue;
}
if (Arrays.equals(addr, sessionAddr.getAddress())) {
sessions.add(session);
}
}
}
return sessions;
}
/**
* Returns the number of connected clients.
*/
public int getConnectionCount ()
{
return _conmap.size();
}
/**
* Returns an iterable over all active client objects.
*/
public Iterable<ClientObject> clientObjects ()
{
return _objmap.values();
}
/**
* Enumerates all active client objects.
*/
public Iterator<ClientObject> enumerateClientObjects ()
{
return _objmap.values().iterator();
}
/**
* Registers an observer that will be notified when clients start and end their sessions.
*/
public void addClientObserver (ClientObserver observer)
{
_clobservers.add(observer);
}
/**
* Removes an observer previously registered with {@link #addClientObserver}.
*/
public void removeClientObserver (ClientObserver observer)
{
_clobservers.remove(observer);
}
/**
* Returns the client instance that manages the client session for the specified authentication
* username or null if that client is not currently connected to the server.
*/
public PresentsSession getClient (Name authUsername)
{
synchronized (_usermap) {
return _usermap.get(authUsername);
}
}
/**
* Returns the client object associated with the specified username. This will return null
* unless the client object is resolved for some reason (like they are logged on).
*/
public ClientObject getClientObject (Name username)
{
return _objmap.get(username);
}
/**
* Resolves the specified client, applies the supplied client operation to them and releases
* the client.
*/
public void applyToClient (Name username, ClientOp clop)
{
resolveClientObject(username, new ClientOpResolver(clop));
}
/**
* Requests that the client object for the specified user be resolved. <em>Note:</em> this
* <b>must</b> be paired with a call to {@link #releaseClientObject} when the caller is
* finished with the client object.
*/
public synchronized void resolveClientObject (
final Name username, final ClientResolutionListener listener)
{
// look to see if the client object is already resolved
final ClientObject clobj = _objmap.get(username);
if (clobj != null) {
// report that the client is resolved on the dobjmgr thread to provide equivalent
// behavior to the case where we actually have to do the resolution
clobj.reference();
_omgr.postRunnable(new Runnable() {
public void run () {
listener.clientResolved(username, clobj);
}
});
return;
}
// look to see if it's currently being resolved
ClientResolver clr = _penders.get(username);
if (clr != null) {
// throw this guy onto the bandwagon
clr.addResolutionListener(listener);
return;
}
// figure out our client resolver class
Class<? extends ClientResolver> resolverClass = null;
for (SessionFactory factory : _factories) {
if ((resolverClass = factory.getClientResolverClass(username)) != null) {
break;
}
}
try {
// create a client resolver instance which will create our client object, populate it
// and notify the listeners
clr = _injector.getInstance(resolverClass);
clr.init(username);
clr.addResolutionListener(this);
clr.addResolutionListener(listener);
_penders.put(username, clr);
// create and register our client object and give it back to the client resolver; we
// need to do this on the dobjmgr thread since we're registering an object
final ClientResolver fclr = clr;
_omgr.postRunnable(new Runnable() {
public void run () {
ClientObject clobj = fclr.createClientObject();
clobj.setLocal(ClientLocal.class, fclr.createLocalAttribute());
clobj.setPermissionPolicy(fclr.createPermissionPolicy());
fclr.objectAvailable(_omgr.registerObject(clobj));
}
});
} catch (Exception e) {
// let the listener know that we're hosed
listener.resolutionFailed(username, e);
}
}
/**
* Releases a client object that was obtained via a call to {@link #resolveClientObject}. If
* this caller is the last reference, the object will be flushed and destroyed.
*/
public void releaseClientObject (Name username)
{
ClientObject clobj = _objmap.get(username);
if (clobj == null) {
log.info("Requested to release unmapped client object", "username", username);
return;
}
// decrement the reference count and stop here if there are remaining references
if (clobj.release()) {
return;
}
log.debug("Destroying client " + clobj.who() + ".");
// we're all clear to go; remove the mapping
_objmap.remove(username);
// and destroy the object itself
_omgr.destroyObject(clobj.getOid());
}
// from interface Lifecycle.Component
public void init ()
{
// start up an interval that will check for and flush expired sessions (this will be
// canceled when the omgr shuts down)
_omgr.newInterval(new Runnable() {
public void run () {
flushSessions();
}
}).schedule(SESSION_FLUSH_INTERVAL, true);
}
// from interface Lifecycle.Component
public void shutdown ()
{
log.info("Client manager shutting down", "ccount", _usermap.size());
// inform all of our clients that they are being shut down
synchronized (_usermap) {
for (PresentsSession pc : _usermap.values()) {
try {
pc.shutdown();
} catch (Exception e) {
log.warning("Client choked in shutdown()",
"client", StringUtil.safeToString(pc), e);
}
}
}
}
/**
* Renames a currently connected client from <code>oldname</code> to <code>newname</code>.
*
* @return true if the client was found and renamed.
*/
protected boolean renameClientObject (Name oldname, Name newname)
{
ClientObject clobj = _objmap.remove(oldname);
if (clobj == null) {
log.warning("Requested to rename unmapped client object", "username", oldname,
new Exception());
return false;
}
_objmap.put(newname, clobj);
return true;
}
// documentation inherited from interface ClientResolutionListener
public synchronized void clientResolved (Name username, ClientObject clobj)
{
// because we added ourselves as a client resolution listener, the client object reference
// count was increased, but we're not a real resolver (who would have to call
// releaseClientObject() to release their reference), so we release our reference
// immediately
clobj.release();
// stuff the object into the mapping table
_objmap.put(username, clobj);
// and remove the resolution listener
_penders.remove(username);
}
// documentation inherited from interface ClientResolutionListener
public synchronized void resolutionFailed (Name username, Exception reason)
{
// clear out their pending record
_penders.remove(username);
}
/**
* Called by the connection manager to let us know when a new connection has been established.
*/
public synchronized void connectionEstablished (
Connection conn, Name authname, AuthRequest req, AuthResponse rsp)
{
String type = authname.getClass().getSimpleName();
// see if a session is already registered with this name
PresentsSession session = getClient(authname);
if (session != null) {
log.info("Resuming session", "type", type, "who", authname, "conn", conn);
session.resumeSession(req, conn);
} else {
log.info("Session initiated", "type", type, "who", authname, "conn", conn);
// figure out our session class
Class<? extends PresentsSession> sessionClass = null;
for (SessionFactory factory : _factories) {
if ((sessionClass = factory.getSessionClass(req)) != null) {
break;
}
}
// create a new session and stick'em in the table
session = _injector.getInstance(sessionClass);
session.startSession(authname, req, conn, rsp.authdata);
// map their session instance
synchronized (_usermap) {
// we refetch the authname from the session for use in the map in case it decides
// to do something crazy like rewrite it in startSession()
_usermap.put(session.getAuthName(), session);
}
}
// map this connection to this session
_conmap.put(conn, session);
}
/**
* Called by the connection manager to let us know when a connection has failed.
*/
public synchronized void connectionFailed (Connection conn, IOException fault)
{
// remove the session from the connection map
PresentsSession session = _conmap.remove(conn);
if (session != null) {
log.info("Unmapped failed session", "session", session, "conn", conn, "fault", fault);
// let the session know the connection went away
session.wasUnmapped();
// and let the session know things went haywire
session.connectionFailed(fault);
} else if (!(conn instanceof AuthingConnection)) {
log.info("Unmapped connection failed?", "conn", conn, "fault", fault, new Exception());
}
}
/**
* Called by the connection manager to let us know when a connection has been closed.
*/
public synchronized void connectionClosed (Connection conn)
{
// remove the session from the connection map
PresentsSession session = _conmap.remove(conn);
if (session != null) {
log.debug("Unmapped session", "session", session, "conn", conn);
// let the session know the connection went away
session.wasUnmapped();
} else {
log.info("Closed unmapped connection '" + conn + "'. " +
"Session probably not yet authenticated.");
}
}
// documentation inherited from interface ReportManager.Reporter
public void appendReport (StringBuilder report, long now, long sinceLast, boolean reset)
{
report.append("* presents.ClientManager:\n");
report.append("- Sessions: ");
synchronized (_usermap) {
report.append(_usermap.size()).append(" total, ");
}
report.append(_conmap.size()).append(" connected, ");
report.append(_penders.size()).append(" pending\n");
report.append("- Mapped users: ").append(_objmap.size()).append("\n");
}
/**
* Called by PresentsSession when it has started its session.
*/
@EventThread
protected void clientSessionDidStart (final PresentsSession session)
{
// let the observers know
_clobservers.apply(new ObserverList.ObserverOp<ClientObserver>() {
public boolean apply (ClientObserver observer) {
observer.clientSessionDidStart(session);
return true;
}
});
}
/**
* Called by PresentsSession when it has ended its session.
*/
@EventThread
protected void clientSessionDidEnd (final PresentsSession session)
{
// notify the observers that the session is ended
_clobservers.apply(new ObserverList.ObserverOp<ClientObserver>() {
public boolean apply (ClientObserver observer) {
observer.clientSessionDidEnd(session);
return true;
}
});
}
/**
* Called by PresentsSession to let us know that we can clear it entirely out of the system.
*/
protected void clearSession (PresentsSession session)
{
// remove the session from the username map
PresentsSession rc;
synchronized (_usermap) {
rc = _usermap.remove(session.getAuthName());
}
// sanity check just because we can
if (rc == null) {
log.info("Cleared session: unregistered!", "session", session);
} else if (rc != session) {
log.info("Cleared session: multiple!", "s1", rc, "s2", session);
} else {
log.info("Cleared session", "session", session);
}
}
/**
* Called once per minute to check for sessions that have been disconnected too long and
* forcibly end their sessions.
*/
protected void flushSessions ()
{
List<PresentsSession> victims = Lists.newArrayList();
long now = System.currentTimeMillis();
// first build a list of our victims
synchronized (_usermap) {
for (PresentsSession session : _usermap.values()) {
if (session.checkExpired(now)) {
victims.add(session);
}
}
}
// now end their sessions
for (PresentsSession session : victims) {
try {
log.info("Session expired, ending session", "session", session,
"dtime", (now-session.getNetworkStamp()) + "ms].");
session.endSession();
} catch (Exception e) {
log.warning("Choke while flushing session", "victim", session, e);
}
}
}
/** Used by {@link ClientManager#applyToClient}. */
protected class ClientOpResolver
implements ClientResolutionListener
{
public ClientOpResolver (ClientOp clop) {
_clop = clop;
}
public void clientResolved (Name username, ClientObject clobj) {
try {
_clop.apply(clobj);
} catch (Exception e) {
log.warning("Client op failed", "username", username, "clop", _clop, e);
} finally {
releaseClientObject(username);
}
}
public void resolutionFailed (Name username, Exception reason) {
_clop.resolutionFailed(reason);
}
protected ClientOp _clop;
}
/** Used to resolve dependencies in {@link PresentsSession} instances that we create. */
protected Injector _injector;
/** A mapping from auth username to session instances. */
protected Map<Name, PresentsSession> _usermap = Maps.newHashMap();
/** A mapping from connections to session instances. */
protected Map<Connection, PresentsSession> _conmap = Maps.newHashMap();
/** A mapping from usernames to client object instances. */
protected Map<Name, ClientObject> _objmap = Maps.newHashMap();
/** A mapping of pending client resolvers. */
protected Map<Name, ClientResolver> _penders = Maps.newHashMap();
/** Lets us know what sort of session classes to use. */
protected List<SessionFactory> _factories = Lists.newArrayList(SessionFactory.DEFAULT);
/** Tracks registered {@link ClientObserver}s. */
protected ObserverList<ClientObserver> _clobservers = ObserverList.newSafeInOrder();
// our injected dependencies
@Inject protected PresentsDObjectMgr _omgr;
/** The frequency with which we check for expired sessions. */
protected static final long SESSION_FLUSH_INTERVAL = 60 * 1000L;
}
@@ -0,0 +1,44 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.util.Name;
import com.threerings.presents.data.ClientObject;
/**
* Entites that wish to resolve client objects must implement this
* interface so as to partake in the asynchronous process of client
* object resolution.
*/
public interface ClientResolutionListener
{
/**
* Called when resolution completed successfully.
*/
void clientResolved (Name username, ClientObject clobj);
/**
* Called when resolution fails.
*/
void resolutionFailed (Name username, Exception reason);
}
@@ -0,0 +1,228 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.util.List;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.samskivert.util.Invoker;
import com.threerings.util.Name;
import com.threerings.presents.annotation.MainInvoker;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.PermissionPolicy;
import com.threerings.presents.dobj.RootDObjectManager;
import static com.threerings.presents.Log.log;
/**
* Used to resolve client data when a user starts a session (or when some other entity needs access
* to a client object). Implementations will want to extend this class and override {@link
* #resolveClientData}, making the necessary database calls and populating the client object
* appropriately.
*/
public class ClientResolver extends Invoker.Unit
{
/**
* Thrown during resolution if the client disconnects.
*/
public static class ClientDisconnectedException extends Exception
{
}
/**
* Initializes this instance.
*
* @param username the username of the user to be resolved.
*/
public void init (Name username)
{
_username = username;
}
/**
* Adds a resolution listener to this active resolver.
*/
public void addResolutionListener (ClientResolutionListener listener)
{
_listeners.add(listener);
}
/**
* Creates the {@link ClientObject} derived class that should be created to kick off the
* resolution process.
*/
public ClientObject createClientObject ()
{
return new ClientObject();
}
/**
* Creates a record that will be maintained only on the server to track client related bits.
*/
public ClientLocal createLocalAttribute ()
{
return new ClientLocal();
}
/**
* Creates a permission policy for use by our client.
*/
public PermissionPolicy createPermissionPolicy ()
{
return new PermissionPolicy();
}
/**
* Called once our client object is registered with the distributed object system.
*/
public void objectAvailable (ClientObject object)
{
// we've got our object, so shunt ourselves over to the invoker thread to perform database
// loading
_clobj = object;
_invoker.postUnit(this);
}
@Override
public boolean invoke ()
{
try {
// allow our derived class to do its database loads
resolveClientData(_clobj);
} catch (Exception cause) {
// keep this around until we're back on the dobj thread
_failure = cause;
}
return true;
}
@Override
public void handleResult ()
{
// if we haven't failed, finish resolution on the dobj thread
if (_failure == null) {
try {
finishResolution(_clobj);
} catch (Exception e) {
_failure = e;
}
}
// if we still haven't failed, then we're good to go
if (_failure == null) {
// and let the listeners in on the secret as well
for (int ii = 0, ll = _listeners.size(); ii < ll; ii++) {
ClientResolutionListener crl = _listeners.get(ii);
try {
// add a reference for each listener
_clobj.reference();
crl.clientResolved(_username, _clobj);
} catch (Exception e) {
log.warning("Client resolution listener choked in clientResolved() " + crl, e);
}
}
} else {
// destroy the dangling user object
_omgr.destroyObject(_clobj.getOid());
// let our listener know that we're hosed
reportFailure(_failure);
}
}
@Override
public String toString ()
{
return "ClientResolver:" + _username;
}
/**
* This method is called on the invoker thread which means that it can do things like blocking
* database requests and generally whatever is necessary to load up all the client data that is
* desired by the implentation system. Any exceptions that are thrown will be caught and
* reported as a failure to the client resolution listener.
*/
protected void resolveClientData (ClientObject clobj)
throws Exception
{
// fill in the username
clobj.username = _username;
}
/**
* This method is called on the dobj thread after resolveClientData returns normally, it should
* finish populating the client object with any data that is NOT loaded from a database.
*/
protected void finishResolution (ClientObject clobj)
{
// nothing to do by default
}
/**
* Reports failure to our resolution listeners.
*/
protected void reportFailure (Exception cause)
{
for (int ii = 0, ll = _listeners.size(); ii < ll; ii++) {
ClientResolutionListener crl = _listeners.get(ii);
try {
crl.resolutionFailed(_username, cause);
} catch (Exception e) {
log.warning("Client resolution listener choked in resolutionFailed()", "crl", crl,
"username", _username, "cause", cause, e);
}
}
}
/**
* Throws an exception if the client being resolved is no longer connected.
*/
protected void enforceConnected ()
throws ClientDisconnectedException
{
if (_clmgr.getClient(_username) == null) {
throw new ClientDisconnectedException();
}
}
/** The name of the user whose client object is being resolved. */
protected Name _username;
/** The entities to notify of success or failure. */
protected List<ClientResolutionListener> _listeners = Lists.newArrayList();
/** The resolving client object. */
protected ClientObject _clobj;
/** A place to keep an exception around for a moment. */
protected Exception _failure;
// dependencies
protected @Inject @MainInvoker Invoker _invoker;
protected @Inject RootDObjectManager _omgr;
protected @Inject ClientManager _clmgr;
}
@@ -0,0 +1,57 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.samskivert.io.PersistenceException;
import com.threerings.util.Name;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.Credentials;
import com.threerings.presents.net.UsernamePasswordCreds;
import com.threerings.presents.server.net.AuthingConnection;
import static com.threerings.presents.Log.log;
/**
* A simple authenticator implementation that simply accepts all authentication requests.
*/
public class DummyAuthenticator extends Authenticator
{
@Override
protected void processAuthentication (AuthingConnection conn, AuthResponse rsp)
throws PersistenceException
{
log.info("Accepting request: " + conn.getAuthRequest());
// we need to provide some sort of authentication username
Credentials creds = conn.getAuthRequest().getCredentials();
if (creds instanceof UsernamePasswordCreds) {
conn.setAuthName(((UsernamePasswordCreds)creds).getUsername());
} else {
conn.setAuthName(new Name(conn.getInetAddress().getHostAddress()));
}
rsp.getData().code = AuthResponseData.SUCCESS;
}
}
@@ -0,0 +1,52 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationMarshaller;
import static com.threerings.presents.Log.log;
/**
* Provides the base class via which invocation service requests are dispatched.
*/
public abstract class InvocationDispatcher<T extends InvocationMarshaller>
{
/** The invocation provider for whom we're dispatching. */
public InvocationProvider provider;
/**
* Creates an instance of the appropriate {@link InvocationMarshaller} derived class for use
* with this dispatcher.
*/
public abstract T createMarshaller ();
/**
* Dispatches the specified method to our provider.
*/
public void dispatchRequest (ClientObject source, int methodId, Object[] args)
throws InvocationException
{
log.warning("Requested to dispatch unknown method", "provider", provider,
"sourceOid", source.getOid(), "methodId", methodId, "args", args);
}
}
@@ -0,0 +1,74 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.util.MessageBundle;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.Permission;
/**
* Used to report failures when executing service requests.
*/
public class InvocationException extends Exception
{
/**
* Requires that the specified client have the specified permissions.
*
* @throws InvocationException if they do not.
*/
public static void requireAccess (ClientObject clobj, Permission perm, Object context)
throws InvocationException
{
String errmsg = clobj.checkAccess(perm, context);
if (errmsg != null) {
throw new InvocationException(errmsg);
}
}
/**
* A version of {@link #requireAccess(ClientObject,Permission,Object)} that takes no context.
*/
public static void requireAccess (ClientObject clobj, Permission perm)
throws InvocationException
{
requireAccess(clobj, perm, null);
}
/**
* Constructs an invocation exception with the supplied cause code string.
*/
public InvocationException (String cause)
{
super(cause);
}
/**
* Constructs an invocation exception with the supplied cause code
* string and qualifying message bundle. The error code will be
* qualified with the message bundle (see {@link MessageBundle#qualify}).
*/
public InvocationException (String bundle, String code)
{
this(MessageBundle.qualify(bundle, code));
}
}
@@ -0,0 +1,315 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.util.IntMap;
import com.samskivert.util.IntMaps;
import com.samskivert.util.LRUHashMap;
import com.samskivert.util.StringUtil;
import com.threerings.io.Streamable;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.data.InvocationMarshaller.ListenerMarshaller;
import com.threerings.presents.dobj.DEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.EventListener;
import com.threerings.presents.dobj.InvocationRequestEvent;
import com.threerings.presents.net.Transport;
import static com.threerings.presents.Log.log;
/**
* The invocation services provide client to server invocations (service requests) and server to
* client invocations (responses and notifications). Via this mechanism, the client can make
* requests of the server, be notified of its response and the server can asynchronously invoke
* code on the client.
*
* <p> Invocations are like remote procedure calls in that they are named and take arguments. All
* arguments must be {@link Streamable} objects, primitive types, or String objects. All arguments
* are passed by value (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 will already be in use for any objects that should be shared).
*
* <p> The server invocation manager listens for invocation requests from the client and passes
* them on to the invocation provider registered for the requested invocation module. It also
* provides a mechanism by which responses and asynchronous notification invocations can be
* delivered to the client.
*/
@Singleton
public class InvocationManager
implements EventListener
{
/**
* Constructs an invocation manager which will use the supplied distributed object manager to
* operate its invocation services. Generally only one invocation manager should be operational
* in a particular system.
*/
@Inject public InvocationManager (PresentsDObjectMgr omgr)
{
_omgr = omgr;
_omgr._invmgr = this;
// create the object on which we'll listen for invocation requests
DObject invobj = _omgr.registerObject(new DObject());
invobj.addListener(this);
_invoid = invobj.getOid();
log.debug("Created invocation service object", "oid", _invoid);
}
/**
* Returns the object id of the invocation services object.
*/
public int getOid ()
{
return _invoid;
}
/**
* 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.
*/
public <T extends InvocationMarshaller> T registerDispatcher (
InvocationDispatcher<T> dispatcher)
{
return registerDispatcher(dispatcher, null);
}
/**
* @Deprecated use {@link #registerDispatcher(InvocationDispatcher)}.
*/
public <T extends InvocationMarshaller> T registerDispatcher (
InvocationDispatcher<T> dispatcher, boolean bootstrap)
{
return registerDispatcher(dispatcher, 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 <T extends InvocationMarshaller> T registerDispatcher (
InvocationDispatcher<T> dispatcher, String group)
{
_omgr.requireEventThread(); // sanity check
// get the next invocation code
int invCode = nextInvCode();
// create the marshaller and initialize it
T marsh = dispatcher.createMarshaller();
marsh.init(_invoid, invCode);
// register the dispatcher
_dispatchers.put(invCode, dispatcher);
// if it's a bootstrap service, slap it in the list
if (group != null) {
_bootlists.put(group, marsh);
}
_recentRegServices.put(Integer.valueOf(invCode), marsh.getClass().getName());
log.debug("Registered service", "code", invCode, "marsh", marsh);
return marsh;
}
/**
* Clears out a dispatcher registration. This should be called to free up resources when an
* invocation service is no longer going to be used.
*/
public void clearDispatcher (InvocationMarshaller marsh)
{
_omgr.requireEventThread(); // sanity check
if (marsh == null) {
log.warning("Refusing to unregister null marshaller.", new Exception());
return;
}
if (_dispatchers.remove(marsh.getInvocationCode()) == null) {
log.warning("Requested to remove unregistered marshaller?", "marsh", marsh,
new Exception());
}
}
/**
* Constructs a list of all bootstrap services registered in any of the supplied groups.
*/
public List<InvocationMarshaller> getBootstrapServices (String[] bootGroups)
{
List<InvocationMarshaller> services = Lists.newArrayList();
for (String group : bootGroups) {
services.addAll(_bootlists.get(group));
}
return services;
}
/**
* Get the class that is being used to dispatch the specified invocation code, for
* informational purposes.
*
* @return the Class, or null if no dispatcher is registered with
* the specified code.
*/
public Class<?> getDispatcherClass (int invCode)
{
Object dispatcher = _dispatchers.get(invCode);
return (dispatcher == null) ? null : dispatcher.getClass();
}
// documentation inherited from interface
public void eventReceived (DEvent event)
{
log.debug("Event received", "event", event);
if (event instanceof InvocationRequestEvent) {
InvocationRequestEvent ire = (InvocationRequestEvent)event;
dispatchRequest(ire.getSourceOid(), ire.getInvCode(),
ire.getMethodId(), ire.getArgs(), ire.getTransport());
}
}
/**
* Called when we receive an invocation request message. Dispatches the request to the
* appropriate invocation provider via the registered invocation dispatcher.
*/
protected void dispatchRequest (
int clientOid, int invCode, int methodId, Object[] args, Transport transport)
{
// make sure the client is still around
ClientObject source = (ClientObject)_omgr.getObject(clientOid);
if (source == null) {
log.info("Client no longer around for invocation request", "clientOid", clientOid,
"code", invCode, "methId", methodId, "args", args);
return;
}
// look up the dispatcher
InvocationDispatcher<?> disp = _dispatchers.get(invCode);
if (disp == null) {
log.info("Received invocation request but dispatcher registration was already cleared",
"code", invCode, "methId", methodId, "args", args,
"marsh", _recentRegServices.get(Integer.valueOf(invCode)));
return;
}
// scan the args, initializing any listeners and keeping track of the "primary" listener
ListenerMarshaller rlist = null;
int acount = args.length;
for (int ii = 0; ii < acount; ii++) {
Object arg = args[ii];
if (arg instanceof ListenerMarshaller) {
ListenerMarshaller list = (ListenerMarshaller)arg;
list.callerOid = clientOid;
list.omgr = _omgr;
list.transport = transport;
// keep track of the listener we'll inform if anything
// goes horribly awry
if (rlist == null) {
rlist = list;
}
}
}
log.debug("Dispatching invreq", "caller", source.who(), "disp", disp,
"methId", methodId, "args", args);
// dispatch the request
try {
if (rlist != null) {
rlist.setInvocationId(StringUtil.shortClassName(disp) +
", methodId=" + methodId);
}
disp.dispatchRequest(source, methodId, args);
} catch (InvocationException ie) {
if (rlist != null) {
rlist.requestFailed(ie.getMessage());
} else {
log.warning("Service request failed but we've got no listener to inform of " +
"the failure", "caller", source.who(), "code", invCode,
"dispatcher", disp, "methodId", methodId, "args", args, "error", ie);
}
} catch (Throwable t) {
log.warning("Dispatcher choked", "disp", disp, "caller", source.who(),
"methId", methodId, "args", args, t);
// avoid logging an error when the listener notices that it's been ignored.
if (rlist != null) {
rlist.setNoResponse();
}
}
}
/**
* Used to generate monotonically increasing provider ids.
*/
protected synchronized int nextInvCode ()
{
return _invCode++;
}
/** The object id of the object on which we receive invocation service requests. */
protected int _invoid = -1;
/** Used to generate monotonically increasing provider ids. */
protected int _invCode;
/** The distributed object manager we're working with. */
protected PresentsDObjectMgr _omgr;
/** A table of invocation dispatchers each mapped by a unique code. */
protected IntMap<InvocationDispatcher<?>> _dispatchers = IntMaps.newHashIntMap();
/** Maps bootstrap group to lists of services to be provided to clients at boot time. */
protected Multimap<String, InvocationMarshaller> _bootlists = ArrayListMultimap.create();
/** Tracks recently registered services so that we can complain informatively if a request
* comes in on a service we don't know about. */
protected final Map<Integer, String> _recentRegServices =
new LRUHashMap<Integer, String>(10000);
/** The text appended to the procedure name when generating a failure response. */
protected static final String FAILED_SUFFIX = "Failed";
}
@@ -0,0 +1,29 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
/**
* All invocation providers must implement this placeholder interface.
*/
public interface InvocationProvider
{
}
@@ -0,0 +1,71 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.presents.client.InvocationReceiver.Registration;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.dobj.InvocationNotificationEvent;
import com.threerings.presents.net.Transport;
import static com.threerings.presents.Log.log;
/**
* Provides basic functionality used by all invocation sender classes.
*/
public abstract class InvocationSender
{
/**
* Requests that the specified invocation notification be packaged up and sent to the supplied
* target client.
*/
public static void sendNotification (
ClientObject target, String receiverCode, int methodId, Object[] args)
{
sendNotification(target, receiverCode, methodId, args, Transport.DEFAULT);
}
/**
* Requests that the specified invocation notification be packaged up and sent to the supplied
* target client.
*/
public static void sendNotification (
ClientObject target, String receiverCode, int methodId, Object[] args, Transport transport)
{
// convert the receiver hash id into the code used on this
// specific client
Registration rreg = target.receivers.get(receiverCode);
if (rreg == null) {
log.warning("Unable to locate receiver for invocation service notification",
"clobj", target.who(), "code", receiverCode, "methId", methodId,
"args", args, new Exception());
} else {
// log.info("Sending notification", "target", target, "code", receiverCode,
// "methodId", methodId, "args", args);
// create and dispatch an invocation notification event
target.postEvent(
new InvocationNotificationEvent(
target.getOid(), rreg.receiverId, methodId, args, transport));
}
}
}
@@ -0,0 +1,102 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.awt.EventQueue;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.threerings.presents.dobj.DEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.DObjectManager;
import com.threerings.presents.dobj.Subscriber;
/**
* A special version of the distributed object manager, modified to operate on the AWT thread so
* that it can run in a client with a GUI and provide a "light" server for local operation of a
* normally distributed application.
*/
@Singleton
public class LocalDObjectMgr extends PresentsDObjectMgr
{
/**
* Creates the dobjmgr and prepares it for operation.
*/
@Inject public LocalDObjectMgr (ReportManager repmgr)
{
super(repmgr);
}
/**
* Creates a {@link DObjectManager} that posts directly to this local object manager, but first
* sets the source oid of all events to properly identify them with the supplied client oid.
* Normally this oid setting happens when an event is received on the server over the network,
* but in local mode we have to do it by hand.
*/
public DObjectManager getClientDObjectMgr (final int clientOid)
{
return new DObjectManager() {
public boolean isManager (DObject object) {
return LocalDObjectMgr.this.isManager(object);
}
public <T extends DObject> void subscribeToObject (int oid, Subscriber<T> target) {
LocalDObjectMgr.this.subscribeToObject(oid, target);
}
public <T extends DObject> void unsubscribeFromObject (int oid, Subscriber<T> target) {
LocalDObjectMgr.this.unsubscribeFromObject(oid, target);
}
public void postEvent (DEvent event) {
event.setSourceOid(clientOid);
LocalDObjectMgr.this.postEvent(event);
}
public void removedLastSubscriber (DObject obj, boolean deathWish) {
LocalDObjectMgr.this.removedLastSubscriber(obj, deathWish);
}
};
}
@Override
public synchronized boolean isDispatchThread ()
{
return EventQueue.isDispatchThread();
}
@Override
public void postEvent (final DEvent event)
{
EventQueue.invokeLater(new Runnable() {
public void run () {
processUnit(event);
}
});
}
@Override
public void postRunnable (Runnable unit)
{
// we just pass this right on to the AWT event queue rather than running them through
// processUnit() which would basically just call run() though we lose a tiny bit of
// inconsequential accounting data
EventQueue.invokeLater(unit);
}
}
@@ -0,0 +1,69 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.util.signal.SignalManager;
import static com.threerings.presents.Log.log;
/**
* Handles signals using Narya's native libsignal.
*/
public class NativeSignalHandler extends AbstractSignalHandler
implements SignalManager.SignalHandler
{
// from interface SignalManager.SignalHandler
public boolean signalReceived (int signo)
{
switch (signo) {
case SignalManager.SIGTERM:
termReceived();
break;
case SignalManager.SIGINT:
intReceived();
break;
case SignalManager.SIGHUP:
hupReceived();
break;
case SignalManager.SIGUSR2:
usr2Received();
break;
default:
log.warning("Received unknown signal", "signo", signo);
break;
}
return true;
}
@Override
protected boolean registerHandlers ()
{
if (!SignalManager.servicesAvailable()) {
return false;
}
SignalManager.registerSignalHandler(SignalManager.SIGTERM, this);
SignalManager.registerSignalHandler(SignalManager.SIGINT, this);
SignalManager.registerSignalHandler(SignalManager.SIGHUP, this);
SignalManager.registerSignalHandler(SignalManager.SIGUSR2, this);
return true;
}
}
@@ -0,0 +1,40 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
* A separate invoker thread on which we perform client authentication. This allows the normal
* server operation to proceed even in the event that our authentication services have gone down
* and attempts to authenticate cause long timeouts and blockage.
*/
@Singleton
public class PresentsAuthInvoker extends ReportingInvoker
{
@Inject public PresentsAuthInvoker (PresentsDObjectMgr omgr, ReportManager repmgr)
{
super("presents.AuthInvoker", omgr, repmgr);
setDaemon(true);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,321 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.util.List;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.util.Invoker;
import com.samskivert.util.Lifecycle;
import com.threerings.presents.server.PresentsDObjectMgr.LongRunnable;
import static com.threerings.presents.Log.log;
/**
* Extends the generic {@link Invoker} and integrates it a bit more into the Presents system.
*/
@Singleton
public class PresentsInvoker extends ReportingInvoker
implements Lifecycle.ShutdownComponent
{
@Inject public PresentsInvoker (PresentsDObjectMgr omgr, Lifecycle cycle, ReportManager repmgr)
{
super("presents.Invoker", omgr, repmgr);
cycle.addComponent(this);
_omgr = omgr;
}
/**
* Adds an invoker that may post to the PresentsInvoker and the DObjectManager and may be
* posted to by the PresentsInvoker and DObjectManager. This invoker will be taken into
* consideration by {@link #postRunnableWhenEmpty(Runnable)} and in waiting for invokers to
* empty out on shutdown.
*/
public void addInterdependentInvoker (Invoker invoker)
{
_interdependentInvokers.add(invoker);
}
/**
* Posts the given runnable to this invoker when it, the DObjectManager and any interdependent
* invokers are all empty.
*/
public void postRunnableWhenEmpty (Runnable onEmpty)
{
postUnit(new EmptyingUnit(onEmpty));
}
@Override // from Invoker, Lifecycle.ShutdownComponent
public void shutdown ()
{
// this will do a sophisticated shutdown of both ourself and the dobjmgr; note: we
// specifically avoid setting _shutdownRequested as it's OK for units to be posted to the
// PresentsInvoker during the shutdown phase, we just delay shutdown until we're able to
// make it to the shutdown unit without queueing up more units for processing
postRunnableWhenEmpty(new Runnable() {
public void run () {
_omgr.harshShutdown(); // end the dobj thread
// Now that things have emptied out, set _shutdownRequested and commit suicide
PresentsInvoker.super.shutdown();
}});
}
@Override // from Invoker
protected void didShutdown ()
{
_server.invokerDidShutdown();
}
/**
* This gets posted to this invoker over and over again until it, any interdependent invokers
* and the DObjectManager are all empty.
*/
protected class EmptyingUnit extends Unit {
public EmptyingUnit (Runnable onEmpty) {
_onEmpty = onEmpty;
}
@Override
public boolean invoke ()
{
if (_loopCount > MAX_LOOPS) {
log.warning("Emptying waiter looped on invoker 10000 times without finishing, "
+ "running onEmpty while items remain in the queue.");
_onEmpty.run();
return false;
// if the invoker queue is not empty, we put ourselves back on it
} else if (getPendingUnits() > 0) {
_loopCount++;
postUnit(this);
return false;
} else if (++_passCount >= MAX_PASSES) {
log.warning("Emptying waiter passed 50 times without finishing, running onEmpty "
+ "while items remain in queue.");
_onEmpty.run();
return false;
} else {
_loopCount = 0;
// The invoker is empty and running this. Check if everything else is empty.
List<BlockingUnit> checkers =
Lists.newArrayListWithCapacity(_interdependentInvokers.size() + 1);
for (Invoker invoker : _interdependentInvokers) {
checkers.add(new BlockingUnit(invoker));
}
checkers.add(new BlockingUnit());
long checkStart = System.currentTimeMillis();
while (true) {
synchronized (_checkMonitor) {
boolean unchecked = false;
for (BlockingUnit checker : checkers) {
if (!checker.run) {
unchecked = true;
break;
}
}
if (unchecked) {
long timeChecking = System.currentTimeMillis() - checkStart;
if (timeChecking >= CHECK_TIMEOUT) {
log.warning("Waited 5 minutes for all the blocking units to "
+ "no avail. Running onEmpty while items may remain in "
+ "the queue.");
releaseCheckers(checkers);
_onEmpty.run();
return false;
}
// At least one checker hasn't started running yet. We need to wait
// till they're all in place before looking at emptiness.
try {
_checkMonitor.wait(CHECK_TIMEOUT - timeChecking);
} catch (InterruptedException e) {
// Not a problem, we'll just check on the checkers again
}
} else {
// All the checkers are blocking their respective threads. That means
// if their queues are empty, nothing else is running.
for (BlockingUnit checker : checkers) {
if (!checker.isEmpty()) {
// Balls, we found an invoker with items in its queue. Let
// everybody get back to work.
releaseCheckers(checkers);
// Post on the busy unit, so when it clears we'll post back
// here
checker.post(new Runnable() {
public void run () {
PresentsInvoker.this.postUnit(EmptyingUnit.this);
}
});
return false;
}
}
// All of the checkers are clean, so they can go back to their business
releaseCheckers(checkers);
if (getPendingUnits() > 0) {
// While we were waiting on the various invokers, one of them gave
// us something to do. We need to process that and try again.
postUnit(this);
} else {
// Everything is gloriously clean. Shut down this invoker and the
// DObjectManager
_onEmpty.run();
}
return false;
}
}
}
}
}
@Override
public long getLongThreshold ()
{
return 60 * 1000;
}
protected void releaseCheckers (List<BlockingUnit> checkers)
{
for (BlockingUnit checker : checkers) {
synchronized (checker) {
checker.released = true;
checker.notify();
}
}
}
/** The runnable to execute when all associated queues are empty or we've given up. */
protected Runnable _onEmpty;
/** The number of times we've been passed to the object manager. */
protected int _passCount;
/** How many times we've looped on the thread we're currently on. */
protected int _loopCount;
/** The maximum number of passes we allow before just ending things. */
protected static final int MAX_PASSES = 50;
/** The maximum number of loops we allow before just ending things. */
protected static final int MAX_LOOPS = 10000;
protected static final long CHECK_TIMEOUT = 5 * 60 * 1000L;
}
/**
* Runs in an Invoker or the DObjectManager and blocks it until released by EmptyingUnit.
*/
protected class BlockingUnit extends Unit implements LongRunnable {
/**
* If the run method of this checker has been entered. If this is true while released is
* false, this checker is blocking its thread.
*/
public boolean run;
/** If this checker no longer needs to block its thread. */
public boolean released;
public BlockingUnit ()
{
_omgr.postRunnable(this);
}
public BlockingUnit (Invoker invoker)
{
_invoker = invoker;
_invoker.postUnit(this);
}
public boolean isEmpty ()
{
return _invoker == null ? _omgr.queueIsEmpty() : (_invoker.getPendingUnits() == 0);
}
public void post (Runnable runnable)
{
if (_invoker != null) {
_invoker.postRunnable(runnable);
} else {
_omgr.postRunnable(runnable);
}
}
@Override
public void run()
{
// Override Invoker.Unit's run altogether to allow this to be posted as a Runnable to
// the DObjectMgr as well.
run = true;
synchronized (_checkMonitor) {
_checkMonitor.notify();
}
synchronized (this) {
while (!released) {
try {
wait();
} catch (InterruptedException e) {
// Not a problem, we'll check our invariant
}
}
}
}
@Override
public boolean invoke ()
{
run();
return false;
}
@Override
public long getLongThreshold ()
{
// Don't bitch about being a long invoker unless this has been blocking for longer than
// the EmptyingUnit timeout
return EmptyingUnit.CHECK_TIMEOUT;
}
protected Invoker _invoker;
}
/**
* Synchronizes between EmptyingUnit and its BlockingUnits when checking that all
* interdependent invokers are blocked.
*/
protected Object _checkMonitor = new Object();
/** Invokers that may post to Presents and may be posted to by Presents. */
protected List<Invoker> _interdependentInvokers = Lists.newArrayList();
/** The distributed object manager with which we interoperate. */
protected PresentsDObjectMgr _omgr;
/** The server we're working for. */
@Inject protected PresentsServer _server;
}
@@ -0,0 +1,101 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.dobj.AccessController;
import com.threerings.presents.dobj.DEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.InvocationRequestEvent;
import com.threerings.presents.dobj.MessageEvent;
import com.threerings.presents.dobj.NamedEvent;
import com.threerings.presents.dobj.ProxySubscriber;
import com.threerings.presents.dobj.Subscriber;
import static com.threerings.presents.Log.log;
/**
* Defines the various object access controllers used by the Presents server.
*/
public class PresentsObjectAccess
{
/**
* Our default access controller. Disallows modification of any object but allows anyone to
* subscribe.
*/
public static AccessController DEFAULT = new AccessController()
{
// documentation inherited from interface
public boolean allowSubscribe (DObject object, Subscriber<?> subscriber)
{
// allow anyone to subscribe
return true;
}
// documentation inherited from interface
public boolean allowDispatch (DObject object, DEvent event)
{
// if the event came from the server, it's cool
if (event.getSourceOid() == -1) {
return true;
} else {
// if it came from the client, it better be a non-modification event
return (event instanceof MessageEvent || event instanceof InvocationRequestEvent);
}
}
};
/**
* Provides access control for client objects.
*/
public static AccessController CLIENT = new AccessController()
{
// documentation inherited from interface
public boolean allowSubscribe (DObject object, Subscriber<?> sub)
{
// if the subscriber is a client, ensure that they are this same user
if (sub instanceof ProxySubscriber) {
ClientObject clobj = ((ProxySubscriber)sub).getClientObject();
if (clobj != object) {
log.warning("Refusing ClientObject subscription request",
"obj", ((ClientObject)object).who(), "sub", clobj.who());
return false;
}
}
return true;
}
// documentation inherited from interface
public boolean allowDispatch (DObject object, DEvent event)
{
// if the event came from the server, it's cool
if (event.getSourceOid() == -1) {
return true;
}
// the client is only allowed to modify the RECEIVERS field
return ((event instanceof NamedEvent) &&
((NamedEvent)event).getName().equals(ClientObject.RECEIVERS));
}
};
}
@@ -0,0 +1,310 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import com.samskivert.util.Invoker;
import com.samskivert.util.Lifecycle;
import com.samskivert.util.RunQueue;
import com.samskivert.util.SystemInfo;
import com.threerings.presents.annotation.AuthInvoker;
import com.threerings.presents.annotation.EventQueue;
import com.threerings.presents.annotation.MainInvoker;
import com.threerings.presents.client.Client;
import com.threerings.presents.dobj.AccessController;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.DObjectManager;
import com.threerings.presents.dobj.RootDObjectManager;
import com.threerings.presents.server.PresentsDObjectMgr.LongRunnable;
import com.threerings.presents.server.net.BindingConnectionManager;
import com.threerings.presents.server.net.ConnectionManager;
import com.threerings.crowd.server.PlaceManager;
import static com.threerings.presents.Log.log;
/**
* The presents server provides a central point of access to the various facilities that make up
* the presents framework. To facilitate extension and customization, a single instance of the
* presents server should be created and initialized in a process. To facilitate easy access to the
* services provided by the presents server, static references to the various managers are made
* available in the <code>PresentsServer</code> class. These will be configured when the singleton
* instance is initialized.
*/
@Singleton
public class PresentsServer
{
/** Configures dependencies needed by the Presents services. */
public static class Module extends AbstractModule
{
@Override protected void configure () {
bindInvokers();
bindConnectionManager();
bind(RunQueue.class).annotatedWith(EventQueue.class).to(PresentsDObjectMgr.class);
bind(DObjectManager.class).to(PresentsDObjectMgr.class);
bind(RootDObjectManager.class).to(PresentsDObjectMgr.class);
bind(Lifecycle.class).toInstance(new Lifecycle());
}
/**
* Binds just the connection manager so this can be overridden if desired.
*/
protected void bindConnectionManager()
{
bind(ConnectionManager.class).to(BindingConnectionManager.class);
}
/**
* Binds just the invokers so this can be overridden if desired.
*/
protected void bindInvokers() {
bind(Invoker.class).annotatedWith(MainInvoker.class).to(PresentsInvoker.class);
bind(Invoker.class).annotatedWith(AuthInvoker.class).to(PresentsAuthInvoker.class);
}
}
/**
* The default entry point for the server.
*/
public static void main (String[] args)
{
Injector injector = Guice.createInjector(new Module());
PresentsServer server = injector.getInstance(PresentsServer.class);
try {
// initialize the server
server.init(injector);
// check to see if we should load and invoke a test module before running the server
String testmod = System.getProperty("test_module");
if (testmod != null) {
try {
log.info("Invoking test module", "mod", testmod);
Class<?> tmclass = Class.forName(testmod);
Runnable trun = (Runnable)tmclass.newInstance();
trun.run();
} catch (Exception e) {
log.warning("Unable to invoke test module '" + testmod + "'.", e);
}
}
// start the server to running (this method call won't return until the server is shut
// down)
server.run();
} catch (Exception e) {
log.warning("Unable to initialize server.", e);
System.exit(-1);
}
}
/** Legacy static reference to the main distributed object manager. Don't use this. If you're
* writing a game, use {@link PlaceManager#_omgr}. */
@Deprecated public static PresentsDObjectMgr omgr;
/** Legacy static reference to the invocation manager. Don't use this. If you're
* writing a game, use {@link PlaceManager#_invmgr}. */
@Deprecated public static InvocationManager invmgr;
/**
* Initializes all of the server services and prepares for operation.
*/
public void init (Injector injector)
throws Exception
{
// output general system information
SystemInfo si = new SystemInfo();
log.info("Starting up server", "os", si.osToString(), "jvm", si.jvmToString());
registerSignalHandlers(injector);
// initialize our deprecated legacy static references
omgr = _omgr;
invmgr = _invmgr;
// configure the dobject manager with our access controller
_omgr.setDefaultAccessController(createDefaultObjectAccessController());
// start the main and auth invoker threads
_invoker.start();
_authInvoker.start();
((PresentsInvoker)_invoker).addInterdependentInvoker(_authInvoker);
// provide our client manager with the injector it needs
_clmgr.setInjector(injector);
// if we have a connection manager that handles its own socket accepting, initialize it
// with the hostname and ports on which to listen
if (_conmgr instanceof BindingConnectionManager) {
((BindingConnectionManager)_conmgr).init(
getBindHostname(), getDatagramHostname(), getListenPorts(), getDatagramPorts());
}
// initialize the time base services
TimeBaseProvider.init(_invmgr, _omgr);
// make the client manager shut down before the invoker and dobj threads; this helps
// application code to avoid long chains of shutdown constraints
_lifecycle.addShutdownConstraint(
_clmgr, Lifecycle.Constraint.RUNS_BEFORE, (PresentsInvoker)_invoker);
// initialize all of our registered components
_lifecycle.init();
}
protected void registerSignalHandlers (Injector injector)
{
// register SIGTERM, SIGINT (ctrl-c) and a SIGHUP handlers
boolean registered = false;
try {
registered = injector.getInstance(SunSignalHandler.class).init();
} catch (Throwable t) {
log.warning("Unable to register Sun signal handlers", "error", t);
}
if (!registered) {
injector.getInstance(NativeSignalHandler.class).init();
}
}
/**
* Starts up all of the server services and enters the main server event loop.
*/
public void run ()
{
// wait till everything in the dobjmgr queue and invokers are processed and then start the
// connection manager
((PresentsInvoker)_invoker).postRunnableWhenEmpty(new Runnable() {
public void run () {
// Take as long as you like opening to the public
_omgr.postRunnable(new LongRunnable() {
public void run () {
openToThePublic();
}
});
}
});
// invoke the dobjmgr event loop
_omgr.run();
}
/**
* Opens the server for connections after the event thread is running and all the invokers are
* clear after starting up.
*/
protected void openToThePublic ()
{
_conmgr.start();
}
/**
* Queues up a request to shutdown on the event thread. This method may be safely called from
* any thread.
*/
public void queueShutdown ()
{
_omgr.postRunnable(new PresentsDObjectMgr.LongRunnable() {
public void run () {
_lifecycle.shutdown();
}
});
}
/**
* Defines the default object access policy for all {@link DObject} instances. The default
* default policy is to allow all subscribers but reject all modifications by the client.
*/
protected AccessController createDefaultObjectAccessController ()
{
return PresentsObjectAccess.DEFAULT;
}
/**
* Returns the hostname on which the connection manager will listen for TCP traffic, or
* <code>null</code> to bind to the wildcard address.
*/
protected String getBindHostname ()
{
return null;
}
/**
* Returns the hostname on which the connection will listen for datagram traffic, or
* <code>null</code> to bind to the wildcard address.
*/
protected String getDatagramHostname ()
{
return null;
}
/**
* Returns the port on which the connection manager will listen for client connections.
*/
protected int[] getListenPorts ()
{
return Client.DEFAULT_SERVER_PORTS;
}
/**
* Returns the ports on which the connection manager will listen for datagrams.
*/
protected int[] getDatagramPorts ()
{
return Client.DEFAULT_DATAGRAM_PORTS;
}
/**
* Called once the invoker and distributed object manager have both completed processing all
* remaining events and are fully shutdown. <em>Note:</em> this is called as the last act of
* the invoker <em>on the invoker thread</em>. In theory no other (important) threads are
* running, so thread safety should not be an issue, but be careful!
*/
protected void invokerDidShutdown ()
{
}
/** The manager of distributed objects. */
@Inject protected PresentsDObjectMgr _omgr;
/** The manager of network connections. */
@Inject protected ConnectionManager _conmgr;
/** The manager of clients. */
@Inject protected ClientManager _clmgr;
/** The manager of invocation services. */
@Inject protected InvocationManager _invmgr;
/** Handles orderly initialization and shutdown of our managers, etc. */
@Inject protected Lifecycle _lifecycle;
/** Used to invoke background tasks that should not be allowed to tie up the distributed object
* manager thread (generally talking to databases and other relatively slow entities). */
@Inject @MainInvoker protected Invoker _invoker;
/** Used to invoke authentication tasks. */
@Inject @AuthInvoker protected Invoker _authInvoker;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,347 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.util.Calendar;
import com.samskivert.util.Calendars;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.Interval;
import com.samskivert.util.ObserverList;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Calendars.Builder;
import com.threerings.util.MessageBundle;
import com.threerings.presents.dobj.RootDObjectManager;
import static com.threerings.presents.Log.log;
/**
* Handles scheduling and execution of automated server reboots. Note that this service simply
* shuts down the server and assumes it will be automatically restarted by some external entity. It
* is generally useful to run a server from a script that automatically restarts it when it
* terminates.
*/
public abstract class RebootManager
{
/**
* An interface for receiving notifications about pending automated shutdowns.
*/
public static interface PendingShutdownObserver
{
/**
* Called prior to an automated shutdown. NOTE that the shutdown is not guaranteed to
* happen, this interface is merely for notification purposes.
*
* @param warningsLeft The number of warnings left prior to the shutdown. This value will
* be 0 on the last warning, usually 1-2 minutes prior to the actual shutdown.
* @param msLeft the approximate number of milliseconds left prior to the shutdown.
*/
void shutdownPlanned (int warningsLeft, long msLeft);
}
/**
* Finishes initialization of the manager.
*/
public void init ()
{
scheduleRegularReboot();
}
/**
* Schedules our next regularly scheduled reboot.
*
* @return true if a reboot was scheduled, false if regularly scheduled reboots are disabled.
*/
public boolean scheduleRegularReboot ()
{
// maybe schedule an automatic reboot based on our configuration
int freq = getDayFrequency();
if (freq == -1) {
return false;
}
Builder cal = Calendars.now().zeroTime().addHours(getRebootHour()).addDays(freq);
// maybe avoid weekends
if (getSkipWeekends()) {
int dow = cal.get(Calendar.DAY_OF_WEEK);
switch (dow) {
case Calendar.SATURDAY:
if (freq > 1) {
cal.addDays(-1);
}
break;
case Calendar.SUNDAY:
if (freq > 2) {
cal.addDays(-2);
}
break;
}
}
scheduleReboot(cal.toTime(), AUTOMATIC_INITIATOR);
return true;
}
/**
* Is the manager planning a shutdown in the near future?
*/
public boolean willShutdownSoon ()
{
return _rebootSoon;
}
/**
* Add an observer to the observer list.
*/
public void addObserver (PendingShutdownObserver observer)
{
_observers.add(observer);
}
/**
* Schedules a reboot for the specified time.
*/
public void scheduleReboot (long rebootTime, String initiator)
{
// if there's already a reboot scheduled, cancel it
if (_interval != null) {
_interval.cancel();
_interval = null;
}
// note our new reboot time and its initiator
_nextReboot = rebootTime;
_initiator = initiator;
// see if the reboot is happening within the time specified by the
// longest warning; if so, issue the appropriate warning
long now = System.currentTimeMillis();
for (int ii = WARNINGS.length - 1; ii >= 0; ii--) {
long warnTime = WARNINGS[ii] * 60 * 1000;
if (now + warnTime >= _nextReboot) {
doWarning(ii);
return;
}
}
// otherwise, it's further off; schedule an interval to wake up when we
// should issue the first pre-reboot warning
_rebootSoon = false;
long firstWarnTime = (_nextReboot - (WARNINGS[0] * 60 * 1000)) - now;
(_interval = _omgr.newInterval(new Runnable() {
public void run () {
doWarning(0);
}
})).schedule(firstWarnTime);
}
/**
* Called by an entity that would like to prevent a reboot.
*/
public int preventReboot (String whereFrom)
{
if (whereFrom == null) {
throw new IllegalArgumentException("whereFrom must be descriptive.");
}
int lockId = _nextRebootLockId++;
_rebootLocks.put(lockId, whereFrom);
return lockId;
}
/**
* Release a reboot lock.
*/
public void allowReboot (int lockId)
{
if (null == _rebootLocks.remove(lockId)) {
throw new IllegalArgumentException("no such lockId (" + lockId + ")");
}
}
/**
* Provides us with our dependencies.
*/
protected RebootManager (PresentsServer server, RootDObjectManager omgr)
{
_server = server;
_omgr = omgr;
}
/**
* Broadcasts a message to everyone on the server. The following messages will be broadcast:
* <ul><li> m.rebooting_now
* <li> m.reboot_warning (minutes) (message or m.reboot_msg_standard)
* <li> m.reboot_delayed
* </ul>
*/
protected abstract void broadcast (String message);
/**
* Returns the frequency in days of our automatic reboots, or -1 to disable automatically
* scheduled reboots.
*/
protected abstract int getDayFrequency ();
/**
* Returns the desired hour at which to perform our reboot.
*/
protected abstract int getRebootHour ();
/**
* Returns true if the reboot manager should avoid scheduling automated reboots on the
* weekends.
*/
protected abstract boolean getSkipWeekends ();
/**
* Returns a custom message to be used when broadcasting a pending reboot.
*/
protected abstract String getCustomRebootMessage ();
/**
* Composes the given reboot message with the minutes and either the custom or standard details
* about the pending reboot.
*/
protected String getRebootMessage (String key, int minutes)
{
String msg = getCustomRebootMessage();
if (StringUtil.isBlank(msg)) {
msg = "m.reboot_msg_standard";
}
return MessageBundle.compose(key, MessageBundle.taint("" + minutes), msg);
}
/**
* Do a warning, schedule the next.
*/
protected void doWarning (final int level)
{
_rebootSoon = true;
if (level == WARNINGS.length) {
if (checkLocks()) {
return;
}
// that's it! do the reboot
log.info("Performing automatic server reboot/shutdown, as scheduled by: " + _initiator);
broadcast("m.rebooting_now");
// wait 1 second, then do it
new Interval() { // Note: This interval does not run on the dobj thread
@Override public void expired () {
_server.queueShutdown(); // this posts a LongRunnable
}
}.schedule(1000);
return;
}
// issue the warning
int minutes = WARNINGS[level];
broadcast(getRebootMessage("m.reboot_warning", minutes));
if (level < WARNINGS.length - 1) {
minutes -= WARNINGS[level + 1];
}
// schedule the next warning
(_interval = _omgr.newInterval(new Runnable() {
public void run () {
doWarning(level + 1);
}
})).schedule(minutes * 60 * 1000);
notifyObservers(level);
}
/**
* Check to see if there are outstanding reboot locks that may delay the reboot, returning
* false if there are none.
*/
protected boolean checkLocks ()
{
if (_rebootLocks.isEmpty()) {
return false;
}
log.info("Reboot delayed due to outstanding locks", "locks", _rebootLocks.elements());
broadcast("m.reboot_delayed");
(_interval = _omgr.newInterval(new Runnable() {
public void run () {
doWarning(WARNINGS.length);
}
})).schedule(60 * 1000);
return true;
}
/**
* Notify all PendingShutdownObservers of the pending shutdown!
*/
protected void notifyObservers (int level)
{
final int warningsLeft = WARNINGS.length - level - 1;
final long msLeft = 1000L * 60L * WARNINGS[level];
_observers.apply(new ObserverList.ObserverOp<PendingShutdownObserver>() {
public boolean apply (PendingShutdownObserver observer) {
observer.shutdownPlanned(warningsLeft, msLeft);
return true;
}
});
}
/** The server that we're going to reboot. */
protected PresentsServer _server;
/** Our distributed object manager. */
protected RootDObjectManager _omgr;
/** The time at which our next reboot is scheduled or 0L. */
protected long _nextReboot;
/** The entity that scheduled the reboot. */
protected String _initiator;
/** True if the reboot is coming soon, within the earliest warning. */
protected boolean _rebootSoon = false;
/** The interval scheduled to perform the next step the reboot process. */
protected Interval _interval;
/** A list of PendingShutdownObservers. */
protected ObserverList<PendingShutdownObserver> _observers = ObserverList.newFastUnsafe();
/** The next reboot lock id. */
protected int _nextRebootLockId = 0;
/** Things that can delay the reboot. */
protected HashIntMap<String> _rebootLocks = new HashIntMap<String>();
/** The minutes at which we give warnings. The last value is also the
* minimum time at which we can possibly reboot after the value of the
* nextReboot field is changed, to prevent accidentally causing instant
* server reboots. */
public static final int[] WARNINGS = { 30, 20, 15, 10, 5, 2 };
protected static final String AUTOMATIC_INITIATOR = "automatic";
}
@@ -0,0 +1,97 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.samskivert.util.StringUtil;
import com.threerings.util.MessageBundle;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.server.net.AuthingConnection;
import static com.threerings.presents.Log.log;
/**
* A simple server that does nothing more than spit out a canned error response to everyone who
* logs in.
*/
public class Rejector extends PresentsServer
{
/** Configures dependencies needed by the Rejector. */
public static class Module extends PresentsServer.Module
{
@Override protected void configure () {
super.configure();
bind(Authenticator.class).to(RejectingAuthenticator.class);
}
}
@Override
protected int[] getListenPorts ()
{
return _ports;
}
public static void main (String[] args)
{
if (args.length < 2) {
System.err.println("Usage: Rejector ports error_msg [args]");
System.exit(-1);
}
_ports = StringUtil.parseIntArray(args[0]);
_errmsg = args[1];
if (args.length > 2) {
String[] eargs = new String[args.length-2];
System.arraycopy(args, 2, eargs, 0, eargs.length);
_errmsg = MessageBundle.tcompose(_errmsg, eargs);
}
Injector injector = Guice.createInjector(new Module());
Rejector server = injector.getInstance(Rejector.class);
try {
server.init(injector);
server.run();
} catch (Exception e) {
log.warning("Unable to initialize server.", e);
}
}
/**
* An authenticator implementation that refuses all authentication requests.
*/
protected class RejectingAuthenticator extends Authenticator
{
@Override
protected void processAuthentication (AuthingConnection conn, AuthResponse rsp)
throws Exception
{
log.info("Rejecting request: " + conn.getAuthRequest());
throw new AuthException(_errmsg);
}
}
protected static String _errmsg;
protected static int[] _ports;
}
@@ -0,0 +1,202 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.util.StringUtil;
import com.threerings.presents.dobj.RootDObjectManager;
import static com.threerings.presents.Log.log;
/**
* Handles the generation of server status reports.
*/
@Singleton
public class ReportManager
{
/**
* Used to generate "state of the server" reports.
* See {@link ReportManager#registerReporter(Reporter)}.
* */
public static interface Reporter
{
/**
* Requests that this reporter append its report to the supplied string buffer.
*
* @param buffer the string buffer to which the report text should be appended.
* @param now the time at which the report generation began, in epoch millis.
* @param sinceLast number of milliseconds since the last time we generated a report.
* @param reset if true, all accumulating stats should be reset, if false they should be
* allowed to continue to accumulate.
*/
void appendReport (StringBuilder buffer, long now, long sinceLast, boolean reset);
}
/** A string constant representing the default report. */
public static final String DEFAULT_TYPE = "";
/** A string constant representing a report with detailed profiling information. */
public static final String PROFILE_TYPE = "profile";
/**
* Starts up our periodic report generation task.
*/
public void activatePeriodicReport ()
{
// queue up an interval which will generate reports as long as the omgr is alive
_omgr.newInterval(new Runnable() {
public void run () {
logReport(LOG_REPORT_HEADER +
generateReport(DEFAULT_TYPE, System.currentTimeMillis(), true));
}
}).schedule(getReportInterval(), true);
}
/**
* Registers a reporter for the default state of server report.
*/
public void registerReporter (Reporter reporter)
{
registerReporter(DEFAULT_TYPE, reporter);
}
/**
* Registers a reporter for the report of the specified type.
*/
public void registerReporter (String type, Reporter reporter)
{
_reporters.put(type, reporter);
}
/**
* Generate a default state of server report.
*/
public String generateReport ()
{
return generateReport(DEFAULT_TYPE);
}
/**
* Generates a report for all system services registered as a {@link Reporter}.
*/
public String generateReport (String type)
{
return generateReport(type, System.currentTimeMillis(), false);
}
/**
* Generates and logs a "state of server" report.
*/
protected String generateReport (String type, long now, boolean reset)
{
long sinceLast = now - _lastReportStamp;
long uptime = now - _serverStartTime;
StringBuilder report = new StringBuilder();
// add standard bits to the default report
if (DEFAULT_TYPE.equals(type)) {
report.append("- Uptime: ");
report.append(StringUtil.intervalToString(uptime)).append("\n");
report.append("- Report period: ");
report.append(StringUtil.intervalToString(sinceLast)).append("\n");
// report on the state of memory
Runtime rt = Runtime.getRuntime();
long total = rt.totalMemory(), max = rt.maxMemory();
long used = (total - rt.freeMemory());
report.append("- Memory: ").append(used/1024).append("k used, ");
report.append(total/1024).append("k total, ");
report.append(max/1024).append("k max\n");
}
for (Reporter rptr : _reporters.get(type)) {
try {
rptr.appendReport(report, now, sinceLast, reset);
} catch (Throwable t) {
log.warning("Reporter choked", "rptr", rptr, t);
}
}
/* The following Interval debug methods are no longer supported,
* but they could be added back easily if needed.
report.append("* samskivert.Interval:\n");
report.append("- Registered intervals: ");
report.append(Interval.registeredIntervalCount());
report.append("\n- Fired since last report: ");
report.append(Interval.getAndClearFiredIntervals());
report.append("\n");
*/
// strip off the final newline
int blen = report.length();
if (report.length() > 0 && report.charAt(blen-1) == '\n') {
report.delete(blen-1, blen);
}
// only reset the last report time if this is a periodic report
if (reset) {
_lastReportStamp = now;
}
return report.toString();
}
/**
* Logs the state of the server report via the default logging mechanism. Derived classes may
* wish to log the state of the server report via a different means.
*/
protected void logReport (String report)
{
log.info(report);
}
/**
* Returns the period on which to schedule our report generation.
*/
protected long getReportInterval ()
{
return REPORT_INTERVAL;
}
/** The time at which the server was started. */
protected long _serverStartTime = System.currentTimeMillis();
/** The last time at which {@link #generateReport(String,long,boolean)} was run. */
protected long _lastReportStamp = _serverStartTime;
/** Used to generate "state of server" reports. */
protected Multimap<String, Reporter> _reporters = ArrayListMultimap.create();
/** We use the root omgr to queue up our reporting interval. */
@Inject protected RootDObjectManager _omgr;
/** The frequency with which we generate "state of server" reports. */
protected static final long REPORT_INTERVAL = 15 * 60 * 1000L;
/** The header to prefix to our logged reports. */
protected static final String LOG_REPORT_HEADER = "State of server report:\n";
}
@@ -0,0 +1,161 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.util.concurrent.Executor;
import com.samskivert.util.Invoker;
import com.samskivert.util.StringUtil;
/**
* Extends invoker with a reporter implementation that shows current queue status, maximum
* historical size and the results of unit profiling if enabled.
*/
public class ReportingInvoker extends Invoker
{
public static class Stats
{
public int unitsRun;
public int maxQueueSize;
}
/**
* Creates a new reporting invoker. The instance will be registered with the report manager
* if profiling is enabled ({@link Invoker#PERF_TRACK}).
*/
public ReportingInvoker (String name, Executor receiver, ReportManager repmgr)
{
super(name, receiver);
if (PERF_TRACK) {
repmgr.registerReporter(ReportManager.DEFAULT_TYPE, _defrep);
repmgr.registerReporter(ReportManager.PROFILE_TYPE, _profrep);
}
}
/**
* Returns a recent snapshot of runtime statistics tracked by the invoker.
*
* @param snapshot if true, the current stats will be snapshotted and reset and the new
* snapshot will be returned. If false, the previous snapshot will be returned. If no snapshot
* has ever been taken, the current stats that have been accumulting since the JVM start will
* be returned.
*/
public Stats getStats (boolean snapshot)
{
synchronized (this) {
if (snapshot) {
_recent = _current;
_current = new Stats();
_current.maxQueueSize = _queue.size();
}
return _recent;
}
}
@Override // from Invoker
protected void willInvokeUnit (Unit unit, long start)
{
super.willInvokeUnit(unit, start);
int queueSize = _queue.size();
synchronized (this) {
// keep track of the largest queue size we've seen
if (queueSize > _current.maxQueueSize) {
_current.maxQueueSize = queueSize;
}
_current.unitsRun++;
_totalUnitsRun++;
// note the currently invoking unit
_currentUnit = unit;
_currentUnitStart = start;
}
}
@Override // from Invoker
protected void didInvokeUnit (Unit unit, long start)
{
super.didInvokeUnit(unit, start);
synchronized (this) {
// clear out our currently invoking unit
_currentUnit = null;
_currentUnitStart = 0L;
}
}
/** Generates a report on our runtime behavior. */
protected ReportManager.Reporter _defrep = new ReportManager.Reporter() {
public void appendReport (StringBuilder buf, long now, long sinceLast, boolean reset) {
buf.append("* " + getName() + ":\n");
int qsize = _queue.size();
buf.append("- Queue size: ").append(qsize).append("\n");
synchronized (this) {
Stats stats = getStats(reset);
buf.append("- Max queue size: ").append(stats.maxQueueSize).append("\n");
buf.append("- Units executed: ").append(stats.unitsRun).append("\n");
buf.append("- Total units executed: ").append(_totalUnitsRun);
long runPerSec = stats.unitsRun/Math.max(1, sinceLast/1000);
buf.append(" (").append(runPerSec).append("/s)\n");
if (_currentUnit != null) {
String uname = StringUtil.safeToString(_currentUnit);
buf.append("- Current unit: ").append(uname).append(" ");
buf.append(now-_currentUnitStart).append("ms\n");
}
}
}
};
/** Generates a report with our profiling data. */
protected ReportManager.Reporter _profrep = new ReportManager.Reporter() {
public void appendReport (StringBuilder buf, long now, long sinceLast, boolean reset) {
buf.append("* " + getName() + ":\n");
if (PresentsDObjectMgr.UNIT_PROF_ENABLED) {
for (Object key : _tracker.keySet()) {
UnitProfile profile = _tracker.get(key);
if (key instanceof Class<?>) {
key = StringUtil.shortClassName((Class<?>)key);
}
buf.append(" ").append(key).append(" ");
buf.append(profile).append("\n");
if (reset) {
profile.clear();
}
}
} else {
buf.append(" - Unit profiling disabled.\n");
}
}
};
/** Used to track runtime statistics. */
protected Stats _recent = new Stats(), _current = _recent;
/** The total number of units run. */
protected int _totalUnitsRun;
/** Records the currently invoking unit. */
protected Object _currentUnit;
/** The time at which our current unit started. */
protected long _currentUnitStart;
}
@@ -0,0 +1,89 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.lang.reflect.Constructor;
import com.threerings.util.Name;
import com.threerings.presents.data.AuthCodes;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.ServiceCreds;
import com.threerings.presents.server.net.AuthingConnection;
import static com.threerings.presents.Log.log;
/**
* Works in conjunction with {@link ServiceCreds} to handle the authentication of service clients
* (bureaus, peers, etc.).
*/
public abstract class ServiceAuthenticator<T extends ServiceCreds> extends ChainedAuthenticator
{
/**
* Creates an authenticator that will handle requests using the supplied credentials class and
* which will create instances of the supplied auth name class to identify those clients. Note
* that the auth name class <em>must</em> have a public constructor that takes a single string.
*/
public ServiceAuthenticator (Class<T> credsClass, Class<? extends Name> authNameClass)
{
_credsClass = credsClass;
try {
_authNamer = authNameClass.getConstructor(String.class);
} catch (NoSuchMethodException nsme) {
throw new IllegalArgumentException("AuthName must have AuthName(String) constructor.");
}
}
@Override // from abstract ChainedAuthenticator
public boolean shouldHandleConnection (AuthingConnection conn)
{
return _credsClass.isInstance(conn.getAuthRequest().getCredentials());
}
@Override // from abstract Authenticator
protected void processAuthentication (AuthingConnection conn, AuthResponse rsp)
throws Exception
{
T creds = _credsClass.cast(conn.getAuthRequest().getCredentials());
if (!areValid(creds)) {
log.warning("Received invalid service auth request?", "creds", creds);
throw new AuthException(AuthCodes.SERVER_ERROR);
}
try {
conn.setAuthName(_authNamer.newInstance(creds.clientId));
rsp.getData().code = AuthResponseData.SUCCESS;
} catch (Exception e) {
log.warning("Failed to construct auth name", "namer", _authNamer, e);
throw new AuthException(AuthCodes.SERVER_ERROR);
}
}
/**
* Returns true if the creds in question are valid.
*/
protected abstract boolean areValid (T creds);
protected Class<T> _credsClass;
protected Constructor<? extends Name> _authNamer;
}
@@ -0,0 +1,78 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.threerings.util.Name;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.Credentials;
/**
* Used to determine what type of {@link PresentsSession} to use to manage an authenticated client
* as well the type of {@link ClientResolver} to use when resolving clients' runtime data.
*/
public abstract class SessionFactory
{
/** The default client factory. */
public static SessionFactory DEFAULT = new SessionFactory() {
@Override public Class<? extends PresentsSession> getSessionClass (AuthRequest areq) {
return PresentsSession.class;
}
@Override public Class<? extends ClientResolver> getClientResolverClass (Name username) {
return ClientResolver.class;
}
};
/**
* Creates a session factory that handles clients with the supplied credentials and
* authentication name.
*/
public static SessionFactory newSessionFactory (
final Class<? extends Credentials> credsClass,
final Class<? extends PresentsSession> sessionClass,
final Class<? extends Name> nameClass,
final Class<? extends ClientResolver> resolverClass)
{
return new SessionFactory() {
@Override public Class<? extends PresentsSession> getSessionClass (AuthRequest areq) {
return credsClass.isInstance(areq.getCredentials()) ? sessionClass : null;
}
@Override
public Class <? extends ClientResolver> getClientResolverClass (Name username) {
return nameClass.isInstance(username) ? resolverClass : null;
}
};
}
/**
* Returns the {@link PresentsSession} derived class to use for the session that authenticated
* with the supplied request or null if this factory does not handle sessions of the supplied
* type.
*/
public abstract Class<? extends PresentsSession> getSessionClass (AuthRequest areq);
/**
* Returns the {@link ClientResolver} derived class to use to resolve a client with the
* specified username or null if this factory does not handle clients of the supplied type.
*/
public abstract Class <? extends ClientResolver> getClientResolverClass (Name username);
}
@@ -0,0 +1,95 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.util.Lifecycle;
/**
* Handles the orderly shutdown of all server services.
*
* @Deprecated use Lifecycle
*/
@Singleton
public class ShutdownManager
{
/** Implementers of this interface will be notified when the server is shutting down. */
public static interface Shutdowner extends Lifecycle.ShutdownComponent
{
}
/** Constraints for use with {@link ShutdownManager#addConstraint}. */
public static enum Constraint { RUNS_BEFORE, RUNS_AFTER }
/**
* Registers an entity that will be notified when the server is shutting down.
*/
public void registerShutdowner (Shutdowner downer)
{
_cycle.addComponent(downer);
}
/**
* Unregisters the shutdowner from hearing when the server is shutdown.
*/
public void unregisterShutdowner (Shutdowner downer)
{
_cycle.removeComponent(downer);
}
/**
* Adds a constraint that a certain shutdowner must be run before another.
*/
public void addConstraint (Shutdowner lhs, Constraint constraint, Shutdowner rhs)
{
switch (constraint) {
case RUNS_BEFORE:
_cycle.addShutdownConstraint(lhs, Lifecycle.Constraint.RUNS_BEFORE, rhs);
break;
case RUNS_AFTER:
_cycle.addShutdownConstraint(lhs, Lifecycle.Constraint.RUNS_AFTER, rhs);
break;
}
}
/**
* Queues up a request to shutdown on the dobjmgr thread. This method may be safely called from
* any thread.
*/
public void queueShutdown ()
{
_server.queueShutdown();
}
/**
* Returns true if we're in the process of shutting down.
*/
public boolean isShuttingDown ()
{
return _cycle.isShuttingDown();
}
@Inject protected Lifecycle _cycle;
@Inject protected PresentsServer _server;
}
@@ -0,0 +1,31 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
/**
* If injected into a presents server, listens for a USR2 Unix signal captured by
* {@link AbstractSignalHandler}.
*/
public interface SignalReceiver
{
void received();
}
@@ -0,0 +1,59 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.SignalUtil;
/**
* Handles signals using Sun's undocumented Signal class.
*/
public class SunSignalHandler extends AbstractSignalHandler
{
@Override
protected boolean registerHandlers ()
{
SignalUtil.register(SignalUtil.Number.TERM, new SignalUtil.Handler() {
public void signalReceived (SignalUtil.Number sig) {
termReceived();
}
});
SignalUtil.register(SignalUtil.Number.INT, new SignalUtil.Handler() {
public void signalReceived (SignalUtil.Number sig) {
intReceived();
}
});
SignalUtil.register(SignalUtil.Number.USR2, new SignalUtil.Handler() {
public void signalReceived (SignalUtil.Number sig) {
usr2Received();
}
});
if (!RunAnywhere.isWindows()) {
SignalUtil.register(SignalUtil.Number.HUP, new SignalUtil.Handler() {
public void signalReceived (SignalUtil.Number sig) {
hupReceived();
}
});
}
return true;
}
}
@@ -0,0 +1,69 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import javax.annotation.Generated;
import com.threerings.presents.client.TimeBaseService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.TimeBaseMarshaller;
/**
* Dispatches requests to the {@link TimeBaseProvider}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from TimeBaseService.java.")
public class TimeBaseDispatcher extends InvocationDispatcher<TimeBaseMarshaller>
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public TimeBaseDispatcher (TimeBaseProvider provider)
{
this.provider = provider;
}
@Override
public TimeBaseMarshaller createMarshaller ()
{
return new TimeBaseMarshaller();
}
@Override
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
case TimeBaseMarshaller.GET_TIME_OID:
((TimeBaseProvider)provider).getTimeOid(
source, (String)args[0], (TimeBaseService.GotTimeBaseListener)args[1]
);
return;
default:
super.dispatchRequest(source, methodId, args);
return;
}
}
}
@@ -0,0 +1,103 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server;
import java.util.HashMap;
import com.google.common.collect.Maps;
import com.threerings.presents.client.TimeBaseService.GotTimeBaseListener;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.TimeBaseCodes;
import com.threerings.presents.data.TimeBaseObject;
import com.threerings.presents.dobj.RootDObjectManager;
/**
* Provides the server-side of the time base services. The time base services provide a means by
* which delta times can be sent over the network which are expanded based on a shared base time
* into full time stamps.
*/
public class TimeBaseProvider
implements InvocationProvider, TimeBaseCodes
{
/**
* Registers the time provider with the appropriate managers. Called by the presents server at
* startup.
*/
public static void init (InvocationManager invmgr, RootDObjectManager omgr)
{
// we'll need these later
_invmgr = invmgr;
_omgr = omgr;
// register a provider instance
invmgr.registerDispatcher(new TimeBaseDispatcher(new TimeBaseProvider()), GLOBAL_GROUP);
}
/**
* Creates a time base object which can subsequently be fetched by the client and used to send
* delta times.
*
* @param timeBase the name of the time base to create.
*
* @return the created and registered time base object.
*/
public static TimeBaseObject createTimeBase (String timeBase)
{
TimeBaseObject object = _omgr.registerObject(new TimeBaseObject());
_timeBases.put(timeBase, object);
return object;
}
/**
* Returns the named timebase object, or null if no time base object has been created with that
* name.
*/
public static TimeBaseObject getTimeBase (String timeBase)
{
return _timeBases.get(timeBase);
}
/**
* Processes a request from a client to fetch the oid of the specified time object.
*/
public void getTimeOid (ClientObject source, String timeBase, GotTimeBaseListener listener)
throws InvocationException
{
// look up the time base object in question
TimeBaseObject time = getTimeBase(timeBase);
if (time == null) {
throw new InvocationException(NO_SUCH_TIME_BASE);
}
// and send the response
listener.gotTimeOid(time.getOid());
}
/** Used to keep track of our time base objects. */
protected static HashMap<String,TimeBaseObject> _timeBases = Maps.newHashMap();
/** The invocation manager with which we interoperate. */
protected static InvocationManager _invmgr;
/** The distributed object manager with which we interoperate. */
protected static RootDObjectManager _omgr;
}
@@ -0,0 +1,108 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server.net;
import com.threerings.util.Name;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.Message;
import static com.threerings.presents.Log.log;
/**
* The authing connection manages the client connection until authentication has completed (for
* better or for worse).
*/
public class AuthingConnection extends Connection
{
public AuthingConnection ()
{
setMessageHandler(new MessageHandler() {
public void handleMessage (Message msg) {
try {
// keep a handle on our auth request
_authreq = (AuthRequest)msg;
// post ourselves for processing by the authmgr
_cmgr.authenticateConnection(AuthingConnection.this);
} catch (ClassCastException cce) {
log.warning("Received non-authreq message during authentication process",
"conn", AuthingConnection.this, "msg", msg);
}
}
});
}
/**
* Returns a reference to the auth request currently being processed.
*/
public AuthRequest getAuthRequest ()
{
return _authreq;
}
/**
* Returns the auth response delivered to the client (only valid after the auth request has
* been processed.
*/
public AuthResponse getAuthResponse ()
{
return _authrsp;
}
/**
* Stores a reference to the auth response delivered to this connection. This is called by the
* auth manager after delivering the auth response to the client.
*/
public void setAuthResponse (AuthResponse authrsp)
{
_authrsp = authrsp;
}
/**
* Returns the username that uniquely identifies this authenticated session. This will be used
* to map Name -> PresentsSession in the ClientManager and used elsewhere.
*/
public Name getAuthName ()
{
return _authname;
}
/**
* During the authentication process, the authenticator must establish the client's
* authentication username and configure it via this method.
*/
public void setAuthName (Name authname)
{
_authname = authname;
}
@Override
public String toString ()
{
return "[mode=AUTHING, addr=" + getInetAddress() + "]";
}
protected AuthRequest _authreq;
protected AuthResponse _authrsp;
protected Name _authname;
}
@@ -0,0 +1,241 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server.net;
import java.util.List;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.net.AddressUtil;
import com.samskivert.util.Lifecycle;
import com.threerings.presents.server.ReportManager;
import static com.threerings.presents.Log.log;
/**
* Binds tcp sockets and listens for datagrams in addition to ConnectionManager's normal duties.
*/
@Singleton
public class BindingConnectionManager extends ConnectionManager
{
@Inject public BindingConnectionManager (Lifecycle cycle, ReportManager repmgr)
throws IOException
{
super(cycle, repmgr);
}
/**
* Configures the connection manager with the hostname and ports on which it will listen for
* socket connections and datagram packets. This must be called before the connection manager
* is started (via {@via #start}) as the sockets will be bound at that time.
*
* @param socketHostname the hostname to which we bind our sockets or null to bind to all
* interfaces.
* @param datagramHostname the hostname to which we bind our datagram socket or null to bind
* to all interfaces.
* @param socketPorts the ports on which to listen for TCP connection.
* @param datagramPorts the ports on which to listen for datagram packets.
*/
public void init (String socketHostname, String datagramHostname,
int[] socketPorts, int[] datagramPorts)
throws IOException
{
Preconditions.checkNotNull(socketPorts, "Socket ports must be non-null.");
Preconditions.checkNotNull(datagramPorts, "Datagram ports must be non-null. " +
"Pass a zero-length array to bind no datagram ports.");
_bindHostname = socketHostname;
_ports = socketPorts;
_datagramHostname = datagramHostname;
_datagramPorts = datagramPorts;
}
@Override
protected void willStart ()
{
super.willStart();
// open our TCP listening ports
int successes = 0;
for (int port : _ports) {
try {
acceptConnections(port);
successes++;
} catch (IOException ioe) {
log.warning("Failure listening to socket", "hostname", _bindHostname,
"port", port, ioe);
}
}
if (successes == 0) {
log.warning("ConnectionManager failed to bind to any ports. Shutting down.");
_server.queueShutdown();
return;
}
// open up the datagram ports as well
for (int port : _datagramPorts) {
try {
acceptDatagrams(port);
} catch (IOException ioe) {
log.warning("Failure opening datagram channel", "hostname", _datagramHostname,
"port", port, ioe);
}
}
}
@Override
protected void didShutdown ()
{
super.didShutdown();
// TODO: consider closing the listen sockets earlier, like in the shutdown method
// unbind our listening sockets; note: because we wait for the objmgr to exit before we do,
// we will still be accepting connections as long as there are events pending.
for (ServerSocketChannel ssocket : _ssockets) {
try {
ssocket.socket().close();
} catch (IOException ioe) {
log.warning("Failed to close listening socket: " + ssocket, ioe);
}
}
// unbind datagram channels, if any
for (DatagramChannel datagramChannel : _datagramChannels) {
datagramChannel.socket().close();
}
}
protected void acceptConnections (int port)
throws IOException
{
// create a listening socket
final ServerSocketChannel ssocket = ServerSocketChannel.open();
ssocket.configureBlocking(false);
InetSocketAddress isa = AddressUtil.getAddress(_bindHostname, port);
ssocket.socket().bind(isa);
// and add it to the select set
SelectionKey sk = ssocket.register(_selector, SelectionKey.OP_ACCEPT);
_handlers.put(sk, new NetEventHandler() {
public int handleEvent (long when) {
try {
handleAcceptedSocket(ssocket.accept());
} catch (IOException ioe) {
log.info("Failure accepting connected socket: " +ioe);
}
// there's no easy way to measure bytes read when accepting a connection, so we
// claim nothing
return 0;
}
public boolean checkIdle (long now) {
return false; // we're never idle
}
public void becameIdle () {
// we're never idle
}
});
_ssockets.add(ssocket);
log.info("Server listening on " + isa + ".");
}
protected void acceptDatagrams (int port)
throws IOException
{
// create a channel
final DatagramChannel channel = DatagramChannel.open();
channel.socket().setTrafficClass(0x10); // IPTOS_LOWDELAY
channel.configureBlocking(false);
InetSocketAddress isa = AddressUtil.getAddress(_datagramHostname, port);
channel.socket().bind(isa);
// and add it to the select set
SelectionKey sk = channel.register(_selector, SelectionKey.OP_READ);
_handlers.put(sk, new NetEventHandler() {
public int handleEvent (long when) {
return handleDatagram(channel, when);
}
public boolean checkIdle (long now) {
return false;// Can't be idle
}
public void becameIdle () {}
});
_datagramChannels.add(channel);
log.info("Server accepting datagrams on " + isa + ".");
}
/**
* Called when a datagram message is ready to be read off its channel.
*/
protected int handleDatagram (DatagramChannel listener, long when)
{
InetSocketAddress source;
_databuf.clear();
try {
source = (InetSocketAddress)listener.receive(_databuf);
} catch (IOException ioe) {
log.warning("Failure receiving datagram.", ioe);
return 0;
}
// make sure we actually read a packet
if (source == null) {
log.info("Psych! Got READ_READY, but no datagram.");
return 0;
}
// flip the buffer and record the size (which must be at least 14 to contain the connection
// id, authentication hash, and a class reference)
int size = _databuf.flip().remaining();
if (size < 14) {
log.warning("Received undersized datagram", "source", source, "size", size);
return 0;
}
// the first four bytes are the connection id
int connectionId = _databuf.getInt();
Connection conn = _connections.get(connectionId);
if (conn != null) {
conn.handleDatagram(source, listener, _databuf, when);
} else {
log.debug("Received datagram for unknown connection", "id", connectionId,
"source", source);
}
return size;
}
protected int[] _ports, _datagramPorts;
protected String _bindHostname, _datagramHostname;
protected List<ServerSocketChannel> _ssockets = Lists.newArrayList();
protected List<DatagramChannel> _datagramChannels = Lists.newArrayList();
}
@@ -0,0 +1,479 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server.net;
import java.io.EOFException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import com.threerings.io.FramedInputStream;
import com.threerings.io.FramingOutputStream;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.presents.net.Message;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.util.DatagramSequencer;
import static com.threerings.presents.Log.log;
/**
* The base connection class implements the net event handler interface and processes raw incoming
* network data into a stream of parsed {@link Message} objects. It also provides the means to send
* messages to the client and facilities for checking delinquency.
*/
public class Connection implements NetEventHandler
{
/** Used with {@link Connection#setMessageHandler}. */
public static interface MessageHandler {
/** Called when a complete message has been parsed from incoming network data. */
void handleMessage (Message message);
}
/** The key used by the NIO code to track this connection. */
public SelectionKey selkey;
/**
* Initializes a connection object with a socket and related info.
*
* @param cmgr The connection manager with which this connection is associated.
* @param channel The socket channel from which we'll be reading messages.
* @param createStamp The time at which this connection was created.
*/
public void init (ConnectionManager cmgr, SocketChannel channel, long createStamp)
throws IOException
{
_cmgr = cmgr;
_channel = channel;
_lastEvent = createStamp;
_connectionId = ++_lastConnectionId;
}
/**
* Instructs the connection to pass parsed messages on to this handler for processing. This
* should be done before the connection is turned loose to process messages.
*/
public void setMessageHandler (MessageHandler handler)
{
_handler = handler;
}
/**
* Configures this connection with a custom class loader.
*/
public void setClassLoader (ClassLoader loader)
{
_loader = loader;
if (_oin != null) {
_oin.setClassLoader(loader);
}
}
/**
* Returns the connection's unique identifier.
*/
public int getConnectionId ()
{
return _connectionId;
}
/**
* Returns the non-blocking socket object used to construct this connection.
*/
public SocketChannel getChannel ()
{
return _channel;
}
/**
* Returns the address associated with this connection or null if it has no underlying socket
* channel.
*/
public InetAddress getInetAddress ()
{
return (_channel == null) ? null : _channel.socket().getInetAddress();
}
/**
* Sets whether we should transmit datagrams.
*/
public void setTransmitDatagrams (boolean transmit)
{
_transmitDatagrams = transmit;
}
/**
* Checks whether we should transmit datagrams.
*/
public boolean getTransmitDatagrams ()
{
return _transmitDatagrams;
}
/**
* Returns the address to which datagrams should be sent or null if no datagram address has
* been established.
*/
public InetSocketAddress getDatagramAddress ()
{
return _datagramAddress;
}
/**
* Returns the channel through which datagrams should be sent or null if no datagram channel
* has been established.
*/
public DatagramChannel getDatagramChannel ()
{
return _datagramChannel;
}
/**
* Sets the secret string used to authenticate datagrams from the client.
*/
public void setDatagramSecret (String secret)
{
try {
_datagramSecret = secret.getBytes("UTF-8");
} catch (Exception e) {
_datagramSecret = new byte[0]; // shouldn't happen
}
}
/**
* Returns true if this connection is closed.
*/
public boolean isClosed ()
{
return (_channel == null);
}
/**
* Closes this connection and unregisters it from the connection manager. This should only be
* called from the conmgr thread.
*/
public void close ()
{
// we shouldn't be closed twice
if (isClosed()) {
log.warning("Attempted to re-close connection " + this + ".", new Exception());
return;
}
// unregister from the select set
_cmgr.connectionClosed(this);
// close our socket
closeSocket();
}
/**
* Queues up a request to have this connection closed by the connection manager once all
* messages in its queue have been written to its target.
*/
public void asyncClose ()
{
_cmgr.postAsyncClose(this);
}
/**
* Posts a message for delivery to this connection. The message will be delivered by the conmgr
* thread as soon as it gets to it.
*/
public void postMessage (Message msg)
{
// pass this along to the connection manager
_cmgr.postMessage(this, msg);
}
/**
* Called when an outgoing socket experiences a connect failure. The connection manager will
* have cleaned up the partial registration needed during the connect process, so we are only
* responsible for closing our socket.
*/
public void connectFailure (IOException ioe)
{
closeSocket();
}
/**
* Called when there is a failure reading or writing to this connection. We notify the
* connection manager and close ourselves down.
*/
public void networkFailure (IOException ioe)
{
// if we're already closed, then something is seriously funny
if (isClosed()) {
log.warning("Failure reported on closed connection " + this + ".", new Exception());
return;
}
// let the connection manager know we're hosed
_cmgr.connectionFailed(this, ioe);
// and close our socket
closeSocket();
}
/**
* Processes a datagram sent to this connection.
*/
public void handleDatagram (InetSocketAddress source, DatagramChannel channel,
ByteBuffer buf, long when)
{
// lazily create our various bits and bobs
if (_digest == null) {
try {
_digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
log.warning("Missing MD5 algorithm.");
return;
}
_sequencer = _cmgr.createDatagramSequencer();
}
// verify the hash
buf.position(12);
_digest.update(buf);
byte[] hash = _digest.digest(_datagramSecret);
buf.position(4);
for (int ii = 0; ii < 8; ii++) {
if (hash[ii] != buf.get()) {
log.warning("Datagram failed hash check", "id", _connectionId, "source", source);
return;
}
}
// update our target address
_datagramAddress = source;
_datagramChannel = channel;
// read the contents through the sequencer
try {
Message msg = _sequencer.readDatagram();
if (msg == null) {
return; // received out of order
}
msg.received = when;
_handler.handleMessage(msg);
} catch (ClassNotFoundException cnfe) {
log.warning("Error reading datagram", "error", cnfe);
} catch (IOException ioe) {
log.warning("Error reading datagram", "error", ioe);
}
}
// from interface NetEventHandler
public int handleEvent (long when)
{
// make a note that we received an event as of this time
_lastEvent = when;
int bytesIn = 0;
try {
// we're lazy about creating our input streams because we may be inheriting them from
// our authing connection and we don't want to unnecessarily create them in that case
if (_fin == null) {
_fin = new FramedInputStream();
_oin = new ObjectInputStream(_fin);
if (_loader != null) {
_oin.setClassLoader(_loader);
}
}
// there may be more than one frame in the buffer, so we keep reading them until we run
// out of data
while (_fin.readFrame(_channel)) {
// make a note of how many bytes are in this frame (including the frame length
// bytes which aren't reported in available())
bytesIn = _fin.available() + 4;
// parse the message and pass it on
Message msg = (Message)_oin.readObject();
msg.received = when;
// Log.info("Read message " + msg + ".");
_handler.handleMessage(msg);
}
} catch (EOFException eofe) {
// close down the socket gracefully
close();
} catch (ClassNotFoundException cnfe) {
log.warning("Error reading message from socket", "channel", _channel, "error", cnfe);
// deal with the failure
String errmsg = "Unable to decode incoming message.";
networkFailure((IOException) new IOException(errmsg).initCause(cnfe));
} catch (IOException ioe) {
// don't log a warning for the ever-popular "the client dropped the connection" failure
String msg = ioe.getMessage();
if (msg == null || msg.indexOf("reset by peer") == -1) {
log.warning("Error reading message from socket", "channel", _channel, "error", ioe);
}
// deal with the failure
networkFailure(ioe);
}
return bytesIn;
}
// from interface NetEventHandler
public boolean checkIdle (long now)
{
long idleMillis = now - _lastEvent;
if (idleMillis < PingRequest.PING_INTERVAL + LATENCY_GRACE) {
return false;
}
if (isClosed()) {
return true;
}
log.info("Disconnecting non-communicative client", "conn", this, "idle", idleMillis + "ms");
return true;
}
// from interface NetEventHandler
public void becameIdle ()
{
_cmgr.closeConnection(this);
}
@Override // from Object
public String toString ()
{
return "[id=" + (hashCode() % 1000) + ", addr=" + getInetAddress() + "]";
}
/**
* Returns the object input stream associated with this connection. This should only be used
* by the connection manager.
*/
protected ObjectInputStream getObjectInputStream ()
{
return _oin;
}
/**
* Instructs this connection to inherit its streams from the supplied connection object. This
* is called by the connection manager when the time comes to pass streams from the authing
* connection to the running connection.
*/
protected void inheritStreams (Connection other)
{
_fin = other._fin;
_oin = other._oin;
_oout = other._oout;
if (_loader != null) {
_oin.setClassLoader(_loader);
}
}
/**
* Returns the object output stream associated with this connection (creating it if
* necessary). This should only be used by the connection manager.
*/
protected ObjectOutputStream getObjectOutputStream (FramingOutputStream fout)
{
// we're lazy about creating our output stream because we may be inheriting it from our
// authing connection and we don't want to unnecessarily create it in that case
if (_oout == null) {
_oout = new ObjectOutputStream(fout);
}
return _oout;
}
/**
* Sets the object output stream used by this connection. This should only be called by the
* connection manager.
*/
protected void setObjectOutputStream (ObjectOutputStream oout)
{
_oout = oout;
}
/**
* Returns a reference to the connection's datagram sequencer. This should only be called by
* the connection manager.
*/
protected DatagramSequencer getDatagramSequencer ()
{
return _sequencer;
}
/**
* Closes the socket associated with this connection. This happens when we receive EOF, are
* requested to close down or when our connection fails.
*/
protected void closeSocket ()
{
if (_channel == null) {
return;
}
log.debug("Closing channel " + this + ".");
try {
_channel.close();
} catch (IOException ioe) {
log.warning("Error closing connection", "conn", this, "error", ioe);
}
// clear out our references to prevent repeat closings
_channel = null;
}
protected ConnectionManager _cmgr;
protected SocketChannel _channel;
protected long _lastEvent;
protected int _connectionId;
protected FramedInputStream _fin;
protected ObjectInputStream _oin;
protected ObjectOutputStream _oout;
protected InetSocketAddress _datagramAddress;
protected DatagramChannel _datagramChannel;
protected byte[] _datagramSecret;
protected boolean _transmitDatagrams;
protected MessageDigest _digest;
protected DatagramSequencer _sequencer;
protected MessageHandler _handler;
protected ClassLoader _loader;
/** The last connection id assigned. */
protected static int _lastConnectionId;
/** The number of milliseconds beyond the ping interval that we allow a client's network
* connection to be idle before we forcibly disconnect them. */
protected static final long LATENCY_GRACE = 30 * 1000L;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server.net;
/**
* When a network event occurs, the connection manager calls the net event
* handler associated with that channel to process the event. There are
* only a few handlers (and probably only ever will be): the one that
* accepts new connections, the one that deals with a connection while the
* client is authenticating and the one that processes messages from
* authenticated connections.
*
* <p> Utilising this interface prevents us from having to do a bunch of
* inefficient and ugly comparisons; instead we can simply call through an
* interface method to the proper code.
*/
public interface NetEventHandler
{
/**
* Called when a network event has occurred on this handler's source.
*
* @return the number of bytes read from the network as a result of
* handling this event.
*/
int handleEvent (long when);
/**
* Called to ensure that this channel has not been idle for longer
* than is possible in happily operating circumstances.
*
* @return true if the handler is idle (in which case it will be
* closed shortly), false if it is not.
*/
boolean checkIdle (long now);
/**
* Called if the handler is deemed to be idle. Should shutdown any associated connection and
* remove any registrations with the connection manager.
*/
void becameIdle ();
}
@@ -0,0 +1,211 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server.net;
import java.io.IOException;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.ClientObserver;
import com.threerings.presents.client.Communicator;
import com.threerings.presents.client.ObserverOps;
import com.threerings.presents.client.SessionObserver;
import com.threerings.presents.dobj.RootDObjectManager;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.LogoffRequest;
import com.threerings.presents.net.Message;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PongResponse;
import com.threerings.presents.net.UpstreamMessage;
import static com.threerings.presents.Log.log;
/**
* Provides Client {@link Communicator} services using non-blocking I/O and the connection manager.
*/
public class ServerCommunicator extends Communicator
{
public ServerCommunicator (Client client, ConnectionManager conmgr, RootDObjectManager rootmgr)
{
super(client);
_conmgr = conmgr;
_rootmgr = rootmgr;
}
@Override // from Communicator
public void logon ()
{
// make sure things are copacetic
if (_conn != null) {
throw new RuntimeException("Communicator already started.");
}
// we assume server entities have no firewall issues and can connect on the first port
try {
Connection conn = new Connection() {
@Override public void postMessage (Message msg) {
super.postMessage(msg);
// outgoing traffic on this connection is used to prevent idleness
// TODO: shouldn't PongResponse handle this?
_lastEvent = System.currentTimeMillis();
if (PING_DEBUG && msg instanceof PingRequest) { // TEMP
log.info("Pinging on server comm " + msg);
}
}
@Override public void connectFailure (IOException ioe) {
_logonError = ioe; // report this as a logon failure
super.connectFailure(ioe);
}
@Override public void networkFailure (final IOException ioe) {
notifyClientObservers(new ObserverOps.Client(_client) {
@Override protected void notify (ClientObserver obs) {
obs.clientConnectionFailed(_client, ioe);
}
});
super.networkFailure(ioe);
}
@Override protected void closeSocket () {
super.closeSocket();
shutdown();
}
};
conn.setMessageHandler(new Connection.MessageHandler() {
public void handleMessage (Message message) {
try {
// our first message will always be an auth response
gotAuthResponse((AuthResponse)message);
} catch (Exception e) {
_logonError = e;
shutdown();
}
}
});
_conmgr.openOutgoingConnection(conn, _client.getHostname(), _client.getPorts()[0]);
_conn = conn;
if (_loader != null) {
_conn.setClassLoader(_loader);
}
// now send our auth request
postMessage(new AuthRequest(_client.getCredentials(), _client.getVersion(),
_client.getBootGroups()));
} catch (IOException ioe) {
_logonError = ioe;
shutdown();
}
}
@Override // from Communicator
public void logoff ()
{
if (_conn != null) {
_conn.postMessage(new LogoffRequest());
_conn.asyncClose();
_conn = null;
}
}
@Override // from Communicator
public void gotBootstrap ()
{
// nothing needed
}
@Override // from Communicator
public void postMessage (final UpstreamMessage msg)
{
// if we're not on the main dobjmgr thread, we need to get there
if (!_rootmgr.isDispatchThread()) {
_rootmgr.postRunnable(new Runnable() {
public void run () {
postMessage(msg);
}
});
return;
}
if (_conn == null) {
log.info("Dropping message for lack of connection.", "client", _client, "msg", msg);
return;
}
// pass this message along to our connection
_conn.postMessage(msg);
// we cheat a bit and claim that we "wrote" when we post our messages so that we don't have
// to modify the connection manager to call a method on Connection every time it writes a
// message from the queue
updateWriteStamp();
}
@Override // from Communicator
public void setClassLoader (ClassLoader loader)
{
_loader = loader;
if (_conn != null) {
_conn.setClassLoader(loader);
}
}
@Override // from Communicator
protected synchronized void logonSucceeded (AuthResponseData data)
{
super.logonSucceeded(data);
// now we can route all messages to the ClientDObjectMgr
_conn.setMessageHandler(new Connection.MessageHandler() {
public void handleMessage (Message message) {
if (PING_DEBUG && message instanceof PongResponse) {
log.info("Got pong from server " + message);
}
processMessage(message);
}
});
}
protected void shutdown ()
{
if (_logonError == null) {
// we were logged on successfully, so report didLogoff first
notifyClientObservers(new ObserverOps.Session(_client) {
@Override protected void notify (SessionObserver obs) {
obs.clientDidLogoff(_client);
}
});
}
clientCleanup(_logonError);
}
protected ConnectionManager _conmgr;
protected RootDObjectManager _rootmgr;
protected Connection _conn;
protected ClassLoader _loader;
protected Exception _logonError;
// TEMP
protected static final boolean PING_DEBUG = Boolean.getBoolean("ping_debug");
}