Imported from archive.
authorJeremy Stanley <fungi@yuggoth.org>
Fri, 30 Sep 2005 19:44:23 +0000 (19:44 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Fri, 30 Sep 2005 19:44:23 +0000 (19:44 +0000)
* command (command:show), mudpy.py (command_show): Added a log
parameter to the admin command show, allowing administrative users
to review recent log entries.

* mudpy.conf (internal:logging), mudpy.py (log): Added an in-memory
ring buffer for logging, configurable via the max_log_lines facet.

* mudpy.py (excepthook): Attempt to log any Python interpreter
exceptions.
(log): Implemented a log priority mechanism to allow filtering logs
by importance. Log entries above an administrative account's
loglevel int facet in will be echoed to their owner's socket if
connected.

command
mudpy.conf
mudpy.py

diff --git a/command b/command
index 6d4509a..a789137 100644 (file)
--- a/command
+++ b/command
@@ -66,5 +66,5 @@ help = Invoke it like this:$(eol)$(eol)   set actor:dominique description You se
 action = command_show(user, parameters)
 administrative = yes
 description = Show element data.
-help = Here are the possible incantations:$(eol)$(eol)   show categories$(eol)   show category actor$(eol)   show element location:1:2:3:4$(eol)   show files$(eol)   show result user.avatar.get("name")$(eol)   show time
+help = Here are the possible incantations:$(eol)$(eol)   show categories$(eol)   show category actor$(eol)   show element location:1:2:3:4$(eol)   show files$(eol)   show log 20$(eol)   show result user.avatar.get("name")$(eol)   show time
 
index 280368a..b70aecf 100644 (file)
@@ -19,7 +19,8 @@ max_avatars = 7
 password_tries = 3
 
 [internal:logging]
-#file = mudpy.log
+file = mudpy.log
+max_log_lines = 1000
 stdout = yes
 #syslog = mudpy
 
index 7e23d23..8519d05 100644 (file)
--- a/mudpy.py
+++ b/mudpy.py
@@ -12,9 +12,29 @@ from random import choice, randrange
 from re import match
 from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
 from stat import S_IMODE, ST_MODE
+from sys import stderr
 from syslog import LOG_PID, LOG_INFO, LOG_DAEMON, closelog, openlog, syslog
 from telnetlib import DO, DONT, ECHO, EOR, GA, IAC, LINEMODE, SB, SE, SGA, WILL, WONT
 from time import asctime, sleep
+from traceback import format_exception
+
+def excepthook(excepttype, value, traceback):
+       """Handle uncaught exceptions."""
+
+       # assemble the list of errors into a single string
+       message = "".join(format_exception(excepttype, value, traceback))
+
+       # try to log it, if possible
+       try: log(message, 9)
+       except: pass
+
+       # try to write it to stderr, if possible
+       try: stderr.write(message)
+       except: pass
+
+# redefine sys.excepthook with ours
+import sys
+sys.excepthook = excepthook
 
 class Element:
        """An element of the universe."""
@@ -41,7 +61,7 @@ class Element:
                        universe.files[self.origin].data.add_section(self.key)
        def destroy(self):
                """Remove an element from the universe and destroy it."""
-               log("Destroying: " + self.key + ".")
+               log("Destroying: " + self.key + ".", 2)
                universe.files[self.origin].data.remove_section(self.key)
                del universe.categories[self.category][self.subkey]
                del universe.contents[self.key]
@@ -52,7 +72,9 @@ class Element:
                        universe.files[self.origin].data.remove_option(self.key, facet)
        def facets(self):
                """Return a list of non-inherited facets for this element."""
-               return universe.files[self.origin].data.options(self.key)
+               if self.key in universe.files[self.origin].data.sections():
+                       return universe.files[self.origin].data.options(self.key)
+               else: return []
        def has_facet(self, facet):
                """Return whether the non-inherited facet exists."""
                return facet in self.facets()
@@ -270,6 +292,7 @@ class Universe:
                self.default_origins = {}
                self.files = {}
                self.private_files = []
+               self.loglist = []
                self.userlist = []
                self.terminate_world = False
                self.reload_modules = False
@@ -348,7 +371,7 @@ class User:
                if name: message = "User " + name
                else: message = "An unnamed user"
                message += " logged out."
-               log(message)
+               log(message, 2)
                self.deactivate_avatar()
                self.connection.close()
                self.remove()
@@ -403,7 +426,7 @@ class User:
                        if old_user.account.get("name") == self.account.get("name") and old_user is not self:
 
                                # make a note of it
-                               log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".")
+                               log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".", 2)
                                old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)", flush=True, add_prompt=False)
 
                                # close the old connection
@@ -428,7 +451,7 @@ class User:
        def authenticate(self):
                """Flag the user as authenticated and disconnect duplicates."""
                if not self.state is "authenticated":
-                       log("User " + self.account.get("name") + " logged in.")
+                       log("User " + self.account.get("name") + " logged in.", 2)
                        self.authenticated = True
                        if self.account.subkey in universe.categories["internal"]["limits"].getlist("default_admins"):
                                self.account.set("administrator", "True")
@@ -580,7 +603,7 @@ class User:
                                        if self.account and self.account.get("name"): logline += self.account.get("name") + ": "
                                        else: logline += "unknown user: "
                                        logline += repr(removed)
-                                       log(logline)
+                                       log(logline, 4)
 
                                # filter out non-printables
                                line = filter(lambda x: " " <= x <= "~", line)
@@ -734,30 +757,49 @@ def broadcast(message, add_prompt=True):
        """Send a message to all connected users."""
        for each_user in universe.userlist: each_user.send("$(eol)" + message, add_prompt=add_prompt)
 
