From 138ac7e001abf8940f3cb2a419ef3dd3556d0086 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Mon, 30 Jan 2006 02:44:48 +0000 Subject: [PATCH] Imported from archive. * command (command:reload), mudpy.py (DataFile.load, DataFile.save): More work on data file reloading * mudpy, mudpy.py (Universe.new): Moved more code out of the mudpy executable into the mudpy.py Python module. (Universe.__init__, command_halt, command_reload): Renamed the reload_modules and terminate_world bools to reload_flag and terminate_flag respectively. * mudpy.conf (internal:limits), mudpy.py (DataFile.save): Implemented a data file rotation/backup scheme, configurable through the default_backup_count int facet. * mudpy.conf (internal:network): Example recommended config binds only to 127.0.0.1 initially, for added security. * mudpy.py (daemonize): More work on daemonization. --- command | 6 +- mudpy | 12 +--- mudpy.conf | 9 +-- mudpy.py | 231 ++++++++++++++++++++++++++++++++++++++----------------------- 4 files changed, 157 insertions(+), 101 deletions(-) diff --git a/command b/command index c983ed3..3e7a42a 100644 --- a/command +++ b/command @@ -48,8 +48,8 @@ help = This will save your account and disconnect your client connection. [command:reload] action = command_reload(actor) administrative = yes -description = Reload code modules and data. -help = This will reload all python code modules, reload configuration files and re-read data files. +description = Reload modules and data. +help = This will reload all python modules and read-only data files. [command:say] action = command_say(actor, parameters) @@ -66,5 +66,5 @@ help = Invoke it like this:$(eol)$(eol) set actor:dominique description You se action = command_show(actor, parameters) administrative = yes description = Show various data. -help = Here are the possible incantations ( is required, [parameter] is optional, (note) is a note):$(eol)$(eol) show categories (list all element category names)$(eol) show category (list the elements in a category)$(eol) show element (list facet definitions for an element)$(eol) show files (list all element data files)$(eol) show file (list elements in a file)$(eol) show log [level [start [stop]]] (list logs above level from start to stop)$(eol) show result (evaluates a python expression)$(eol) show time (returns the current universal elapsed time counter) +help = Here are the possible incantations ( is required, [parameter] is optional, (note) is a note):$(eol)$(eol) show categories (list all element category names)$(eol) show category (list the elements in a category)$(eol) show element (list facet definitions for an element)$(eol) show file (list elements in a file)$(eol) show files (list all element data files)$(eol) show log [level [start [stop]]] (list logs above level from start to stop)$(eol) show result (evaluates a python expression)$(eol) show time (returns several current timer values) diff --git a/mudpy b/mudpy index 431c9a0..7a5d5b2 100755 --- a/mudpy +++ b/mudpy @@ -9,6 +9,7 @@ import mudpy # a consistent list so we can reimport these on reload importlist = [ + "argv", "create_pidfile", "daemonize", "log", @@ -20,7 +21,6 @@ importlist = [ for item in importlist: exec("from mudpy import " + item) # log an initial message -from sys import argv log("Started mudpy with command line: " + " ".join(argv)) # fork and disassociate @@ -31,21 +31,15 @@ create_pidfile(universe) # loop indefinitely while the world is not flagged for termination or # there are connected users -while not universe.terminate_world or universe.userlist: +while not universe.terminate_flag or universe.userlist: # the world was flagged for a reload of all code/data - if universe.reload_modules: + if universe.reload_flag: # reload the mudpy module reload(mudpy) for item in importlist: exec("from mudpy import " + item) - # move data into new persistent objects - reload_data() - - # reset the reload flag - universe.reload_modules = False - # do what needs to be done on each pulse on_pulse() diff --git a/mudpy.conf b/mudpy.conf index 3e85357..0ef7482 100644 --- a/mudpy.conf +++ b/mudpy.conf @@ -15,22 +15,23 @@ punctuation_say = . [internal:limits] #default_admins = admin +#default_backup_count = 10 max_avatars = 7 password_tries = 3 [internal:logging] -file = mudpy.log +#file = mudpy.log max_log_lines = 1000 -stdout = yes +#stdout = yes #syslog = mudpy [internal:network] -host = +host = 127.0.0.1 port = 6669 [internal:process] #daemon = yes -pidfile = mudpy.pid +#pidfile = mudpy.pid [internal:time] definition_d = 24h diff --git a/mudpy.py b/mudpy.py index 8b91054..5a1b4f7 100644 --- a/mudpy.py +++ b/mudpy.py @@ -6,63 +6,19 @@ # import some things we need from ConfigParser import RawConfigParser from md5 import new as new_md5 -from os import _exit, R_OK, W_OK, access, chmod, close, fork, getpid, makedirs, remove, setsid, stat -from os.path import abspath, dirname, exists, isabs, join as path_join +from os import _exit, R_OK, W_OK, access, chdir, chmod, close, fork, getcwd, getpid, listdir, makedirs, remove, rename, setsid, stat, umask +from os.path import abspath, basename, dirname, exists, isabs, join as path_join from random import choice, randrange from re import match from signal import SIGHUP, SIGTERM, signal from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket from stat import S_IMODE, ST_MODE -from sys import stderr +from sys import argv, 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 - -def sighook(what, where): - """Handle external signals.""" - - # a generic message - message = "Caught signal: " - - # for a hangup signal - if what == SIGHUP: - message += "hangup (reloading)" - universe.reload_modules = True - - # for a terminate signal - elif what == SIGTERM: - message += "terminate (halting)" - universe.terminate_world = True - - # catchall for unexpected signals - else: message += str(what) + " (unhandled)" - - # log what happened - log(message, 8) - -# assign the sgnal handlers -signal(SIGHUP, sighook) -signal(SIGTERM, sighook) - class Element: """An element of the universe.""" def __init__(self, key, universe, filename=None): @@ -227,9 +183,9 @@ class Element: # events are elements themselves event = Element("event:" + self.key + ":" + counter) - def send(self, message, eol="$(eol)"): + def send(self, message, eol="$(eol)", raw=False, flush=False, add_prompt=True, just_prompt=False): """Convenience method to pass messages to an owner.""" - if self.owner: self.owner.send(message, eol) + if self.owner: self.owner.send(message, eol, raw, flush, add_prompt, just_prompt) def can_run(self, command): """Check if the user can run this command object.""" @@ -274,7 +230,7 @@ class Element: def move_direction(self, direction): """Relocate the element in a specified direction.""" self.echo_to_location(self.get("name") + " exits " + self.universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".") - self.send("You exit " + self.universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".") + self.send("You exit " + self.universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".", add_prompt=False) self.go_to(self.universe.contents[self.get("location")].link_neighbor(direction)) self.echo_to_location(self.get("name") + " arrives from " + self.universe.categories["internal"]["directions"].getdict(direction)["enter"] + ".") def look_at(self, key): @@ -342,7 +298,9 @@ class DataFile: if self.data.has_option("__control__", "default_files"): origins = makedict(self.data.get("__control__", "default_files")) for key in origins.keys(): - if not key in includes: includes.append(key) + if not isabs(origins[key]): + origins[key] = path_join(dirname(self.filename), origins[key]) + if not origins[key] in includes: includes.append(origins[key]) self.universe.default_origins[key] = origins[key] if not key in self.universe.categories: self.universe.categories[key] = {} @@ -371,6 +329,25 @@ class DataFile: if not exists(dirname(self.filename)): makedirs(dirname(self.filename)) + # backup the file + if self.data.has_option("__control__", "backup_count"): + max_count = self.data.has_option("__control__", "backup_count") + else: max_count = universe.categories["internal"]["limits"].getint("default_backup_count") + if exists(self.filename) and max_count: + backups = [] + for candidate in listdir(dirname(self.filename)): + if match(basename(self.filename) + """\.\d+$""", candidate): + backups.append(int(candidate.split(".")[-1])) + backups.sort() + backups.reverse() + for old_backup in backups: + if old_backup >= max_count-1: + remove(self.filename+"."+str(old_backup)) + elif not exists(self.filename+"."+str(old_backup+1)): + rename(self.filename+"."+str(old_backup), self.filename+"."+str(old_backup+1)) + if not exists(self.filename+".0"): + rename(self.filename, self.filename+".0") + # our data file file_descriptor = file(self.filename, "w") @@ -402,18 +379,19 @@ class DataFile: class Universe: """The universe.""" - def __init__(self, filename=""): + def __init__(self, filename="", load=False): """Initialize the universe.""" self.categories = {} self.contents = {} self.default_origins = {} - self.private_files = [] self.loglines = [] self.pending_events_long = {} self.pending_events_short = {} + self.private_files = [] + self.reload_flag = False + self.startdir = getcwd() + self.terminate_flag = False self.userlist = [] - self.terminate_world = False - self.reload_modules = False if not filename: possible_filenames = [ ".mudpyrc", @@ -429,9 +407,9 @@ class Universe: for filename in possible_filenames: if access(filename, R_OK): break if not isabs(filename): - filename = abspath(filename) + filename = path_join(self.startdir, filename) self.filename = filename - self.load() + if load: self.load() def load(self): """Load universe data from persistent storage.""" @@ -470,6 +448,14 @@ class Universe: element.update_location() element.clean_contents() + def new(self): + new_universe = Universe() + for attribute in vars(self).keys(): + exec("new_universe." + attribute + " = self." + attribute) + new_universe.reload_flag = False + del self + return new_universe + def save(self): """Save the universe to persistent storage.""" for key in self.files: self.files[key].save() @@ -636,6 +622,10 @@ class User: # strip extra $(eol) off if present while output.startswith("$(eol)"): output = output[6:] while output.endswith("$(eol)"): output = output[:-6] + extra_lines = output.find("$(eol)$(eol)$(eol)") + while extra_lines > -1: + output = output[:extra_lines] + output[extra_lines+6:] + extra_lines = output.find("$(eol)$(eol)$(eol)") # we'll take out GA or EOR and add them back on the end if output.endswith(IAC+GA) or output.endswith(IAC+EOR): @@ -646,7 +636,11 @@ class User: # start with a newline, append the message, then end # with the optional eol string passed to this function # and the ansi escape to return to normal text - if not just_prompt: output = "$(eol)$(eol)" + output + if not just_prompt: + if not self.output_queue or not self.output_queue[-1].endswith("\r\n"): + output = "$(eol)$(eol)" + output + elif not self.output_queue[-1].endswith("\r\n"+chr(27)+"[0m"+"\r\n") and not self.output_queue[-1].endswith("\r\n\r\n"): + output = "$(eol)" + output output += eol + chr(27) + "[0m" # tack on a prompt if active @@ -673,7 +667,7 @@ class User: """All the things to do to the user per increment.""" # if the world is terminating, disconnect - if universe.terminate_world: + if universe.terminate_flag: self.state = "disconnecting" self.menu_seen = False @@ -695,7 +689,8 @@ class User: self.enqueue_input() # there is input waiting in the queue - if self.input_queue: handle_user_input(self) + if self.input_queue: + handle_user_input(self) def flush(self): """Try to send the last item in the queue and remove it.""" @@ -877,22 +872,6 @@ class User: """List names of assigned avatars.""" return [ universe.contents[avatar].get("name") for avatar in self.account.getlist("avatars") ] -def create_pidfile(universe): - """Write a file containing the current process ID.""" - pid = str(getpid()) - log("Process ID: " + pid) - file_name = universe.contents["internal:process"].get("pidfile") - if file_name: - file_descriptor = file(file_name, 'w') - file_descriptor.write(pid + "\n") - file_descriptor.flush() - file_descriptor.close() - -def remove_pidfile(universe): - """Remove the file containing the current process ID.""" - file_name = universe.contents["internal:process"].get("pidfile") - if file_name and access(file_name, W_OK): remove(file_name) - def makelist(value): """Turn string into list type.""" if value[0] + value[-1] == "[]": return eval(value) @@ -913,6 +892,8 @@ def log(message, level=0): # a couple references we need file_name = universe.categories["internal"]["logging"].get("file") + if not isabs(file_name): + file_name = path_join(universe.startdir, file_name) max_log_lines = universe.categories["internal"]["logging"].getint("max_log_lines") syslog_name = universe.categories["internal"]["logging"].get("syslog") timestamp = asctime()[4:19] @@ -1186,12 +1167,16 @@ def on_pulse(): for user in universe.userlist: user.pulse() # update the log every now and then - if check_time("frequency_log"): - log(str(len(universe.userlist)) + " connection(s)") + if not universe.categories["internal"]["counters"].getint("mark"): + universe.save() + universe.categories["internal"]["counters"].set("mark", universe.categories["internal"]["time"].getint("frequency_log")) + else: universe.categories["internal"]["counters"].set("mark", universe.categories["internal"]["counters"].getint("mark") - 1) # periodically save everything - if check_time("frequency_save"): + if not universe.categories["internal"]["counters"].getint("save"): universe.save() + universe.categories["internal"]["counters"].set("save", universe.categories["internal"]["time"].getint("frequency_save")) + else: universe.categories["internal"]["counters"].set("save", universe.categories["internal"]["counters"].getint("save") - 1) # pause for a configurable amount of time (decimal seconds) sleep(universe.categories["internal"]["time"].getfloat("increment")) @@ -1403,6 +1388,9 @@ def get_choice_action(user, choice): def handle_user_input(user): """The main handler, branches to a state-specific handler.""" + # if the user's client echo is off, send a blank line for aesthetics + if user.echoing: user.received_newline = True + # check to make sure the state is expected, then call that handler if "handler_" + user.state in globals(): exec("handler_" + user.state + "(user)") @@ -1412,9 +1400,6 @@ def handle_user_input(user): # since we got input, flag that the menu/prompt needs to be redisplayed user.menu_seen = False - # if the user's client echo is off, send a blank line for aesthetics - if user.echoing: user.received_newline = True - def generic_menu_handler(user): """A generic menu choice handler.""" @@ -1584,7 +1569,7 @@ def command_halt(actor, parameters): log(message, 8) # set a flag to terminate the world - universe.terminate_world = True + universe.terminate_flag = True def command_reload(actor): """Reload all code modules, configs and data.""" @@ -1595,7 +1580,7 @@ def command_reload(actor): log("User " + actor.owner.account.get("name") + " reloaded the world.", 8) # set a flag to reload - universe.reload_modules = True + universe.reload_flag = True def command_quit(actor): """Leave the world and go back to the main menu.""" @@ -1785,6 +1770,8 @@ def command_show(actor, parameters): if match("^\d+$", arguments[1]) and 0 <= int(arguments[1]) <= 9: level = int(arguments[1]) else: level = -1 + elif 0 <= actor.owner.account.getint("loglevel") <= 9: + level = actor.owner.account.getint("loglevel") else: level = 1 if level > -1 and start > -1 and stop > -1: message = get_loglines(level, start, stop) @@ -1875,13 +1862,87 @@ def daemonize(): """Fork and disassociate from everything.""" if universe.contents["internal:process"].getboolean("daemon"): import sys + from resource import getrlimit, RLIMIT_NOFILE log("Disassociating from the controlling terminal.") if fork(): _exit(0) setsid() + if fork(): _exit(0) + chdir("/") + umask(0) for stdpipe in range(3): close(stdpipe) sys.stdin = sys.__stdin__ = file("/dev/null", "r") sys.stdout = sys.stderr = sys.__stdout__ = sys.__stderr__ = file("/dev/null", "w") +def create_pidfile(universe): + """Write a file containing the current process ID.""" + pid = str(getpid()) + log("Process ID: " + pid) + file_name = universe.contents["internal:process"].get("pidfile") + if not isabs(file_name): + file_name = path_join(universe.startdir, file_name) + if file_name: + file_descriptor = file(file_name, 'w') + file_descriptor.write(pid + "\n") + file_descriptor.flush() + file_descriptor.close() + +def remove_pidfile(universe): + """Remove the file containing the current process ID.""" + file_name = universe.contents["internal:process"].get("pidfile") + if not isabs(file_name): + file_name = path_join(universe.startdir, file_name) + if file_name and access(file_name, W_OK): remove(file_name) + +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 + +def sighook(what, where): + """Handle external signals.""" + + # a generic message + message = "Caught signal: " + + # for a hangup signal + if what == SIGHUP: + message += "hangup (reloading)" + universe.reload_flag = True + + # for a terminate signal + elif what == SIGTERM: + message += "terminate (halting)" + universe.terminate_flag = True + + # catchall for unexpected signals + else: message += str(what) + " (unhandled)" + + # log what happened + log(message, 8) + +# redefine sys.excepthook with ours +import sys +sys.excepthook = excepthook + +# assign the sgnal handlers +signal(SIGHUP, sighook) +signal(SIGTERM, sighook) + # if there is no universe, create an empty one -if not "universe" in locals(): universe = Universe() +if not "universe" in locals(): + if len(argv) > 1: conffile = argv[1] + else: conffile = "" + universe = Universe(conffile, True) +elif universe.reload_flag: + universe = universe.new() + reload_data() -- 2.11.0