diff --git a/src/server/amp.py b/src/server/amp.py index 74218de077..03b7033cb5 100644 --- a/src/server/amp.py +++ b/src/server/amp.py @@ -31,11 +31,11 @@ from src.utils.utils import to_str, variable_from_module # so as to not have to load them on the portal too. Note: It's doubtful # if this really matters, considering many of the # protocols require import of django components (at least settings). -_ServerConfig = None -_ScriptDB = None -_PlayerDB = None -_ServerSession = None -_ = None #i18n hook +#_ServerConfig = None +#_ScriptDB = None +#_PlayerDB = None +#_ServerSession = None +#_ = None #i18n hook # communication bits @@ -46,7 +46,7 @@ SLOGIN = chr(4) # server session login SDISCONN = chr(5) # server session disconnect SDISCONNALL = chr(6) # server session disconnect all SSHUTD = chr(7) # server shutdown -SSYNC = chr(8) # server session sync +SSYNC = chr(8) # server sessigon sync MAXLEN = 65535 # max allowed data length in AMP protocol @@ -121,7 +121,7 @@ class AmpClientFactory(protocol.ReconnectingClientFactory): if hasattr(self, "server_restart_mode"): self.maxDelay = 1 else: - # Don't translate this; avoiding loading django on portal side. + # Don't translate this; avoid loading django on portal side. self.maxDelay = 10 self.portal.sessions.announce_all(" ... Portal lost connection to Server.") protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason) @@ -312,6 +312,10 @@ class AMPProtocol(amp.AMP): def amp_msg_portal2server(self, sessid, msg, ipart, nparts, data): """ Relays message to server. This method is executed on the Server. + + Since AMP has a limit of 65355 bytes per message, it's possible the + data comes in multiple chunks; if so (nparts>1) we buffer the data + and wait for the remaining parts to arrive before continuing. """ #print "msg portal -> server (server side):", sessid, msg global MSGBUFFER @@ -437,34 +441,13 @@ class AMPProtocol(amp.AMP): """ data = loads(data) + server_sessionhandler = self.factory.server.sessions #print "serveradmin (server side):", sessid, operation, data - # late import of django-related stuff. This avoids having to - # load these also for the portal side. - global _ServerConfig, _ScriptDB, _PlayerDB, _ServerSession, _ - if not _ServerConfig: - from src.server.models import ServerConfig as _ServerConfig - if not _ScriptDB: - from src.scripts.models import ScriptDB as _ScriptDB - if not _PlayerDB: - from src.players.models import PlayerDB as _PlayerDB - if not _ServerSession: - from src.server.serversession import ServerSession as _ServerSession - if not _: - from django.utils.translation import ugettext as _ - if operation == PCONN: #portal_session_connect # create a new session and sync it - sess = _ServerSession() - sess.sessionhandler = self.factory.server.sessions - sess.load_sync_data(data) - if sess.logged_in and sess.uid: - # this can happen in the case of auto-authenticating protocols like SSH - sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid) - sess.at_sync() # this runs initialization without acr - - self.factory.server.sessions.portal_connect(sessid, sess) + server_sessionhandler.portal_connect(data) elif operation == PDISCONN: #'portal_session_disconnect' # session closed from portal side @@ -474,23 +457,7 @@ class AMPProtocol(amp.AMP): # force a resync of sessions when portal reconnects to server (e.g. after a server reboot) # the data kwarg contains a dict {sessid: {arg1:val1,...}} representing the attributes # to sync for each session. - sesslist = [] - server_sessionhandler = self.factory.server.sessions - for sessid, sessdict in data.items(): - sess = _ServerSession() - sess.sessionhandler = server_sessionhandler - sess.load_sync_data(sessdict) - if sess.uid: - sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid) - sesslist.append(sess) - # replace sessions on server - server_sessionhandler.portal_session_sync(sesslist) - # after sync is complete we force-validate all scripts (this starts everything) - init_mode = _ServerConfig.objects.conf("server_restart_mode", default=None) - _ScriptDB.objects.validate(init_mode=init_mode) - _ServerConfig.objects.conf("server_restart_mode", delete=True) - # let the server announce the reconnection - server_sessionhandler.announce_all(_(" ... Server restarted.")) + server_sessionhandler.portal_session_sync(data) else: raise Exception("operation %(op)s not recognized." % {'op': operation}) @@ -517,20 +484,20 @@ class AMPProtocol(amp.AMP): operations on the portal. This is executed on the Portal. """ data = loads(data) + portal_sessionhandler = self.factory.portal.sessions #print "portaladmin (portal side):", sessid, ord(operation), data if operation == SLOGIN: # 'server_session_login' # a session has authenticated; sync it. - sess = self.factory.portal.sessions.get_session(sessid) - sess.load_sync_data(data) + portal_sessionhandler.server_logged_in(sessid, data) elif operation == SDISCONN: #'server_session_disconnect' # the server is ordering to disconnect the session - self.factory.portal.sessions.server_disconnect(sessid, reason=data) + portal_sessionhandler.server_disconnect(sessid, reason=data) elif operation == SDISCONNALL: #'server_session_disconnect_all' # server orders all sessions to disconnect - self.factory.portal.sessions.server_disconnect_all(reason=data) + portal_sessionhandler.server_disconnect_all(reason=data) elif operation == SSHUTD: #server_shutdown' # the server orders the portal to shut down @@ -538,19 +505,9 @@ class AMPProtocol(amp.AMP): elif operation == SSYNC: #'server_session_sync' # server wants to save session data to the portal, maybe because - # it's about to shut down. We don't overwrite any sessions, - # just update data on them and remove eventual ones that are - # out of sync (shouldn't happen normally). - portal_sessionhandler = self.factory.portal.sessions - to_save = [sessid for sessid in data if sessid in portal_sessionhandler.sessions] - to_delete = [sessid for sessid in data if sessid not in to_save] - # save protocols - for sessid in to_save: - portal_sessionhandler.sessions[sessid].load_sync_data(data[sessid]) - # disconnect missing protocols - for sessid in to_delete: - portal_sessionhandler.server_disconnect(sessid) - # save a flag in case connection is soon lost. + # it's about to shut down. + portal_sessionhandler.server_session_sync(data) + # set a flag in case we are about to shut down soon self.factory.server_restart_mode = True else: raise Exception("operation %(op)s not recognized." % {'op': operation}) diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 7b469895c3..bc6d8ec40f 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -16,7 +16,11 @@ import time from django.conf import settings from src.commands.cmdhandler import CMD_LOGINSTART -_PLAYERDB = None +# delayed imports +_PlayerDB = None +_ServerSession = None +_ServerConfig = None +_ScriptDB = None # AMP signals PCONN = chr(1) # portal session connect @@ -36,6 +40,20 @@ SERVERNAME = settings.SERVERNAME MULTISESSION_MODE = settings.MULTISESSION_MODE IDLE_TIMEOUT = settings.IDLE_TIMEOUT +def delayed_import(): + "Helper method for delayed import of all needed entities" + global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB + if not _ServerSession: + from src.server.serversession import ServerSession as _ServerSession + if not _PlayerDB: + from src.players.models import PlayerDB as _PlayerDB + if not _ServerConfig: + from src.server.models import ServerConfig as _ServerConfig + if not _ScriptDB: + from src.scripts.models import ScriptDB as _ScriptDB + # including once to avoid warnings in Python syntax checkers + _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB + #----------------------------------------------------------- # SessionHandler base class #------------------------------------------------------------ @@ -99,13 +117,29 @@ class ServerSessionHandler(SessionHandler): self.server = None self.server_data = {"servername":SERVERNAME} - def portal_connect(self, sessid, session): + def portal_connect(self, portalsession): """ Called by Portal when a new session has connected. Creates a new, unlogged-in game session. + + portalsession is a dictionary of all property:value keys + defining the session and which is marked to + be synced. """ - self.sessions[sessid] = session - session.execute_cmd(CMD_LOGINSTART) + delayed_import() + global _ServerSession, _PlayerDB, _ScriptDB + + sess = _ServerSession() + sess.sessionhandler = self + sess.load_sync_data(portalsession) + if sess.logged_in and sess.uid: + # this can happen in the case of auto-authenticating protocols like SSH + sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid) + sess.at_sync() + # validate all script + _ScriptDB.objects.validate() + self.sessions[sess.sessid] = sess + sess.execute_cmd(CMD_LOGINSTART) def portal_disconnect(self, sessid): """ @@ -118,21 +152,37 @@ class ServerSessionHandler(SessionHandler): del self.sessions[session.sessid] self.session_count(-1) - def portal_session_sync(self, sesslist): + def portal_session_sync(self, portalsessions): """ Syncing all session ids of the portal with the ones of the server. This is instantiated by the portal when reconnecting. - sesslist is a complete list of (sessid, session) pairs, matching the list on the portal. - if session was logged in, the amp handler will have logged them in before this point. + portalsessions is a dictionary {sessid: {property:value},...} defining + each session and the properties in it which should be synced. """ + delayed_import() + global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB + for sess in self.sessions.values(): # we delete the old session to make sure to catch eventual lingering references. del sess - for sess in sesslist: - self.sessions[sess.sessid] = sess + + for sessid, sessdict in portalsessions.items(): + sess = _ServerSession() + sess.sessionhandler = self + sess.load_sync_data(sessdict) + if sess.uid: + sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid) + self.sessions[sessid] = sess sess.at_sync() + # after sync is complete we force-validate all scripts (this also starts them) + init_mode = _ServerConfig.objects.conf("server_restart_mode", default=None) + _ScriptDB.objects.validate(init_mode=init_mode) + _ServerConfig.objects.conf("server_restart_mode", delete=True) + # announce the reconnection + self.announce_all(_(" ... server restarted.")) + def portal_shutdown(self): """ Called by server when shutting down the portal. @@ -367,13 +417,35 @@ class PortalSessionHandler(SessionHandler): del session self.sessions = {} + def server_logged_in(self, sessid, data): + "The server tells us that the session has been authenticated. Updated it." + sess = self.get_session(sessid) + sess.load_sync_data(data) + + def server_session_sync(self, serversessions): + """ + Server wants to save data to the portal, maybe because it's about to shut down. + We don't overwrite any sessions here, just update them in-place and remove + any that are out of sync (which should normally not be the case) + + serversessions - dictionary {sessid:{property:value},...} describing the properties + to sync on all sessions + """ + to_save = [sessid for sessid, syncdata in serversessions if sessid in self.sessions] + to_delete = [sessid for sessid, syncdata in serversessions if sessid not in to_save] + # save protocols + for sessid in to_save: + self.sessions[sessid].load_sync_data(serversessions[sessid]) + # disconnect out-of-sync missing protocols + for sessid in to_delete: + self.server_disconnect(sessid) + def count_loggedin(self, include_unloggedin=False): """ Count loggedin connections, alternatively count all connections. """ return len(self.get_sessions(include_unloggedin=include_unloggedin)) - def session_from_suid(self, suid): """ Given a session id, retrieve the session (this is primarily