Trunk: Merged griatch-branch. This implements a new reload mechanism - splitting Evennia into two processes: Server and Portal with different tasks. Also cleans and fixes several bugs in script systems as well as introduces i18n (courtesy of raydeejay).
This commit is contained in:
parent
14dae44a46
commit
f13e8cdf7c
50 changed files with 3175 additions and 2565 deletions
81
src/utils/evennia-mode.el
Normal file
81
src/utils/evennia-mode.el
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
;
|
||||
; Emacs major mode for editing Evennia batch-command files (*.ev files).
|
||||
; Griatch 2011-09. Tested with GNU Emacs 23. Released under same license as Evennia.
|
||||
;
|
||||
; For batch-code files it's better to simply use the normal Python mode.
|
||||
;
|
||||
; Features:
|
||||
; Syntax hilighting
|
||||
; Auto-indenting properly when pressing <tab>.
|
||||
;
|
||||
; Installation:
|
||||
; - Copy this file, evennia-mode.el, to a location where emacs looks for plugins
|
||||
; (usually .emacs.d/ at least under Linux)
|
||||
; - If you don't have that directory, either look on the web for how to find it
|
||||
; or create it yourself - create a new directory .emacs.d/ some place and add
|
||||
; the following to emacs' configuration file (.emacs):
|
||||
; (add-to-list 'load-path "<PATH>/.emacs.d/")
|
||||
; where PATH is the place you created the directory. Now Emacs will know to
|
||||
; look here for plugins. Copy this file there.
|
||||
; - In emacs config file (.emacs), next add the following line:
|
||||
; (require 'evennia-mode)
|
||||
; - (re)start emacs
|
||||
; - Open a batch file with the ending *.ev. The mode will start automatically
|
||||
; (otherwise you can manually start it with M-x evennia-mode).
|
||||
;
|
||||
; Report bugs to evennia's issue tracker.
|
||||
;
|
||||
|
||||
(defvar evennia-mode-hook nil)
|
||||
|
||||
; Add keyboard shortcuts (not used)
|
||||
(defvar evennia-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map "\C-j" 'newline-and-indent)
|
||||
map)
|
||||
"Keymap for evennia major mode")
|
||||
|
||||
; Autoload this when .ev file opens.
|
||||
(add-to-list 'auto-mode-alist '("\\.ev\\'" . evennia-mode))
|
||||
|
||||
; Syntax hilighting
|
||||
(defconst evennia-font-lock-keywords
|
||||
(list
|
||||
'("^ *#.*" . font-lock-comment-face)
|
||||
'("^[^ |^#]*" . font-lock-variable-name-face))
|
||||
;'("^[^ #].*" . font-lock-variable-name-face)) ; more extreme hilight
|
||||
"Minimal highlighting for evennia ev files."
|
||||
)
|
||||
|
||||
; Auto-indentation
|
||||
(defun evennia-indent-line ()
|
||||
"Indent current line as batch-code"
|
||||
(interactive)
|
||||
(beginning-of-line)
|
||||
(if (looking-at "^ *#") ; a comment line
|
||||
(indent-line-to 0)
|
||||
(progn
|
||||
(forward-line -1) ; back up one line
|
||||
(if (looking-at "^ *#") ; previous line was comment
|
||||
(progn
|
||||
(forward-line)
|
||||
(indent-line-to 0))
|
||||
(progn
|
||||
(forward-line)
|
||||
(indent-line-to 1)))))
|
||||
)
|
||||
|
||||
; Register with Emacs system
|
||||
(defun evennia-mode ()
|
||||
"Major mode for editing Evennia batch-command files."
|
||||
(interactive)
|
||||
(kill-all-local-variables)
|
||||
(use-local-map evennia-mode-map)
|
||||
(set (make-local-variable 'indent-line-function) 'evennia-indent-line)
|
||||
(set (make-local-variable 'font-lock-defaults) '(evennia-font-lock-keywords))
|
||||
(setq major-mode 'evennia-mode)
|
||||
(setq mode-name "evennia")
|
||||
(run-hooks 'evennia-mode-hook)
|
||||
)
|
||||
|
||||
(provide 'evennia-mode)
|
||||
|
|
@ -1,658 +0,0 @@
|
|||
# MIT Licensed
|
||||
# Copyright (c) 2009-2010 Peter Shinners <pete@shinners.org>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without
|
||||
# restriction, including without limitation the rights to use,
|
||||
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the
|
||||
# Software is furnished to do so, subject to the following
|
||||
# conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""
|
||||
This module intends to be a full featured replacement for Python's reload
|
||||
function. It is targeted towards making a reload that works for Python
|
||||
plugins and extensions used by longer running applications.
|
||||
|
||||
Reimport currently supports Python 2.4 through 2.6.
|
||||
|
||||
By its very nature, this is not a completely solvable problem. The goal of
|
||||
this module is to make the most common sorts of updates work well. It also
|
||||
allows individual modules and package to assist in the process. A more
|
||||
detailed description of what happens is at
|
||||
http://code.google.com/p/reimport .
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ["reimport", "modified"]
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import gc
|
||||
import inspect
|
||||
import weakref
|
||||
import traceback
|
||||
import time
|
||||
|
||||
|
||||
|
||||
__version__ = "1.2"
|
||||
__author__ = "Peter Shinners <pete@shinners.org>"
|
||||
__license__ = "MIT"
|
||||
__url__ = "http://code.google.com/p/reimport"
|
||||
|
||||
|
||||
|
||||
_previous_scan_time = time.time() - 1.0
|
||||
_module_timestamps = {}
|
||||
|
||||
|
||||
# find the 'instance' old style type
|
||||
class _OldClass: pass
|
||||
_InstanceType = type(_OldClass())
|
||||
del _OldClass
|
||||
|
||||
|
||||
|
||||
def reimport(*modules):
|
||||
"""Reimport python modules. Multiple modules can be passed either by
|
||||
name or by reference. Only pure python modules can be reimported.
|
||||
|
||||
For advanced control, global variables can be placed in modules
|
||||
that allows finer control of the reimport process.
|
||||
|
||||
If a package module has a true value for "__package_reimport__"
|
||||
then that entire package will be reimported when any of its children
|
||||
packages or modules are reimported.
|
||||
|
||||
If a package module defines __reimported__ it must be a callable
|
||||
function that accepts one argument and returns a bool. The argument
|
||||
is the reference to the old version of that module before any
|
||||
cleanup has happend. The function should normally return True to
|
||||
allow the standard reimport cleanup. If the function returns false
|
||||
then cleanup will be disabled for only that module. Any exceptions
|
||||
raised during the callback will be handled by traceback.print_exc,
|
||||
similar to what happens with tracebacks in the __del__ method.
|
||||
"""
|
||||
__internal_swaprefs_ignore__ = "reimport"
|
||||
reloadSet = set()
|
||||
|
||||
if not modules:
|
||||
return
|
||||
|
||||
# Get names of all modules being reloaded
|
||||
for module in modules:
|
||||
name, target = _find_exact_target(module)
|
||||
if not target:
|
||||
raise ValueError("Module %r not found" % module)
|
||||
if not _is_code_module(target):
|
||||
raise ValueError("Cannot reimport extension, %r" % name)
|
||||
|
||||
reloadSet.update(_find_reloading_modules(name))
|
||||
|
||||
# Sort module names
|
||||
reloadNames = _package_depth_sort(reloadSet, False)
|
||||
|
||||
# Check for SyntaxErrors ahead of time. This won't catch all
|
||||
# possible SyntaxErrors or any other ImportErrors. But these
|
||||
# should be the most common problems, and now is the cleanest
|
||||
# time to abort.
|
||||
# I know this gets compiled again anyways. It could be
|
||||
# avoided with py_compile, but I will not be the creator
|
||||
# of messy .pyc files!
|
||||
for name in reloadNames:
|
||||
filename = getattr(sys.modules[name], "__file__", None)
|
||||
if not filename:
|
||||
continue
|
||||
pyname = os.path.splitext(filename)[0] + ".py"
|
||||
try:
|
||||
data = open(pyname, "rU").read() + "\n"
|
||||
except (IOError, OSError):
|
||||
continue
|
||||
|
||||
compile(data, pyname, "exec", 0, False) # Let this raise exceptions
|
||||
|
||||
# Move modules out of sys
|
||||
oldModules = {}
|
||||
for name in reloadNames:
|
||||
oldModules[name] = sys.modules.pop(name)
|
||||
ignores = (id(oldModules), id(__builtins__))
|
||||
prevNames = set(sys.modules)
|
||||
|
||||
# Python will munge the parent package on import. Remember original value
|
||||
parentPackageName = name.rsplit(".", 1)
|
||||
parentPackage = None
|
||||
parentPackageDeleted = lambda: None
|
||||
if len(parentPackageName) == 2:
|
||||
parentPackage = sys.modules.get(parentPackageName[0], None)
|
||||
parentValue = getattr(parentPackage, parentPackageName[1], parentPackageDeleted)
|
||||
|
||||
# Reimport modules, trying to rollback on exceptions
|
||||
try:
|
||||
for name in reloadNames:
|
||||
if name not in sys.modules:
|
||||
__import__(name)
|
||||
|
||||
except StandardError:
|
||||
# Try to dissolve any newly import modules and revive the old ones
|
||||
newNames = set(sys.modules) - prevNames
|
||||
newNames = _package_depth_sort(newNames, True)
|
||||
for name in newNames:
|
||||
_unimport_module(sys.modules[name], ignores)
|
||||
assert name not in sys.modules
|
||||
|
||||
sys.modules.update(oldModules)
|
||||
raise
|
||||
|
||||
newNames = set(sys.modules) - prevNames
|
||||
newNames = _package_depth_sort(newNames, True)
|
||||
|
||||
# Update timestamps for loaded time
|
||||
now = time.time() - 1.0
|
||||
for name in newNames:
|
||||
_module_timestamps[name] = (now, True)
|
||||
|
||||
# Fix Python automatically shoving of children into parent packages
|
||||
if parentPackage and parentValue:
|
||||
if parentValue == parentPackageDeleted:
|
||||
delattr(parentPackage, parentPackageName[1])
|
||||
else:
|
||||
setattr(parentPackage, parentPackageName[1], parentValue)
|
||||
parentValue = parentPackage = parentPackageDeleted = None
|
||||
|
||||
# Push exported namespaces into parent packages
|
||||
pushSymbols = {}
|
||||
for name in newNames:
|
||||
oldModule = oldModules.get(name)
|
||||
if not oldModule:
|
||||
continue
|
||||
parents = _find_parent_importers(name, oldModule, newNames)
|
||||
pushSymbols[name] = parents
|
||||
for name, parents in pushSymbols.iteritems():
|
||||
for parent in parents:
|
||||
oldModule = oldModules[name]
|
||||
newModule = sys.modules[name]
|
||||
_push_imported_symbols(newModule, oldModule, parent)
|
||||
# Rejigger the universe
|
||||
for name in newNames:
|
||||
old = oldModules.get(name)
|
||||
if not old:
|
||||
continue
|
||||
new = sys.modules[name]
|
||||
rejigger = True
|
||||
reimported = getattr(new, "__reimported__", None)
|
||||
if reimported:
|
||||
try:
|
||||
rejigger = reimported(old)
|
||||
except StandardError:
|
||||
# What else can we do? the callbacks must go on
|
||||
# Note, this is same as __del__ behaviour. /shrug
|
||||
traceback.print_exc()
|
||||
|
||||
if rejigger:
|
||||
_rejigger_module(old, new, ignores)
|
||||
else:
|
||||
_unimport_module(new, ignores)
|
||||
|
||||
|
||||
|
||||
def modified(path=None):
|
||||
"""Find loaded modules that have changed on disk under the given path.
|
||||
If no path is given then all modules are searched.
|
||||
"""
|
||||
global _previous_scan_time
|
||||
modules = []
|
||||
|
||||
if path:
|
||||
path = os.path.normpath(path) + os.sep
|
||||
|
||||
defaultTime = (_previous_scan_time, False)
|
||||
pycExt = __debug__ and ".pyc" or ".pyo"
|
||||
|
||||
for name, module in sys.modules.items():
|
||||
filename = _is_code_module(module)
|
||||
if not filename:
|
||||
continue
|
||||
|
||||
filename = os.path.normpath(filename)
|
||||
prevTime, prevScan = _module_timestamps.setdefault(name, defaultTime)
|
||||
if path and not filename.startswith(path):
|
||||
continue
|
||||
|
||||
# Get timestamp of .pyc if this is first time checking this module
|
||||
if not prevScan:
|
||||
pycName = os.path.splitext(filename)[0] + pycExt
|
||||
if pycName != filename:
|
||||
try:
|
||||
prevTime = os.path.getmtime(pycName)
|
||||
except OSError:
|
||||
pass
|
||||
_module_timestamps[name] = (prevTime, True)
|
||||
|
||||
# Get timestamp of source file
|
||||
try:
|
||||
diskTime = os.path.getmtime(filename)
|
||||
except OSError:
|
||||
diskTime = None
|
||||
|
||||
if diskTime is not None and prevTime < diskTime:
|
||||
modules.append(name)
|
||||
|
||||
_previous_scan_time = time.time()
|
||||
return modules
|
||||
|
||||
|
||||
|
||||
def _is_code_module(module):
|
||||
"""Determine if a module comes from python code"""
|
||||
# getsourcefile will not return "bare" pyc modules. we can reload those?
|
||||
try:
|
||||
return inspect.getsourcefile(module) or ""
|
||||
except TypeError:
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
def _find_exact_target(module):
|
||||
"""Given a module name or object, find the
|
||||
base module where reimport will happen."""
|
||||
# Given a name or a module, find both the name and the module
|
||||
actualModule = sys.modules.get(module)
|
||||
if actualModule is not None:
|
||||
name = module
|
||||
else:
|
||||
for name, mod in sys.modules.iteritems():
|
||||
if mod is module:
|
||||
actualModule = module
|
||||
break
|
||||
else:
|
||||
return "", None
|
||||
|
||||
# Find highest level parent package that has package_reimport magic
|
||||
parentName = name
|
||||
while True:
|
||||
splitName = parentName.rsplit(".", 1)
|
||||
if len(splitName) <= 1:
|
||||
return name, actualModule
|
||||
parentName = splitName[0]
|
||||
|
||||
parentModule = sys.modules.get(parentName)
|
||||
if getattr(parentModule, "__package_reimport__", None):
|
||||
name = parentName
|
||||
actualModule = parentModule
|
||||
|
||||
|
||||
|
||||
def _find_reloading_modules(name):
|
||||
"""Find all modules that will be reloaded from given name"""
|
||||
modules = [name]
|
||||
childNames = name + "."
|
||||
for name in sys.modules.keys():
|
||||
if name.startswith(childNames) and _is_code_module(sys.modules[name]):
|
||||
modules.append(name)
|
||||
return modules
|
||||
|
||||
|
||||
|
||||
def _package_depth_sort(names, reverse):
|
||||
"""Sort a list of module names by their package depth"""
|
||||
def packageDepth(name):
|
||||
return name.count(".")
|
||||
return sorted(names, key=packageDepth, reverse=reverse)
|
||||
|
||||
|
||||
|
||||
def _find_module_exports(module):
|
||||
allNames = getattr(module, "__all__", ())
|
||||
if not allNames:
|
||||
allNames = [n for n in dir(module) if n[0] != "_"]
|
||||
return set(allNames)
|
||||
|
||||
|
||||
|
||||
def _find_parent_importers(name, oldModule, newNames):
|
||||
"""Find parents of reimported module that have all exported symbols"""
|
||||
parents = []
|
||||
|
||||
# Get exported symbols
|
||||
exports = _find_module_exports(oldModule)
|
||||
if not exports:
|
||||
return parents
|
||||
|
||||
# Find non-reimported parents that have all old symbols
|
||||
parent = name
|
||||
while True:
|
||||
names = parent.rsplit(".", 1)
|
||||
if len(names) <= 1:
|
||||
break
|
||||
parent = names[0]
|
||||
if parent in newNames:
|
||||
continue
|
||||
parentModule = sys.modules[parent]
|
||||
if not exports - set(dir(parentModule)):
|
||||
parents.append(parentModule)
|
||||
|
||||
return parents
|
||||
|
||||
|
||||
def _push_imported_symbols(newModule, oldModule, parent):
|
||||
"""Transfer changes symbols from a child module to a parent package"""
|
||||
# This assumes everything in oldModule is already found in parent
|
||||
oldExports = _find_module_exports(oldModule)
|
||||
newExports = _find_module_exports(newModule)
|
||||
|
||||
# Delete missing symbols
|
||||
for name in oldExports - newExports:
|
||||
delattr(parent, name)
|
||||
|
||||
# Add new symbols
|
||||
for name in newExports - oldExports:
|
||||
setattr(parent, name, getattr(newModule, name))
|
||||
|
||||
# Update existing symbols
|
||||
for name in newExports & oldExports:
|
||||
oldValue = getattr(oldModule, name)
|
||||
if getattr(parent, name) is oldValue:
|
||||
setattr(parent, name, getattr(newModule, name))
|
||||
|
||||
|
||||
|
||||
# To rejigger is to copy internal values from new to old
|
||||
# and then to swap external references from old to new
|
||||
|
||||
|
||||
def _rejigger_module(old, new, ignores):
|
||||
"""Mighty morphin power modules"""
|
||||
__internal_swaprefs_ignore__ = "rejigger_module"
|
||||
oldVars = vars(old)
|
||||
newVars = vars(new)
|
||||
ignores += (id(oldVars),)
|
||||
old.__doc__ = new.__doc__
|
||||
|
||||
# Get filename used by python code
|
||||
filename = new.__file__
|
||||
|
||||
for name, value in newVars.iteritems():
|
||||
if name in oldVars:
|
||||
oldValue = oldVars[name]
|
||||
if oldValue is value:
|
||||
continue
|
||||
|
||||
if _from_file(filename, value):
|
||||
if inspect.isclass(value):
|
||||
_rejigger_class(oldValue, value, ignores)
|
||||
|
||||
elif inspect.isfunction(value):
|
||||
_rejigger_func(oldValue, value, ignores)
|
||||
|
||||
setattr(old, name, value)
|
||||
|
||||
for name in oldVars.keys():
|
||||
if name not in newVars:
|
||||
value = getattr(old, name)
|
||||
delattr(old, name)
|
||||
if _from_file(filename, value):
|
||||
if inspect.isclass(value) or inspect.isfunction(value):
|
||||
_remove_refs(value, ignores)
|
||||
|
||||
_swap_refs(old, new, ignores)
|
||||
|
||||
|
||||
|
||||
def _from_file(filename, value):
|
||||
"""Test if object came from a filename, works for pyc/py confusion"""
|
||||
try:
|
||||
objfile = inspect.getsourcefile(value)
|
||||
except TypeError:
|
||||
return False
|
||||
return bool(objfile) and objfile.startswith(filename)
|
||||
|
||||
|
||||
|
||||
def _rejigger_class(old, new, ignores):
|
||||
"""Mighty morphin power classes"""
|
||||
__internal_swaprefs_ignore__ = "rejigger_class"
|
||||
oldVars = vars(old)
|
||||
newVars = vars(new)
|
||||
ignores += (id(oldVars),)
|
||||
|
||||
for name, value in newVars.iteritems():
|
||||
if name in ("__dict__", "__doc__", "__weakref__"):
|
||||
continue
|
||||
|
||||
if name in oldVars:
|
||||
oldValue = oldVars[name]
|
||||
if oldValue is value:
|
||||
continue
|
||||
|
||||
if inspect.isclass(value) and value.__module__ == new.__module__:
|
||||
_rejigger_class(oldValue, value, ignores)
|
||||
|
||||
elif inspect.isfunction(value):
|
||||
_rejigger_func(oldValue, value, ignores)
|
||||
|
||||
setattr(old, name, value)
|
||||
|
||||
for name in oldVars.keys():
|
||||
if name not in newVars:
|
||||
value = getattr(old, name)
|
||||
delattr(old, name)
|
||||
_remove_refs(value, ignores)
|
||||
|
||||
_swap_refs(old, new, ignores)
|
||||
|
||||
|
||||
|
||||
def _rejigger_func(old, new, ignores):
|
||||
"""Mighty morphin power functions"""
|
||||
__internal_swaprefs_ignore__ = "rejigger_func"
|
||||
old.func_code = new.func_code
|
||||
old.func_doc = new.func_doc
|
||||
old.func_defaults = new.func_defaults
|
||||
old.func_dict = new.func_dict
|
||||
_swap_refs(old, new, ignores)
|
||||
|
||||
|
||||
|
||||
def _unimport_module(old, ignores):
|
||||
"""Remove traces of a module"""
|
||||
__internal_swaprefs_ignore__ = "unimport_module"
|
||||
oldValues = vars(old).values()
|
||||
ignores += (id(oldValues),)
|
||||
|
||||
# Get filename used by python code
|
||||
filename = old.__file__
|
||||
fileext = os.path.splitext(filename)
|
||||
if fileext in (".pyo", ".pyc", ".pyw"):
|
||||
filename = filename[:-1]
|
||||
|
||||
for value in oldValues:
|
||||
try: objfile = inspect.getsourcefile(value)
|
||||
except TypeError: objfile = ""
|
||||
|
||||
if objfile == filename:
|
||||
if inspect.isclass(value):
|
||||
_unimport_class(value, ignores)
|
||||
|
||||
elif inspect.isfunction(value):
|
||||
_remove_refs(value, ignores)
|
||||
|
||||
_remove_refs(old, ignores)
|
||||
|
||||
|
||||
|
||||
def _unimport_class(old, ignores):
|
||||
"""Remove traces of a class"""
|
||||
__internal_swaprefs_ignore__ = "unimport_class"
|
||||
oldItems = vars(old).items()
|
||||
ignores += (id(oldItems),)
|
||||
|
||||
for name, value in oldItems:
|
||||
if name in ("__dict__", "__doc__", "__weakref__"):
|
||||
continue
|
||||
|
||||
if inspect.isclass(value) and value.__module__ == old.__module__:
|
||||
_unimport_class(value, ignores)
|
||||
|
||||
elif inspect.isfunction(value):
|
||||
_remove_refs(value, ignores)
|
||||
|
||||
_remove_refs(old, ignores)
|
||||
|
||||
|
||||
|
||||
_recursive_tuple_swap = set()
|
||||
|
||||
|
||||
def _bonus_containers():
|
||||
"""Find additional container types, if they are loaded. Returns
|
||||
(deque, defaultdict).
|
||||
Any of these will be None if not loaded.
|
||||
"""
|
||||
deque = defaultdict = None
|
||||
collections = sys.modules.get("collections", None)
|
||||
if collections:
|
||||
deque = getattr(collections, "collections", None)
|
||||
defaultdict = getattr(collections, "defaultdict", None)
|
||||
return deque, defaultdict
|
||||
|
||||
|
||||
|
||||
def _find_sequence_indices(container, value):
|
||||
"""Find indices of value in container. The indices will
|
||||
be in reverse order, to allow safe editing.
|
||||
"""
|
||||
indices = []
|
||||
for i in range(len(container)-1, -1, -1):
|
||||
if container[i] is value:
|
||||
indices.append(i)
|
||||
return indices
|
||||
|
||||
|
||||
def _swap_refs(old, new, ignores):
|
||||
"""Swap references from one object to another"""
|
||||
__internal_swaprefs_ignore__ = "swap_refs"
|
||||
# Swap weak references
|
||||
refs = weakref.getweakrefs(old)
|
||||
if refs:
|
||||
try:
|
||||
newRef = weakref.ref(new)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
for oldRef in refs:
|
||||
_swap_refs(oldRef, newRef, ignores + (id(refs),))
|
||||
del refs
|
||||
|
||||
deque, defaultdict = _bonus_containers()
|
||||
|
||||
# Swap through garbage collector
|
||||
referrers = gc.get_referrers(old)
|
||||
for container in referrers:
|
||||
if id(container) in ignores:
|
||||
continue
|
||||
containerType = type(container)
|
||||
|
||||
if containerType is list or containerType is deque:
|
||||
for index in _find_sequence_indices(container, old):
|
||||
container[index] = new
|
||||
|
||||
elif containerType is tuple:
|
||||
# protect from recursive tuples
|
||||
orig = container
|
||||
if id(orig) in _recursive_tuple_swap:
|
||||
continue
|
||||
_recursive_tuple_swap.add(id(orig))
|
||||
try:
|
||||
container = list(container)
|
||||
for index in _find_sequence_indices(container, old):
|
||||
container[index] = new
|
||||
container = tuple(container)
|
||||
_swap_refs(orig, container, ignores + (id(referrers),))
|
||||
finally:
|
||||
_recursive_tuple_swap.remove(id(orig))
|
||||
|
||||
elif containerType is dict or containerType is defaultdict:
|
||||
if "__internal_swaprefs_ignore__" not in container:
|
||||
try:
|
||||
if old in container:
|
||||
container[new] = container.pop(old)
|
||||
except TypeError: # Unhashable old value
|
||||
pass
|
||||
for k,v in container.iteritems():
|
||||
if v is old:
|
||||
container[k] = new
|
||||
|
||||
elif containerType is set:
|
||||
container.remove(old)
|
||||
container.add(new)
|
||||
|
||||
elif containerType is type:
|
||||
if old in container.__bases__:
|
||||
bases = list(container.__bases__)
|
||||
bases[bases.index(old)] = new
|
||||
container.__bases__ = tuple(bases)
|
||||
|
||||
elif type(container) is old:
|
||||
container.__class__ = new
|
||||
|
||||
elif containerType is _InstanceType:
|
||||
if container.__class__ is old:
|
||||
container.__class__ = new
|
||||
|
||||
|
||||
|
||||
def _remove_refs(old, ignores):
|
||||
"""Remove references to a discontinued object"""
|
||||
__internal_swaprefs_ignore__ = "remove_refs"
|
||||
|
||||
# Ignore builtin immutables that keep no other references
|
||||
if old is None or isinstance(old, (int, basestring, float, complex)):
|
||||
return
|
||||
|
||||
deque, defaultdict = _bonus_containers()
|
||||
|
||||
# Remove through garbage collector
|
||||
for container in gc.get_referrers(old):
|
||||
if id(container) in ignores:
|
||||
continue
|
||||
containerType = type(container)
|
||||
|
||||
if containerType is list or containerType is deque:
|
||||
for index in _find_sequence_indices(container, old):
|
||||
del container[index]
|
||||
|
||||
elif containerType is tuple:
|
||||
orig = container
|
||||
container = list(container)
|
||||
for index in _find_sequence_indices(container, old):
|
||||
del container[index]
|
||||
container = tuple(container)
|
||||
_swap_refs(orig, container, ignores)
|
||||
|
||||
elif containerType is dict or containerType is defaultdict:
|
||||
if "__internal_swaprefs_ignore__" not in container:
|
||||
try:
|
||||
container.pop(old, None)
|
||||
except TypeError: # Unhashable old value
|
||||
pass
|
||||
for k,v in container.items():
|
||||
if v is old:
|
||||
del container[k]
|
||||
|
||||
elif containerType is set:
|
||||
container.remove(old)
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
"""
|
||||
This holds the mechanism for reloading the game modules on the
|
||||
fly. It's in this separate module since it's not a good idea to
|
||||
keep it in server.py since it messes with importing, and it's
|
||||
also not good to tie such important functionality to a user-definable
|
||||
command class.
|
||||
"""
|
||||
|
||||
import time
|
||||
from django.db.models.loading import AppCache
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.conf import settings
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.objects.models import ObjectDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.comms.models import Channel, Msg
|
||||
from src.help.models import HelpEntry
|
||||
|
||||
from src.typeclasses import models as typeclassmodels
|
||||
from src.comms import channelhandler
|
||||
from src.comms.models import Channel
|
||||
from src.utils import reimport, utils, logger
|
||||
|
||||
def start_reload_loop():
|
||||
"""
|
||||
This starts the asynchronous reset loop. While
|
||||
important that it runs asynchronously (to not block the
|
||||
mud while its running), the order at which things are
|
||||
updated does matter.
|
||||
"""
|
||||
|
||||
def run_loop():
|
||||
""
|
||||
cemit_info('-'*50)
|
||||
cemit_info(" Starting asynchronous server reload.")
|
||||
reload_modules()
|
||||
reload_scripts()
|
||||
reload_commands()
|
||||
reset_loop()
|
||||
|
||||
def at_return(r):
|
||||
"default callback"
|
||||
cemit_info(" Asynchronous server reload finished.\n" + '-'*50)
|
||||
def at_err(e):
|
||||
"error callback"
|
||||
string = " Reload: Asynchronous reset exited with an error:\n {r%s{n" % e.getErrorMessage()
|
||||
cemit_info(string)
|
||||
|
||||
utils.run_async(run_loop, at_return, at_err)
|
||||
|
||||
|
||||
def reload_modules():
|
||||
"""
|
||||
Reload modules that don't have any variables that can be reset.
|
||||
Note that python reloading is a tricky art and strange things have
|
||||
been known to happen if debugging and reloading a lot. A server
|
||||
cold reboot is often needed eventually.
|
||||
|
||||
"""
|
||||
# We protect e.g. src/ from reload since reloading it in a running
|
||||
# server can create unexpected results (and besides, non-evennia devs
|
||||
# should never need to do that anyway). Updating src requires a server
|
||||
# reboot. Modules in except_dirs are considered ok to reload despite being
|
||||
# inside src/
|
||||
protected_dirs = ('src.', 'django.', 'twisted.') # note that these MUST be tuples!
|
||||
except_dirs = ('src.commands.default.',) # "
|
||||
|
||||
# flag 'dangerous' typeclasses (those which retain a memory
|
||||
# reference, notably Scripts with a timer component) for
|
||||
# non-reload, since these cannot be safely cleaned from memory
|
||||
# without causing havoc. A server reboot is required for updating
|
||||
# these (or killing all running, timed scripts).
|
||||
unsafe_modules = []
|
||||
for scriptobj in ScriptDB.objects.get_all_scripts():
|
||||
if (scriptobj.interval > -1) and scriptobj.typeclass_path:
|
||||
unsafe_modules.append(scriptobj.typeclass_path)
|
||||
unsafe_modules = list(set(unsafe_modules))
|
||||
|
||||
def safe_dir_to_reload(modpath):
|
||||
"Check so modpath is not a subdir of a protected dir, and not an ok exception"
|
||||
return not any(modpath.startswith(pdir) and not any(modpath.startswith(edir) for edir in except_dirs) for pdir in protected_dirs)
|
||||
def safe_mod_to_reload(modpath):
|
||||
"Check so modpath is not in an unsafe module"
|
||||
return not any(mpath.startswith(modpath) for mpath in unsafe_modules)
|
||||
|
||||
#cemit_info(" Cleaning module caches ...")
|
||||
|
||||
# clean as much of the caches as we can
|
||||
cache = AppCache()
|
||||
cache.app_store = SortedDict()
|
||||
#cache.app_models = SortedDict() # cannot clean this, it resets ContentTypes!
|
||||
cache.app_errors = {}
|
||||
cache.handled = {}
|
||||
cache.loaded = False
|
||||
|
||||
# find modified modules
|
||||
modified = reimport.modified()
|
||||
safe_dir_modified = [mod for mod in modified if safe_dir_to_reload(mod)]
|
||||
unsafe_dir_modified = [mod for mod in modified if mod not in safe_dir_modified]
|
||||
safe_modified = [mod for mod in safe_dir_modified if safe_mod_to_reload(mod)]
|
||||
unsafe_mod_modified = [mod for mod in safe_dir_modified if mod not in safe_modified]
|
||||
|
||||
string = ""
|
||||
if unsafe_dir_modified or unsafe_mod_modified:
|
||||
|
||||
if unsafe_mod_modified:
|
||||
string += "\n {rModules containing Script classes with a timer component{n"
|
||||
string += "\n {rand which has already spawned instances cannot be reloaded safely.{n"
|
||||
string += "\n {rThese module(s) can only be reloaded by server reboot:{n\n %s\n"
|
||||
string = string % ", ".join(unsafe_dir_modified + unsafe_mod_modified)
|
||||
|
||||
if string:
|
||||
cemit_info(string)
|
||||
|
||||
if safe_modified:
|
||||
cemit_info(" Reloading safe module(s):{n\n %s" % "\n ".join(safe_modified))
|
||||
reimport.reimport(*safe_modified)
|
||||
wait_time = 5
|
||||
cemit_info(" Waiting %s secs to give modules time to re-cache ..." % wait_time)
|
||||
time.sleep(wait_time)
|
||||
cemit_info(" ... all safe modules reloaded.")
|
||||
else:
|
||||
cemit_info(" No modules reloaded.")
|
||||
|
||||
# clean out cache dictionary of typeclasses, exits and channels
|
||||
channelhandler.CHANNELHANDLER.update()
|
||||
|
||||
# run through all objects in database, forcing re-caching.
|
||||
|
||||
|
||||
def reload_scripts(scripts=None, obj=None, key=None, dbref=None):
|
||||
"""
|
||||
Run a validation of the script database.
|
||||
obj - only validate scripts on this object
|
||||
key - only validate scripts with this key
|
||||
dbref - only validate the script with this unique idref
|
||||
emit_to_obj - which object to receive error message
|
||||
|
||||
"""
|
||||
|
||||
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts,
|
||||
obj=obj, key=key,
|
||||
dbref=dbref,
|
||||
init_mode=False)
|
||||
if nr_started or nr_stopped:
|
||||
string = " Started %s script(s). Stopped %s invalid script(s)." % \
|
||||
(nr_started, nr_stopped)
|
||||
cemit_info(string)
|
||||
|
||||
def reload_commands():
|
||||
from src.commands import cmdsethandler
|
||||
cmdsethandler.CACHED_CMDSETS = {}
|
||||
#cemit_info(" Cleaned cmdset cache.")
|
||||
|
||||
def reset_loop():
|
||||
"Reload and restart all entities that can be reloaded."
|
||||
# run the reset loop on all objects
|
||||
cemit_info(" Resetting all cached database entities ...")
|
||||
t1 = time.time()
|
||||
[h.locks.reset() for h in HelpEntry.objects.all()]
|
||||
[m.locks.reset() for m in Msg.objects.all()]
|
||||
[c.locks.reset() for c in Channel.objects.all()]
|
||||
[s.locks.reset() for s in ScriptDB.objects.all()]
|
||||
[(o.typeclass(o), o.cmdset.reset(), o.locks.reset(), o.at_cache()) for o in ObjectDB.get_all_cached_instances()]
|
||||
[(p.typeclass(p), p.cmdset.reset(), p.locks.reset()) for p in PlayerDB.get_all_cached_instances()]
|
||||
|
||||
t2 = time.time()
|
||||
cemit_info(" ... Reset finished in %g seconds." % (t2-t1))
|
||||
|
||||
def cemit_info(message):
|
||||
"""
|
||||
Sends the info to a pre-set channel. This channel is
|
||||
set by CHANNEL_MUDINFO in settings.
|
||||
"""
|
||||
|
||||
logger.log_infomsg(message)
|
||||
infochan = None
|
||||
try:
|
||||
infochan = settings.CHANNEL_MUDINFO
|
||||
infochan = Channel.objects.get_channel(infochan[0])
|
||||
except Exception:
|
||||
pass
|
||||
if infochan:
|
||||
cname = infochan.key
|
||||
cmessage = "\n".join(["[%s][reload]: %s" % (cname, line) for line in message.split('\n')])
|
||||
infochan.msg(cmessage)
|
||||
else:
|
||||
cmessage = "\n".join(["[MUDINFO][reload] %s" % line for line in message.split('\n')])
|
||||
logger.log_infomsg(cmessage)
|
||||
|
|
@ -3,6 +3,8 @@ General helper functions that don't fit neatly under any given category.
|
|||
|
||||
They provide some useful string and conversion methods that might
|
||||
be of use when designing your own game.
|
||||
|
||||
|
||||
"""
|
||||
import os, sys, imp
|
||||
import textwrap
|
||||
|
|
@ -475,6 +477,7 @@ def check_evennia_dependencies():
|
|||
twisted_min = '10.0'
|
||||
django_min = '1.2'
|
||||
south_min = '0.7'
|
||||
nt_stop_python_min = '2.7'
|
||||
|
||||
errstring = ""
|
||||
no_error = True
|
||||
|
|
@ -483,7 +486,9 @@ def check_evennia_dependencies():
|
|||
pversion = ".".join([str(num) for num in sys.version_info if type(num) == int])
|
||||
if pversion < python_min:
|
||||
errstring += "\n WARNING: Python %s used. Evennia recommends version %s or higher (but not 3.x)." % (pversion, python_min)
|
||||
no_error = False
|
||||
if os.name == 'nt' and pversion < nt_stop_python_min:
|
||||
errstring += "\n WARNING: Windows requires Python %s or higher in order to restart/stop the server from the command line."
|
||||
errstring += "\n (You need to restart/stop from inside the game.)" % nt_stop_python_min
|
||||
# Twisted
|
||||
try:
|
||||
import twisted
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue