diff --git a/evennia/objects/models.py b/evennia/objects/models.py index ef5edaaf8f..826ed25200 100644 --- a/evennia/objects/models.py +++ b/evennia/objects/models.py @@ -265,19 +265,26 @@ class ObjectDB(TypedObject): self.save(update_fields=["db_location"]) location = property(__location_get, __location_set, __location_del) - def _db_location_post_save(self): + def at_db_location_postsave(self, new): """ - This is called automatically after the location field was saved, - no matter how. It checks for a variable _safe_contents_update to - know if the save was triggered via the proper handler or not. - - Since we cannot know at this point was old_location was, we - trigger a full-on contents_cache update here. + This is called automatically after the location field was + saved, no matter how. It checks for a variable + _safe_contents_update to know if the save was triggered via + the location handler (which updates the contents cache) or + not. """ if not hasattr(self, "_safe_contents_update"): - logger.log_warn("db_location direct save triggered contents_cache.init() for all objects!") - [o.contents_cache.init() for o in self.__dbclass__.get_all_cached_instances()] + # changed/set outside of the location handler + if new: + # if new, there is no previous location to worry about + if self.db_location: + self.db_location.contents_cache.add(self) + else: + # Since we cannot know at this point was old_location was, we + # trigger a full-on contents_cache update here. + logger.log_warn("db_location direct save triggered contents_cache.init() for all objects!") + [o.contents_cache.init() for o in self.__dbclass__.get_all_cached_instances()] class Meta: "Define Django meta options" diff --git a/evennia/server/oobhandler.py b/evennia/server/oobhandler.py index 763925017e..1a13f228e7 100644 --- a/evennia/server/oobhandler.py +++ b/evennia/server/oobhandler.py @@ -53,18 +53,22 @@ if not _OOB_ERROR: class OOBFieldMonitor(object): """ This object should be stored on the - tracked object as "_oob_at__update". - the update() method will be called by the + tracked object as "_oob_at__postsave". + the update() method w ill be called by the save mechanism, which in turn will call the user-customizable func() """ - def __init__(self): + def __init__(self, obj): """ This initializes the monitor with the object it sits on. + + Args: + obj (Object): object handler is defined on. """ + self.obj = obj self.subscribers = defaultdict(list) - def __call__(self, obj, fieldname): + def __call__(self, fieldname): """ Called by the save() mechanism when the given field has updated. @@ -74,7 +78,7 @@ class OOBFieldMonitor(object): # a potential list of oob commands to call when this # field changes. for (oobfuncname, args, kwargs) in oobtuples: - OOB_HANDLER.execute_cmd(sessid, oobfuncname, fieldname, obj, *args, **kwargs) + OOB_HANDLER.execute_cmd(sessid, oobfuncname, fieldname, self.obj, *args, **kwargs) def add(self, sessid, oobfuncname, *args, **kwargs): """ @@ -156,7 +160,7 @@ class OOBHandler(TickerHandler): fieldmonitorname = self._get_fieldmonitor_name(fieldname) if not hasattr(obj, fieldmonitorname): # assign a new fieldmonitor to the object - _SA(obj, fieldmonitorname, OOBFieldMonitor()) + _SA(obj, fieldmonitorname, OOBFieldMonitor(obj)) # register the session with the monitor _GA(obj, fieldmonitorname).add(sessid, oobfuncname, *args, **kwargs) diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 1d83dcbffb..b150b4a522 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -7,7 +7,7 @@ It is stored on the Server side (as opposed to protocol-specific sessions which are stored on the Portal side) """ -import time +from time import time from datetime import datetime from django.conf import settings from evennia.comms.models import ChannelDB @@ -48,6 +48,7 @@ class ServerSession(Session): self.player = None self.cmdset_storage_string = "" self.cmdset = CmdSetHandler(self, True) + self.cmd_per_second = 0.0 def __cmdset_storage_get(self): return [path.strip() for path in self.cmdset_storage_string.split(',')] @@ -98,7 +99,7 @@ class ServerSession(Session): self.uid = self.player.id self.uname = self.player.username self.logged_in = True - self.conn_time = time.time() + self.conn_time = time() self.puid = None self.puppet = None self.cmdset_storage = settings.CMDSET_SESSION @@ -184,12 +185,11 @@ class ServerSession(Session): and command counters. """ # Store the timestamp of the user's last command. - self.cmd_last = time.time() if not idle: # Increment the user's command counter. self.cmd_total += 1 # Player-visible idle time, not used in idle timeout calcs. - self.cmd_last_visible = time.time() + self.cmd_last_visible = time() def data_in(self, text=None, **kwargs): """ @@ -200,6 +200,10 @@ class ServerSession(Session): oobhandler at this point. """ + now = time() + self.cmd_per_second = 1.0 / (now - self.cmd_last) + self.cmd_last = now + #explicitly check for None since text can be an empty string, which is #also valid if text is not None: diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 4f4949d53d..116402e189 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -12,11 +12,12 @@ There are two similar but separate stores of sessions: """ -import time +from time import time from django.conf import settings from evennia.commands.cmdhandler import CMD_LOGINSTART from evennia.utils.utils import variable_from_module, is_iter, \ to_str, to_unicode, strip_control_sequences, make_iter +from evennia.utils import logger try: import cPickle as pickle @@ -46,11 +47,15 @@ PCONNSYNC = chr(10) # portal post-syncing session # i18n from django.utils.translation import ugettext as _ -SERVERNAME = settings.SERVERNAME +_SERVERNAME = settings.SERVERNAME _MULTISESSION_MODE = settings.MULTISESSION_MODE -IDLE_TIMEOUT = settings.IDLE_TIMEOUT +_IDLE_TIMEOUT = settings.IDLE_TIMEOUT +_MAX_SERVER_COMMANDS_PER_SECOND = 100.0 +_MAX_SESSION_COMMANDS_PER_SECOND = 5.0 +_ERROR_COMMAND_OVERFLOW = "You entered commands too fast. Wait a moment and try again." + def delayed_import(): "Helper method for delayed import of all needed entities" global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB @@ -130,7 +135,9 @@ class ServerSessionHandler(SessionHandler): """ self.sessions = {} self.server = None - self.server_data = {"servername": SERVERNAME} + self.server_data = {"servername": _SERVERNAME} + self.cmd_last = time() + self.cmd_per_second = 0.0 def portal_connect(self, portalsession): """ @@ -359,11 +366,11 @@ class ServerSessionHandler(SessionHandler): Check all currently connected sessions (logged in and not) and see if any are dead or idle """ - tcurr = time.time() + tcurr = time() reason = _("Idle timeout exceeded, disconnecting.") for session in (session for session in self.sessions.values() - if session.logged_in and IDLE_TIMEOUT > 0 - and (tcurr - session.cmd_last) > IDLE_TIMEOUT): + if session.logged_in and _IDLE_TIMEOUT > 0 + and (tcurr - session.cmd_last) > _IDLE_TIMEOUT): self.disconnect(session, reason=reason) def player_count(self, count=True): @@ -493,6 +500,17 @@ class ServerSessionHandler(SessionHandler): """ session = self.sessions.get(sessid, None) if session: + + now = time() + self.cmd_per_second = 1.0 / (now - self.cmd_last) + self.cmd_last = now + + if self.cmd_per_second > _MAX_SERVER_COMMANDS_PER_SECOND: + if session.cmd_per_second > _MAX_SESSION_COMMANDS_PER_SECOND: + session.data.out(text=_ERROR_COMMAND_OVERFLOW) + logger.log_infomsg("overflow kicked in for session %s: %s" % (session.sessid, text)) + return + text = text and to_unicode(strip_control_sequences(text), encoding=session.encoding) if "oob" in kwargs: # incoming data is always on the form (cmdname, args, kwargs) diff --git a/evennia/utils/idmapper/models.py b/evennia/utils/idmapper/models.py index 3c067e62f9..128fac3886 100644 --- a/evennia/utils/idmapper/models.py +++ b/evennia/utils/idmapper/models.py @@ -329,7 +329,17 @@ class SharedMemoryModel(Model): super(SharedMemoryModel, self).delete(*args, **kwargs) def save(self, *args, **kwargs): - "save method tracking process/thread issues" + """ + Central database save operation. + + Arguments as per django documentation + + Calls: + self.at__postsave(new) + # this is a wrapper set by oobhandler: + self._oob_at__postsave() + + """ if _IS_SUBPROCESS: # we keep a store of objects modified in subprocesses so @@ -348,24 +358,26 @@ class SharedMemoryModel(Model): callFromThread(_save_callback, self, *args, **kwargs) # update field-update hooks and eventual OOB watchers + new = False if "update_fields" in kwargs and kwargs["update_fields"]: # get field objects from their names update_fields = (self._meta.get_field_by_name(field)[0] for field in kwargs.get("update_fields")) else: # meta.fields are already field objects; get them all + new =True update_fields = self._meta.fields for field in update_fields: fieldname = field.name # if a hook is defined it must be named exactly on this form - hookname = "_at_%s_postsave" % fieldname + hookname = "at_%s_postsave" % fieldname if hasattr(self, hookname) and callable(_GA(self, hookname)): - _GA(self, hookname)() + _GA(self, hookname)(new) # if a trackerhandler is set on this object, update it with the # fieldname and the new value fieldtracker = "_oob_at_%s_postsave" % fieldname if hasattr(self, fieldtracker): - _GA(self, fieldtracker)(self, fieldname) + _GA(self, fieldtracker)(fieldname) class WeakSharedMemoryModelBase(SharedMemoryModelBase):