#MUDPyE - (M)ulti-(U)ser (D)imension (Py)thon (E)ngine #Copyright (C) 2005 Corey Staten #This program is free software; you can redistribute it and/or #modify it under the terms of the GNU General Public License #as published by the Free Software Foundation; either version 2 #of the License, or (at your option) any later version. #This program is distributed in the hope that it will be useful, #but WITHOUT ANY WARRANTY; without even the implied warranty of #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #GNU General Public License for more details. #You should have received a copy of the GNU General Public License #along with this program; if not, write to the Free Software #Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #Send feedback/questions to MUDPyE@gmail.com import copy import cPickle import gzip import mudPickle import operator import os import re import time import types import mpmudobject import mptraceback import whichdb import zlib from mpmudobject import * def ExtractStringList(listString): stringList = listString.split(",") stringList = [element.strip().replace("'", "").replace("\"", "") for element in stringList] stringList = [element for element in stringList if (len(element) > 0)] return stringList class PseudoModule(object): """Simple class which routes calls to __getattribute__ and __setattribute__ to a dict.""" def __init__(self, modDict): object.__setattr__(self, "modDict", modDict) def __getattribute__(self, key): return object.__getattribute__(self, "modDict")[key] def __setattr__(self, key, value): raise AttributeError, "PseudoModule's are read-only." def __delattr__(self, key): raise AttributeError, "PseudoModule's are read-only." class SourceDatabase(object): """A database which keeps track of compressed string representations of source files.""" importRe = re.compile("^\\s*_imports\\s*=\\s*\\[\\s*((?:[\"']\\w*[\"']\\s*\\,?\\s*)*)\\s*\\]\\s*$", re.MULTILINE) parentRe = re.compile("^\\s*_parents\\s*=\\s*\\[\\s*((?:[\"']\\w*[\"']\\s*\\,?\\s*)*)\\s*\\]\\s*$", re.MULTILINE) def __init__(self, mudWorld, fileName, globalsFileName): self.mudWorld = mudWorld self.contextCache = {} self.fileName = fileName self.isOpen = False self.OpenDB() self.scriptGlobals = {"mudWorld":mudWorld} self.pseudoModules = {} try: globalsFile = file(globalsFileName, "rb") except IOError, excInst: mudWorld.loggers["engine.db"].warning("Unable to open script globals file [%s]: %s" % (globalsFileName, str(excInst))) else: try: globalsText = globalsFile.read() exec globalsText in self.scriptGlobals except IOError, excInst: mudWorld.loggers["engine.db"].warning("Unable to read from script globals file: %s" % str(excInst)) except: mudWorld.loggers["mud.scripterror"].error("Error executing script globals:\r\n%s" % mptraceback.format_exc()) else: globalsFile.close() def OpenDB(self): """Opens the database from the file in self.fileName. Attempts to import whatever dbm is used by the file.""" dbModuleName = whichdb.whichdb(self.fileName) if not((dbModuleName is None) or (len(dbModuleName) == 0)): try: dbModule = __import__(dbModuleName) except ImportError: mudWorld.loggers["engine.db"].critical("Module [%s] not available to open SourceDatabase file [%s]." % (dbModuleName, self.fileName)) raise else: dbModule = __import__("anydbm") try: self.db = dbModule.open(self.fileName, "c") except IOError, excInst: mudWorld.loggers["engine.db"].critical("Unable to open SourceDatabase file [%s]: %s" % (self.fileName, str(excInst))) raise self.isOpen = True def CloseDB(self): """Internal function for closing the database. Does not wipe the context cache! Note that calling most database functions after closing will cause an exception.""" self.db.close() self.isOpen = False def EmptyCache(self): """Empties the cache of source context's.""" self.contextCache = {} def UnloadContext(self, objType): """Unloads the given type's source context. Useful for making sure the program reloads a revisionto the source.""" #Remove any pseudo-modules present. try: contextDict = contextCache[objType] pseudoModuleNames = contextDict["_imports"] for pseudoModuleName in pseudoModuleNames: pseudoModule = contextDict[pseudoModuleName] self.pseudoModules.remove(pseudoModule) except: mudWorld.loggers["mud.scripterror"].warning("Unable to clear PseudoModule's from context [%s]; possible memory" "leak:\r\n" % (objType, mptraceback.format_exc())) try: del contextCache[objType] except KeyError: mudWorld.loggers["engine.db"].warning("Attempt to unload source context [%s]; not in database." % objType) def BackupDB(self, backupPath): """Backs up a zipped version of the database to backupPath. Names the file based on the current time.""" self.CloseDB() backupFileName = os.path.join(backupPath, "%s %s.gz" % (time.asctime(),self.fileName)) try: backupFile = gzip.GzipFile(backupFileName, "wb", 9) except IOError, excInst: self.mudWorld.loggers["engine.db"].error("Unable to open SourceDatabase backup file [%s]: %s" % (backupFileName, str(excInst))) else: try: dbFile = file(self.fileName, "rb") except IOError, excInst: self.mudWorld.loggers["engine.db"].error("Unable to open SourceDatabase file [%s] for backup reading: %s" % (self.fileName, str(excInst))) else: try: buffer = dbFile.read(8192) while len(buffer): backupFile.write(buffer) buffer = dbFile.read(8192) except IOError, excInst: self.mudWorld.loggers["engine.db"].error("Error transferring SourceDatabase [%s] to backup [%s]: %s" % (self.fileName, backupFileName, str(excInst))) dbFile.close() backupFile.close() self.OpenDB() def GetContext(self, objType): """Returns a source context dictionary for objType, loading it if necessary. Context dictionaries are used for executing code on Mud Objects.""" try: return self.contextCache[objType] except KeyError: return self.LoadContext(objType) def LoadContext(self, objType): """Loads the object context for objType, either from its source file or the database.""" #We set this so that we don't have to continually pass it to the recursive functions. try: srcFileName = self.mudWorld.objIndex[objType] srcFile = file(srcFileName, "rb") except (KeyError, IOError), excInst: self.mudWorld.loggers["engine.db"].warning("Cannot get source file for for objType [%s]: %s" % (objType, str(excInst))) try: zipText = self.db[objType] srcFileText = zlib.decompress(zipText) except KeyError: self.mudWorld.loggers["engine.db"].error("No source text available for type [%s]." % objType) return None else: srcFileText = srcFile.read() srcFileText = srcFileText.replace("\r", "").strip() srcFile.close() srcFileStats = os.stat(srcFileName) objStampName = "__mtime__%s" % (objType,) if not(self.db.has_key(objStampName) and (self.db[objStampName] == str(srcFileStats.st_mtime))): self.db[objStampName] = str(srcFileStats.st_mtime) self.db[objType] = zlib.compress(srcFileText,9) newContextDict = self._ConstructContextDict(objType, srcFileText) if newContextDict != None: self.contextCache[objType] = newContextDict #Update all pseudo-module dicts. if self.pseudoModules.has_key(objType): for pseudoModule in self.pseudoModules[objType]: object.__setattr__(pseudoModule, "modDict", newContextDict) return newContextDict else: return None def _ConstructContextDict(self, objType, srcFileText): """Constructs a new context dictionary given the text of a source file.""" contextDict = {} contextDict.update(self.scriptGlobals) #First, we have to search for the _imports and _parents lines. contextDict["_srcType"] = objType importMatch = SourceDatabase.importRe.search(srcFileText) parentMatch = SourceDatabase.parentRe.search(srcFileText) if not(importMatch is None): importList = ExtractStringList(importMatch.group(1)) for importName in importList: self._Import(contextDict, importName) if not(parentMatch is None): parentList = ExtractStringList(parentMatch.group(1)) for parentName in parentList: self._Derive(contextDict, parentName) contextDict["_srcType"] = objType try: exec srcFileText in contextDict except: self.mudWorld.loggers["mud.scripterror"].error("Error constructing context for objtype [%s]:\r\n%s" % (objType, mptraceback.format_exc())) return None if not(contextDict.has_key("_imports")): contextDict["_imports"] = [] if not(contextDict.has_key("_parents")): contextDict["_parents"] = [] if not(contextDict.has_key("_version")): contextDict["_version"] = 1 #Wrap all the functions. wrapKeys = [key for key,val in contextDict.items() if type(val) is types.FunctionType] for wrapKey in wrapKeys: contextDict[wrapKey] = mpmudobject.MudFunctionWrapper(contextDict[wrapKey], contextDict) return contextDict def _Import(self, objDict, importType): """Imports the context of an objType as a PseudoModule(acts as a module in the context's global space).""" if self.mudWorld.transitions.has_key(importType): self.mudWorld.loggers["engine.db"].warning("Deprecated import type [%s] encountered in [%s]." % (importType, objDict["_srcType"])) newImportType = self.mudWorld.transitions[importType] objDict["_imports"][objDict["_imports"].index(importType)] = newImportType importType = newImportType importDict = self.GetContext(importType) if importDict is None: self.mudWorld.loggers["mud.scripterror"].error("Attempt to import non-existant PseudoModule [%s].in [%s]" % (importType, objDict["_srcType"])) objDict[importType] = None return None pseudoModule = PseudoModule(importDict) objDict[importType] = pseudoModule if self.pseudoModules.has_key(importType): self.pseudoModules[importType].append(pseudoModule) else: self.pseudoModules[importType] = [pseudoModule] def _Derive(self, childDict, parentType): """Derives an object from the context of a given parentType.""" if self.mudWorld.transitions.has_key(parentType): self.mudWorld.loggers["engine.db"].warning("Deprecated parent type [%s] encountered in [%s]." % (parentType, childDict["_srcType"])) newParentType = self.mudWorld.transitions[parentType] childDict["_parents"][childDict["_parents"].index(parentType)] = newParentType parentType = newParentType parentDict = self.GetContext(parentType) if parentDict is None: self.mudWorld.loggers["mud.scripterror"].error("Attempt to derive from non-existant objType [%s] in [%s]" % (parentType, childDict["_srcType"])) return None tempDict = {} tempDict.update(parentDict) tempDict.update(childDict) childDict.update(tempDict) class ObjectDatabase: """Database used to store MudObject data.""" def __init__(self, mudWorld, fileName): self.mudWorld = mudWorld self.fileName = fileName self.objCache = {} self.dirtyObjs = [] self.refDict = {} self.isOpen = False self.mudObjectRefType = GenMudObjectRefType(mudWorld) self.OpenDB() if not(self.db.has_key("__db__nextid")): self.db["__db__nextid"] = "1" def OpenDB(self): """Opens the database from the file in self.fileName. Attempts to import whatever dbm is used by the file.""" dbModuleName = whichdb.whichdb(self.fileName) if not((dbModuleName is None) or (len(dbModuleName) == 0)): try: dbModule = __import__(dbModuleName) except ImportError: mudWorld.loggers["engine.db"].critical("Module [%s] not available to open ObjectDatabase file [%s]." % (dbModuleName, self.fileName)) raise else: dbModule = __import__("anydbm") try: self.db = dbModule.open(self.fileName, "c") except IOError, excInst: mudWorld.loggers["engine.db"].critical("Unable to open ObjectDatabase file [%s]: %s" % (self.fileName, str(excInst))) raise self.isOpen = True def CloseDB(self): """Internal function for closing the database. Does not wipe the context cache! Note that calling most database functions after closing will cause an exception.""" self.db.sync() self.db.close() self.isOpen = False def EmptyCache(self): """Saves dirtied objects and then clears the memory cache.""" self._SaveDirtyObjs() self.objCache = {} self.dirtyObjs = [] def BackupDB(self, backupPath): """Backs up a zipped version of the database to backupPath. Names the file based on the current time.""" self.CloseDB() backupFileName = os.path.join(backupPath, "%s %s.gz" % (time.asctime(),self.fileName)) try: backupFile = gzip.GzipFile(backupFileName, "wb", 9) except IOError, excInst: self.mudWorld.loggers["engine.db"].error("Unable to open ObjectDatabase backup file [%s]: %s" % (backupFileName, str(excInst))) else: try: dbFile = file(self.fileName, "rb") except IOError, excInst: self.mudWorld.loggers["engine.db"].error("Unable to open ObjectDatabase file [%s] for backup reading: %s" % (self.fileName, str(excInst))) else: try: buffer = dbFile.read(8192) while len(buffer): backupFile.write(buffer) buffer = dbFile.read(8192) except IOError, excInst: self.mudWorld.loggers["engine.db"].error("Error transferring ObjectDatabase [%s] to backup [%s]: %s" % (self.fileName, backupFileName, str(excInst))) dbFile.close() backupFile.close() self.OpenDB() def Update(self): """Generalized update function which should be called periodically. Currently, all it does is save dirtied objects.""" self._SaveDirtyObjs() def DirtyObj(self, ID): """Dirties an object if it is not dirty already. Note, MudObject's inline this, so this should only be used under special circumstances.""" if ID not in self.dirtyObjs: self.dirtyObjs.append(ID) def HasObj(self, ID): return self.db.has_key(ID) def GetObjRef(self, ID): """Should be 'GetObj' for scripts, used to get a proxy object for MudObject's.""" if not(self.refDict.has_key(ID)): self.refDict[ID] = self.mudObjectRefType(ID) return self.refDict[ID] def GetObjDict(self, ID): """Finds the object with ID in the cache or loads it from disk. This is where cache-ing is handled.""" if not(self.objCache.has_key(ID)): if self.db.has_key(ID): loadDict = self.LoadObjDict(ID) if loadDict is None: self.mudWorld.loggers["engine.db"].debug("Attempt to access non-existant object [%s]." % ID) return None self.objCache[ID] = loadDict else: self.mudWorld.loggers["engine.db"].debug("Attempt to access non-existant object [%s]." % ID) return None return self.objCache[ID] def LoadObjDict(self, ID): """Loads an object from the hard-disk database. Will throw an exception if object isn't present.""" loadDict = mudPickle.loads(self.db[ID], self.GetObjRef) objType = loadDict["_objType"] if self.mudWorld.transitions.has_key(objType): newObjType = self.mudWorld.transitions[objType] loadDict["_objType"] = newObjType self.mudWorld.loggers["engine.db"].info("Transitioned object [%s] from [%s] to [%s]" % (ID, objType, newObjType)) objRef = self.GetObjRef(ID) object.__setattr__(objRef, "objDict", loadDict) ExParentToChild(objRef, "_Sys_Load") return loadDict def UnloadObjDict(self, ID): """Unloads the object with the given ID, even if it is persistant.""" if ID in self.dirtyObjs: ExChildToParent(self.GetObjRef(ID), "_Sys_Unload") self.db[ID] = mudPickle.dumps(self.objCache[ID], 2) self.dirtyObjs.remove(ID) object.__setattr__(self.refDict[ID], "objDict", None) del self.objCache[ID] def _SaveDirtyObjs(self): """Saves dirtied objects to the database, and then synchronizes.""" for dirtyObj in self.dirtyObjs: if not(self.objCache.has_key(dirtyObj)): self.mudWorld.loggers["engine.db"].error("Dirtied object ID [%s] not in database." % dirtyObj) continue self.db[dirtyObj] = mudPickle.dumps(self.objCache[dirtyObj], 2) self.dirtyObjs = [] self.db.sync() def CreateObj(self, objType, *createArgs): """Creates an object with the next ID in line.""" IDNum = int(self.db["__db__nextid"]) self.db["__db__nextid"] = str(IDNum + 1) return self.CreateObjAsID(str(IDNum), objType, *createArgs) def CreateObjAsID(self, ID, objType, *createArgs): """Does the actual work of creation an object. Returns the object's ID, which is always the same as the ID argument passed(redundant, but returning the MudObject itself might start bad habits. If an object currently has this ID, it will be deleted.""" if objType not in self.mudWorld.objIndex: return None if self.mudWorld.transitions.has_key(objType): self.mudWorld.loggers["engine.db"].warning("Object created with deprecated object type [%s]." % (objType)) objType = transitions[objType] newObjDict = {"_ID":ID, "_objType":objType, "_persist":False, "_versions":{}} if self.HasObj(ID): self.DelObj(ID) self.objCache[ID] = newObjDict newObjRef = self.GetObjRef(ID) ExParentToChild(newObjRef, "_Sys_Create", *createArgs) ExParentToChild(newObjRef, "_Sys_Load") self.db[ID] = mudPickle.dumps(newObjDict) return newObjRef def DelObj(self, objRef): ExChildToParent(objRef, "_Sys_Unload") ExChildToParent(objRef, "_Sys_Destroy") ID = objRef._ID self.UnloadObjDict(ID) del self.db[ID] del self.refDict[ID]