From 5583a8d7582bb38979c0c6684b8543e410685299 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 7 Apr 2016 22:57:55 +0200 Subject: [PATCH] Made channels log to a log file by default. Added a default /history switch to the channel commands (for viewing past entries) and implemented a very efficient tail_log_file function to scan the log and display it. --- evennia/comms/channelhandler.py | 32 ++++++++++-- evennia/comms/comms.py | 55 ++++++++++---------- evennia/utils/logger.py | 91 +++++++++++++++++++++++++-------- 3 files changed, 124 insertions(+), 54 deletions(-) diff --git a/evennia/comms/channelhandler.py b/evennia/comms/channelhandler.py index 621474cf1..04271399b 100644 --- a/evennia/comms/channelhandler.py +++ b/evennia/comms/channelhandler.py @@ -27,6 +27,7 @@ from builtins import object from evennia.comms.models import ChannelDB from evennia.commands import cmdset, command +from evennia.utils.logger import tail_log_file from django.utils.translation import ugettext as _ @@ -34,11 +35,21 @@ class ChannelCommand(command.Command): """ {channelkey} channel + {channeldesc} + Usage: {lower_channelkey} - {lower_channelkey} history [-] + {lower_channelkey}/history [start] + + Switch: + history: View the 20 last messages, optionally + beginning messages from the end. + + Example: + {lower_channelkey} Hello World! + {lower_channelkey}/history + {lower_channelkey}/history 30 - {channeldesc} """ # this flag is what identifies this cmd as a channel cmd # and branches off to the system send-to-channel command @@ -54,6 +65,13 @@ class ChannelCommand(command.Command): """ # cmdhandler sends channame:msg here. channelname, msg = self.args.split(":", 1) + self.history_start = None + if msg.startswith("/history"): + arg = msg[8:] + try: + self.history_start = int(arg) if arg else 0 + except ValueError: + pass self.args = (channelname.strip(), msg.strip()) def func(self): @@ -79,7 +97,13 @@ class ChannelCommand(command.Command): string = _("You are not permitted to send to channel '%s'.") self.msg(string % channelkey) return - channel.msg(msg, senders=self.caller, online=True) + if self.history_start is not None: + # Try to view history + log_file = channel.attributes.get("log_file", default="channel_%s.log" % channel.key) + self.msg("".join(line.split("[-]", 1)[1] if "[-]" in line else line + for line in tail_log_file(log_file, self.history_start, 20))) + else: + channel.msg(msg, senders=self.caller, online=True) def get_extra_info(self, caller, **kwargs): """ @@ -175,7 +199,7 @@ class ChannelHandler(object): locks="cmd:all();%s" % channel.locks, help_category="Channel names", obj=channel, - arg_regex=r"\s.*?", + arg_regex=r"\s.*?|/history.*?", is_channel=True) # format the help entry key = channel.key diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index 08b1df99c..001a662d9 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -27,7 +27,7 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)): """ self.at_channel_creation() - + self.attributes.add("log_file", "channel_%s.log" % self.key) if hasattr(self, "_createdict"): # this is only set if the channel was created # with the utils.create.create_channel function. @@ -37,8 +37,6 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)): self.db_key = "#i" % self.dbid elif cdict["key"] and self.key != cdict["key"]: self.key = cdict["key"] - if cdict.get("keep_log"): - self.db_keep_log = cdict["keep_log"] if cdict.get("aliases"): self.aliases.add(cdict["aliases"]) if cdict.get("locks"): @@ -183,14 +181,18 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)): msg.message = body return msg - def distribute_message(self, msg, online=False): + def distribute_message(self, msgobj, online=False): """ Method for grabbing all listeners that a message should be sent to on this channel, and sending them a message. - msg (str): Message to distribute. - online (bool): Only send to receivers who are actually online - (not currently used): + Args: + msgobj (Msg or TempMsg): Message to distribute. + online (bool): Only send to receivers who are actually online + (not currently used): + + Notes: + This is also where logging happens, if enabled. """ # get all players connected to this channel and send to them @@ -198,12 +200,16 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)): try: # note our addition of the from_channel keyword here. This could be checked # by a custom player.msg() to treat channel-receives differently. - entity.msg(msg.message, from_obj=msg.senders, options={"from_channel":self.id}) + entity.msg(msgobj.message, from_obj=msgobj.senders, options={"from_channel":self.id}) except AttributeError as e: logger.log_trace("%s\nCannot send msg to '%s'." % (e, entity)) + if msgobj.keep_log: + # log to file + logger.log_file(msgobj.message, self.attributes.get("log_file") or "channel_%s.log" % self.key) + def msg(self, msgobj, header=None, senders=None, sender_strings=None, - persistent=False, online=False, emit=False, external=False): + keep_log=None, online=False, emit=False, external=False): """ Send the given message to all players connected to channel. Note that no permission-checking is done here; it is assumed to have been @@ -222,9 +228,11 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)): sender_strings (list, optional): Name strings of senders. Used for external connections where the sender is not a player or object. When this is defined, external will be assumed. - persistent (bool, optional): Ignored if msgobj is a Msg or TempMsg. - If True, a Msg will be created, using header and senders - keywords. If False, other keywords will be ignored. + keep_log (bool or None, optional): This allows to temporarily change the logging status of + this channel message. If `None`, the Channel's `keep_log` Attribute will + be used. If `True` or `False`, that logging status will be used for this + message only (note that for unlogged channels, a `True` value here will + create a new log file only for this message). online (bool, optional) - If this is set true, only messages people who are online. Otherwise, messages all players connected. This can make things faster, but may not trigger listeners on players @@ -239,25 +247,14 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)): successful, `False` otherwise. """ - if senders: - senders = make_iter(senders) - else: - senders = [] + senders = make_iter(senders) if senders else [] if isinstance(msgobj, basestring): - # given msgobj is a string - msg = msgobj - if persistent and self.db.keep_log: - msgobj = Msg() - msgobj.save() - else: - # Use TempMsg, so this message is not stored. - msgobj = TempMsg() - msgobj.header = header - msgobj.message = msg - msgobj.channels = [self] # add this channel + # given msgobj is a string - convert to msgobject (always TempMsg) + msgobj = TempMsg(senders=senders, header=header, message=msgobj, channels=[self]) + # we store the logging setting for use in distribute_message() + msgobj.keep_log = keep_log if keep_log is not None else self.db.keep_log - if not msgobj.senders: - msgobj.senders = senders + # start the sending msgobj = self.pre_send_message(msgobj) if not msgobj: return False diff --git a/evennia/utils/logger.py b/evennia/utils/logger.py index 66545d32b..9f4cb232e 100644 --- a/evennia/utils/logger.py +++ b/evennia/utils/logger.py @@ -152,7 +152,32 @@ log_depmsg = log_dep # Arbitrary file logger -LOG_FILE_HANDLES = {} # holds open log handles +_LOG_FILE_HANDLES = {} # holds open log handles + +def _open_log_file(filename): + """ + Helper to open the log file (always in the log dir) and cache its + handle. Will create a new file in the log dir if one didn't + exist. + """ + global _LOG_FILE_HANDLES, _LOGDIR + if not _LOGDIR: + from django.conf import settings + _LOGDIR = settings.LOG_DIR + + filename = os.path.join(_LOGDIR, filename) + if filename in _LOG_FILE_HANDLES: + # cache the handle + return _LOG_FILE_HANDLES[filename] + else: + try: + filehandle = open(filename, "a+") # append mode + reading + _LOG_FILE_HANDLES[filename] = filehandle + return filehandle + except IOError: + log_trace() + return None + def log_file(msg, filename="game.log"): """ @@ -164,17 +189,9 @@ def log_file(msg, filename="game.log"): on new lines following datetime info. """ - global LOG_FILE_HANDLES, _LOGDIR, _TIMEZONE - - if not _LOGDIR: - from django.conf import settings - _LOGDIR = settings.LOG_DIR - if not _TIMEZONE: - from django.utils import timezone as _TIMEZONE - def callback(filehandle, msg): "Writing to file and flushing result" - msg = "\n%s [-] %s" % (_TIMEZONE.now(), msg.strip()) + msg = "\n%s [-] %s" % (timeformat(), msg.strip()) filehandle.write(msg) # since we don't close the handle, we need to flush # manually or log file won't be written to until the @@ -186,15 +203,47 @@ def log_file(msg, filename="game.log"): log_trace() # save to server/logs/ directory - filename = os.path.join(_LOGDIR, filename) + filehandle = _open_log_file(filename) + if filehandle: + deferToThread(callback, filehandle, msg).addErrback(errback) + + +def tail_log_file(filename, offset, nlines): + """ + Return the tail of the log file. + + Args: + filename (str): The name of the log file, presumed to be in + the Evennia log dir. + offset (int): The line offset *from the end of the file* to start + reading from. 0 means to start at the latest entry. + nlines (int): How many lines to return, counting backwards + from the offset. If file is shorter, will get all lines. + + Returns: + lines (list): The nline entries from the end of the file, or + all if the file is shorter than nlines. + + """ + lines_found = [] + filehandle = _open_log_file(filename) + if filehandle: + # step backwards in chunks and stop only when we have enough lines + buffer_size = 4098 + block_count = -1 + while len(lines_found) < (offset + nlines): + try: + # scan backwards in file, starting from the end + filehandle.seek(block_count * buffer_size, os.SEEK_END) + except IOError: + # file too small for this seek, take what we've got + filehandle.seek(0) + lines_found = filehandle.readlines() + break + lines_found = filehandle.readlines() + block_count -= 1 + # return the right number of lines + return lines_found[-nlines-offset:-offset if offset else None] + + - if filename in LOG_FILE_HANDLES: - filehandle = LOG_FILE_HANDLES[filename] - else: - try: - filehandle = open(filename, "a") - LOG_FILE_HANDLES[filename] = filehandle - except IOError: - log_trace() - return - deferToThread(callback, filehandle, msg).addErrback(errback)