From: Jeremy Stanley Date: Fri, 30 Sep 2005 19:44:23 +0000 (+0000) Subject: Imported from archive. X-Git-Tag: 0.0.1~329 X-Git-Url: https://mudpy.org/gitweb?p=mudpy.git;a=commitdiff_plain;h=b1176dd6b9ee7b3ba4831e959a18e81a6729ba54 Imported from archive. * 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. --- diff --git a/command b/command index 6d4509a..a789137 100644 --- 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 diff --git a/mudpy.conf b/mudpy.conf index 280368a..b70aecf 100644 --- a/mudpy.conf +++ b/mudpy.conf @@ -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 diff --git a/mudpy.py b/mudpy.py index 7e23d23..8519d05 100644 --- 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):