-def log(message):
+def log(message, level=0):
        """Log a message."""
 
-       # the time in posix log timestamp format
+       # a couple references we need
+       file_name = universe.categories["internal"]["logging"].get("file")
+       max_log_lines = universe.categories["internal"]["logging"].getint("max_log_lines")
+       syslog_name = universe.categories["internal"]["logging"].get("syslog")
        timestamp = asctime()[4:19]
 
-       file_name = universe.categories["internal"]["logging"].get("file")
+       # turn the message into a list of lines
+       lines = filter(lambda x: x!="", [(x.rstrip()) for x in message.split("\n")])
+
+       # send the timestamp and line to a file
        if file_name:
                file_descriptor = file(file_name, "a")
-               file_descriptor.write(timestamp + " " + message + "\n")
+               for line in lines: file_descriptor.write(timestamp + " " + line + "\n")
                file_descriptor.flush()
                file_descriptor.close()
 
-       # send the timestamp and message to standard output
+       # send the timestamp and line to standard output
        if universe.categories["internal"]["logging"].getboolean("stdout"):
-               print(timestamp + " " + message)
+               for line in lines: print(timestamp + " " + line)
 
-       # send the message to the system log
-       syslog_name = universe.categories["internal"]["logging"].get("syslog")
+       # send the line to the system log
        if syslog_name:
                openlog(syslog_name, LOG_PID, LOG_INFO | LOG_DAEMON)
-               syslog(message)
+               for line in lines: syslog(line)
                closelog()
 
+       # display to connected administrators
+       for user in universe.userlist:
+               if user.state == "active" and user.account.getboolean("administrator") and user.account.getint("loglevel") <= level:
+                       # iterate over every line in the message
+                       full_message = ""
+                       for line in lines:
+                               full_message += "$(bld)$(red)" + timestamp + " " + line + "$(nrm)$(eol)"
+                       user.send(full_message, flush=True)
+
+       # add to the recent log list
+       for line in lines:
+               while 0 < len(universe.loglist) >= max_log_lines: del universe.loglist[0]
+               universe.loglist.append(timestamp + " " + line)
+
 def wrap_ansi_text(text, width):
        """Wrap text with arbitrary width while ignoring ANSI colors."""
 
@@ -917,7 +959,7 @@ def replace_macros(user, text, is_input=False):
                else:
                        text = text.replace(macro, "")
                        if not is_input:
-                               log("Unexpected replacement macro " + macro + " encountered.")
+                               log("Unexpected replacement macro " + macro + " encountered.", 6)
 
        # replace the look-like-a-macro sequence
        text = text.replace("$_(", "$(")
@@ -978,7 +1020,7 @@ def check_for_connection(listening_socket):
                return None
 
        # note that we got one
-       log("Connection from " + address[0])
+       log("Connection from " + address[0], 2)
 
        # disable blocking so we can proceed whether or not we can send/receive
        connection.setblocking(0)
@@ -1217,7 +1259,7 @@ def handler_entering_account_name(user):
                else:
                        user.account = Element("account:" + name, universe)
                        user.account.set("name", name)
-                       log("New user: " + name)
+                       log("New user: " + name, 2)
                        user.state = "checking_new_account_name"
 
        # if the user entered nothing for a name, then buhbye
@@ -1341,7 +1383,7 @@ def command_halt(user, parameters):
 
        # let everyone know
        broadcast(message, add_prompt=False)
-       log(message)
+       log(message, 8)
 
        # set a flag to terminate the world
        universe.terminate_world = True
@@ -1351,7 +1393,7 @@ def command_reload(user):
 
        # let the user know and log
        user.send("Reloading all code modules, configs and data.")
-       log("User " + user.account.get("name") + " reloaded the world.")
+       log("User " + user.account.get("name") + " reloaded the world.", 8)
 
        # set a flag to reload
        universe.reload_modules = True
@@ -1512,6 +1554,15 @@ def command_show(user, parameters):
                                        message = repr(eval(" ".join(arguments[1:])))
                                except:
                                        message = "Your expression raised an exception!"
+               elif arguments[0] == "log":
+                       if match("^\d+$", arguments[1]) and int(arguments[1]) > 0:
+                               linecount = int(arguments[1])
+                               if linecount > len(universe.loglist): linecount = len(universe.loglist)
+                               message = "There are " + str(len(universe.loglist)) + " log lines in memory."
+                               message += " The most recent " + str(linecount) + " lines are:$(eol)$(eol)"
+                               for line in universe.loglist[-linecount:]:
+                                       message += "   " + line + "$(eol)"
+                       else: message = "\"" + arguments[1] + "\" is not a positive integer greater than 0."
        if not message:
                if parameters: message = "I don't know what \"" + parameters + "\" is."
                else: message = "What do you want to show?"
@@ -1534,7 +1585,7 @@ def command_create(user, parameters):
                                        if filename not in universe.files:
                                                message += " Warning: \"" + filename + "\" is not yet included in any other file and will not be read on startup unless this is remedied."
                                Element(element, universe, filename)
-                               log(logline)
+                               log(logline, 6)
                elif len(arguments) > 2: message = "You can only specify an element and a filename."
        user.send(message)
 
@@ -1546,7 +1597,7 @@ def command_destroy(user, parameters):
                else:
                        universe.contents[parameters].destroy()
                        message = "You destroy \"" + parameters + "\" within the universe."
-                       log(user.account.get("name") + " destroyed an element: " + parameters)
+                       log(user.account.get("name") + " destroyed an element: " + parameters, 6)
        user.send(message)
 
 def command_set(user, parameters):