mirror of
https://github.com/evennia/evennia.git
synced 2026-04-02 05:57:16 +02:00
Reshuffling the Evennia package into the new template paradigm.
This commit is contained in:
parent
2846e64833
commit
2b3a32e447
371 changed files with 17250 additions and 304 deletions
15
lib/scripts/__init__.py
Normal file
15
lib/scripts/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"""
|
||||
Makes it easier to import by grouping all relevant things already at this
|
||||
level.
|
||||
|
||||
You can henceforth import most things directly from src.scripts
|
||||
Also, the initiated object manager is available as src.scripts.manager.
|
||||
|
||||
"""
|
||||
|
||||
# Note - we MUST NOT import src.scripts.scripts here, or
|
||||
# proxy models will fall under Django migrations.
|
||||
#from src.scripts.scripts import *
|
||||
#from src.scripts.models import ScriptDB
|
||||
|
||||
#manager = ScriptDB.objects
|
||||
40
lib/scripts/admin.py
Normal file
40
lib/scripts/admin.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#
|
||||
# This sets up how models are displayed
|
||||
# in the web admin interface.
|
||||
#
|
||||
from src.typeclasses.admin import AttributeInline, TagInline
|
||||
|
||||
from src.scripts.models import ScriptDB
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
class ScriptTagInline(TagInline):
|
||||
model = ScriptDB.db_tags.through
|
||||
|
||||
|
||||
class ScriptAttributeInline(AttributeInline):
|
||||
model = ScriptDB.db_attributes.through
|
||||
|
||||
|
||||
class ScriptDBAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = ('id', 'db_key', 'db_typeclass_path',
|
||||
'db_obj', 'db_interval', 'db_repeats', 'db_persistent')
|
||||
list_display_links = ('id', 'db_key')
|
||||
ordering = ['db_obj', 'db_typeclass_path']
|
||||
search_fields = ['^db_key', 'db_typeclass_path']
|
||||
save_as = True
|
||||
save_on_top = True
|
||||
list_select_related = True
|
||||
raw_id_fields = ('db_obj',)
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': (('db_key', 'db_typeclass_path'), 'db_interval',
|
||||
'db_repeats', 'db_start_delay', 'db_persistent',
|
||||
'db_obj')}),
|
||||
)
|
||||
inlines = [ScriptTagInline, ScriptAttributeInline]
|
||||
|
||||
|
||||
admin.site.register(ScriptDB, ScriptDBAdmin)
|
||||
234
lib/scripts/manager.py
Normal file
234
lib/scripts/manager.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
"""
|
||||
The custom manager for Scripts.
|
||||
"""
|
||||
|
||||
from django.db.models import Q
|
||||
from src.typeclasses.managers import TypedObjectManager, TypeclassManager
|
||||
from src.typeclasses.managers import returns_typeclass_list
|
||||
from src.utils.utils import make_iter
|
||||
__all__ = ("ScriptManager",)
|
||||
_GA = object.__getattribute__
|
||||
|
||||
VALIDATE_ITERATION = 0
|
||||
|
||||
|
||||
class ScriptDBManager(TypedObjectManager):
|
||||
"""
|
||||
This Scriptmanager implements methods for searching
|
||||
and manipulating Scripts directly from the database.
|
||||
|
||||
Evennia-specific search methods (will return Typeclasses or
|
||||
lists of Typeclasses, whereas Django-general methods will return
|
||||
Querysets or database objects).
|
||||
|
||||
dbref (converter)
|
||||
get_id (or dbref_search)
|
||||
get_dbref_range
|
||||
object_totals
|
||||
typeclass_search
|
||||
get_all_scripts_on_obj
|
||||
get_all_scripts
|
||||
delete_script
|
||||
remove_non_persistent
|
||||
validate
|
||||
script_search (equivalent to ev.search_script)
|
||||
copy_script
|
||||
|
||||
"""
|
||||
@returns_typeclass_list
|
||||
def get_all_scripts_on_obj(self, obj, key=None):
|
||||
"""
|
||||
Returns as result all the Scripts related to a particular object.
|
||||
key can be given as a dbref or name string. If given, only scripts
|
||||
matching the key on the object will be returned.
|
||||
"""
|
||||
if not obj:
|
||||
return []
|
||||
player = _GA(_GA(obj, "__class__"), "__name__") == "PlayerDB"
|
||||
if key:
|
||||
dbref = self.dbref(key)
|
||||
if dbref or dbref == 0:
|
||||
if player:
|
||||
return self.filter(db_player=obj, id=dbref)
|
||||
else:
|
||||
return self.filter(db_obj=obj, id=dbref)
|
||||
elif player:
|
||||
return self.filter(db_player=obj, db_key=key)
|
||||
else:
|
||||
return self.filter(db_obj=obj, db_key=key)
|
||||
elif player:
|
||||
return self.filter(db_player=obj)
|
||||
else:
|
||||
return self.filter(db_obj=obj)
|
||||
|
||||
@returns_typeclass_list
|
||||
def get_all_scripts(self, key=None):
|
||||
"""
|
||||
Return all scripts, alternative only
|
||||
scripts with a certain key/dbref
|
||||
"""
|
||||
if key:
|
||||
script = []
|
||||
dbref = self.dbref(key)
|
||||
if dbref or dbref == 0:
|
||||
script = self.dbref_search(dbref)
|
||||
if not script:
|
||||
script = self.filter(db_key=key)
|
||||
return script
|
||||
return self.all()
|
||||
|
||||
def delete_script(self, dbref):
|
||||
"""
|
||||
This stops and deletes a specific script directly
|
||||
from the script database. This might be
|
||||
needed for global scripts not tied to
|
||||
a specific game object.
|
||||
"""
|
||||
scripts = self.get_id(dbref)
|
||||
for script in make_iter(scripts):
|
||||
script.stop()
|
||||
|
||||
def remove_non_persistent(self, obj=None):
|
||||
"""
|
||||
This cleans up the script database of all non-persistent
|
||||
scripts, or only those on obj. It is called every time the server
|
||||
restarts.
|
||||
"""
|
||||
if obj:
|
||||
to_stop = self.filter(db_obj=obj, db_persistent=False, db_is_active=True)
|
||||
to_delete = self.filter(db_obj=obj, db_persistent=False, db_is_active=False)
|
||||
else:
|
||||
to_stop = self.filter(db_persistent=False, db_is_active=True)
|
||||
to_delete = self.filter(db_persistent=False, db_is_active=False)
|
||||
nr_deleted = to_stop.count() + to_delete.count()
|
||||
for script in to_stop:
|
||||
script.stop()
|
||||
for script in to_delete:
|
||||
script.delete()
|
||||
return nr_deleted
|
||||
|
||||
def validate(self, scripts=None, obj=None, key=None, dbref=None,
|
||||
init_mode=False):
|
||||
"""
|
||||
This will step through the script database and make sure
|
||||
all objects run scripts that are still valid in the context
|
||||
they are in. This is called by the game engine at regular
|
||||
intervals but can also be initiated by player scripts.
|
||||
If key and/or obj is given, only update the related
|
||||
script/object.
|
||||
|
||||
Only one of the arguments are supposed to be supplied
|
||||
at a time, since they are exclusive to each other.
|
||||
|
||||
scripts = a list of scripts objects obtained somewhere.
|
||||
obj = validate only scripts defined on a special object.
|
||||
key = validate only scripts with a particular key
|
||||
dbref = validate only the single script with this particular id.
|
||||
|
||||
init_mode - This is used during server upstart and can have
|
||||
three values:
|
||||
False (no init mode). Called during run.
|
||||
"reset" - server reboot. Kill non-persistent scripts
|
||||
"reload" - server reload. Keep non-persistent scripts.
|
||||
|
||||
This method also makes sure start any scripts it validates,
|
||||
this should be harmless, since already-active scripts
|
||||
have the property 'is_running' set and will be skipped.
|
||||
"""
|
||||
|
||||
# we store a variable that tracks if we are calling a
|
||||
# validation from within another validation (avoids
|
||||
# loops).
|
||||
|
||||
global VALIDATE_ITERATION
|
||||
if VALIDATE_ITERATION > 0:
|
||||
# we are in a nested validation. Exit.
|
||||
VALIDATE_ITERATION -= 1
|
||||
return None, None
|
||||
VALIDATE_ITERATION += 1
|
||||
|
||||
# not in a validation - loop. Validate as normal.
|
||||
|
||||
nr_started = 0
|
||||
nr_stopped = 0
|
||||
|
||||
if init_mode:
|
||||
if init_mode == 'reset':
|
||||
# special mode when server starts or object logs in.
|
||||
# This deletes all non-persistent scripts from database
|
||||
nr_stopped += self.remove_non_persistent(obj=obj)
|
||||
# turn off the activity flag for all remaining scripts
|
||||
scripts = self.get_all_scripts()
|
||||
for script in scripts:
|
||||
script.is_active = False
|
||||
|
||||
elif not scripts:
|
||||
# normal operation
|
||||
if dbref and self.dbref(dbref, reqhash=False):
|
||||
scripts = self.get_id(dbref)
|
||||
elif obj:
|
||||
#print "calling get_all_scripts_on_obj", obj, key, VALIDATE_ITERATION
|
||||
scripts = self.get_all_scripts_on_obj(obj, key=key)
|
||||
else:
|
||||
scripts = self.get_all_scripts(key=key) #self.model.get_all_cached_instances()
|
||||
|
||||
if not scripts:
|
||||
# no scripts available to validate
|
||||
VALIDATE_ITERATION -= 1
|
||||
return None, None
|
||||
|
||||
#print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts))
|
||||
for script in scripts:
|
||||
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script), init_mode)
|
||||
if script.is_valid():
|
||||
nr_started += script.start(force_restart=init_mode)
|
||||
#print "back from start. nr_started=", nr_started
|
||||
else:
|
||||
script.stop()
|
||||
nr_stopped += 1
|
||||
VALIDATE_ITERATION -= 1
|
||||
return nr_started, nr_stopped
|
||||
|
||||
@returns_typeclass_list
|
||||
def script_search(self, ostring, obj=None, only_timed=False):
|
||||
"""
|
||||
Search for a particular script.
|
||||
|
||||
ostring - search criterion - a script ID or key
|
||||
obj - limit search to scripts defined on this object
|
||||
only_timed - limit search only to scripts that run
|
||||
on a timer.
|
||||
"""
|
||||
|
||||
ostring = ostring.strip()
|
||||
|
||||
dbref = self.dbref(ostring)
|
||||
if dbref or dbref == 0:
|
||||
# this is a dbref, try to find the script directly
|
||||
dbref_match = self.dbref_search(dbref)
|
||||
if dbref_match and not ((obj and obj != dbref_match.obj)
|
||||
or (only_timed and dbref_match.interval)):
|
||||
return [dbref_match]
|
||||
|
||||
# not a dbref; normal search
|
||||
obj_restriction = obj and Q(db_obj=obj) or Q()
|
||||
timed_restriction = only_timed and Q(interval__gt=0) or Q()
|
||||
scripts = self.filter(timed_restriction & obj_restriction & Q(db_key__iexact=ostring))
|
||||
return scripts
|
||||
|
||||
def copy_script(self, original_script, new_key=None, new_obj=None, new_locks=None):
|
||||
"""
|
||||
Make an identical copy of the original_script
|
||||
"""
|
||||
typeclass = original_script.typeclass_path
|
||||
new_key = new_key if new_key is not None else original_script.key
|
||||
new_obj = new_obj if new_obj is not None else original_script.obj
|
||||
new_locks = new_locks if new_locks is not None else original_script.db_lock_storage
|
||||
|
||||
from src.utils import create
|
||||
new_script = create.create_script(typeclass, key=new_key, obj=new_obj,
|
||||
locks=new_locks, autostart=True)
|
||||
return new_script
|
||||
|
||||
class ScriptManager(ScriptDBManager, TypeclassManager):
|
||||
pass
|
||||
41
lib/scripts/migrations/0001_initial.py
Normal file
41
lib/scripts/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('objects', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('typeclasses', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ScriptDB',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('db_key', models.CharField(max_length=255, verbose_name=b'key', db_index=True)),
|
||||
('db_typeclass_path', models.CharField(help_text=b"this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name=b'typeclass')),
|
||||
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
|
||||
('db_lock_storage', models.TextField(help_text=b"locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name=b'locks', blank=True)),
|
||||
('db_desc', models.CharField(max_length=255, verbose_name=b'desc', blank=True)),
|
||||
('db_interval', models.IntegerField(default=-1, help_text=b'how often to repeat script, in seconds. -1 means off.', verbose_name=b'interval')),
|
||||
('db_start_delay', models.BooleanField(default=False, help_text=b'pause interval seconds before starting.', verbose_name=b'start delay')),
|
||||
('db_repeats', models.IntegerField(default=0, help_text=b'0 means off.', verbose_name=b'number of repeats')),
|
||||
('db_persistent', models.BooleanField(default=False, verbose_name=b'survive server reboot')),
|
||||
('db_is_active', models.BooleanField(default=False, verbose_name=b'script active')),
|
||||
('db_attributes', models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute', null=True)),
|
||||
('db_obj', models.ForeignKey(blank=True, to='objects.ObjectDB', help_text=b'the object to store this script on, if not a global script.', null=True, verbose_name=b'scripted object')),
|
||||
('db_player', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, help_text=b'the player to store this script on (should not be set if obj is set)', null=True, verbose_name=b'scripted player')),
|
||||
('db_tags', models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag', null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Script',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
1
lib/scripts/migrations/__init__.py
Normal file
1
lib/scripts/migrations/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
176
lib/scripts/models.py
Normal file
176
lib/scripts/models.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""
|
||||
Scripts are entities that perform some sort of action, either only
|
||||
once or repeatedly. They can be directly linked to a particular
|
||||
Evennia Object or be stand-alonw (in the latter case it is considered
|
||||
a 'global' script). Scripts can indicate both actions related to the
|
||||
game world as well as pure behind-the-scenes events and
|
||||
effects. Everything that has a time component in the game (i.e. is not
|
||||
hard-coded at startup or directly created/controlled by players) is
|
||||
handled by Scripts.
|
||||
|
||||
Scripts have to check for themselves that they should be applied at a
|
||||
particular moment of time; this is handled by the is_valid() hook.
|
||||
Scripts can also implement at_start and at_end hooks for preparing and
|
||||
cleaning whatever effect they have had on the game object.
|
||||
|
||||
Common examples of uses of Scripts:
|
||||
- load the default cmdset to the player object's cmdhandler
|
||||
when logging in.
|
||||
- switch to a different state, such as entering a text editor,
|
||||
start combat or enter a dark room.
|
||||
- Weather patterns in-game
|
||||
- merge a new cmdset with the default one for changing which
|
||||
commands are available at a particular time
|
||||
- give the player/object a time-limited bonus/effect
|
||||
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from src.typeclasses.models import TypedObject
|
||||
from src.scripts.manager import ScriptDBManager
|
||||
from src.utils.utils import dbref, to_str
|
||||
|
||||
__all__ = ("ScriptDB",)
|
||||
_GA = object.__getattribute__
|
||||
_SA = object.__setattr__
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# ScriptDB
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class ScriptDB(TypedObject):
|
||||
"""
|
||||
The Script database representation.
|
||||
|
||||
The TypedObject supplies the following (inherited) properties:
|
||||
key - main name
|
||||
name - alias for key
|
||||
typeclass_path - the path to the decorating typeclass
|
||||
typeclass - auto-linked typeclass
|
||||
date_created - time stamp of object creation
|
||||
permissions - perm strings
|
||||
dbref - #id of object
|
||||
db - persistent attribute storage
|
||||
ndb - non-persistent attribute storage
|
||||
|
||||
The ScriptDB adds the following properties:
|
||||
desc - optional description of script
|
||||
obj - the object the script is linked to, if any
|
||||
player - the player the script is linked to (exclusive with obj)
|
||||
interval - how often script should run
|
||||
start_delay - if the script should start repeating right away
|
||||
repeats - how many times the script should repeat
|
||||
persistent - if script should survive a server reboot
|
||||
is_active - bool if script is currently running
|
||||
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# ScriptDB Database Model setup
|
||||
#
|
||||
# These database fields are all set using their corresponding properties,
|
||||
# named same as the field, but withtou the db_* prefix.
|
||||
|
||||
# inherited fields (from TypedObject):
|
||||
# db_key, db_typeclass_path, db_date_created, db_permissions
|
||||
|
||||
# optional description.
|
||||
db_desc = models.CharField('desc', max_length=255, blank=True)
|
||||
# A reference to the database object affected by this Script, if any.
|
||||
db_obj = models.ForeignKey("objects.ObjectDB", null=True, blank=True, verbose_name='scripted object',
|
||||
help_text='the object to store this script on, if not a global script.')
|
||||
db_player = models.ForeignKey("players.PlayerDB", null=True, blank=True, verbose_name="scripted player",
|
||||
help_text='the player to store this script on (should not be set if obj is set)')
|
||||
# how often to run Script (secs). -1 means there is no timer
|
||||
db_interval = models.IntegerField('interval', default=-1, help_text='how often to repeat script, in seconds. -1 means off.')
|
||||
# start script right away or wait interval seconds first
|
||||
db_start_delay = models.BooleanField('start delay', default=False, help_text='pause interval seconds before starting.')
|
||||
# how many times this script is to be repeated, if interval!=0.
|
||||
db_repeats = models.IntegerField('number of repeats', default=0, help_text='0 means off.')
|
||||
# defines if this script should survive a reboot or not
|
||||
db_persistent = models.BooleanField('survive server reboot', default=False)
|
||||
# defines if this script has already been started in this session
|
||||
db_is_active = models.BooleanField('script active', default=False)
|
||||
|
||||
# Database manager
|
||||
objects = ScriptDBManager()
|
||||
|
||||
class Meta:
|
||||
"Define Django meta options"
|
||||
verbose_name = "Script"
|
||||
|
||||
#
|
||||
#
|
||||
# ScriptDB class properties
|
||||
#
|
||||
#
|
||||
|
||||
# obj property
|
||||
def __get_obj(self):
|
||||
"""
|
||||
property wrapper that homogenizes access to either
|
||||
the db_player or db_obj field, using the same obj
|
||||
property name
|
||||
"""
|
||||
obj = _GA(self, "db_player")
|
||||
if not obj:
|
||||
obj = _GA(self, "db_obj")
|
||||
return obj
|
||||
|
||||
def __set_obj(self, value):
|
||||
"""
|
||||
Set player or obj to their right database field. If
|
||||
a dbref is given, assume ObjectDB.
|
||||
"""
|
||||
try:
|
||||
value = _GA(value, "dbobj")
|
||||
except AttributeError:
|
||||
pass
|
||||
if isinstance(value, (basestring, int)):
|
||||
from src.objects.models import ObjectDB
|
||||
value = to_str(value, force_string=True)
|
||||
if (value.isdigit() or value.startswith("#")):
|
||||
dbid = dbref(value, reqhash=False)
|
||||
if dbid:
|
||||
try:
|
||||
value = ObjectDB.objects.get(id=dbid)
|
||||
except ObjectDoesNotExist:
|
||||
# maybe it is just a name that happens to look like a dbid
|
||||
pass
|
||||
if value.__class__.__name__ == "PlayerDB":
|
||||
fname = "db_player"
|
||||
_SA(self, fname, value)
|
||||
else:
|
||||
fname = "db_obj"
|
||||
_SA(self, fname, value)
|
||||
# saving the field
|
||||
_GA(self, "save")(update_fields=[fname])
|
||||
obj = property(__get_obj, __set_obj)
|
||||
object = property(__get_obj, __set_obj)
|
||||
|
||||
|
||||
# def at_typeclass_error(self):
|
||||
# """
|
||||
# If this is called, it means the typeclass has a critical
|
||||
# error and cannot even be loaded. We don't allow a script
|
||||
# to be created under those circumstances. Already created,
|
||||
# permanent scripts are set to already be active so they
|
||||
# won't get activated now (next reboot the bug might be fixed)
|
||||
# """
|
||||
# # By setting is_active=True, we trick the script not to run "again".
|
||||
# self.is_active = True
|
||||
# return super(ScriptDB, self).at_typeclass_error()
|
||||
#
|
||||
# delete_iter = 0
|
||||
# def delete(self):
|
||||
# "Delete script"
|
||||
# if self.delete_iter > 0:
|
||||
# return
|
||||
# self.delete_iter += 1
|
||||
# _GA(self, "attributes").clear()
|
||||
# super(ScriptDB, self).delete()
|
||||
126
lib/scripts/scripthandler.py
Normal file
126
lib/scripts/scripthandler.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
The script handler makes sure to check through all stored scripts
|
||||
to make sure they are still relevant.
|
||||
An scripthandler is automatically added to all game objects. You
|
||||
access it through the property 'scripts' on the game object.
|
||||
"""
|
||||
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.utils import create
|
||||
from src.utils import logger
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
class ScriptHandler(object):
|
||||
"""
|
||||
Implements the handler. This sits on each game object.
|
||||
"""
|
||||
def __init__(self, obj):
|
||||
"""
|
||||
Set up internal state.
|
||||
obj - a reference to the object this handler is attached to.
|
||||
|
||||
We retrieve all scripts attached to this object and check
|
||||
if they are all peristent. If they are not, they are just
|
||||
cruft left over from a server shutdown.
|
||||
"""
|
||||
self.obj = obj
|
||||
|
||||
def __str__(self):
|
||||
"List the scripts tied to this object"
|
||||
scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj)
|
||||
string = ""
|
||||
for script in scripts:
|
||||
interval = "inf"
|
||||
next_repeat = "inf"
|
||||
repeats = "inf"
|
||||
if script.interval > 0:
|
||||
interval = script.interval
|
||||
if script.repeats:
|
||||
repeats = script.repeats
|
||||
try:
|
||||
next_repeat = script.time_until_next_repeat()
|
||||
except:
|
||||
next_repeat = "?"
|
||||
string += _("\n '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s") % \
|
||||
{"key": script.key, "next_repeat": next_repeat,
|
||||
"interval": interval, "repeats": repeats, "desc": script.desc}
|
||||
return string.strip()
|
||||
|
||||
def add(self, scriptclass, key=None, autostart=True):
|
||||
"""
|
||||
Add an script to this object.
|
||||
|
||||
scriptclass - either a class object
|
||||
inheriting from Script, an instantiated script object
|
||||
or a python path to such a class object.
|
||||
key - optional identifier for the script (often set in script
|
||||
definition)
|
||||
autostart - start the script upon adding it
|
||||
"""
|
||||
if self.obj.__class__.__name__ == "PlayerDB":
|
||||
# we add to a Player, not an Object
|
||||
script = create.create_script(scriptclass, key=key, player=self.obj,
|
||||
autostart=autostart)
|
||||
else:
|
||||
# the normal - adding to an Object
|
||||
script = create.create_script(scriptclass, key=key, obj=self.obj,
|
||||
autostart=autostart)
|
||||
if not script:
|
||||
logger.log_errmsg("Script %s could not be created and/or started." % scriptclass)
|
||||
return False
|
||||
return True
|
||||
|
||||
def start(self, scriptid):
|
||||
"""
|
||||
Find an already added script and force-start it
|
||||
"""
|
||||
scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptid)
|
||||
num = 0
|
||||
for script in scripts:
|
||||
num += script.start()
|
||||
return num
|
||||
|
||||
def get(self, scriptid):
|
||||
"""
|
||||
Return one or all scripts on this object matching scriptid. Will return
|
||||
a list.
|
||||
"""
|
||||
return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptid)
|
||||
|
||||
def delete(self, scriptid=None):
|
||||
"""
|
||||
Forcibly delete a script from this object.
|
||||
|
||||
scriptid can be a script key or the path to a script (in the
|
||||
latter case all scripts with this path will be deleted!)
|
||||
If no scriptid is set, delete all scripts on the object.
|
||||
|
||||
"""
|
||||
delscripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptid)
|
||||
if not delscripts:
|
||||
delscripts = [script for script in ScriptDB.objects.get_all_scripts_on_obj(self.obj) if script.path == scriptid]
|
||||
num = 0
|
||||
for script in delscripts:
|
||||
num += script.stop()
|
||||
return num
|
||||
|
||||
def stop(self, scriptid=None):
|
||||
"""
|
||||
Alias for delete. scriptid can be a script key or a script path string.
|
||||
"""
|
||||
return self.delete(scriptid)
|
||||
|
||||
def all(self, scriptid=None):
|
||||
"""
|
||||
Get all scripts stored in the handler, alternatively all matching a key.
|
||||
"""
|
||||
return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptid)
|
||||
|
||||
def validate(self, init_mode=False):
|
||||
"""
|
||||
Runs a validation on this object's scripts only.
|
||||
This should be called regularly to crank the wheels.
|
||||
"""
|
||||
ScriptDB.objects.validate(obj=self.obj, init_mode=init_mode)
|
||||
|
||||
610
lib/scripts/scripts.py
Normal file
610
lib/scripts/scripts.py
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
"""
|
||||
This module contains the base Script class that all
|
||||
scripts are inheriting from.
|
||||
|
||||
It also defines a few common scripts.
|
||||
"""
|
||||
|
||||
from twisted.internet.defer import Deferred, maybeDeferred
|
||||
from twisted.internet.task import LoopingCall
|
||||
from django.conf import settings
|
||||
from src.typeclasses.models import TypeclassBase
|
||||
from django.utils.translation import ugettext as _
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.scripts.manager import ScriptManager
|
||||
from src.comms import channelhandler
|
||||
from src.utils import logger
|
||||
|
||||
__all__ = ["Script", "DoNothing", "CheckSessions",
|
||||
"ValidateScripts", "ValidateChannelHandler"]
|
||||
|
||||
_GA = object.__getattribute__
|
||||
_SESSIONS = None
|
||||
|
||||
class ExtendedLoopingCall(LoopingCall):
|
||||
"""
|
||||
LoopingCall that can start at a delay different
|
||||
than self.interval.
|
||||
"""
|
||||
start_delay = None
|
||||
callcount = 0
|
||||
|
||||
def start(self, interval, now=True, start_delay=None, count_start=0):
|
||||
"""
|
||||
Start running function every interval seconds.
|
||||
|
||||
This overloads the LoopingCall default by offering
|
||||
the start_delay keyword and ability to repeat.
|
||||
|
||||
start_delay: The number of seconds before starting.
|
||||
If None, wait interval seconds. Only
|
||||
valid is now is False.
|
||||
repeat_start: the task will track how many times it has run.
|
||||
this will change where it starts counting from.
|
||||
Note that as opposed to Twisted's inbuild
|
||||
counter, this will count also if force_repeat()
|
||||
was called (so it will not just count the number
|
||||
of interval seconds since start).
|
||||
"""
|
||||
assert not self.running, ("Tried to start an already running "
|
||||
"ExtendedLoopingCall.")
|
||||
if interval < 0:
|
||||
raise ValueError, "interval must be >= 0"
|
||||
|
||||
self.running = True
|
||||
d = self.deferred = Deferred()
|
||||
self.starttime = self.clock.seconds()
|
||||
self._expectNextCallAt = self.starttime
|
||||
self.interval = interval
|
||||
self._runAtStart = now
|
||||
self.callcount = max(0, count_start)
|
||||
|
||||
if now:
|
||||
self()
|
||||
else:
|
||||
if start_delay is not None and start_delay >= 0:
|
||||
# we set start_delay after the _reshedule call to make
|
||||
# next_call_time() find it until next reshedule.
|
||||
self.interval = start_delay
|
||||
self._reschedule()
|
||||
self.interval = interval
|
||||
self.start_delay = start_delay
|
||||
else:
|
||||
self._reschedule()
|
||||
return d
|
||||
|
||||
def __call__(self):
|
||||
"tick one step"
|
||||
self.callcount += 1
|
||||
super(ExtendedLoopingCall, self).__call__()
|
||||
|
||||
def _reschedule(self):
|
||||
"""
|
||||
Handle call rescheduling including
|
||||
nulling start_delay and stopping if
|
||||
number of repeats is reached.
|
||||
"""
|
||||
self.start_delay = None
|
||||
super(ExtendedLoopingCall, self)._reschedule()
|
||||
|
||||
def force_repeat(self):
|
||||
"Force-fire the callback"
|
||||
assert self.running, ("Tried to fire an ExtendedLoopingCall "
|
||||
"that was not running.")
|
||||
if self.call is not None:
|
||||
self.call.cancel()
|
||||
self._expectNextCallAt = self.clock.seconds()
|
||||
self()
|
||||
|
||||
def next_call_time(self):
|
||||
"""
|
||||
Return the time in seconds until the next call. This takes
|
||||
start_delay into account.
|
||||
"""
|
||||
if self.running:
|
||||
currentTime = self.clock.seconds()
|
||||
return self._expectNextCallAt - currentTime
|
||||
return None
|
||||
|
||||
#
|
||||
# Base script, inherit from Script below instead.
|
||||
#
|
||||
class ScriptBase(ScriptDB):
|
||||
"""
|
||||
Base class for scripts. Don't inherit from this, inherit
|
||||
from the class 'Script' instead.
|
||||
"""
|
||||
__metaclass__ = TypeclassBase
|
||||
objects = ScriptManager()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
This has to be located at this level, having it in the
|
||||
parent doesn't work.
|
||||
"""
|
||||
try:
|
||||
return other.dbid == self.dbid
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _start_task(self):
|
||||
"start task runner"
|
||||
|
||||
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
||||
|
||||
if self.db._paused_time:
|
||||
# the script was paused; restarting
|
||||
callcount = self.db._paused_callcount or 0
|
||||
self.ndb._task.start(self.db_interval,
|
||||
now=False,
|
||||
start_delay=self.db._paused_time,
|
||||
count_start=callcount)
|
||||
del self.db._paused_time
|
||||
del self.db._paused_repeats
|
||||
else:
|
||||
# starting script anew
|
||||
self.ndb._task.start(self.db_interval,
|
||||
now=not self.db_start_delay)
|
||||
|
||||
def _stop_task(self):
|
||||
"stop task runner"
|
||||
task = self.ndb._task
|
||||
if task and task.running:
|
||||
task.stop()
|
||||
|
||||
def _step_errback(self, e):
|
||||
"callback for runner errors"
|
||||
cname = self.__class__.__name__
|
||||
estring = _("Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'.") % \
|
||||
{"key": self.key, "dbid": self.dbid, "cname": cname,
|
||||
"err": e.getErrorMessage()}
|
||||
try:
|
||||
self.db_obj.msg(estring)
|
||||
except Exception:
|
||||
pass
|
||||
logger.log_errmsg(estring)
|
||||
|
||||
def _step_callback(self):
|
||||
"step task runner. No try..except needed due to defer wrap."
|
||||
|
||||
if not self.is_valid():
|
||||
self.stop()
|
||||
return
|
||||
|
||||
# call hook
|
||||
self.at_repeat()
|
||||
|
||||
# check repeats
|
||||
callcount = self.ndb._task.callcount
|
||||
maxcount = self.db_repeats
|
||||
if maxcount > 0 and maxcount <= callcount:
|
||||
#print "stopping script!"
|
||||
self.stop()
|
||||
|
||||
def _step_task(self):
|
||||
"Step task. This groups error handling."
|
||||
try:
|
||||
return maybeDeferred(self._step_callback).addErrback(self._step_errback)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
|
||||
# Public methods
|
||||
|
||||
def time_until_next_repeat(self):
|
||||
"""
|
||||
Returns the time in seconds until the script will be
|
||||
run again. If this is not a stepping script, returns None.
|
||||
This is not used in any way by the script's stepping
|
||||
system; it's only here for the user to be able to
|
||||
check in on their scripts and when they will next be run.
|
||||
"""
|
||||
task = self.ndb._task
|
||||
if task:
|
||||
try:
|
||||
return int(round(task.next_call_time()))
|
||||
except TypeError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def remaining_repeats(self):
|
||||
"Get the number of returning repeats. Returns None if unlimited repeats."
|
||||
task = self.ndb._task
|
||||
if task:
|
||||
return max(0, self.db_repeats - task.callcount)
|
||||
|
||||
def start(self, force_restart=False):
|
||||
"""
|
||||
Called every time the script is started (for
|
||||
persistent scripts, this is usually once every server start)
|
||||
|
||||
force_restart - if True, will always restart the script, regardless
|
||||
of if it has started before.
|
||||
|
||||
returns 0 or 1 to indicated the script has been started or not.
|
||||
Used in counting.
|
||||
"""
|
||||
|
||||
if self.is_active and not force_restart:
|
||||
# script already runs and should not be restarted.
|
||||
return 0
|
||||
|
||||
obj = self.obj
|
||||
if obj:
|
||||
# check so the scripted object is valid and initalized
|
||||
try:
|
||||
obj.cmdset
|
||||
except AttributeError:
|
||||
# this means the object is not initialized.
|
||||
logger.log_trace()
|
||||
self.is_active = False
|
||||
return 0
|
||||
|
||||
# try to restart a paused script
|
||||
if self.unpause():
|
||||
return 1
|
||||
|
||||
# start the script from scratch
|
||||
self.is_active = True
|
||||
try:
|
||||
self.at_start()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
|
||||
if self.db_interval > 0:
|
||||
self._start_task()
|
||||
return 1
|
||||
|
||||
def stop(self, kill=False):
|
||||
"""
|
||||
Called to stop the script from running.
|
||||
This also deletes the script.
|
||||
|
||||
kill - don't call finishing hooks.
|
||||
"""
|
||||
#print "stopping script %s" % self.key
|
||||
#import pdb
|
||||
#pdb.set_trace()
|
||||
if not kill:
|
||||
try:
|
||||
self.at_stop()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
self._stop_task()
|
||||
try:
|
||||
self.delete()
|
||||
except AssertionError:
|
||||
logger.log_trace()
|
||||
return 0
|
||||
return 1
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
This stops a running script and stores its active state.
|
||||
It WILL NOT call that at_stop() hook.
|
||||
"""
|
||||
if not self.db._paused_time:
|
||||
# only allow pause if not already paused
|
||||
task = self.ndb._task
|
||||
if task:
|
||||
self.db._paused_time = task.next_call_time()
|
||||
self.db._paused_callcount = task.callcount
|
||||
self._stop_task()
|
||||
self.is_active = False
|
||||
|
||||
def unpause(self):
|
||||
"""
|
||||
Restart a paused script. This WILL call the at_start() hook.
|
||||
"""
|
||||
if self.db._paused_time:
|
||||
# only unpause if previously paused
|
||||
self.is_active = True
|
||||
|
||||
try:
|
||||
self.at_start()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
|
||||
self._start_task()
|
||||
return True
|
||||
|
||||
def force_repeat(self):
|
||||
"""
|
||||
Fire a premature triggering of the script callback. This
|
||||
will reset the timer and count down repeats as if the script
|
||||
had fired normally.
|
||||
"""
|
||||
task = self.ndb._task
|
||||
if task:
|
||||
task.force_repeat()
|
||||
|
||||
# hooks
|
||||
def at_script_creation(self):
|
||||
"placeholder"
|
||||
pass
|
||||
|
||||
def is_valid(self):
|
||||
"placeholder"
|
||||
pass
|
||||
|
||||
def at_start(self):
|
||||
"placeholder."
|
||||
pass
|
||||
|
||||
def at_stop(self):
|
||||
"placeholder"
|
||||
pass
|
||||
|
||||
def at_repeat(self):
|
||||
"placeholder"
|
||||
pass
|
||||
|
||||
def at_init(self):
|
||||
"called when typeclass re-caches. Usually not used for scripts."
|
||||
pass
|
||||
|
||||
#
|
||||
# Base Script - inherit from this
|
||||
#
|
||||
|
||||
class Script(ScriptBase):
|
||||
"""
|
||||
This is the base TypeClass for all Scripts. Scripts describe events,
|
||||
timers and states in game, they can have a time component or describe
|
||||
a state that changes under certain conditions.
|
||||
|
||||
Script API:
|
||||
|
||||
* Available properties (only available on initiated Typeclass objects)
|
||||
|
||||
key (string) - name of object
|
||||
name (string)- same as key
|
||||
aliases (list of strings) - aliases to the object. Will be saved to
|
||||
database as AliasDB entries but returned as strings.
|
||||
dbref (int, read-only) - unique #id-number. Also "id" can be used.
|
||||
date_created (string) - time stamp of object creation
|
||||
permissions (list of strings) - list of permission strings
|
||||
|
||||
desc (string) - optional description of script, shown in listings
|
||||
obj (Object) - optional object that this script is connected to
|
||||
and acts on (set automatically
|
||||
by obj.scripts.add())
|
||||
interval (int) - how often script should run, in seconds.
|
||||
<=0 turns off ticker
|
||||
start_delay (bool) - if the script should start repeating right
|
||||
away or wait self.interval seconds
|
||||
repeats (int) - how many times the script should repeat before
|
||||
stopping. <=0 means infinite repeats
|
||||
persistent (bool) - if script should survive a server shutdown or not
|
||||
is_active (bool) - if script is currently running
|
||||
|
||||
* Handlers
|
||||
|
||||
locks - lock-handler: use locks.add() to add new lock strings
|
||||
db - attribute-handler: store/retrieve database attributes on this
|
||||
self.db.myattr=val, val=self.db.myattr
|
||||
ndb - non-persistent attribute handler: same as db but does not
|
||||
create a database entry when storing data
|
||||
|
||||
* Helper methods
|
||||
|
||||
start() - start script (this usually happens automatically at creation
|
||||
and obj.script.add() etc)
|
||||
stop() - stop script, and delete it
|
||||
pause() - put the script on hold, until unpause() is called. If script
|
||||
is persistent, the pause state will survive a shutdown.
|
||||
unpause() - restart a previously paused script. The script will
|
||||
continue as if it was never paused.
|
||||
force_repeat() - force-step the script, regardless of how much remains
|
||||
until next step. This counts like a normal firing in all ways.
|
||||
time_until_next_repeat() - if a timed script (interval>0), returns
|
||||
time until next tick
|
||||
remaining_repeats() - number of repeats remaining, if limited
|
||||
|
||||
* Hook methods
|
||||
|
||||
at_script_creation() - called only once, when an object of this
|
||||
class is first created.
|
||||
is_valid() - is called to check if the script is valid to be running
|
||||
at the current time. If is_valid() returns False, the
|
||||
running script is stopped and removed from the game. You
|
||||
can use this to check state changes (i.e. an script
|
||||
tracking some combat stats at regular intervals is only
|
||||
valid to run while there is actual combat going on).
|
||||
at_start() - Called every time the script is started, which for
|
||||
persistent scripts is at least once every server start.
|
||||
Note that this is unaffected by self.delay_start, which
|
||||
only delays the first call to at_repeat(). It will also
|
||||
be called after a pause, to allow for setting up the script.
|
||||
at_repeat() - Called every self.interval seconds. It will be called
|
||||
immediately upon launch unless self.delay_start is True,
|
||||
which will delay the first call of this method by
|
||||
self.interval seconds. If self.interval<=0, this method
|
||||
will never be called.
|
||||
at_stop() - Called as the script object is stopped and is about to
|
||||
be removed from the game, e.g. because is_valid()
|
||||
returned False or self.stop() was called manually.
|
||||
at_server_reload() - Called when server reloads. Can be used to save
|
||||
temporary variables you want should survive a reload.
|
||||
at_server_shutdown() - called at a full server shutdown.
|
||||
|
||||
"""
|
||||
def at_first_save(self):
|
||||
"""
|
||||
This is called after very first time this object is saved.
|
||||
Generally, you don't need to overload this, but only the hooks
|
||||
called by this method.
|
||||
"""
|
||||
self.at_script_creation()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this will only be set if the utils.create_script
|
||||
# function was used to create the object. We want
|
||||
# the create call's kwargs to override the values
|
||||
# set by hooks.
|
||||
cdict = self._createdict
|
||||
updates = []
|
||||
if not cdict.get("key"):
|
||||
if not self.db_key:
|
||||
self.db_key = "#%i" % self.dbid
|
||||
updates.append("db_key")
|
||||
elif self.db_key != cdict["key"]:
|
||||
self.db_key = cdict["key"]
|
||||
updates.append("db_key")
|
||||
if cdict.get("interval") and self.interval != cdict["interval"]:
|
||||
self.db_interval = cdict["interval"]
|
||||
updates.append("db_interval")
|
||||
if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]:
|
||||
self.db_start_delay = cdict["start_delay"]
|
||||
updates.append("db_start_delay")
|
||||
if cdict.get("repeats") and self.repeats != cdict["repeats"]:
|
||||
self.db_repeats = cdict["repeats"]
|
||||
updates.append("db_repeats")
|
||||
if cdict.get("persistent") and self.persistent != cdict["persistent"]:
|
||||
self.db_persistent = cdict["persistent"]
|
||||
updates.append("db_persistent")
|
||||
if updates:
|
||||
self.save(update_fields=updates)
|
||||
if not cdict.get("autostart"):
|
||||
# don't auto-start the script
|
||||
return
|
||||
|
||||
# auto-start script (default)
|
||||
self.start()
|
||||
|
||||
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
Only called once, by the create function.
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
Is called to check if the script is valid to run at this time.
|
||||
Should return a boolean. The method is assumed to collect all needed
|
||||
information from its related self.obj.
|
||||
"""
|
||||
return not self._is_deleted
|
||||
|
||||
def at_start(self):
|
||||
"""
|
||||
Called whenever the script is started, which for persistent
|
||||
scripts is at least once every server start. It will also be called
|
||||
when starting again after a pause (such as after a server reload)
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
Called repeatedly if this Script is set to repeat
|
||||
regularly.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_stop(self):
|
||||
"""
|
||||
Called whenever when it's time for this script to stop
|
||||
(either because is_valid returned False or it runs out of iterations)
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_reload(self):
|
||||
"""
|
||||
This hook is called whenever the server is shutting down for
|
||||
restart/reboot. If you want to, for example, save non-persistent
|
||||
properties across a restart, this is the place to do it.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_shutdown(self):
|
||||
"""
|
||||
This hook is called whenever the server is shutting down fully
|
||||
(i.e. not for a restart).
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Some useful default Script types used by Evennia.
|
||||
|
||||
class DoNothing(Script):
|
||||
"An script that does nothing. Used as default fallback."
|
||||
def at_script_creation(self):
|
||||
"Setup the script"
|
||||
self.key = "sys_do_nothing"
|
||||
self.desc = _("This is an empty placeholder script.")
|
||||
|
||||
|
||||
class Store(Script):
|
||||
"Simple storage script"
|
||||
def at_script_creation(self):
|
||||
"Setup the script"
|
||||
self.key = "sys_storage"
|
||||
self.desc = _("This is a generic storage container.")
|
||||
|
||||
|
||||
class CheckSessions(Script):
|
||||
"Check sessions regularly."
|
||||
def at_script_creation(self):
|
||||
"Setup the script"
|
||||
self.key = "sys_session_check"
|
||||
self.desc = _("Checks sessions so they are live.")
|
||||
self.interval = 60 # repeat every 60 seconds
|
||||
self.persistent = True
|
||||
|
||||
def at_repeat(self):
|
||||
"called every 60 seconds"
|
||||
global _SESSIONS
|
||||
if not _SESSIONS:
|
||||
from src.server.sessionhandler import SESSIONS as _SESSIONS
|
||||
#print "session check!"
|
||||
#print "ValidateSessions run"
|
||||
_SESSIONS.validate_sessions()
|
||||
|
||||
_FLUSH_CACHE = None
|
||||
_IDMAPPER_CACHE_MAX_MEMORY = settings.IDMAPPER_CACHE_MAXSIZE
|
||||
class ValidateIdmapperCache(Script):
|
||||
"""
|
||||
Check memory use of idmapper cache
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = "sys_cache_validate"
|
||||
self.desc = _("Restrains size of idmapper cache.")
|
||||
self.interval = 61 * 5 # staggered compared to session check
|
||||
self.persistent = True
|
||||
|
||||
def at_repeat(self):
|
||||
"Called every ~5 mins"
|
||||
global _FLUSH_CACHE
|
||||
if not _FLUSH_CACHE:
|
||||
from src.utils.idmapper.base import conditional_flush as _FLUSH_CACHE
|
||||
_FLUSH_CACHE(_IDMAPPER_CACHE_MAX_MEMORY)
|
||||
|
||||
class ValidateScripts(Script):
|
||||
"Check script validation regularly"
|
||||
def at_script_creation(self):
|
||||
"Setup the script"
|
||||
self.key = "sys_scripts_validate"
|
||||
self.desc = _("Validates all scripts regularly.")
|
||||
self.interval = 3600 # validate every hour.
|
||||
self.persistent = True
|
||||
|
||||
def at_repeat(self):
|
||||
"called every hour"
|
||||
#print "ValidateScripts run."
|
||||
ScriptDB.objects.validate()
|
||||
|
||||
|
||||
class ValidateChannelHandler(Script):
|
||||
"Update the channelhandler to make sure it's in sync."
|
||||
def at_script_creation(self):
|
||||
"Setup the script"
|
||||
self.key = "sys_channels_validate"
|
||||
self.desc = _("Updates the channel handler")
|
||||
self.interval = 3700 # validate a little later than ValidateScripts
|
||||
self.persistent = True
|
||||
|
||||
def at_repeat(self):
|
||||
"called every hour+"
|
||||
#print "ValidateChannelHandler run."
|
||||
channelhandler.CHANNELHANDLER.update()
|
||||
|
||||
329
lib/scripts/tickerhandler.py
Normal file
329
lib/scripts/tickerhandler.py
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
"""
|
||||
TickerHandler
|
||||
|
||||
This implements an efficient Ticker which uses a subscription
|
||||
model to 'tick' subscribed objects at regular intervals.
|
||||
|
||||
The ticker mechanism is used by importing and accessing
|
||||
the instantiated TICKER_HANDLER instance in this module. This
|
||||
instance is run by the server; it will save its status across
|
||||
server reloads and be started automaticall on boot.
|
||||
|
||||
Example:
|
||||
|
||||
from src.scripts.tickerhandler import TICKER_HANDLER
|
||||
|
||||
# tick myobj every 15 seconds
|
||||
TICKER_HANDLER.add(myobj, 15)
|
||||
|
||||
The handler will by default try to call a hook "at_tick()"
|
||||
on the subscribing object. The hook's name can be changed
|
||||
if the "hook_key" keyword is given to the add() method (only
|
||||
one such alternate name per interval though). The
|
||||
handler will transparently set up and add new timers behind
|
||||
the scenes to tick at given intervals, using a TickerPool.
|
||||
|
||||
To remove:
|
||||
|
||||
TICKER_HANDLER.remove(myobj, 15)
|
||||
|
||||
The interval must be given since a single object can be subcribed
|
||||
to many different tickers at the same time.
|
||||
|
||||
|
||||
The TickerHandler's functionality can be overloaded by modifying the
|
||||
Ticker class and then changing TickerPool and TickerHandler to use the
|
||||
custom classes
|
||||
|
||||
class MyTicker(Ticker):
|
||||
# [doing custom stuff]
|
||||
|
||||
class MyTickerPool(TickerPool):
|
||||
ticker_class = MyTicker
|
||||
class MyTickerHandler(TickerHandler):
|
||||
ticker_pool_class = MyTickerPool
|
||||
|
||||
If one wants to duplicate TICKER_HANDLER's auto-saving feature in
|
||||
a custom handler one can make a custom AT_STARTSTOP_MODULE entry to
|
||||
call the handler's save() and restore() methods when the server reboots.
|
||||
|
||||
"""
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
from src.scripts.scripts import ExtendedLoopingCall
|
||||
from src.server.models import ServerConfig
|
||||
from src.utils.logger import log_trace
|
||||
from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj
|
||||
|
||||
_GA = object.__getattribute__
|
||||
_SA = object.__setattr__
|
||||
|
||||
|
||||
class Ticker(object):
|
||||
"""
|
||||
Represents a repeatedly running task that calls
|
||||
hooks repeatedly. Overload _callback to change the
|
||||
way it operates.
|
||||
"""
|
||||
|
||||
@inlineCallbacks
|
||||
def _callback(self):
|
||||
"""
|
||||
This will be called repeatedly every self.interval seconds.
|
||||
self.subscriptions contain tuples of (obj, args, kwargs) for
|
||||
each subscribing object.
|
||||
|
||||
If overloading, this callback is expected to handle all
|
||||
subscriptions when it is triggered. It should not return
|
||||
anything and should not traceback on poorly designed hooks.
|
||||
The callback should ideally work under @inlineCallbacks so it can yield
|
||||
appropriately.
|
||||
"""
|
||||
for key, (obj, args, kwargs) in self.subscriptions.items():
|
||||
hook_key = yield kwargs.get("hook_key", "at_tick")
|
||||
if not obj:
|
||||
# object was deleted between calls
|
||||
self.validate()
|
||||
continue
|
||||
try:
|
||||
yield _GA(obj, hook_key)(*args, **kwargs)
|
||||
except Exception:
|
||||
log_trace()
|
||||
|
||||
def __init__(self, interval):
|
||||
"""
|
||||
Set up the ticker
|
||||
"""
|
||||
self.interval = interval
|
||||
self.subscriptions = {}
|
||||
# set up a twisted asynchronous repeat call
|
||||
self.task = ExtendedLoopingCall(self._callback)
|
||||
|
||||
def validate(self, start_delay=None):
|
||||
"""
|
||||
Start/stop the task depending on how many
|
||||
subscribers we have using it.
|
||||
"""
|
||||
subs = self.subscriptions
|
||||
if None in subs.values():
|
||||
# clean out objects that may have been deleted
|
||||
subs = dict((store_key, obj) for store_key, obj in subs if obj)
|
||||
self.subscriptions = subs
|
||||
if self.task.running:
|
||||
if not subs:
|
||||
self.task.stop()
|
||||
elif subs:
|
||||
#print "starting with start_delay=", start_delay
|
||||
self.task.start(self.interval, now=False, start_delay=start_delay)
|
||||
|
||||
def add(self, store_key, obj, *args, **kwargs):
|
||||
"""
|
||||
Sign up a subscriber to this ticker. If kwargs contains
|
||||
a keyword _start_delay, this will be used to delay the start
|
||||
of the trigger instead of interval.
|
||||
"""
|
||||
start_delay = kwargs.pop("_start_delay", None)
|
||||
self.subscriptions[store_key] = (obj, args, kwargs)
|
||||
self.validate(start_delay=start_delay)
|
||||
|
||||
def remove(self, store_key):
|
||||
"""
|
||||
Unsubscribe object from this ticker
|
||||
"""
|
||||
self.subscriptions.pop(store_key, False)
|
||||
self.validate()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Kill the Task, regardless of subscriptions
|
||||
"""
|
||||
self.subscriptions = {}
|
||||
self.validate()
|
||||
|
||||
|
||||
class TickerPool(object):
|
||||
"""
|
||||
This maintains a pool of Twisted LoopingCall tasks
|
||||
for calling subscribed objects at given times.
|
||||
"""
|
||||
ticker_class = Ticker
|
||||
|
||||
def __init__(self):
|
||||
"Initialize the pool"
|
||||
self.tickers = {}
|
||||
|
||||
def add(self, store_key, obj, interval, *args, **kwargs):
|
||||
"""
|
||||
Add new ticker subscriber
|
||||
"""
|
||||
if interval not in self.tickers:
|
||||
self.tickers[interval] = self.ticker_class(interval)
|
||||
self.tickers[interval].add(store_key, obj, *args, **kwargs)
|
||||
|
||||
def remove(self, store_key, interval):
|
||||
"""
|
||||
Remove subscription from pool
|
||||
"""
|
||||
if interval in self.tickers:
|
||||
self.tickers[interval].remove(store_key)
|
||||
|
||||
def stop(self, interval=None):
|
||||
"""
|
||||
Stop all scripts in pool. This is done at server reload since
|
||||
restoring the pool will automatically re-populate the pool.
|
||||
If interval is given, only stop tickers with that interval.
|
||||
"""
|
||||
if interval and interval in self.tickers:
|
||||
self.tickers[interval].stop()
|
||||
else:
|
||||
for ticker in self.tickers.values():
|
||||
ticker.stop()
|
||||
|
||||
|
||||
class TickerHandler(object):
|
||||
"""
|
||||
The Tickerhandler maintains a pool of tasks for subscribing
|
||||
objects to various tick rates. The pool maintains creation
|
||||
instructions and and re-applies them at a server restart.
|
||||
"""
|
||||
ticker_pool_class = TickerPool
|
||||
|
||||
def __init__(self, save_name="ticker_storage"):
|
||||
"""
|
||||
Initialize handler
|
||||
"""
|
||||
self.ticker_storage = {}
|
||||
self.save_name = save_name
|
||||
self.ticker_pool = self.ticker_pool_class()
|
||||
|
||||
def _store_key(self, obj, interval, idstring=""):
|
||||
"""
|
||||
Tries to create a store_key for the object.
|
||||
Returns a tuple (isdb, store_key) where isdb
|
||||
is a boolean True if obj was a database object,
|
||||
False otherwise.
|
||||
"""
|
||||
if hasattr(obj, "db_key"):
|
||||
# create a store_key using the database representation
|
||||
objkey = pack_dbobj(obj)
|
||||
isdb = True
|
||||
else:
|
||||
# non-db object, look for a property "key" on it, otherwise
|
||||
# use its memory location.
|
||||
try:
|
||||
objkey = _GA(obj, "key")
|
||||
except AttributeError:
|
||||
objkey = id(obj)
|
||||
isdb = False
|
||||
# return sidb and store_key
|
||||
return isdb, (objkey, interval, idstring)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save ticker_storage as a serialized string into a temporary
|
||||
ServerConf field. Whereas saving is done on the fly, if called by
|
||||
server when it shuts down, the current timer of each ticker will be
|
||||
saved so it can start over from that point.
|
||||
"""
|
||||
if self.ticker_storage:
|
||||
start_delays = dict((interval, ticker.task.next_call_time())
|
||||
for interval, ticker in self.ticker_pool.tickers.items())
|
||||
# update the timers for the tickers
|
||||
#for (obj, interval, idstring), (args, kwargs) in self.ticker_storage.items():
|
||||
for store_key, (args, kwargs) in self.ticker_storage.items():
|
||||
interval = store_key[1]
|
||||
# this is a mutable, so it's updated in-place in ticker_storage
|
||||
kwargs["_start_delay"] = start_delays.get(interval, None)
|
||||
ServerConfig.objects.conf(key=self.save_name,
|
||||
value=dbserialize(self.ticker_storage))
|
||||
else:
|
||||
ServerConfig.objects.conf(key=self.save_name, delete=True)
|
||||
|
||||
def restore(self):
|
||||
"""
|
||||
Restore ticker_storage from database and re-initialize the handler from storage. This is triggered by the server at restart.
|
||||
"""
|
||||
# load stored command instructions and use them to re-initialize handler
|
||||
ticker_storage = ServerConfig.objects.conf(key=self.save_name)
|
||||
if ticker_storage:
|
||||
self.ticker_storage = dbunserialize(ticker_storage)
|
||||
#print "restore:", self.ticker_storage
|
||||
for store_key, (args, kwargs) in self.ticker_storage.items():
|
||||
if len(store_key) == 2:
|
||||
# old form of store_key - update it
|
||||
store_key = (store_key[0], store_key[1], "")
|
||||
obj, interval, idstring = store_key
|
||||
obj = unpack_dbobj(obj)
|
||||
_, store_key = self._store_key(obj, interval, idstring)
|
||||
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs)
|
||||
|
||||
def add(self, obj, interval, idstring="", *args, **kwargs):
|
||||
"""
|
||||
Add object to tickerhandler. The object must have an at_tick
|
||||
method. This will be called every interval seconds until the
|
||||
object is unsubscribed from the ticker.
|
||||
"""
|
||||
isdb, store_key = self._store_key(obj, interval, idstring)
|
||||
if isdb:
|
||||
self.ticker_storage[store_key] = (args, kwargs)
|
||||
self.save()
|
||||
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs)
|
||||
|
||||
def remove(self, obj, interval=None, idstring=""):
|
||||
"""
|
||||
Remove object from ticker, or only this object ticking
|
||||
at a given interval.
|
||||
"""
|
||||
if interval:
|
||||
isdb, store_key = self._store_key(obj, interval, idstring)
|
||||
if isdb:
|
||||
self.ticker_storage.pop(store_key, None)
|
||||
self.save()
|
||||
self.ticker_pool.remove(store_key, interval)
|
||||
else:
|
||||
# remove all objects with any intervals
|
||||
intervals = self.ticker_pool.tickers.keys()
|
||||
should_save = False
|
||||
for interval in intervals:
|
||||
isdb, store_key = self._store_key(obj, interval, idstring)
|
||||
if isdb:
|
||||
self.ticker_storage.pop(store_key, None)
|
||||
should_save = True
|
||||
self.ticker_pool.remove(store_key, interval)
|
||||
if should_save:
|
||||
self.save()
|
||||
|
||||
|
||||
|
||||
def clear(self, interval=None):
|
||||
"""
|
||||
Stop/remove all tickers from handler, or the ones
|
||||
with a given interval. This is the only supported
|
||||
way to kill tickers for non-db objects. If interval
|
||||
is given, only stop tickers with this interval.
|
||||
"""
|
||||
self.ticker_pool.stop(interval)
|
||||
if interval:
|
||||
self.ticker_storage = dict((store_key, store_key) for store_key in self.ticker_storage if store_key[1] != interval)
|
||||
else:
|
||||
self.ticker_storage = {}
|
||||
self.save()
|
||||
|
||||
def all(self, interval=None):
|
||||
"""
|
||||
Get the subsciptions for a given interval. If interval
|
||||
is not given, return a dictionary with lists for every
|
||||
interval in the tickerhandler.
|
||||
"""
|
||||
if interval is None:
|
||||
# return dict of all, ordered by interval
|
||||
return dict((interval, ticker.subscriptions.values())
|
||||
for interval, ticker in self.ticker_pool.tickers.items())
|
||||
else:
|
||||
# get individual interval
|
||||
ticker = self.ticker_pool.tickers.get(interval, None)
|
||||
if ticker:
|
||||
return ticker.subscriptions.values()
|
||||
|
||||
|
||||
# main tickerhandler
|
||||
TICKER_HANDLER = TickerHandler()
|
||||
Loading…
Add table
Add a link
Reference in a new issue