# 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):
# 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."""
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):
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] = {}
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")
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",
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."""
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()
# 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):
# 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
"""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
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."""
"""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)
# 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]
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"))
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)")
# 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."""
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."""
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."""
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)
"""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()