#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 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, modType, sourceDB): object.__setattr__(self, "modDict", None) object.__setattr__(self, "modType", modType) object.__setattr__(self, "sourceDB", sourceDB) def __getattribute__(self, key): modDict = object.__getattribute__(self, "modDict") modType = object.__getattribute__(self, "modType") #Make sure the object is loaded. if modDict is None: modDict = object.__getattribute__(self, "sourceDB").GetContext(modType) if modDict is None: object.__getattribute__(self, "sourceDB").mudWorld.loggers["mud.scripterror"].error("Invalid existant pseudo-module [%s]." % modType) return None object.__setattr__(self, "modDict", modDict) return 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." def __getitem__(self, key): modDict = object.__getattribute__(self, "modDict") modType = object.__getattribute__(self, "modType") #Make sure the object is loaded. if modDict is None: modDict = object.__getattribute__(self, "sourceDB").GetContext(modType) if modDict is None: MudObjectRef.mudWorld.loggers["mud.scripterror"].error("Invalid existant pseudo-module [%s]." % modType) return None object.__setattr__(self, "modDict", modDict) return modDict[key] def __setitem__(self, key, value): raise AttributeError, "PseudoModule's are read-only." def __delitem__(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: self.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: self.mudWorld.loggers["engine.db"].warning("Unable to read from script globals file: %s" % str(excInst)) except: self.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: self.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: self.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.""" for objType in self.contextCache.keys(): self.UnloadContext(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(), os.path.basename(self.fileName))).replace(":", "-") try: backupFile = gzip.GzipFile(backupFileName, "wb", 9) except IOError, excInst: self.OpenDB() 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.OpenDB() 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.OpenDB() self.mudWorld.loggers["engine.db"].error("Error transferring SourceDatabase [%s] to backup [%s]: %s" % (self.fileName, backupFileName, str(excInst))) dbFile.close() backupFile.close() if not(self.isOpen): self.OpenDB() def GetModule(self, objType): try: return self.pseudoModules[objType] except KeyError: pseudoModule = PseudoModule(objType, self) self.pseudoModules[objType] = pseudoModule return pseudoModule 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) contextDict = {} self.contextCache[objType] = contextDict result = self._ConstructContextDict(contextDict, objType, srcFileText) if result == False: self.UnloadContext(objType) return None else: return contextDict def UnloadContext(self, objType): """Unloads the given type's source context. Useful for making sure the program reloads a revision to the source.""" if self.pseudoModules.has_key(objType): object.__setattr__(self.pseudoModules[objType], "modDict", None) try: del self.contextCache[objType] except KeyError: self.mudWorld.loggers["engine.db"].warning("Attempt to unload source context [%s]; not in database." % objType) def _ConstructContextDict(self, contextDict, objType, srcFileText): """Constructs a new context dictionary given the text of a source file.""" contextDict.update(self.scriptGlobals) #First, we have to search for the _imports and _parents lines. contextDict["_srcType"] = objType contextDict["_imports"] = [] contextDict["_parents"] = [] contextDict["_version"] = 1 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 False #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 True 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 if self.GetContext(importType) 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 = self.GetModule(importType) objDict[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() 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._SaveDirtyObjs() self.CloseDB() backupFileName = os.path.join(backupPath, "%s %s.gz" % (time.asctime(), os.path.basename(self.fileName))).replace(":", "-") try: backupFile = gzip.GzipFile(backupFileName, "wb", 9) except IOError, excInst: self.OpenDB() 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.OpenDB() 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.OpenDB() self.mudWorld.loggers["engine.db"].error("Error transferring ObjectDatabase [%s] to backup [%s]: %s" % (self.fileName, backupFileName, str(excInst))) dbFile.close() backupFile.close() if not(self.isOpen): 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.""" nextIDStr = "__db__nextid_%s" % objType try: IDNum = int(self.db[nextIDStr]) self.db[nextIDStr] = str(IDNum + 1) except KeyError: IDNum = 1 self.db[nextIDStr] = "2" ID = "%s%d" % (objType, IDNum) return self.CreateObjAsID(ID, objType, **createArgs) def CreateObjAsID(self, ID, objType, **createArgs): """Does the actual work of creating an object. Returns a reference to the object. 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]