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
|
|
@ -87,7 +87,7 @@ class CmdBoot(MuxCommand):
|
|||
feedback += "\nReason given: %s" % reason
|
||||
|
||||
for session in boot_list:
|
||||
name = session.name
|
||||
name = session.uname
|
||||
session.msg(feedback)
|
||||
session.disconnect()
|
||||
caller.msg("You booted %s." % name)
|
||||
|
|
|
|||
|
|
@ -408,7 +408,8 @@ class CmdCreate(ObjManipCommand):
|
|||
if caller.location:
|
||||
obj.home = caller.location
|
||||
obj.move_to(caller.location, quiet=True)
|
||||
caller.msg(string)
|
||||
if string:
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class CmdDebug(MuxCommand):
|
||||
|
|
@ -1077,7 +1078,7 @@ class CmdOpen(ObjManipCommand):
|
|||
exit_obj.destination = destination
|
||||
string = "Created new Exit '%s' from %s to %s (aliases: %s)." % (exit_name,location.name,
|
||||
destination.name,
|
||||
exit_aliases)
|
||||
", ".join([str(e) for e in exit_aliases]))
|
||||
else:
|
||||
string = "Error: Exit '%s' not created." % (exit_name)
|
||||
# emit results
|
||||
|
|
@ -1824,17 +1825,19 @@ class CmdScript(MuxCommand):
|
|||
attach scripts
|
||||
|
||||
Usage:
|
||||
@script[/switch] <obj> = <script.path or scriptkey>
|
||||
@script[/switch] <obj> [= <script.path or scriptkey>]
|
||||
|
||||
Switches:
|
||||
start - start a previously added script
|
||||
stop - stop a previously added script
|
||||
|
||||
Attaches the given script to the object and starts it. Script path can be given
|
||||
from the base location for scripts as given in settings.
|
||||
If stopping/starting an already existing script, the script's key
|
||||
can be given instead (if giving a path, *all* scripts with this path
|
||||
on <obj> will be affected).
|
||||
Attaches the given script to the object and starts it. Script path
|
||||
can be given from the base location for scripts as given in
|
||||
settings. If stopping/starting an already existing script, the
|
||||
script's key can be given instead (if giving a path, *all* scripts
|
||||
with this path on <obj> will be affected). If no script name is given,
|
||||
all scripts on the object is affected (or displayed if no start/stop
|
||||
switch is set).
|
||||
"""
|
||||
|
||||
key = "@script"
|
||||
|
|
@ -1847,8 +1850,8 @@ class CmdScript(MuxCommand):
|
|||
|
||||
caller = self.caller
|
||||
|
||||
if not self.rhs:
|
||||
string = "Usage: @script[/switch] <obj> = <script.path or script key>"
|
||||
if not self.args:
|
||||
string = "Usage: @script[/switch] <obj> [= <script.path or script key>]"
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
|
|
@ -1857,33 +1860,52 @@ class CmdScript(MuxCommand):
|
|||
return
|
||||
|
||||
string = ""
|
||||
if not self.switches:
|
||||
# adding a new script, and starting it
|
||||
ok = obj.scripts.add(self.rhs, autostart=True)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be added and/or started." % self.rhs
|
||||
if not self.rhs:
|
||||
# no rhs means we want to operate on all scripts
|
||||
scripts = obj.scripts.all()
|
||||
if not scripts:
|
||||
string += "No scripts defined on %s." % obj.key
|
||||
elif not self.switches:
|
||||
# view all scripts
|
||||
from src.commands.default.system import format_script_list
|
||||
string += format_script_list(scripts)
|
||||
elif "start" in self.switches:
|
||||
num = sum([obj.scripts.start(script.key) for script in scripts])
|
||||
string += "%s scripts started on %s." % num
|
||||
elif "stop" in self.switches:
|
||||
for script in scripts:
|
||||
string += "Stopping script %s." % script.key
|
||||
script.stop()
|
||||
string = string.strip()
|
||||
obj.scripts.validate()
|
||||
else: # rhs exists
|
||||
if not self.switches:
|
||||
# adding a new script, and starting it
|
||||
ok = obj.scripts.add(self.rhs, autostart=True)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be added and/or started." % self.rhs
|
||||
else:
|
||||
string = "Script successfully added and started."
|
||||
|
||||
else:
|
||||
string = "Script successfully added and started."
|
||||
|
||||
else:
|
||||
paths = [self.rhs] + ["%s.%s" % (prefix, self.rhs)
|
||||
for prefix in settings.SCRIPT_TYPECLASS_PATHS]
|
||||
if "stop" in self.switches:
|
||||
# we are stopping an already existing script
|
||||
for path in paths:
|
||||
ok = obj.scripts.stop(path)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be stopped. Does it exist?" % path
|
||||
else:
|
||||
string = "Script stopped and removed from object."
|
||||
break
|
||||
if "start" in self.switches:
|
||||
# we are starting an already existing script
|
||||
for path in paths:
|
||||
ok = obj.scripts.start(path)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be (re)started." % path
|
||||
else:
|
||||
string = "Script started successfully."
|
||||
break
|
||||
paths = [self.rhs] + ["%s.%s" % (prefix, self.rhs)
|
||||
for prefix in settings.SCRIPT_TYPECLASS_PATHS]
|
||||
if "stop" in self.switches:
|
||||
# we are stopping an already existing script
|
||||
for path in paths:
|
||||
ok = obj.scripts.stop(path)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be stopped. Does it exist?" % path
|
||||
else:
|
||||
string = "Script stopped and removed from object."
|
||||
break
|
||||
if "start" in self.switches:
|
||||
# we are starting an already existing script
|
||||
for path in paths:
|
||||
ok = obj.scripts.start(path)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be (re)started." % path
|
||||
else:
|
||||
string = "Script started successfully."
|
||||
break
|
||||
caller.msg(string.strip())
|
||||
|
|
|
|||
|
|
@ -34,11 +34,12 @@ class DefaultCmdSet(CmdSet):
|
|||
|
||||
# System commands
|
||||
self.add(system.CmdReload())
|
||||
self.add(system.CmdReset())
|
||||
self.add(system.CmdShutdown())
|
||||
self.add(system.CmdPy())
|
||||
self.add(system.CmdScripts())
|
||||
self.add(system.CmdObjects())
|
||||
self.add(system.CmdService())
|
||||
self.add(system.CmdShutdown())
|
||||
self.add(system.CmdVersion())
|
||||
self.add(system.CmdTime())
|
||||
self.add(system.CmdServerLoad())
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from src.comms import irc, imc2
|
|||
from src.comms.channelhandler import CHANNELHANDLER
|
||||
from src.utils import create, utils
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
def find_channel(caller, channelname, silent=False, noaliases=False):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ class CmdLook(MuxCommand):
|
|||
"""
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
|
||||
if args:
|
||||
# Use search to handle duplicate/nonexistant results.
|
||||
looking_at_obj = caller.search(args, use_nicks=True)
|
||||
|
|
@ -345,7 +344,7 @@ class CmdQuit(MuxCommand):
|
|||
def func(self):
|
||||
"hook function"
|
||||
for session in self.caller.sessions:
|
||||
session.msg("Quitting. Hope to see you soon again.")
|
||||
session.msg("{RQuitting{n. Hope to see you soon again.")
|
||||
session.session_disconnect()
|
||||
|
||||
class CmdWho(MuxCommand):
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from src.scripts.models import ScriptDB
|
|||
from src.objects.models import ObjectDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.server.models import ServerConfig
|
||||
from src.utils import reloads, create, logger, utils, gametime
|
||||
from src.utils import create, logger, utils, gametime
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
|
||||
|
||||
|
|
@ -26,8 +26,9 @@ class CmdReload(MuxCommand):
|
|||
Usage:
|
||||
@reload
|
||||
|
||||
This reloads the system modules and
|
||||
re-validates all scripts.
|
||||
This restarts the server. The Portal is not
|
||||
affected. Non-persistent scripts will survive a @reload (use
|
||||
@reset to purge) and at_reload() hooks will be called.
|
||||
"""
|
||||
key = "@reload"
|
||||
locks = "cmd:perm(reload) or perm(Immortals)"
|
||||
|
|
@ -37,7 +38,62 @@ class CmdReload(MuxCommand):
|
|||
"""
|
||||
Reload the system.
|
||||
"""
|
||||
reloads.start_reload_loop()
|
||||
SESSIONS.announce_all(" Server restarting ...")
|
||||
SESSIONS.server.shutdown(mode='reload')
|
||||
|
||||
class CmdReset(MuxCommand):
|
||||
"""
|
||||
Reset and reboot the system
|
||||
|
||||
Usage:
|
||||
@reset
|
||||
|
||||
A cold reboot. This works like a mixture of @reload and @shutdown,
|
||||
- all shutdown hooks will be called and non-persistent scrips will
|
||||
be purged. But the Portal will not be affected and the server will
|
||||
automatically restart again.
|
||||
"""
|
||||
key = "@reset"
|
||||
aliases = ['@reboot']
|
||||
locks = "cmd:perm(reload) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Reload the system.
|
||||
"""
|
||||
SESSIONS.announce_all(" Server restarting ...")
|
||||
SESSIONS.server.shutdown(mode='reset')
|
||||
|
||||
|
||||
class CmdShutdown(MuxCommand):
|
||||
|
||||
"""
|
||||
@shutdown
|
||||
|
||||
Usage:
|
||||
@shutdown [announcement]
|
||||
|
||||
Gracefully shut down both Server and Portal.
|
||||
"""
|
||||
key = "@shutdown"
|
||||
locks = "cmd:perm(shutdown) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Define function"
|
||||
try:
|
||||
session = self.caller.sessions[0]
|
||||
except Exception:
|
||||
return
|
||||
self.caller.msg('Shutting down server ...')
|
||||
announcement = "\nServer is being SHUT DOWN!\n"
|
||||
if self.args:
|
||||
announcement += "%s\n" % self.args
|
||||
logger.log_infomsg('Server shutdown by %s.' % self.caller.name)
|
||||
SESSIONS.announce_all(announcement)
|
||||
SESSIONS.portal_shutdown()
|
||||
SESSIONS.server.shutdown(mode='shutdown')
|
||||
|
||||
class CmdPy(MuxCommand):
|
||||
"""
|
||||
|
|
@ -115,6 +171,58 @@ class CmdPy(MuxCommand):
|
|||
except AssertionError: # this is a strange thing; the script looses its id somehow..?
|
||||
pass
|
||||
|
||||
|
||||
# helper function. Kept outside so it can be imported and run
|
||||
# by other commands.
|
||||
|
||||
def format_script_list(scripts):
|
||||
"Takes a list of scripts and formats the output."
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
|
||||
table = [["id"], ["obj"], ["key"], ["intval"], ["next"], ["rept"], ["db"], ["typeclass"], ["desc"]]
|
||||
for script in scripts:
|
||||
|
||||
table[0].append(script.id)
|
||||
if not hasattr(script, 'obj') or not script.obj:
|
||||
table[1].append("<Global>")
|
||||
else:
|
||||
table[1].append(script.obj.key)
|
||||
table[2].append(script.key)
|
||||
if not hasattr(script, 'interval') or script.interval < 0:
|
||||
table[3].append("--")
|
||||
else:
|
||||
table[3].append("%ss" % script.interval)
|
||||
next = script.time_until_next_repeat()
|
||||
if not next:
|
||||
table[4].append("--")
|
||||
else:
|
||||
table[4].append("%ss" % next)
|
||||
|
||||
if not hasattr(script, 'repeats') or not script.repeats:
|
||||
table[5].append("--")
|
||||
else:
|
||||
table[5].append("%s" % script.repeats)
|
||||
if script.persistent:
|
||||
table[6].append("*")
|
||||
else:
|
||||
table[6].append("-")
|
||||
typeclass_path = script.typeclass_path.rsplit('.', 1)
|
||||
table[7].append("%s" % typeclass_path[-1])
|
||||
table[8].append(script.desc)
|
||||
|
||||
ftable = utils.format_table(table)
|
||||
string = ""
|
||||
for irow, row in enumerate(ftable):
|
||||
if irow == 0:
|
||||
srow = "\n" + "".join(row)
|
||||
srow = "{w%s{n" % srow.rstrip()
|
||||
else:
|
||||
srow = "\n" + "{w%s{n" % row[0] + "".join(row[1:])
|
||||
string += srow.rstrip()
|
||||
return string.strip()
|
||||
|
||||
|
||||
class CmdScripts(MuxCommand):
|
||||
"""
|
||||
Operate on scripts.
|
||||
|
|
@ -137,54 +245,7 @@ class CmdScripts(MuxCommand):
|
|||
aliases = "@listscripts"
|
||||
locks = "cmd:perm(listscripts) or perm(Wizards)"
|
||||
help_category = "System"
|
||||
|
||||
def format_script_list(self, scripts):
|
||||
"Takes a list of scripts and formats the output."
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
|
||||
table = [["id"], ["obj"], ["key"], ["intval"], ["next"], ["rept"], ["db"], ["typeclass"], ["desc"]]
|
||||
for script in scripts:
|
||||
|
||||
table[0].append(script.id)
|
||||
if not hasattr(script, 'obj') or not script.obj:
|
||||
table[1].append("<Global>")
|
||||
else:
|
||||
table[1].append(script.obj.key)
|
||||
table[2].append(script.key)
|
||||
if not hasattr(script, 'interval') or script.interval < 0:
|
||||
table[3].append("--")
|
||||
else:
|
||||
table[3].append("%ss" % script.interval)
|
||||
next = script.time_until_next_repeat()
|
||||
if not next:
|
||||
table[4].append("--")
|
||||
else:
|
||||
table[4].append("%ss" % next)
|
||||
|
||||
if not hasattr(script, 'repeats') or not script.repeats:
|
||||
table[5].append("--")
|
||||
else:
|
||||
table[5].append("%s" % script.repeats)
|
||||
if script.persistent:
|
||||
table[6].append("*")
|
||||
else:
|
||||
table[6].append("-")
|
||||
typeclass_path = script.typeclass_path.rsplit('.', 1)
|
||||
table[7].append("%s" % typeclass_path[-1])
|
||||
table[8].append(script.desc)
|
||||
|
||||
ftable = utils.format_table(table)
|
||||
string = ""
|
||||
for irow, row in enumerate(ftable):
|
||||
if irow == 0:
|
||||
srow = "\n" + "".join(row)
|
||||
srow = "{w%s{n" % srow.rstrip()
|
||||
else:
|
||||
srow = "\n" + "{w%s{n" % row[0] + "".join(row[1:])
|
||||
string += srow.rstrip()
|
||||
return string.strip()
|
||||
|
||||
|
||||
def func(self):
|
||||
"implement method"
|
||||
|
||||
|
|
@ -232,7 +293,7 @@ class CmdScripts(MuxCommand):
|
|||
else:
|
||||
# multiple matches.
|
||||
string = "Multiple script matches. Please refine your search:\n"
|
||||
string += self.format_script_list(scripts)
|
||||
string += format_script_list(scripts)
|
||||
elif self.switches and self.switches[0] in ("validate", "valid", "val"):
|
||||
# run validation on all found scripts
|
||||
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts)
|
||||
|
|
@ -240,7 +301,7 @@ class CmdScripts(MuxCommand):
|
|||
string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped)
|
||||
else:
|
||||
# No stopping or validation. We just want to view things.
|
||||
string = self.format_script_list(scripts)
|
||||
string = format_script_list(scripts)
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -411,34 +472,6 @@ class CmdService(MuxCommand):
|
|||
caller.msg("Starting service '%s'." % self.args)
|
||||
service.startService()
|
||||
|
||||
class CmdShutdown(MuxCommand):
|
||||
|
||||
"""
|
||||
@shutdown
|
||||
|
||||
Usage:
|
||||
@shutdown [announcement]
|
||||
|
||||
Shut the game server down gracefully.
|
||||
"""
|
||||
key = "@shutdown"
|
||||
locks = "cmd:perm(shutdown) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Define function"
|
||||
try:
|
||||
session = self.caller.sessions[0]
|
||||
except Exception:
|
||||
return
|
||||
self.caller.msg('Shutting down server ...')
|
||||
announcement = "\nServer is being SHUT DOWN!\n"
|
||||
if self.args:
|
||||
announcement += "%s\n" % self.args
|
||||
logger.log_infomsg('Server shutdown by %s.' % self.caller.name)
|
||||
SESSIONS.announce_all(announcement)
|
||||
SESSIONS.server.shutdown()
|
||||
|
||||
class CmdVersion(MuxCommand):
|
||||
"""
|
||||
@version - game version
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ except ImportError:
|
|||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
from src.utils import create, ansi
|
||||
from src.server import session, sessionhandler
|
||||
from src.server import serversession, sessionhandler
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.server.models import ServerConfig
|
||||
from src.comms.models import Channel, Msg, PlayerChannelConnection, ExternalChannelConnection
|
||||
|
|
@ -46,15 +46,37 @@ def cleanup():
|
|||
ExternalChannelConnection.objects.all().delete()
|
||||
ServerConfig.objects.all().delete()
|
||||
|
||||
class FakeSession(session.Session):
|
||||
class FakeSessionHandler(sessionhandler.ServerSessionHandler):
|
||||
"""
|
||||
Fake sessionhandler, without an amp connection
|
||||
"""
|
||||
def portal_shutdown(self):
|
||||
pass
|
||||
def disconnect(self, session, reason=""):
|
||||
pass
|
||||
def login(self, session):
|
||||
pass
|
||||
def session_sync(self):
|
||||
pass
|
||||
def data_out(self, session, string="", data=""):
|
||||
return string
|
||||
|
||||
SESSIONS = FakeSessionHandler()
|
||||
|
||||
class FakeSession(serversession.ServerSession):
|
||||
"""
|
||||
A fake session that
|
||||
implements dummy versions of the real thing; this is needed to
|
||||
mimic a logged-in player.
|
||||
"""
|
||||
protocol_key = "TestProtocol"
|
||||
sessdict = {'protocol_key':'telnet', 'address':('0.0.0.0','5000'), 'sessid':2, 'uid':2, 'uname':None,
|
||||
'logged_in':False, 'cid':None, 'ndb':{}, 'encoding':'utf-8',
|
||||
'conn_time':time.time(), 'cmd_last':time.time(), 'cmd_last_visible':time.time(), 'cmd_total':1}
|
||||
|
||||
def connectionMade(self):
|
||||
self.session_connect('0,0,0,0')
|
||||
self.load_sync_data(self.sessdict)
|
||||
self.sessionhandler = SESSIONS
|
||||
def disconnectClient(self):
|
||||
pass
|
||||
def lineReceived(self, raw_string):
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ class CmdQuit(MuxCommand):
|
|||
"Simply close the connection."
|
||||
session = self.caller
|
||||
session.msg("Good bye! Disconnecting ...")
|
||||
session.at_disconnect()
|
||||
session.session_disconnect()
|
||||
|
||||
class CmdUnconnectedLook(MuxCommand):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -387,8 +387,8 @@ def start_scripts(validate=False):
|
|||
"""
|
||||
|
||||
if validate:
|
||||
from src.utils import reloads
|
||||
reloads.reload_scripts()
|
||||
from src.scripts.models import ScriptDB
|
||||
ScriptDB.objects.validate()
|
||||
return
|
||||
if not search.scripts("IMC2_Send_IsAlive"):
|
||||
create.create_script(Send_IsAlive)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ be able to delete connections on the fly).
|
|||
|
||||
from django.db import models
|
||||
from src.utils.idmapper.models import SharedMemoryModel
|
||||
#from src.server.sessionhandler import SESSIONS
|
||||
from src.comms import managers
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.utils import logger
|
||||
|
|
|
|||
|
|
@ -724,7 +724,7 @@ class ObjectDB(TypedObject):
|
|||
Destroys all of the exits and any exits pointing to this
|
||||
object as a destination.
|
||||
"""
|
||||
for out_exit in self.exits:
|
||||
for out_exit in [exi for exi in ObjectDB.objects.get_contents(self) if exi.db_destination]:
|
||||
out_exit.delete()
|
||||
for in_exit in ObjectDB.objects.filter(db_destination=self):
|
||||
in_exit.delete()
|
||||
|
|
@ -779,6 +779,7 @@ class ObjectDB(TypedObject):
|
|||
new_key = "%s_copy" % self.key
|
||||
return ObjectDB.objects.copy_object(self, new_key=new_key)
|
||||
|
||||
delete_iter = 0
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this object.
|
||||
|
|
@ -786,13 +787,20 @@ class ObjectDB(TypedObject):
|
|||
objects to their respective home locations, as well as clean
|
||||
up all exits to/from the object.
|
||||
"""
|
||||
if self.delete_iter > 0:
|
||||
# make sure to only call delete once on this object
|
||||
# (avoid recursive loops)
|
||||
return False
|
||||
|
||||
if not self.at_object_delete():
|
||||
# this is an extra pre-check
|
||||
# run before deletion mechanism
|
||||
# is kicked into gear.
|
||||
self.delete_iter == 0
|
||||
return False
|
||||
|
||||
self.delete_iter += 1
|
||||
|
||||
# See if we need to kick the player off.
|
||||
|
||||
for session in self.sessions:
|
||||
|
|
|
|||
|
|
@ -75,6 +75,17 @@ class Object(TypeClass):
|
|||
"""
|
||||
pass
|
||||
|
||||
def at_init(self):
|
||||
"""
|
||||
This is always called whenever this
|
||||
object initiated -- both when the object
|
||||
is first created as well as after each restart.
|
||||
It is also called after each server reload, so
|
||||
if something should survive a warm-reboot (rebooting
|
||||
the server without the players logging out), put it here.
|
||||
"""
|
||||
pass
|
||||
|
||||
def basetype_posthook_setup(self):
|
||||
"""
|
||||
Called once, after basetype_setup and at_object_creation. This should generally not be overloaded unless
|
||||
|
|
@ -87,9 +98,26 @@ class Object(TypeClass):
|
|||
def at_cache(self):
|
||||
"""
|
||||
Called whenever this object is cached to the idmapper backend.
|
||||
This is the place to put eventual reloads of non-persistent attributes
|
||||
you saved in the at_server_reload() below.
|
||||
"""
|
||||
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
|
||||
|
||||
def at_cmdset_get(self):
|
||||
"""
|
||||
Called just before cmdsets on this object are requested by the
|
||||
|
|
@ -384,9 +412,9 @@ class Character(Object):
|
|||
the script is permanently stored to this object (the permanent
|
||||
keyword creates a script to do this), we should never need to
|
||||
do this again for as long as this object exists.
|
||||
pass
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def at_after_move(self, source_location):
|
||||
"Default is to look around after a move."
|
||||
self.execute_cmd('look')
|
||||
|
|
@ -512,7 +540,7 @@ class Exit(Object):
|
|||
self.locks.add("traverse:all()") # who can pass through exit by default
|
||||
self.locks.add("get:false()") # noone can pick up the exit
|
||||
|
||||
# an exit should have a destination (this is replaced at creation time)
|
||||
# an exit should have a destination (this is replaced at creation time)
|
||||
if self.dbobj.location:
|
||||
self.destination = self.dbobj.location
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,14 @@ class PlayerManager(TypedObjectManager):
|
|||
"""
|
||||
return User.objects.filter(email__iexact=uemail)
|
||||
|
||||
@returns_typeclass
|
||||
@returns_player
|
||||
def get_player_from_uid(self, uid):
|
||||
"""
|
||||
Returns a player object based on User id.
|
||||
"""
|
||||
return User.objects.get(id=uid)
|
||||
|
||||
@returns_typeclass
|
||||
def get_player_from_name(self, uname):
|
||||
"Get player object based on name"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,17 @@ class Player(TypeClass):
|
|||
pass
|
||||
|
||||
|
||||
def at_init(self):
|
||||
"""
|
||||
This is always called whenever this
|
||||
object initiated -- both when the object
|
||||
is first created as well as after each restart.
|
||||
It is also called after each server reload, so
|
||||
if something should survive a warm-reboot (rebooting
|
||||
the server without the players logging out), put it here.
|
||||
"""
|
||||
pass
|
||||
|
||||
# Note that the hooks below also exist
|
||||
# in the character object's typeclass. You
|
||||
# can often ignore these and rely on the
|
||||
|
|
@ -101,3 +112,18 @@ class Player(TypeClass):
|
|||
itself as a sender in the msg() call.
|
||||
"""
|
||||
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
|
||||
|
|
|
|||
|
|
@ -88,10 +88,12 @@ class ScriptManager(TypedObjectManager):
|
|||
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. It causes non-persistent
|
||||
scripts to be removed and persistent scripts to be
|
||||
force-restarted.
|
||||
|
||||
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.
|
||||
|
|
@ -100,6 +102,7 @@ class ScriptManager(TypedObjectManager):
|
|||
# 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.
|
||||
|
|
@ -113,14 +116,15 @@ class ScriptManager(TypedObjectManager):
|
|||
nr_stopped = 0
|
||||
|
||||
if init_mode:
|
||||
# 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)
|
||||
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.dbobj.is_active = False
|
||||
|
||||
|
||||
elif not scripts:
|
||||
# normal operation
|
||||
if dbref and self.dbref(dbref):
|
||||
|
|
@ -137,8 +141,8 @@ class ScriptManager(TypedObjectManager):
|
|||
|
||||
#print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts))
|
||||
for script in scripts:
|
||||
if script.is_valid():
|
||||
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script.dbobj), init_mode)
|
||||
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script.dbobj), init_mode)
|
||||
if script.is_valid():
|
||||
nr_started += script.start(force_restart=init_mode)
|
||||
#print "back from start. nr_started=", nr_started
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -265,3 +265,10 @@ class ScriptDB(TypedObject):
|
|||
# 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):
|
||||
if self.delete_iter > 0:
|
||||
return
|
||||
self.delete_iter += 1
|
||||
super(ScriptDB, self).delete()
|
||||
|
|
|
|||
|
|
@ -35,17 +35,26 @@ class ScriptClass(TypeClass):
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def _start_task(self):
|
||||
def _start_task(self, start_now=True):
|
||||
"start task runner"
|
||||
#print "_start_task: self.interval:", self.key, self.interval, self.dbobj.db_interval
|
||||
|
||||
self.ndb.twisted_task = LoopingCall(self._step_task)
|
||||
self.ndb.twisted_task.start(self.interval, now=not self.start_delay)
|
||||
self.ndb.time_last_called = int(time())
|
||||
if self.ndb._paused_time:
|
||||
# we had paused the script, restarting
|
||||
#print " start with paused time:", self.key, self.ndb._paused_time
|
||||
self.ndb.twisted_task.start(self.ndb._paused_time, now=False)
|
||||
else:
|
||||
# starting script anew.
|
||||
#print "_start_task: self.interval:", self.key, self.dbobj.interval
|
||||
self.ndb.twisted_task.start(self.dbobj.interval, now=start_now and not self.start_delay)
|
||||
self.ndb.time_last_called = int(time())
|
||||
|
||||
def _stop_task(self):
|
||||
"stop task runner"
|
||||
try:
|
||||
#print "stopping twisted task:", id(self.ndb.twisted_task), self.obj
|
||||
self.ndb.twisted_task.stop()
|
||||
if self.ndb.twisted_task and not self.ndb.twisted_task.running:
|
||||
self.ndb.twisted_task.stop()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
def _step_err_callback(self, e):
|
||||
|
|
@ -73,6 +82,16 @@ class ScriptClass(TypeClass):
|
|||
self.dbobj.db_repeats -= 1
|
||||
self.ndb.time_last_called = int(time())
|
||||
self.save()
|
||||
|
||||
if self.ndb._paused_time:
|
||||
# this means we were running an unpaused script, for the time remaining
|
||||
# after the pause. Now we start a normal-running timer again.
|
||||
#print "switching to normal run:", self.key
|
||||
del self.ndb._paused_time
|
||||
self._stop_task()
|
||||
self._start_task(start_now=False)
|
||||
|
||||
|
||||
def _step_task(self):
|
||||
"step task"
|
||||
try:
|
||||
|
|
@ -92,7 +111,10 @@ class ScriptClass(TypeClass):
|
|||
check in on their scripts and when they will next be run.
|
||||
"""
|
||||
try:
|
||||
return max(0, (self.ndb.time_last_called + self.dbobj.db_interval) - int(time()))
|
||||
if self.ndb._paused_time:
|
||||
return max(0, (self.ndb.time_last_called + self.ndb._paused_time) - int(time()))
|
||||
else:
|
||||
return max(0, (self.ndb.time_last_called + self.dbobj.db_interval) - int(time()))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
|
@ -122,7 +144,12 @@ class ScriptClass(TypeClass):
|
|||
# this means the object is not initialized.
|
||||
self.dbobj.is_active = False
|
||||
return 0
|
||||
# try to start the script
|
||||
|
||||
# try to restart a paused script
|
||||
if self.unpause():
|
||||
return 1
|
||||
|
||||
# try to start the script from scratch
|
||||
try:
|
||||
self.dbobj.is_active = True
|
||||
self.at_start()
|
||||
|
|
@ -162,6 +189,37 @@ class ScriptClass(TypeClass):
|
|||
return 0
|
||||
return 1
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
This stops a running script and stores its active state.
|
||||
"""
|
||||
#print "pausing", self.key, self.time_until_next_repeat()
|
||||
dt = self.time_until_next_repeat()
|
||||
if dt == None:
|
||||
return
|
||||
self.db._paused_time = dt
|
||||
self._stop_task()
|
||||
|
||||
def unpause(self):
|
||||
"""
|
||||
Restart a paused script. This WILL call at_start().
|
||||
"""
|
||||
#print "unpausing", self.key, self.db._paused_time
|
||||
dt = self.db._paused_time
|
||||
if dt == None:
|
||||
return False
|
||||
try:
|
||||
self.dbobj.is_active = True
|
||||
self.at_start()
|
||||
self.ndb._paused_time = dt
|
||||
self._start_task(start_now=False)
|
||||
del self.db._paused_time
|
||||
except Exception, e:
|
||||
logger.log_trace()
|
||||
self.dbobj.is_active = False
|
||||
return False
|
||||
return True
|
||||
|
||||
# hooks
|
||||
def at_script_creation(self):
|
||||
"placeholder"
|
||||
|
|
@ -178,154 +236,7 @@ class ScriptClass(TypeClass):
|
|||
def at_repeat(self):
|
||||
"placeholder"
|
||||
pass
|
||||
|
||||
|
||||
# class ScriptClass(TypeClass):
|
||||
# """
|
||||
# Base class for all Scripts.
|
||||
# """
|
||||
|
||||
# # private methods for handling timers.
|
||||
|
||||
# def __eq__(self, other):
|
||||
# """
|
||||
# This has to be located at this level, having it in the
|
||||
# parent doesn't work.
|
||||
# """
|
||||
# if other:
|
||||
# return other.id == self.id
|
||||
# return False
|
||||
|
||||
# def _start_task(self):
|
||||
# "start the task runner."
|
||||
# print "self_interval:", self.interval
|
||||
# if self.interval > 0:
|
||||
# #print "Starting task runner"
|
||||
# start_now = not self.start_delay
|
||||
# self.ndb.twisted_task = task.LoopingCall(self._step_task)
|
||||
# self.ndb.twisted_task.start(self.interval, now=start_now)
|
||||
# self.ndb.time_last_called = int(time())
|
||||
# #self.save()
|
||||
# def _stop_task(self):
|
||||
# "stop the task runner"
|
||||
# if hasattr(self.ndb, "twisted_task"):
|
||||
# self.ndb.twisted_task.stop()
|
||||
# def _step_task(self):
|
||||
# "perform one repeat step of the script"
|
||||
# #print "Stepping task runner (obj %s)" % id(self)
|
||||
# #print "Has dbobj: %s" % hasattr(self, 'dbobj')
|
||||
# if not self.is_valid():
|
||||
# #the script is not valid anymore. Abort.
|
||||
# self.stop()
|
||||
# return
|
||||
# try:
|
||||
# self.at_repeat()
|
||||
# if self.repeats:
|
||||
# if self.repeats <= 1:
|
||||
# self.stop()
|
||||
# return
|
||||
# else:
|
||||
# self.repeats -= 1
|
||||
# self.ndb.time_last_called = int(time())
|
||||
# self.save()
|
||||
# except Exception:
|
||||
# logger.log_trace()
|
||||
# self._stop_task()
|
||||
|
||||
# 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.
|
||||
# """
|
||||
# if self.interval and hasattr(self.ndb, 'time_last_called'):
|
||||
# return max(0, (self.ndb.time_last_called + self.interval) - int(time()))
|
||||
# else:
|
||||
# return None
|
||||
|
||||
# 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.
|
||||
# """
|
||||
# #print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj),
|
||||
# # self.is_active, force_restart)
|
||||
# if force_restart:
|
||||
# self.is_active = False
|
||||
|
||||
# should_start = True
|
||||
# if self.obj:
|
||||
# try:
|
||||
# #print "checking cmdset ... for obj", self.obj
|
||||
# dummy = object.__getattribute__(self.obj, 'cmdset')
|
||||
# #print "... checked cmdset"
|
||||
# except AttributeError:
|
||||
# #print "self.obj.cmdset not found. Setting is_active=False."
|
||||
# self.is_active = False
|
||||
# should_start = False
|
||||
# if self.is_active and not force_restart:
|
||||
# should_start = False
|
||||
|
||||
# if should_start:
|
||||
# #print "... starting."
|
||||
# try:
|
||||
# self.is_active = True
|
||||
# self.at_start()
|
||||
# self._start_task()
|
||||
# return 1
|
||||
# except Exception:
|
||||
# #print ".. error when starting"
|
||||
# logger.log_trace()
|
||||
# self.is_active = False
|
||||
# return 0
|
||||
# else:
|
||||
# # avoid starting over.
|
||||
# #print "... Start cancelled (invalid start or already running)."
|
||||
# return 0 # this is used by validate() for counting started scripts
|
||||
|
||||
# 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
|
||||
# if not kill:
|
||||
# try:
|
||||
# self.at_stop()
|
||||
# except Exception:
|
||||
# logger.log_trace()
|
||||
# if self.interval:
|
||||
# try:
|
||||
# self._stop_task()
|
||||
# except Exception:
|
||||
# pass
|
||||
# self.is_running = False
|
||||
# try:
|
||||
# self.delete()
|
||||
# except AssertionError:
|
||||
# return 0
|
||||
# return 1
|
||||
|
||||
# def is_valid(self):
|
||||
# "placeholder"
|
||||
# pass
|
||||
# def at_start(self):
|
||||
# "placeholder."
|
||||
# pass
|
||||
# def at_stop(self):
|
||||
# "placeholder"
|
||||
# pass
|
||||
# def at_repeat(self):
|
||||
# "placeholder"
|
||||
# pass
|
||||
|
||||
|
||||
#
|
||||
# Base Script - inherit from this
|
||||
|
|
@ -359,7 +270,8 @@ class Script(ScriptClass):
|
|||
def at_start(self):
|
||||
"""
|
||||
Called whenever the script is started, which for persistent
|
||||
scripts is at least once every server start.
|
||||
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
|
||||
|
||||
|
|
@ -377,6 +289,20 @@ class Script(ScriptClass):
|
|||
"""
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
394
src/server/amp.py
Normal file
394
src/server/amp.py
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
"""
|
||||
Contains the protocols, commands, and client factory needed for the server
|
||||
to service the MUD portal proxy.
|
||||
|
||||
The separation works like this:
|
||||
|
||||
Portal - (AMP client) handles protocols. It contains a list of connected sessions in a
|
||||
dictionary for identifying the respective player connected. If it looses the AMP connection
|
||||
it will automatically try to reconnect.
|
||||
|
||||
Server - (AMP server) Handles all mud operations. The server holds its own list
|
||||
of sessions tied to player objects. This is synced against the portal at startup
|
||||
and when a session connects/disconnects
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
from twisted.protocols import amp
|
||||
from twisted.internet import protocol, defer, reactor
|
||||
from django.conf import settings
|
||||
from src.utils import utils
|
||||
from src.server.models import ServerConfig
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.server.serversession import ServerSession
|
||||
|
||||
PORTAL_RESTART = os.path.join(settings.GAME_DIR, "portal.restart")
|
||||
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server.restart")
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
# Signals
|
||||
|
||||
|
||||
|
||||
|
||||
def get_restart_mode(restart_file):
|
||||
"""
|
||||
Parse the server/portal restart status
|
||||
"""
|
||||
if os.path.exists(restart_file):
|
||||
flag = open(restart_file, 'r').read()
|
||||
return flag == "True"
|
||||
return False
|
||||
|
||||
class AmpServerFactory(protocol.ServerFactory):
|
||||
"""
|
||||
This factory creates new AMPProtocol protocol instances to use for accepting
|
||||
connections from TCPServer.
|
||||
"""
|
||||
def __init__(self, server):
|
||||
"""
|
||||
server: The Evennia server service instance
|
||||
protocol: The protocol the factory creates instances of.
|
||||
"""
|
||||
self.server = server
|
||||
self.protocol = AMPProtocol
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Start a new connection, and store it on the service object
|
||||
"""
|
||||
#print "Evennia Server connected to Portal at %s." % addr
|
||||
self.server.amp_protocol = AMPProtocol()
|
||||
self.server.amp_protocol.factory = self
|
||||
return self.server.amp_protocol
|
||||
|
||||
|
||||
|
||||
class AmpClientFactory(protocol.ReconnectingClientFactory):
|
||||
"""
|
||||
This factory creates new AMPProtocol protocol instances to use to connect
|
||||
to the MUD server. It also maintains the portal attribute
|
||||
on the ProxyService instance, which is used for piping input
|
||||
from Telnet to the MUD server.
|
||||
"""
|
||||
# Initial reconnect delay in seconds.
|
||||
initialDelay = 1
|
||||
#factor = 1.5
|
||||
maxDelay = 1
|
||||
|
||||
def __init__(self, portal):
|
||||
self.portal = portal
|
||||
self.protocol = AMPProtocol
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
"""
|
||||
Called when starting to try to connect to the MUD server.
|
||||
"""
|
||||
pass
|
||||
#print 'AMP started to connect:', connector
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Creates an AMPProtocol instance when connecting to the server.
|
||||
"""
|
||||
#print "Portal connected to Evennia server at %s." % addr
|
||||
self.resetDelay()
|
||||
self.portal.amp_protocol = AMPProtocol()
|
||||
self.portal.amp_protocol.factory = self
|
||||
return self.portal.amp_protocol
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
"""
|
||||
Called when the AMP connection to the MUD server is lost.
|
||||
"""
|
||||
if not get_restart_mode(SERVER_RESTART):
|
||||
self.portal.sessions.announce_all(_(" Portal lost connection to Server."))
|
||||
protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
"""
|
||||
Called when an AMP connection attempt to the MUD server fails.
|
||||
"""
|
||||
self.portal.sessions.announce_all(" ...")
|
||||
protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
|
||||
|
||||
|
||||
class MsgPortal2Server(amp.Command):
|
||||
"""
|
||||
Message portal -> server
|
||||
"""
|
||||
arguments = [('sessid', amp.Integer()),
|
||||
('msg', amp.String()),
|
||||
('data', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = []
|
||||
|
||||
class MsgServer2Portal(amp.Command):
|
||||
"""
|
||||
Message server -> portal
|
||||
"""
|
||||
arguments = [('sessid', amp.Integer()),
|
||||
('msg', amp.String()),
|
||||
('data', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = []
|
||||
|
||||
class ServerAdmin(amp.Command):
|
||||
"""
|
||||
Portal -> Server
|
||||
|
||||
Sent when the portal needs to perform admin
|
||||
operations on the server, such as when a new
|
||||
session connects or resyncs
|
||||
"""
|
||||
arguments = [('sessid', amp.Integer()),
|
||||
('operation', amp.String()),
|
||||
('data', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = []
|
||||
|
||||
class PortalAdmin(amp.Command):
|
||||
"""
|
||||
Server -> Portal
|
||||
|
||||
Sent when the server needs to perform admin
|
||||
operations on the portal.
|
||||
"""
|
||||
arguments = [('sessid', amp.Integer()),
|
||||
('operation', amp.String()),
|
||||
('data', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = []
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Core AMP protocol for communication Server <-> Portal
|
||||
#------------------------------------------------------------
|
||||
|
||||
class AMPProtocol(amp.AMP):
|
||||
"""
|
||||
This is the protocol that the MUD server and the proxy server
|
||||
communicate to each other with. AMP is a bi-directional protocol, so
|
||||
both the proxy and the MUD use the same commands and protocol.
|
||||
|
||||
AMP specifies responder methods here and connect them to amp.Command
|
||||
subclasses that specify the datatypes of the input/output of these methods.
|
||||
"""
|
||||
|
||||
# helper methods
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
This is called when a connection is established
|
||||
between server and portal. It is called on both sides,
|
||||
so we need to make sure to only trigger resync from the
|
||||
server side.
|
||||
"""
|
||||
if hasattr(self.factory, "portal"):
|
||||
sessdata = self.factory.portal.sessions.get_all_sync_data()
|
||||
self.call_remote_ServerAdmin(0,
|
||||
"PSYNC",
|
||||
data=sessdata)
|
||||
if get_restart_mode(SERVER_RESTART):
|
||||
msg = _(" ... Server restarted.")
|
||||
self.factory.portal.sessions.announce_all(msg)
|
||||
|
||||
|
||||
# Error handling
|
||||
|
||||
def errback(self, e, info):
|
||||
"error handler, to avoid dropping connections on server tracebacks."
|
||||
e.trap(Exception)
|
||||
print _("AMP Error for %(info)s: %(e)s") % {'info': info, 'e': e.getErrorMessage()}
|
||||
|
||||
|
||||
# Message definition + helper methods to call/create each message type
|
||||
|
||||
# Portal -> Server Msg
|
||||
|
||||
def amp_msg_portal2server(self, sessid, msg, data):
|
||||
"""
|
||||
Relays message to server. This method is executed on the Server.
|
||||
"""
|
||||
#print "msg portal -> server (server side):", sessid, msg
|
||||
self.factory.server.sessions.data_in(sessid, msg, pickle.loads(utils.to_str(data)))
|
||||
return {}
|
||||
MsgPortal2Server.responder(amp_msg_portal2server)
|
||||
|
||||
def call_remote_MsgPortal2Server(self, sessid, msg, data=""):
|
||||
"""
|
||||
Access method called by the Portal and executed on the Portal.
|
||||
"""
|
||||
#print "msg portal->server (portal side):", sessid, msg
|
||||
self.callRemote(MsgPortal2Server,
|
||||
sessid=sessid,
|
||||
msg=msg,
|
||||
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgPortal2Server")
|
||||
|
||||
# Server -> Portal message
|
||||
|
||||
def amp_msg_server2portal(self, sessid, msg, data):
|
||||
"""
|
||||
Relays message to Portal. This method is executed on the Portal.
|
||||
"""
|
||||
#print "msg server->portal (portal side):", sessid, msg
|
||||
self.factory.portal.sessions.data_out(sessid, msg, pickle.loads(utils.to_str(data)))
|
||||
return {}
|
||||
MsgServer2Portal.responder(amp_msg_server2portal)
|
||||
|
||||
def call_remote_MsgServer2Portal(self, sessid, msg, data=""):
|
||||
"""
|
||||
Access method called by the Server and executed on the Server.
|
||||
"""
|
||||
#print "msg server->portal (server side):", sessid, msg, data
|
||||
self.callRemote(MsgServer2Portal,
|
||||
sessid=sessid,
|
||||
msg=utils.to_str(msg),
|
||||
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgServer2Portal")
|
||||
|
||||
|
||||
# Server administration from the Portal side
|
||||
|
||||
def amp_server_admin(self, sessid, operation, data):
|
||||
"""
|
||||
This allows the portal to perform admin
|
||||
operations on the server. This is executed on the Server.
|
||||
|
||||
"""
|
||||
data = pickle.loads(utils.to_str(data))
|
||||
|
||||
#print "serveradmin (server side):", sessid, operation, data
|
||||
|
||||
if operation == 'PCONN': #portal_session_connect
|
||||
# create a new, session and sync it
|
||||
sess = ServerSession()
|
||||
sess.sessionhandler = self.factory.server.sessions
|
||||
sess.load_sync_data(data)
|
||||
if sess.logged_in and sess.uid:
|
||||
# this can happen in the case of auto-authenticating protocols like SSH
|
||||
|
||||
sess.player = PlayerDB.objects.get_player_from_uid(sess.uid)
|
||||
sess.at_sync() # this runs initialization without acr
|
||||
|
||||
self.factory.server.sessions.portal_connect(sessid, sess)
|
||||
|
||||
elif operation == 'PDISCONN': #'portal_session_disconnect'
|
||||
# session closed from portal side
|
||||
self.factory.server.sessions.portal_disconnect(sessid)
|
||||
|
||||
elif operation == 'PSYNC': #'portal_session_sync'
|
||||
# force a resync of sessions when portal reconnects to server (e.g. after a server reboot)
|
||||
# the data kwarg contains a dict {sessid: {arg1:val1,...}} representing the attributes
|
||||
# to sync for each session.
|
||||
sesslist = []
|
||||
server_sessionhandler = self.factory.server.sessions
|
||||
for sessid, sessdict in data.items():
|
||||
sess = ServerSession()
|
||||
sess.sessionhandler = server_sessionhandler
|
||||
sess.load_sync_data(sessdict)
|
||||
if sess.uid:
|
||||
sess.player = PlayerDB.objects.get_player_from_uid(sess.uid)
|
||||
sess.at_sync()
|
||||
sesslist.append(sess)
|
||||
# replace sessions on server
|
||||
server_sessionhandler.portal_session_sync(sesslist)
|
||||
|
||||
# after sync is complete we force-validate all scripts (this starts everthing)
|
||||
init_mode = ServerConfig.objects.conf("server_restart_mode", default=None)
|
||||
ScriptDB.objects.validate(init_mode=init_mode)
|
||||
ServerConfig.objects.conf("server_restart_mode", delete=True)
|
||||
|
||||
else:
|
||||
raise Exception(_("operation %(op)s not recognized.") % {'op': operation})
|
||||
|
||||
|
||||
return {}
|
||||
ServerAdmin.responder(amp_server_admin)
|
||||
|
||||
def call_remote_ServerAdmin(self, sessid, operation="", data=""):
|
||||
"""
|
||||
Access method called by the Portal and Executed on the Portal.
|
||||
"""
|
||||
#print "serveradmin (portal side):", sessid, operation, data
|
||||
data = utils.to_str(pickle.dumps(data))
|
||||
|
||||
self.callRemote(ServerAdmin,
|
||||
sessid=sessid,
|
||||
operation=operation,
|
||||
data=data).addErrback(self.errback, "ServerAdmin")
|
||||
|
||||
# Portal administraton from the Server side
|
||||
|
||||
def amp_portal_admin(self, sessid, operation, data):
|
||||
"""
|
||||
This allows the server to perform admin
|
||||
operations on the portal. This is executed on the Portal.
|
||||
"""
|
||||
data = pickle.loads(utils.to_str(data))
|
||||
|
||||
#print "portaladmin (portal side):", sessid, operation, data
|
||||
if operation == 'SLOGIN': # 'server_session_login'
|
||||
# a session has authenticated; sync it.
|
||||
sess = self.factory.portal.sessions.get_session(sessid)
|
||||
sess.load_sync_data(data)
|
||||
|
||||
elif operation == 'SDISCONN': #'server_session_disconnect'
|
||||
# the server is ordering to disconnect the session
|
||||
self.factory.portal.sessions.server_disconnect(sessid, reason=data)
|
||||
|
||||
elif operation == 'SDISCONNALL': #'server_session_disconnect_all'
|
||||
# server orders all sessions to disconnect
|
||||
self.factory.portal.sessions.server_disconnect_all(reason=data)
|
||||
|
||||
elif operation == 'SSHUTD': #server_shutdown'
|
||||
# the server orders the portal to shut down
|
||||
self.factory.portal.shutdown(restart=False)
|
||||
|
||||
elif operation == 'SSYNC': #'server_session_sync'
|
||||
# server wants to save session data to the portal, maybe because
|
||||
# it's about to shut down. We don't overwrite any sessions,
|
||||
# just update data on them and remove eventual ones that are
|
||||
# out of sync (shouldn't happen normally).
|
||||
|
||||
portal_sessionhandler = self.factory.portal.sessions.sessions
|
||||
|
||||
to_save = [sessid for sessid in data if sessid in portal_sessionhandler.sessions]
|
||||
to_delete = [sessid for sessid in data if sessid not in to_save]
|
||||
|
||||
# save protocols
|
||||
for sessid in to_save:
|
||||
portal_sessionhandler.sessions[sessid].load_sync_data(data[sessid])
|
||||
# disconnect missing protocols
|
||||
for sessid in to_delete:
|
||||
portal_sessionhandler.server_disconnect(sessid)
|
||||
else:
|
||||
raise Exception(_("operation %(op)s not recognized.") % {'op': operation})
|
||||
return {}
|
||||
PortalAdmin.responder(amp_portal_admin)
|
||||
|
||||
def call_remote_PortalAdmin(self, sessid, operation="", data=""):
|
||||
"""
|
||||
Access method called by the server side.
|
||||
"""
|
||||
#print "portaladmin (server side):", sessid, operation, data
|
||||
data = utils.to_str(pickle.dumps(data))
|
||||
|
||||
self.callRemote(PortalAdmin,
|
||||
sessid=sessid,
|
||||
operation=operation,
|
||||
data=data).addErrback(self.errback, "PortalAdmin")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -13,6 +13,9 @@ from src.server.models import ServerConfig
|
|||
from src.help.models import HelpEntry
|
||||
from src.utils import create
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
def create_config_values():
|
||||
"""
|
||||
Creates the initial config values.
|
||||
|
|
@ -31,7 +34,7 @@ def create_objects():
|
|||
Creates the #1 player and Limbo room.
|
||||
"""
|
||||
|
||||
print " Creating objects (Player #1 and Limbo room) ..."
|
||||
print _(" Creating objects (Player #1 and Limbo room) ...")
|
||||
|
||||
# Set the initial User's account object's username on the #1 object.
|
||||
# This object is pure django and only holds name, email and password.
|
||||
|
|
@ -55,7 +58,7 @@ def create_objects():
|
|||
typeclass=character_typeclass,
|
||||
user=god_user)
|
||||
god_character.id = 1
|
||||
god_character.db.desc = 'This is User #1.'
|
||||
god_character.db.desc = _('This is User #1.')
|
||||
god_character.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all();puppet:false()")
|
||||
|
||||
god_character.save()
|
||||
|
|
@ -63,12 +66,13 @@ def create_objects():
|
|||
# Limbo is the initial starting room.
|
||||
|
||||
room_typeclass = settings.BASE_ROOM_TYPECLASS
|
||||
limbo_obj = create.create_object(room_typeclass, 'Limbo')
|
||||
limbo_obj = create.create_object(room_typeclass, _('Limbo'))
|
||||
limbo_obj.id = 2
|
||||
string = "Welcome to your new %chEvennia%cn-based game."
|
||||
string = " Welcome to your new {wEvennia{n-based game."
|
||||
string += " From here you are ready to begin development."
|
||||
string += " If you should need help or would like to participate"
|
||||
string += " in community discussions, visit http://evennia.com."
|
||||
string = _(string)
|
||||
limbo_obj.db.desc = string
|
||||
limbo_obj.save()
|
||||
|
||||
|
|
@ -80,7 +84,7 @@ def create_channels():
|
|||
"""
|
||||
Creates some sensible default channels.
|
||||
"""
|
||||
print " Creating default channels ..."
|
||||
print _(" Creating default channels ...")
|
||||
|
||||
# public channel
|
||||
key, aliases, desc, locks = settings.CHANNEL_PUBLIC
|
||||
|
|
@ -103,12 +107,12 @@ def import_MUX_help_files():
|
|||
"""
|
||||
Imports the MUX help files.
|
||||
"""
|
||||
print " Importing MUX help database (devel reference only) ..."
|
||||
print _(" Importing MUX help database (devel reference only) ...")
|
||||
management.call_command('loaddata', '../src/help/mux_help_db.json', verbosity=0)
|
||||
# categorize the MUX help files into its own category.
|
||||
default_category = "MUX"
|
||||
print " Moving imported help db to help category '%s'." \
|
||||
% default_category
|
||||
print _(" Moving imported help db to help category '%(default)s'." \
|
||||
% {'default': default_category})
|
||||
HelpEntry.objects.all_to_category(default_category)
|
||||
|
||||
def create_system_scripts():
|
||||
|
|
@ -118,7 +122,7 @@ def create_system_scripts():
|
|||
"""
|
||||
from src.scripts import scripts
|
||||
|
||||
print " Creating and starting global scripts ..."
|
||||
print _(" Creating and starting global scripts ...")
|
||||
|
||||
# check so that all sessions are alive.
|
||||
script1 = create.create_script(scripts.CheckSessions)
|
||||
|
|
@ -127,7 +131,7 @@ def create_system_scripts():
|
|||
# update the channel handler to make sure it's in sync
|
||||
script3 = create.create_script(scripts.ValidateChannelHandler)
|
||||
if not script1 or not script2 or not script3:
|
||||
print " Error creating system scripts."
|
||||
print _(" Error creating system scripts.")
|
||||
|
||||
def start_game_time():
|
||||
"""
|
||||
|
|
@ -136,7 +140,7 @@ def start_game_time():
|
|||
the total run time of the server as well as its current uptime
|
||||
(the uptime can also be found directly from the server though).
|
||||
"""
|
||||
print " Starting in-game time ..."
|
||||
print _(" Starting in-game time ...")
|
||||
from src.utils import gametime
|
||||
gametime.init_gametime()
|
||||
|
||||
|
|
@ -155,17 +159,17 @@ def create_admin_media_links():
|
|||
dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
|
||||
apath = os.path.join(settings.ADMIN_MEDIA_ROOT)
|
||||
if os.path.isdir(apath):
|
||||
print " ADMIN_MEDIA_ROOT already exists. Ignored."
|
||||
print _(" ADMIN_MEDIA_ROOT already exists. Ignored.")
|
||||
return
|
||||
if os.name == 'nt':
|
||||
print " Admin-media files copied to ADMIN_MEDIA_ROOT (Windows mode)."
|
||||
print _(" Admin-media files copied to ADMIN_MEDIA_ROOT (Windows mode).")
|
||||
os.mkdir(apath)
|
||||
os.system('xcopy "%s" "%s" /e /q /c' % (dpath, apath))
|
||||
if os.name == 'posix':
|
||||
os.symlink(dpath, apath)
|
||||
print " Admin-media symlinked to ADMIN_MEDIA_ROOT."
|
||||
print _(" Admin-media symlinked to ADMIN_MEDIA_ROOT.")
|
||||
else:
|
||||
print " Admin-media files should be copied manually to ADMIN_MEDIA_ROOT."
|
||||
print _(" Admin-media files should be copied manually to ADMIN_MEDIA_ROOT.")
|
||||
|
||||
def handle_setup(last_step):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ from src.utils.idmapper.models import SharedMemoryModel
|
|||
from src.utils import logger, utils
|
||||
from src.server.manager import ServerConfigManager
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# ServerConfig
|
||||
|
|
@ -82,7 +85,7 @@ class ServerConfig(SharedMemoryModel):
|
|||
"Setter. Allows for self.value = value"
|
||||
if utils.has_parent('django.db.models.base.Model', value):
|
||||
# we have to protect against storing db objects.
|
||||
logger.log_errmsg("ServerConfig cannot store db objects! (%s)" % value)
|
||||
logger.log_errmsg(_("ServerConfig cannot store db objects! (%s)" % value))
|
||||
return
|
||||
self.db_value = pickle.dumps(value)
|
||||
self.save()
|
||||
|
|
|
|||
304
src/server/portal.py
Normal file
304
src/server/portal.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
"""
|
||||
This module implements the main Evennia server process, the core of
|
||||
the game engine.
|
||||
|
||||
This module should be started with the 'twistd' executable since it
|
||||
sets up all the networking features. (this is done automatically
|
||||
by game/evennia.py).
|
||||
|
||||
"""
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
if os.name == 'nt':
|
||||
# For Windows batchfile we need an extra path insertion here.
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from twisted.application import internet, service
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.web import server, static
|
||||
from django.conf import settings
|
||||
from src.utils.utils import get_evennia_version
|
||||
from src.server.sessionhandler import PORTAL_SESSIONS
|
||||
|
||||
if os.name == 'nt':
|
||||
# For Windows we need to handle pid files manually.
|
||||
PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, 'portal.pid')
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Evennia Portal settings
|
||||
#------------------------------------------------------------
|
||||
|
||||
VERSION = get_evennia_version()
|
||||
|
||||
SERVERNAME = settings.SERVERNAME
|
||||
|
||||
PORTAL_RESTART = os.path.join(settings.GAME_DIR, 'portal.restart')
|
||||
|
||||
TELNET_PORTS = settings.TELNET_PORTS
|
||||
SSL_PORTS = settings.SSL_PORTS
|
||||
SSH_PORTS = settings.SSH_PORTS
|
||||
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
|
||||
|
||||
TELNET_INTERFACES = settings.TELNET_INTERFACES
|
||||
SSL_INTERFACES = settings.SSL_INTERFACES
|
||||
SSH_INTERFACES = settings.SSH_INTERFACES
|
||||
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
|
||||
|
||||
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
|
||||
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
|
||||
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
|
||||
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
|
||||
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||
IMC2_ENABLED = settings.IMC2_ENABLED
|
||||
IRC_ENABLED = settings.IRC_ENABLED
|
||||
|
||||
AMP_HOST = settings.AMP_HOST
|
||||
AMP_PORT = settings.AMP_PORT
|
||||
AMP_ENABLED = AMP_HOST and AMP_PORT
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Portal Service object
|
||||
#------------------------------------------------------------
|
||||
class Portal(object):
|
||||
|
||||
"""
|
||||
The main Portal server handler. This object sets up the database and
|
||||
tracks and interlinks all the twisted network services that make up
|
||||
Portal.
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
"""
|
||||
Setup the server.
|
||||
|
||||
application - an instantiated Twisted application
|
||||
|
||||
"""
|
||||
sys.path.append('.')
|
||||
|
||||
# create a store of services
|
||||
self.services = service.IServiceCollection(application)
|
||||
self.amp_protocol = None # set by amp factory
|
||||
self.sessions = PORTAL_SESSIONS
|
||||
self.sessions.portal = self
|
||||
|
||||
print '\n' + '-'*50
|
||||
|
||||
# Make info output to the terminal.
|
||||
self.terminal_output()
|
||||
|
||||
print '-'*50
|
||||
|
||||
# set a callback if the server is killed abruptly,
|
||||
# by Ctrl-C, reboot etc.
|
||||
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _abrupt=True)
|
||||
|
||||
self.game_running = False
|
||||
|
||||
def terminal_output(self):
|
||||
"""
|
||||
Outputs server startup info to the terminal.
|
||||
"""
|
||||
print _(' %(servername)s Portal (%(version)s) started.') % {'servername': SERVERNAME, 'version': VERSION}
|
||||
if AMP_ENABLED:
|
||||
print " amp (Server): %s" % AMP_PORT
|
||||
if TELNET_ENABLED:
|
||||
ports = ", ".join([str(port) for port in TELNET_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in TELNET_INTERFACES if iface != '0.0.0.0'])
|
||||
print " telnet%s: %s" % (ifaces, ports)
|
||||
if SSH_ENABLED:
|
||||
ports = ", ".join([str(port) for port in SSH_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in SSH_INTERFACES if iface != '0.0.0.0'])
|
||||
print " ssh%s: %s" % (ifaces, ports)
|
||||
if SSL_ENABLED:
|
||||
ports = ", ".join([str(port) for port in SSL_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in SSL_INTERFACES if iface != '0.0.0.0'])
|
||||
print " ssl%s: %s" % (ifaces, ports)
|
||||
if WEBSERVER_ENABLED:
|
||||
clientstring = ""
|
||||
if WEBCLIENT_ENABLED:
|
||||
clientstring = '/client'
|
||||
ports = ", ".join([str(port) for port in WEBSERVER_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in WEBSERVER_INTERFACES if iface != '0.0.0.0'])
|
||||
print " webserver%s%s: %s" % (clientstring, ifaces, ports)
|
||||
|
||||
def set_restart_mode(self, mode=None):
|
||||
"""
|
||||
This manages the flag file that tells the runner if the server should
|
||||
be restarted or is shutting down. Valid modes are True/False and None.
|
||||
If mode is None, no change will be done to the flag file.
|
||||
"""
|
||||
if mode == None:
|
||||
return
|
||||
f = open(PORTAL_RESTART, 'w')
|
||||
print _("writing mode=%(mode)s to %(portal_restart)s") % {'mode': mode, 'portal_restart': PORTAL_RESTART}
|
||||
f.write(str(mode))
|
||||
f.close()
|
||||
|
||||
def shutdown(self, restart=None, _abrupt=False):
|
||||
"""
|
||||
Shuts down the server from inside it.
|
||||
|
||||
restart - True/False sets the flags so the server will be
|
||||
restarted or not. If None, the current flag setting
|
||||
(set at initialization or previous runs) is used.
|
||||
_abrupt - this is set if server is stopped by a kill command,
|
||||
in which case the reactor is dead anyway.
|
||||
|
||||
Note that restarting (regardless of the setting) will not work
|
||||
if the Portal is currently running in daemon mode. In that
|
||||
case it always needs to be restarted manually.
|
||||
"""
|
||||
self.set_restart_mode(restart)
|
||||
if not _abrupt:
|
||||
reactor.callLater(0, reactor.stop)
|
||||
if os.name == 'nt' and os.path.exists(PORTAL_PIDFILE):
|
||||
# for Windows we need to remove pid files manually
|
||||
os.remove(PORTAL_PIDFILE)
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Start the Portal proxy server and add all active services
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
# twistd requires us to define the variable 'application' so it knows
|
||||
# what to execute from.
|
||||
application = service.Application('Portal')
|
||||
|
||||
# The main Portal server program. This sets up the database
|
||||
# and is where we store all the other services.
|
||||
PORTAL = Portal(application)
|
||||
|
||||
if AMP_ENABLED:
|
||||
|
||||
# The AMP protocol handles the communication between
|
||||
# the portal and the mud server. Only reason to ever deactivate
|
||||
# it would be during testing and debugging.
|
||||
|
||||
from src.server import amp
|
||||
|
||||
factory = amp.AmpClientFactory(PORTAL)
|
||||
amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
|
||||
amp_client.setName('evennia_amp')
|
||||
PORTAL.services.addService(amp_client)
|
||||
|
||||
# We group all the various services under the same twisted app.
|
||||
# These will gradually be started as they are initialized below.
|
||||
|
||||
if TELNET_ENABLED:
|
||||
|
||||
# Start telnet game connections
|
||||
|
||||
from src.server import telnet
|
||||
|
||||
for interface in TELNET_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(TELNET_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in TELNET_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = telnet.TelnetProtocol
|
||||
factory.sessionhandler = PORTAL_SESSIONS
|
||||
telnet_service = internet.TCPServer(port, factory, interface=interface)
|
||||
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
||||
PORTAL.services.addService(telnet_service)
|
||||
|
||||
if SSL_ENABLED:
|
||||
|
||||
# Start SSL game connection (requires PyOpenSSL).
|
||||
|
||||
from src.server import ssl
|
||||
|
||||
for interface in SSL_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(SSL_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSL_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.sessionhandler = PORTAL_SESSIONS
|
||||
factory.protocol = ssl.SSLProtocol
|
||||
ssl_service = internet.SSLServer(port, factory, ssl.getSSLContext(), interface=interface)
|
||||
ssl_service.setName('EvenniaSSL%s' % pstring)
|
||||
PORTAL.services.addService(ssl_service)
|
||||
|
||||
if SSH_ENABLED:
|
||||
|
||||
# Start SSH game connections. Will create a keypair in evennia/game if necessary.
|
||||
|
||||
from src.server import ssh
|
||||
|
||||
for interface in SSH_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(SSH_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSH_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
|
||||
'protocolArgs':(),
|
||||
'sessions':PORTAL_SESSIONS})
|
||||
ssh_service = internet.TCPServer(port, factory, interface=interface)
|
||||
ssh_service.setName('EvenniaSSH%s' % pstring)
|
||||
PORTAL.services.addService(ssh_service)
|
||||
|
||||
if WEBSERVER_ENABLED:
|
||||
|
||||
# Start a django-compatible webserver.
|
||||
|
||||
from twisted.python import threadpool
|
||||
from src.server.webserver import DjangoWebRoot, WSGIWebServer
|
||||
|
||||
# start a thread pool and define the root url (/) as a wsgi resource
|
||||
# recognized by Django
|
||||
threads = threadpool.ThreadPool()
|
||||
web_root = DjangoWebRoot(threads)
|
||||
# point our media resources to url /media
|
||||
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
|
||||
|
||||
if WEBCLIENT_ENABLED:
|
||||
# create ajax client processes at /webclientdata
|
||||
from src.server.webclient import WebClient
|
||||
webclient = WebClient()
|
||||
webclient.sessionhandler = PORTAL_SESSIONS
|
||||
web_root.putChild("webclientdata", webclient)
|
||||
|
||||
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||
|
||||
for interface in WEBSERVER_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(WEBSERVER_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in WEBSERVER_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
# create the webserver
|
||||
webserver = WSGIWebServer(threads, port, web_site, interface=interface)
|
||||
webserver.setName('EvenniaWebServer%s' % pstring)
|
||||
PORTAL.services.addService(webserver)
|
||||
|
||||
if IRC_ENABLED:
|
||||
|
||||
# IRC channel connections
|
||||
|
||||
from src.comms import irc
|
||||
irc.connect_all()
|
||||
|
||||
if IMC2_ENABLED:
|
||||
|
||||
# IMC2 channel connections
|
||||
|
||||
from src.comms import imc2
|
||||
imc2.connect_all()
|
||||
|
||||
if os.name == 'nt':
|
||||
# Windows only: Set PID file manually
|
||||
f = open(os.path.join(settings.GAME_DIR, 'portal.pid'), 'w')
|
||||
f.write(str(os.getpid()))
|
||||
f.close()
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
This module implements the main Evennia server process, the core of
|
||||
the game engine. Don't import this module directly! If you need to
|
||||
access the server processes from code, instead go via the session-
|
||||
handler: src.sessionhandler.SESSIONS.server
|
||||
the game engine.
|
||||
|
||||
This module should be started with the 'twistd' executable since it
|
||||
sets up all the networking features. (this is done automatically
|
||||
|
|
@ -12,6 +10,7 @@ by game/evennia.py).
|
|||
import time
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
if os.name == 'nt':
|
||||
# For Windows batchfile we need an extra path insertion here.
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||||
|
|
@ -22,14 +21,23 @@ from twisted.internet import protocol, reactor, defer
|
|||
from twisted.web import server, static
|
||||
from django.db import connection
|
||||
from django.conf import settings
|
||||
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.server.models import ServerConfig
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.server import initial_setup
|
||||
|
||||
from src.utils.utils import get_evennia_version
|
||||
from src.comms import channelhandler
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
if os.name == 'nt':
|
||||
# For Windows we need to handle pid files manually.
|
||||
SERVER_PIDFILE = os.path.join(settings.GAME_DIR, 'server.pid')
|
||||
|
||||
SERVER_RESTART = os.path.join(settings.GAME_DIR, 'server.restart')
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Evennia Server settings
|
||||
|
|
@ -38,23 +46,10 @@ from src.comms import channelhandler
|
|||
SERVERNAME = settings.SERVERNAME
|
||||
VERSION = get_evennia_version()
|
||||
|
||||
TELNET_PORTS = settings.TELNET_PORTS
|
||||
SSL_PORTS = settings.SSL_PORTS
|
||||
SSH_PORTS = settings.SSH_PORTS
|
||||
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
|
||||
AMP_ENABLED = True
|
||||
AMP_HOST = settings.AMP_HOST
|
||||
AMP_PORT = settings.AMP_PORT
|
||||
|
||||
TELNET_INTERFACES = settings.TELNET_INTERFACES
|
||||
SSL_INTERFACES = settings.SSL_INTERFACES
|
||||
SSH_INTERFACES = settings.SSH_INTERFACES
|
||||
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
|
||||
|
||||
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
|
||||
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
|
||||
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
|
||||
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
|
||||
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||
IMC2_ENABLED = settings.IMC2_ENABLED
|
||||
IRC_ENABLED = settings.IRC_ENABLED
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Evennia Main Server object
|
||||
|
|
@ -75,10 +70,13 @@ class Evennia(object):
|
|||
|
||||
"""
|
||||
sys.path.append('.')
|
||||
|
||||
|
||||
# create a store of services
|
||||
self.services = service.IServiceCollection(application)
|
||||
|
||||
self.amp_protocol = None # set by amp factory
|
||||
self.sessions = SESSIONS
|
||||
self.sessions.server = self
|
||||
|
||||
print '\n' + '-'*50
|
||||
|
||||
# Database-specific startup optimizations.
|
||||
|
|
@ -87,20 +85,11 @@ class Evennia(object):
|
|||
# Run the initial setup if needed
|
||||
self.run_initial_setup()
|
||||
|
||||
# we have to null this here.
|
||||
SESSIONS.session_count(0)
|
||||
# we link ourself to the sessionhandler so other modules don't have to
|
||||
# re-import the server module itself (which would re-initialize it).
|
||||
SESSIONS.server = self
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
# initialize channelhandler
|
||||
channelhandler.CHANNELHANDLER.update()
|
||||
|
||||
# init all global scripts
|
||||
ScriptDB.objects.validate(init_mode=True)
|
||||
|
||||
|
||||
# Make info output to the terminal.
|
||||
self.terminal_output()
|
||||
|
||||
|
|
@ -138,7 +127,7 @@ class Evennia(object):
|
|||
if not last_initial_setup_step:
|
||||
# None is only returned if the config does not exist,
|
||||
# i.e. this is an empty DB that needs populating.
|
||||
print ' Server started for the first time. Setting defaults.'
|
||||
print _(' Server started for the first time. Setting defaults.')
|
||||
initial_setup.handle_setup(0)
|
||||
print '-'*50
|
||||
elif int(last_initial_setup_step) >= 0:
|
||||
|
|
@ -146,51 +135,86 @@ class Evennia(object):
|
|||
# modules and setup will resume from this step, retrying
|
||||
# the last failed module. When all are finished, the step
|
||||
# is set to -1 to show it does not need to be run again.
|
||||
print ' Resuming initial setup from step %s.' % \
|
||||
last_initial_setup_step
|
||||
print _(' Resuming initial setup from step %(last)s.' % \
|
||||
{'last': last_initial_setup_step})
|
||||
initial_setup.handle_setup(int(last_initial_setup_step))
|
||||
print '-'*50
|
||||
|
||||
|
||||
def terminal_output(self):
|
||||
"""
|
||||
Outputs server startup info to the terminal.
|
||||
"""
|
||||
print ' %s (%s) started on port(s):' % (SERVERNAME, VERSION)
|
||||
if TELNET_ENABLED:
|
||||
ports = ", ".join([str(port) for port in TELNET_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in TELNET_INTERFACES if iface != '0.0.0.0'])
|
||||
print " telnet%s: %s" % (ifaces, ports)
|
||||
if SSH_ENABLED:
|
||||
ports = ", ".join([str(port) for port in SSH_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in SSH_INTERFACES if iface != '0.0.0.0'])
|
||||
print " ssh%s: %s" % (ifaces, ports)
|
||||
if SSL_ENABLED:
|
||||
ports = ", ".join([str(port) for port in SSL_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in SSL_INTERFACES if iface != '0.0.0.0'])
|
||||
print " ssl%s: %s" % (ifaces, ports)
|
||||
if WEBSERVER_ENABLED:
|
||||
clientstring = ""
|
||||
if WEBCLIENT_ENABLED:
|
||||
clientstring = '/client'
|
||||
ports = ", ".join([str(port) for port in WEBSERVER_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in WEBSERVER_INTERFACES if iface != '0.0.0.0'])
|
||||
print " webserver%s%s: %s" % (clientstring, ifaces, ports)
|
||||
print _(' %(servername)s Portal (%(version)s) started.') % {'servername': SERVERNAME, 'version': VERSION}
|
||||
print ' amp (Portal): %s' % AMP_PORT
|
||||
|
||||
def shutdown(self, message="{rThe server has been shutdown. Disconnecting.{n", _abrupt=False):
|
||||
def set_restart_mode(self, mode=None):
|
||||
"""
|
||||
If called directly, this disconnects everyone cleanly and shuts down the
|
||||
reactor. If the server is killed by other means (Ctrl-C, reboot etc), this
|
||||
might be called as a callback, at which point the reactor is already dead
|
||||
and should not be tried to stop again (_abrupt=True).
|
||||
This manages the flag file that tells the runner if the server is
|
||||
reloading, resetting or shutting down. Valid modes are
|
||||
'reload', 'reset', 'shutdown' and None.
|
||||
If mode is None, no change will be done to the flag file.
|
||||
|
||||
message - message to send to all connected sessions
|
||||
_abrupt - only to be used by internal callback_mechanism.
|
||||
Either way, the active restart setting (Restart=True/False) is
|
||||
returned so the server knows which more it's in.
|
||||
"""
|
||||
if mode == None:
|
||||
if os.path.exists(SERVER_RESTART) and 'True' == open(SERVER_RESTART, 'r').read():
|
||||
mode = 'reload'
|
||||
else:
|
||||
mode = 'shutdown'
|
||||
else:
|
||||
restart = mode in ('reload', 'reset')
|
||||
f = open(SERVER_RESTART, 'w')
|
||||
f.write(str(restart))
|
||||
f.close()
|
||||
return mode
|
||||
|
||||
def shutdown(self, mode=None, _abrupt=False):
|
||||
"""
|
||||
SESSIONS.disconnect_all_sessions(reason=message)
|
||||
Shuts down the server from inside it.
|
||||
|
||||
mode - sets the server restart mode.
|
||||
'reload' - server restarts, no "persistent" scripts are stopped, at_reload hooks called.
|
||||
'reset' - server restarts, non-persistent scripts stopped, at_shutdown hooks called.
|
||||
'shutdown' - like reset, but server will not auto-restart.
|
||||
None - keep currently set flag from flag file.
|
||||
_abrupt - this is set if server is stopped by a kill command,
|
||||
in which case the reactor is dead anyway.
|
||||
"""
|
||||
mode = self.set_restart_mode(mode)
|
||||
|
||||
# call shutdown hooks on all cached objects
|
||||
|
||||
from src.objects.models import ObjectDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.server.models import ServerConfig
|
||||
|
||||
if mode == 'reload':
|
||||
# call restart hooks
|
||||
[(o.typeclass(o), o.at_server_reload()) for o in ObjectDB.get_all_cached_instances()]
|
||||
[(p.typeclass(p), p.at_server_reload()) for p in PlayerDB.get_all_cached_instances()]
|
||||
[(s.typeclass(s), s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()]
|
||||
|
||||
ServerConfig.objects.conf("server_restart_mode", "reload")
|
||||
|
||||
else:
|
||||
if mode == 'reset':
|
||||
# don't call disconnect hooks on reset
|
||||
[(o.typeclass(o), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
|
||||
else: # shutdown
|
||||
[(o.typeclass(o), o.at_disconnect(), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
|
||||
[(p.typeclass(p), p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()]
|
||||
[(s.typeclass(s), s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()]
|
||||
|
||||
ServerConfig.objects.conf("server_restart_mode", "reset")
|
||||
|
||||
if not _abrupt:
|
||||
reactor.callLater(0, reactor.stop)
|
||||
|
||||
|
||||
if os.name == 'nt' and os.path.exists(SERVER_PIDFILE):
|
||||
# for Windows we need to remove pid files manually
|
||||
os.remove(SERVER_PIDFILE)
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Start the Evennia game server and add all active services
|
||||
|
|
@ -208,108 +232,24 @@ application = service.Application('Evennia')
|
|||
# and is where we store all the other services.
|
||||
EVENNIA = Evennia(application)
|
||||
|
||||
# We group all the various services under the same twisted app.
|
||||
# These will gradually be started as they are initialized below.
|
||||
# The AMP protocol handles the communication between
|
||||
# the portal and the mud server. Only reason to ever deactivate
|
||||
# it would be during testing and debugging.
|
||||
|
||||
if TELNET_ENABLED:
|
||||
if AMP_ENABLED:
|
||||
|
||||
# Start telnet game connections
|
||||
from src.server import amp
|
||||
|
||||
from src.server import telnet
|
||||
|
||||
for interface in TELNET_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(TELNET_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in TELNET_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = telnet.TelnetProtocol
|
||||
telnet_service = internet.TCPServer(port, factory, interface=interface)
|
||||
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
||||
EVENNIA.services.addService(telnet_service)
|
||||
|
||||
if SSL_ENABLED:
|
||||
|
||||
# Start SSL game connection (requires PyOpenSSL).
|
||||
|
||||
from src.server import ssl
|
||||
|
||||
for interface in SSL_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(SSL_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSL_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = ssl.SSLProtocol
|
||||
ssl_service = internet.SSLServer(port, factory, ssl.getSSLContext(), interface=interface)
|
||||
ssl_service.setName('EvenniaSSL%s' % pstring)
|
||||
EVENNIA.services.addService(ssl_service)
|
||||
|
||||
if SSH_ENABLED:
|
||||
|
||||
# Start SSH game connections. Will create a keypair in evennia/game if necessary.
|
||||
|
||||
from src.server import ssh
|
||||
|
||||
for interface in SSH_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(SSH_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSH_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
|
||||
'protocolArgs':()})
|
||||
ssh_service = internet.TCPServer(port, factory, interface=interface)
|
||||
ssh_service.setName('EvenniaSSH%s' % pstring)
|
||||
EVENNIA.services.addService(ssh_service)
|
||||
|
||||
if WEBSERVER_ENABLED:
|
||||
|
||||
# Start a django-compatible webserver.
|
||||
|
||||
from twisted.python import threadpool
|
||||
from src.server.webserver import DjangoWebRoot, WSGIWebServer
|
||||
|
||||
# start a thread pool and define the root url (/) as a wsgi resource
|
||||
# recognized by Django
|
||||
threads = threadpool.ThreadPool()
|
||||
web_root = DjangoWebRoot(threads)
|
||||
# point our media resources to url /media
|
||||
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
|
||||
|
||||
if WEBCLIENT_ENABLED:
|
||||
# create ajax client processes at /webclientdata
|
||||
from src.server.webclient import WebClient
|
||||
web_root.putChild("webclientdata", WebClient())
|
||||
|
||||
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||
|
||||
for interface in WEBSERVER_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(WEBSERVER_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in WEBSERVER_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
# create the webserver
|
||||
webserver = WSGIWebServer(threads, port, web_site, interface=interface)
|
||||
webserver.setName('EvenniaWebServer%s' % pstring)
|
||||
EVENNIA.services.addService(webserver)
|
||||
|
||||
if IRC_ENABLED:
|
||||
|
||||
# IRC channel connections
|
||||
|
||||
from src.comms import irc
|
||||
irc.connect_all()
|
||||
|
||||
if IMC2_ENABLED:
|
||||
|
||||
# IMC2 channel connections
|
||||
|
||||
from src.comms import imc2
|
||||
imc2.connect_all()
|
||||
factory = amp.AmpServerFactory(EVENNIA)
|
||||
amp_service = internet.TCPServer(AMP_PORT, factory)
|
||||
amp_service.setName("EvenniaPortal")
|
||||
EVENNIA.services.addService(amp_service)
|
||||
|
||||
# clear server startup mode
|
||||
ServerConfig.objects.conf("server_starting_mode", delete=True)
|
||||
|
||||
if os.name == 'nt':
|
||||
# Windows only: Set PID file manually
|
||||
f = open(os.path.join(settings.GAME_DIR, 'server.pid'), 'w')
|
||||
f.write(str(os.getpid()))
|
||||
f.close()
|
||||
|
|
|
|||
247
src/server/serversession.py
Normal file
247
src/server/serversession.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""
|
||||
This defines a the Server's generic session object. This object represents
|
||||
a connection to the outside world but don't know any details about how the
|
||||
connection actually happens (so it's the same for telnet, web, ssh etc).
|
||||
|
||||
It is stored on the Server side (as opposed to protocol-specific sessions which
|
||||
are stored on the Portal side)
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.comms.models import Channel
|
||||
from src.utils import logger
|
||||
from src.commands import cmdhandler
|
||||
|
||||
IDLE_COMMAND = settings.IDLE_COMMAND
|
||||
|
||||
from src.server.session import Session
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Server Session
|
||||
#------------------------------------------------------------
|
||||
|
||||
class ServerSession(Session):
|
||||
"""
|
||||
This class represents a player's session and is a template for
|
||||
individual protocols to communicate with Evennia.
|
||||
|
||||
Each player gets a session assigned to them whenever they connect
|
||||
to the game server. All communication between game and player goes
|
||||
through their session.
|
||||
|
||||
"""
|
||||
|
||||
def at_sync(self):
|
||||
"""
|
||||
This is called whenever a session has been resynced with the portal.
|
||||
At this point all relevant attributes have already been set and self.player
|
||||
been assigned (if applicable).
|
||||
|
||||
Since this is often called after a server restart we need to set up
|
||||
the session as it was.
|
||||
"""
|
||||
if not self.logged_in:
|
||||
return
|
||||
|
||||
player = self.get_player()
|
||||
character = self.get_character()
|
||||
if player:
|
||||
player.at_init()
|
||||
if character:
|
||||
character.at_init()
|
||||
# start (persistent) scripts on this object
|
||||
ScriptDB.objects.validate(obj=character)
|
||||
|
||||
def session_login(self, player):
|
||||
"""
|
||||
Startup mechanisms that need to run at login. This is called
|
||||
by the login command (which need to have handled authentication
|
||||
already before calling this method)
|
||||
|
||||
player - the connected player
|
||||
"""
|
||||
|
||||
# actually do the login by assigning session data
|
||||
|
||||
self.player = player
|
||||
self.user = player.user
|
||||
self.uid = self.user.id
|
||||
self.uname = self.user.username
|
||||
self.logged_in = True
|
||||
self.conn_time = time.time()
|
||||
|
||||
# Update account's last login time.
|
||||
self.user.last_login = datetime.now()
|
||||
self.user.save()
|
||||
|
||||
# player init
|
||||
player.at_init()
|
||||
|
||||
# Check if this is the first time the *player* logs in
|
||||
if player.db.FIRST_LOGIN:
|
||||
player.at_first_login()
|
||||
del player.db.FIRST_LOGIN
|
||||
player.at_pre_login()
|
||||
|
||||
character = player.character
|
||||
character.at_init()
|
||||
if character:
|
||||
# this player has a character. Check if it's the
|
||||
# first time *this character* logs in
|
||||
if character.db.FIRST_LOGIN:
|
||||
character.at_first_login()
|
||||
del character.db.FIRST_LOGIN
|
||||
# run character login hook
|
||||
character.at_pre_login()
|
||||
|
||||
# this is always called first
|
||||
player.at_init()
|
||||
|
||||
self.log(_('Logged in: %(self)s') % {'self': self})
|
||||
|
||||
# start (persistent) scripts on this object
|
||||
ScriptDB.objects.validate(obj=self.player.character)
|
||||
|
||||
#add session to connected list
|
||||
self.sessionhandler.login(self)
|
||||
|
||||
# post-login hooks
|
||||
player.at_post_login()
|
||||
if character:
|
||||
character.at_post_login()
|
||||
|
||||
def session_disconnect(self):
|
||||
"""
|
||||
Clean up the session, removing it from the game and doing some
|
||||
accounting. This method is used also for non-loggedin
|
||||
accounts.
|
||||
"""
|
||||
if self.logged_in:
|
||||
player = self.get_player()
|
||||
character = self.get_character()
|
||||
if character:
|
||||
character.at_disconnect()
|
||||
uaccount = player.user
|
||||
uaccount.last_login = datetime.now()
|
||||
uaccount.save()
|
||||
self.logged_in = False
|
||||
self.sessionhandler.disconnect(self)
|
||||
|
||||
def get_player(self):
|
||||
"""
|
||||
Get the player associated with this session
|
||||
"""
|
||||
if self.logged_in:
|
||||
return self.player
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_character(self):
|
||||
"""
|
||||
Returns the in-game character associated with this session.
|
||||
This returns the typeclass of the object.
|
||||
"""
|
||||
player = self.get_player()
|
||||
if player:
|
||||
return player.character
|
||||
return None
|
||||
|
||||
def log(self, message, channel=True):
|
||||
"""
|
||||
Emits session info to the appropriate outputs and info channels.
|
||||
"""
|
||||
if channel:
|
||||
try:
|
||||
cchan = settings.CHANNEL_CONNECTINFO
|
||||
cchan = Channel.objects.get_channel(cchan[0])
|
||||
cchan.msg("[%s]: %s" % (cchan.key, message))
|
||||
except Exception:
|
||||
pass
|
||||
logger.log_infomsg(message)
|
||||
|
||||
def update_session_counters(self, idle=False):
|
||||
"""
|
||||
Hit this when the user enters a command in order to update idle timers
|
||||
and command counters.
|
||||
"""
|
||||
# Store the timestamp of the user's last command.
|
||||
self.cmd_last = time.time()
|
||||
if not idle:
|
||||
# Increment the user's command counter.
|
||||
self.cmd_total += 1
|
||||
# Player-visible idle time, not used in idle timeout calcs.
|
||||
self.cmd_last_visible = time.time()
|
||||
|
||||
def execute_cmd(self, command_string):
|
||||
"""
|
||||
Execute a command string on the server.
|
||||
"""
|
||||
# handle the 'idle' command
|
||||
if str(command_string).strip() == IDLE_COMMAND:
|
||||
self.update_session_counters(idle=True)
|
||||
return
|
||||
|
||||
# all other inputs, including empty inputs
|
||||
character = self.get_character()
|
||||
|
||||
|
||||
if character:
|
||||
character.execute_cmd(command_string)
|
||||
else:
|
||||
if self.logged_in:
|
||||
# there is no character, but we are logged in. Use player instead.
|
||||
self.get_player().execute_cmd(command_string)
|
||||
else:
|
||||
# we are not logged in. Use special unlogged-in call.
|
||||
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
|
||||
self.update_session_counters()
|
||||
|
||||
def data_out(self, msg, data=None):
|
||||
"""
|
||||
Send Evennia -> Player
|
||||
"""
|
||||
self.sessionhandler.data_out(self, msg, data)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.address == other.address
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
String representation of the user session class. We use
|
||||
this a lot in the server logs.
|
||||
"""
|
||||
if self.logged_in:
|
||||
symbol = '#'
|
||||
else:
|
||||
symbol = '?'
|
||||
try:
|
||||
address = ":".join([str(part) for part in self.address])
|
||||
except Exception:
|
||||
address = self.address
|
||||
return "<%s> %s@%s" % (symbol, self.uname, address)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Unicode representation
|
||||
"""
|
||||
return u"%s" % str(self)
|
||||
|
||||
|
||||
# easy-access functions
|
||||
|
||||
def login(self, player):
|
||||
"alias for at_login"
|
||||
self.session_login(player)
|
||||
def disconnect(self):
|
||||
"alias for session_disconnect"
|
||||
self.session_disconnect()
|
||||
def msg(self, string='', data=None):
|
||||
"alias for at_data_out"
|
||||
self.data_out(string, data=data)
|
||||
|
|
@ -1,412 +1,125 @@
|
|||
"""
|
||||
This defines a generic session class.
|
||||
|
||||
All protocols should implement this class and its hook methods.
|
||||
|
||||
|
||||
The process of first connect:
|
||||
|
||||
- The custom connection-handler for the respective
|
||||
protocol should be called by the transport connection itself.
|
||||
- The connect-handler handles whatever internal settings are needed
|
||||
- The connection-handler calls session_connect()
|
||||
- session_connect() setups sessions then calls session.at_connect()
|
||||
|
||||
Disconnecting is a bit more complex in order to avoid circular calls
|
||||
depending on if the disconnect happens automatically or manually from
|
||||
a command.
|
||||
|
||||
The process at automatic disconnect:
|
||||
- The custom disconnect-handler for the respective protocol
|
||||
should be called by the transport connection itself. This handler
|
||||
should be defined with a keyword argument 'step' defaulting to 1.
|
||||
- since step=1, the disconnect-handler calls session_disconnect()
|
||||
- session_disconnect() removes session, then calls session.at_disconnect()
|
||||
- session.at_disconnect() calls the custom disconnect-handler with
|
||||
step=2 as argument
|
||||
- since step=2, the disconnect-handler closes the connection and
|
||||
performs all needed protocol cleanup.
|
||||
|
||||
The process of manual disconnect:
|
||||
- The command/outside function calls session.session_disconnect().
|
||||
- from here the process proceeds as the automatic disconnect above.
|
||||
This defines a generic session class. All connection instances (both
|
||||
on Portal and Server side) should inherit from this class.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
#from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
#from src.objects.models import ObjectDB
|
||||
from src.comms.models import Channel
|
||||
from src.utils import logger, reloads
|
||||
from src.commands import cmdhandler
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||
IDLE_COMMAND = settings.IDLE_COMMAND
|
||||
|
||||
|
||||
|
||||
class IOdata(object):
|
||||
"""
|
||||
A simple storage object that allows for storing
|
||||
new attributes on it at creation.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
"Give keyword arguments to store as new arguments on the object."
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
|
||||
def _login(session, player):
|
||||
"""
|
||||
For logging a player in. Removed this from CmdConnect because ssh
|
||||
wanted to call it for autologin.
|
||||
"""
|
||||
# We are logging in, get/setup the player object controlled by player
|
||||
|
||||
# Check if this is the first time the
|
||||
# *player* connects (should be set by the
|
||||
if player.db.FIRST_LOGIN:
|
||||
player.at_first_login()
|
||||
del player.db.FIRST_LOGIN
|
||||
player.at_pre_login()
|
||||
|
||||
character = player.character
|
||||
if character:
|
||||
# this player has a character. Check if it's the
|
||||
# first time *this character* logs in (this should be
|
||||
# set by the initial create command)
|
||||
if character.db.FIRST_LOGIN:
|
||||
character.at_first_login()
|
||||
del character.db.FIRST_LOGIN
|
||||
# run character login hook
|
||||
character.at_pre_login()
|
||||
|
||||
# actually do the login
|
||||
session.session_login(player)
|
||||
|
||||
# post-login hooks
|
||||
player.at_post_login()
|
||||
if character:
|
||||
character.at_post_login()
|
||||
character.execute_cmd('look')
|
||||
else:
|
||||
player.execute_cmd('look')
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# SessionBase class
|
||||
# Server Session
|
||||
#------------------------------------------------------------
|
||||
|
||||
class SessionBase(object):
|
||||
class Session(object):
|
||||
"""
|
||||
This class represents a player's session and is a template for
|
||||
individual protocols to communicate with Evennia.
|
||||
This class represents a player's session and is a template for
|
||||
both portal- and server-side sessions.
|
||||
|
||||
Each player gets a session assigned to them whenever they connect
|
||||
to the game server. All communication between game and player goes
|
||||
through their session.
|
||||
Each connection will see two session instances created:
|
||||
|
||||
1) A Portal session. This is customized for the respective connection
|
||||
protocols that Evennia supports, like Telnet, SSH etc. The Portal session
|
||||
must call init_session() as part of its initialization. The respective
|
||||
hook methods should be connected to the methods unique for the respective
|
||||
protocol so that there is a unified interface to Evennia.
|
||||
2) A Server session. This is the same for all connected players, regardless
|
||||
of how they connect.
|
||||
|
||||
The Portal and Server have their own respective sessionhandlers. These are synced
|
||||
whenever new connections happen or the Server restarts etc, which means much of the
|
||||
same information must be stored in both places e.g. the portal can re-sync with the
|
||||
server when the server reboots.
|
||||
|
||||
"""
|
||||
|
||||
# use this to uniquely identify the protocol name, e.g. "telnet" or "comet"
|
||||
protocol_key = "BaseProtocol"
|
||||
# names of attributes that should be affected by syncing.
|
||||
_attrs_to_sync = ['protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname',
|
||||
'logged_in', 'cid', 'encoding',
|
||||
'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total']
|
||||
|
||||
def session_connect(self, address, suid=None):
|
||||
def init_session(self, protocol_key, address, sessionhandler):
|
||||
"""
|
||||
The setup of the session. An address (usually an IP address) on any form is required.
|
||||
|
||||
This should be called by the protocol at connection time.
|
||||
|
||||
suid = this is a session id. Needed by some transport protocols.
|
||||
Initialize the Session. This should be called by the protocol when
|
||||
a new session is established.
|
||||
protocol_key - telnet, ssh, ssl or web
|
||||
address - client address
|
||||
sessionhandler - reference to the sessionhandler instance
|
||||
"""
|
||||
# This is currently 'telnet', 'ssh', 'ssl' or 'web'
|
||||
self.protocol_key = protocol_key
|
||||
# Protocol address tied to this session
|
||||
self.address = address
|
||||
|
||||
# user setup
|
||||
self.name = None
|
||||
# suid is used by some protocols, it's a hex key.
|
||||
self.suid = None
|
||||
|
||||
# unique id for this session
|
||||
self.sessid = 0 # no sessid yet
|
||||
# database id for the user connected to this session
|
||||
self.uid = None
|
||||
self.suid = suid
|
||||
# user name, for easier tracking of sessions
|
||||
self.uname = None
|
||||
# if user has authenticated already or not
|
||||
self.logged_in = False
|
||||
|
||||
# database id of character/object connected to this player session (if any)
|
||||
self.cid = None
|
||||
self.encoding = "utf-8"
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
# The time the user last issued a command.
|
||||
self.cmd_last = current_time
|
||||
# Player-visible idle time, excluding the IDLE command.
|
||||
self.cmd_last_visible = current_time
|
||||
# The time when the user connected.
|
||||
self.conn_time = current_time
|
||||
# Total number of commands issued.
|
||||
self.cmd_total = 0
|
||||
#self.channels_subscribed = {}
|
||||
SESSIONS.add_unloggedin_session(self)
|
||||
# calling hook
|
||||
self.at_connect()
|
||||
|
||||
def session_login(self, player):
|
||||
"""
|
||||
Startup mechanisms that need to run at login
|
||||
|
||||
player - the connected player
|
||||
"""
|
||||
# Check if this is the first time the *player* logs in
|
||||
if player.db.FIRST_LOGIN:
|
||||
player.at_first_login()
|
||||
del player.db.FIRST_LOGIN
|
||||
player.at_pre_login()
|
||||
|
||||
character = player.character
|
||||
if character:
|
||||
# this player has a character. Check if it's the
|
||||
# first time *this character* logs in
|
||||
if character.db.FIRST_LOGIN:
|
||||
character.at_first_login()
|
||||
del character.db.FIRST_LOGIN
|
||||
# run character login hook
|
||||
character.at_pre_login()
|
||||
|
||||
# actually do the login by assigning session data
|
||||
|
||||
self.player = player
|
||||
self.user = player.user
|
||||
self.uid = self.user.id
|
||||
self.name = self.user.username
|
||||
self.logged_in = True
|
||||
# session time statistics
|
||||
self.conn_time = time.time()
|
||||
self.cmd_last_visible = self.conn_time
|
||||
self.cmd_last = self.conn_time
|
||||
self.cmd_total = 0
|
||||
|
||||
# a back-reference to the relevant sessionhandler this
|
||||
# session is stored in.
|
||||
self.sessionhandler = sessionhandler
|
||||
|
||||
def get_sync_data(self):
|
||||
"""
|
||||
Return all data relevant to sync the session
|
||||
"""
|
||||
sessdata = {}
|
||||
for attrname in self._attrs_to_sync:
|
||||
sessdata[attrname] = self.__dict__.get(attrname, None)
|
||||
return sessdata
|
||||
|
||||
def load_sync_data(self, sessdata):
|
||||
"""
|
||||
Takes a session dictionary, as created by get_sync_data,
|
||||
and loads it into the correct attributes of the session.
|
||||
"""
|
||||
for attrname, value in sessdata.items():
|
||||
self.__dict__[attrname] = value
|
||||
|
||||
# Update account's last login time.
|
||||
self.user.last_login = datetime.now()
|
||||
self.user.save()
|
||||
self.log('Logged in: %s' % self)
|
||||
|
||||
# start (persistent) scripts on this object
|
||||
reloads.reload_scripts(obj=self.player.character)
|
||||
|
||||
#add session to connected list
|
||||
SESSIONS.add_loggedin_session(self)
|
||||
|
||||
#call login hook
|
||||
self.at_login(player)
|
||||
|
||||
# post-login hooks
|
||||
player.at_post_login()
|
||||
if character:
|
||||
character.at_post_login()
|
||||
|
||||
def session_disconnect(self):
|
||||
def at_sync(self):
|
||||
"""
|
||||
Clean up the session, removing it from the game and doing some
|
||||
accounting. This method is used also for non-loggedin
|
||||
accounts.
|
||||
|
||||
Note that this methods does not close the connection - this is protocol-dependent
|
||||
and have to be done right after this function!
|
||||
"""
|
||||
if self.logged_in:
|
||||
player = self.get_player()
|
||||
uaccount = player.user
|
||||
uaccount.last_login = datetime.now()
|
||||
uaccount.save()
|
||||
self.at_disconnect()
|
||||
self.logged_in = False
|
||||
SESSIONS.remove_session(self)
|
||||
|
||||
def session_validate(self):
|
||||
"""
|
||||
Validate the session to make sure they have not been idle for too long
|
||||
"""
|
||||
if IDLE_TIMEOUT > 0 and (time.time() - self.cmd_last) > IDLE_TIMEOUT:
|
||||
self.msg("Idle timeout exceeded, disconnecting.")
|
||||
self.session_disconnect()
|
||||
|
||||
def get_player(self):
|
||||
"""
|
||||
Get the player associated with this session
|
||||
"""
|
||||
if self.logged_in:
|
||||
return self.player
|
||||
else:
|
||||
return None
|
||||
|
||||
# if self.logged_in:
|
||||
# character = ObjectDB.objects.get_object_with_user(self.uid)
|
||||
# if not character:
|
||||
# string = "No player match for session uid: %s" % self.uid
|
||||
# logger.log_errmsg(string)
|
||||
# return None
|
||||
# return character.player
|
||||
# return None
|
||||
|
||||
def get_character(self):
|
||||
"""
|
||||
Returns the in-game character associated with a session.
|
||||
This returns the typeclass of the object.
|
||||
"""
|
||||
player = self.get_player()
|
||||
if player:
|
||||
return player.character
|
||||
return None
|
||||
|
||||
def log(self, message, channel=True):
|
||||
"""
|
||||
Emits session info to the appropriate outputs and info channels.
|
||||
"""
|
||||
if channel:
|
||||
try:
|
||||
cchan = settings.CHANNEL_CONNECTINFO
|
||||
cchan = Channel.objects.get_channel(cchan[0])
|
||||
cchan.msg("[%s]: %s" % (cchan.key, message))
|
||||
except Exception:
|
||||
pass
|
||||
logger.log_infomsg(message)
|
||||
|
||||
def update_session_counters(self, idle=False):
|
||||
"""
|
||||
Hit this when the user enters a command in order to update idle timers
|
||||
and command counters.
|
||||
"""
|
||||
# Store the timestamp of the user's last command.
|
||||
self.cmd_last = time.time()
|
||||
if not idle:
|
||||
# Increment the user's command counter.
|
||||
self.cmd_total += 1
|
||||
# Player-visible idle time, not used in idle timeout calcs.
|
||||
self.cmd_last_visible = time.time()
|
||||
|
||||
def execute_cmd(self, command_string):
|
||||
"""
|
||||
Execute a command string.
|
||||
"""
|
||||
|
||||
# handle the 'idle' command
|
||||
if str(command_string).strip() == IDLE_COMMAND:
|
||||
self.update_session_counters(idle=True)
|
||||
return
|
||||
|
||||
# all other inputs, including empty inputs
|
||||
character = self.get_character()
|
||||
|
||||
if character:
|
||||
# normal operation.
|
||||
character.execute_cmd(command_string)
|
||||
#import cProfile
|
||||
#cProfile.runctx("character.execute_cmd(command_string)",
|
||||
# {"command_string":command_string,"character":character}, {}, "execute_cmd.profile")
|
||||
else:
|
||||
if self.logged_in:
|
||||
# there is no character, but we are logged in. Use player instead.
|
||||
self.get_player().execute_cmd(command_string)
|
||||
else:
|
||||
# we are not logged in. Use special unlogged-in call.
|
||||
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
|
||||
self.update_session_counters()
|
||||
|
||||
def get_data_obj(self, **kwargs):
|
||||
"""
|
||||
Create a data object, storing keyword arguments on itself as arguments.
|
||||
"""
|
||||
return IOdata(**kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.address == other.address
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
String representation of the user session class. We use
|
||||
this a lot in the server logs.
|
||||
"""
|
||||
if self.logged_in:
|
||||
symbol = '#'
|
||||
else:
|
||||
symbol = '?'
|
||||
return "<%s> %s@%s" % (symbol, self.name, self.address,)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Unicode representation
|
||||
"""
|
||||
return u"%s" % str(self)
|
||||
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Session class - inherit from this
|
||||
#------------------------------------------------------------
|
||||
|
||||
class Session(SessionBase):
|
||||
"""
|
||||
The main class to inherit from. Overload the methods here.
|
||||
"""
|
||||
|
||||
# exchange this for a unique name you can use to identify the
|
||||
# protocol type this session uses
|
||||
protocol_key = "TemplateProtocol"
|
||||
|
||||
#
|
||||
# Hook methods
|
||||
#
|
||||
|
||||
def at_connect(self):
|
||||
"""
|
||||
This method is called by the connection mechanic after
|
||||
connection has been made. The session is added to the
|
||||
sessionhandler and basic accounting has been made at this
|
||||
point.
|
||||
|
||||
This is the place to put e.g. welcome screens specific to the
|
||||
protocol.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
This method is called by the login mechanic whenever the user
|
||||
has finished authenticating. The user has been moved to the
|
||||
right sessionhandler list and basic book keeping has been
|
||||
done at this point (so logged_in=True).
|
||||
Called after a session has been fully synced (including
|
||||
secondary operations such as setting self.player based
|
||||
on uid etc).
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_disconnect(self):
|
||||
"""
|
||||
This method is called just before cleaning up the session
|
||||
(so still logged_in=True at this point).
|
||||
|
||||
This method should not be called from commands, instead it
|
||||
is called automatically by session_disconnect() as part of
|
||||
the cleanup.
|
||||
|
||||
This method MUST call the protocol-dependant disconnect-handler
|
||||
with step=2 to finalize the closing of the connection!
|
||||
"""
|
||||
# self.my-disconnect-handler(step=2)
|
||||
pass
|
||||
# access hooks
|
||||
|
||||
def at_data_in(self, string="", data=None):
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
Player -> Evennia
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_data_out(self, string="", data=None):
|
||||
"""
|
||||
Evennia -> Player
|
||||
|
||||
string - an string of any form to send to the player
|
||||
data - a data structure of any form
|
||||
|
||||
generic hook called from the outside to disconnect this session
|
||||
should be connected to the protocols actual disconnect mechanism.
|
||||
"""
|
||||
pass
|
||||
|
||||
# easy-access functions
|
||||
def login(self, player):
|
||||
"alias for at_login"
|
||||
self.at_login(player)
|
||||
def disconnect(self):
|
||||
"alias for session_disconnect"
|
||||
self.session_disconnect()
|
||||
def msg(self, string='', data=None):
|
||||
"alias for at_data_out"
|
||||
self.at_data_out(string, data=data)
|
||||
def data_out(self, msg, data=None):
|
||||
"""
|
||||
generic hook for sending data out through the protocol. Server
|
||||
protocols can use this right away. Portal sessions
|
||||
should overload this to format/handle the outgoing data as needed.
|
||||
"""
|
||||
pass
|
||||
|
||||
def data_in(self, msg, data=None):
|
||||
"""
|
||||
hook for protocols to send incoming data to the engine.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,72 @@
|
|||
"""
|
||||
This module handles sessions of users connecting
|
||||
to the server.
|
||||
This module defines handlers for storing sessions when handles
|
||||
sessions of users connecting to the server.
|
||||
|
||||
Since Evennia supports several different connection
|
||||
protocols, it is important to have a joint place
|
||||
to store session info. It also makes it easier
|
||||
to dispatch data.
|
||||
|
||||
Whereas server.py handles all setup of the server
|
||||
and database itself, this file handles all that
|
||||
comes after initial startup.
|
||||
|
||||
All new sessions (of whatever protocol) are responsible for
|
||||
registering themselves with this module.
|
||||
There are two similar but separate stores of sessions:
|
||||
ServerSessionHandler - this stores generic game sessions
|
||||
for the game. These sessions has no knowledge about
|
||||
how they are connected to the world.
|
||||
PortalSessionHandler - this stores sessions created by
|
||||
twisted protocols. These are dumb connectors that
|
||||
handle network communication but holds no game info.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from src.server.models import ServerConfig
|
||||
from src.utils import utils
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION
|
||||
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||
|
||||
#------------------------------------------------------------
|
||||
# SessionHandler class
|
||||
#------------------------------------------------------------
|
||||
|
||||
class SessionHandler(object):
|
||||
"""
|
||||
This handler holds a stack of sessions.
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Init the handler.
|
||||
"""
|
||||
self.sessions = {}
|
||||
|
||||
def get_sessions(self, include_unloggedin=False):
|
||||
"""
|
||||
Returns the connected session objects.
|
||||
"""
|
||||
if include_unloggedin:
|
||||
return self.sessions.values()
|
||||
else:
|
||||
return [session for session in self.sessions.values() if session.logged_in]
|
||||
|
||||
def get_session(self, sessid):
|
||||
"""
|
||||
Get session by sessid
|
||||
"""
|
||||
return self.sessions.get(sessid, None)
|
||||
|
||||
def get_all_sync_data(self):
|
||||
"""
|
||||
Create a dictionary of sessdata dicts representing all
|
||||
sessions in store.
|
||||
"""
|
||||
sessdict = {}
|
||||
for sess in self.sessions.values():
|
||||
# copy all relevant data from all sessions
|
||||
sessdict[sess.sessid] = sess.get_sync_data()
|
||||
return sessdict
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Server-SessionHandler class
|
||||
#------------------------------------------------------------
|
||||
|
||||
class ServerSessionHandler(SessionHandler):
|
||||
"""
|
||||
This object holds the stack of sessions active in the game at
|
||||
any time.
|
||||
|
|
@ -38,102 +79,144 @@ class SessionHandler(object):
|
|||
|
||||
"""
|
||||
|
||||
# AMP communication methods
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Init the handler. We track two types of sessions, those
|
||||
who have just connected (unloggedin) and those who have
|
||||
logged in (authenticated).
|
||||
Init the handler.
|
||||
"""
|
||||
self.unloggedin = []
|
||||
self.loggedin = []
|
||||
|
||||
# we keep a link to the server here, for the rest of the game to access.
|
||||
self.sessions = {}
|
||||
self.server = None
|
||||
|
||||
def add_unloggedin_session(self, session):
|
||||
def portal_connect(self, sessid, session):
|
||||
"""
|
||||
Call at first connect. This adds a not-yet authenticated session.
|
||||
"""
|
||||
self.unloggedin.insert(0, session)
|
||||
Called by Portal when a new session has connected.
|
||||
Creates a new, unlogged-in game session.
|
||||
"""
|
||||
self.sessions[sessid] = session
|
||||
session.execute_cmd('look')
|
||||
|
||||
def portal_disconnect(self, sessid):
|
||||
"""
|
||||
Called by Portal when portal reports a closing of a session
|
||||
from the portal side.
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
del self.sessions[session.sessid]
|
||||
self.session_count(-1)
|
||||
|
||||
def portal_session_sync(self, sesslist):
|
||||
"""
|
||||
Syncing all session ids of the portal with the ones of the server. This is instantiated
|
||||
by the portal when reconnecting.
|
||||
|
||||
def add_loggedin_session(self, session):
|
||||
sesslist is a complete list of (sessid, session) pairs, matching the list on the portal.
|
||||
if session was logged in, the amp handler will have logged them in before this point.
|
||||
"""
|
||||
for sess in self.sessions.values():
|
||||
# we delete the old session to make sure to catch eventual lingering references.
|
||||
del sess
|
||||
for sess in sesslist:
|
||||
self.sessions[sess.sessid] = sess
|
||||
sess.at_sync()
|
||||
|
||||
def portal_shutdown(self):
|
||||
"""
|
||||
Called by server when shutting down the portal.
|
||||
"""
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(0,
|
||||
operation='SSHUTD',
|
||||
data="")
|
||||
# server-side access methods
|
||||
|
||||
def disconnect(self, session, reason=""):
|
||||
"""
|
||||
Called from server side to remove session and inform portal
|
||||
of this fact.
|
||||
"""
|
||||
session = self.sessions.get(session.sessid, None)
|
||||
if session:
|
||||
sessid = session.sessid
|
||||
del self.sessions[sessid]
|
||||
# inform portal that session should be closed.
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(sessid,
|
||||
operation='SDISCONN',
|
||||
data=reason)
|
||||
self.session_count(-1)
|
||||
|
||||
|
||||
def login(self, session):
|
||||
"""
|
||||
Log in the previously unloggedin session and the player we by
|
||||
now should know is connected to it. After this point we
|
||||
assume the session to be logged in one way or another.
|
||||
"""
|
||||
# prep the session with player/user info
|
||||
|
||||
|
||||
|
||||
if not ALLOW_MULTISESSION:
|
||||
# disconnect previous sessions.
|
||||
self.disconnect_duplicate_sessions(session)
|
||||
|
||||
# store/move the session to the right list
|
||||
try:
|
||||
self.unloggedin.remove(session)
|
||||
except ValueError:
|
||||
pass
|
||||
self.loggedin.insert(0, session)
|
||||
session.logged_in = True
|
||||
self.session_count(1)
|
||||
# sync the portal to this session
|
||||
sessdata = session.get_sync_data()
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(session.sessid,
|
||||
operation='SLOGIN',
|
||||
data=sessdata)
|
||||
|
||||
def session_sync(self):
|
||||
"""
|
||||
This is called by the server when it reboots. It syncs all session data
|
||||
to the portal.
|
||||
"""
|
||||
sessdata = self.get_all_sync_data()
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(0,
|
||||
'SSYNC',
|
||||
data=sessdata)
|
||||
|
||||
def remove_session(self, session):
|
||||
"""
|
||||
Remove session from the handler
|
||||
"""
|
||||
removed = False
|
||||
try:
|
||||
self.unloggedin.remove(session)
|
||||
except Exception:
|
||||
try:
|
||||
self.loggedin.remove(session)
|
||||
except Exception:
|
||||
return
|
||||
self.session_count(-1)
|
||||
|
||||
def get_sessions(self, include_unloggedin=False):
|
||||
"""
|
||||
Returns the connected session objects.
|
||||
"""
|
||||
if include_unloggedin:
|
||||
return self.loggedin + self.unloggedin
|
||||
else:
|
||||
return self.loggedin
|
||||
|
||||
def disconnect_all_sessions(self, reason="You have been disconnected."):
|
||||
"""
|
||||
Cleanly disconnect all of the connected sessions.
|
||||
"""
|
||||
sessions = self.get_sessions(include_unloggedin=True)
|
||||
for session in sessions:
|
||||
session.at_data_out(reason)
|
||||
session.session_disconnect()
|
||||
|
||||
for session in self.sessions:
|
||||
del session
|
||||
self.session_count(0)
|
||||
# tell portal to disconnect all sessions
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(0,
|
||||
operation='SDISCONNALL',
|
||||
data=reason)
|
||||
|
||||
def disconnect_duplicate_sessions(self, curr_session):
|
||||
"""
|
||||
Disconnects any existing sessions with the same game object.
|
||||
"""
|
||||
reason = "Your account has been logged in from elsewhere. Disconnecting."
|
||||
curr_char = curr_session.get_character()
|
||||
doublet_sessions = [sess for sess in self.get_sessions()
|
||||
if sess.get_character() == curr_char and sess != curr_session]
|
||||
logged_out = 0
|
||||
for session in doublet_sessions:
|
||||
session.msg(reason)
|
||||
self.remove_session(session)
|
||||
logged_out += 1
|
||||
self.session_count(-logged_out)
|
||||
return logged_out
|
||||
doublet_sessions = [sess for sess in self.sessions
|
||||
if sess.logged_in
|
||||
and sess.get_character() == curr_char
|
||||
and sess != curr_session]
|
||||
reason = _("Logged in from elsewhere. Disconnecting.")
|
||||
for sessid in doublet_sessions:
|
||||
self.disconnect(session, reason)
|
||||
self.session_count(-1)
|
||||
|
||||
|
||||
def validate_sessions(self):
|
||||
"""
|
||||
Check all currently connected sessions (logged in and not)
|
||||
and see if any are dead.
|
||||
"""
|
||||
for session in self.get_sessions(include_unloggedin=True):
|
||||
session.session_validate()
|
||||
|
||||
tcurr = time.time()
|
||||
invalid_sessions = [session for session in self.sessions.values()
|
||||
if session.logged_in and IDLE_TIMEOUT > 0
|
||||
and (tcurr - session.cmd_last) > IDLE_TIMEOUT]
|
||||
for session in invalid_sessions:
|
||||
self.disconnect(session, reason=_("Idle timeout exceeded, disconnecting."))
|
||||
self.session_count(-1)
|
||||
|
||||
def session_count(self, num=None):
|
||||
"""
|
||||
Count up/down the number of connected, authenticated users.
|
||||
|
|
@ -160,7 +243,7 @@ class SessionHandler(object):
|
|||
may have more than one session connected if ALLOW_MULTISESSION is True)
|
||||
Only logged-in players are counted here.
|
||||
"""
|
||||
return len(set(sess.uid for sess in self.get_sessions()))
|
||||
return len(set(session.uid for session in self.sessions.values() if session.logged_in))
|
||||
|
||||
def sessions_from_player(self, player):
|
||||
"""
|
||||
|
|
@ -172,7 +255,7 @@ class SessionHandler(object):
|
|||
except User.DoesNotExist:
|
||||
return None
|
||||
uid = uobj.id
|
||||
return [session for session in self.loggedin if session.uid == uid]
|
||||
return [session for session in self.sessions.values() if session.logged_in and session.uid == uid]
|
||||
|
||||
def sessions_from_character(self, character):
|
||||
"""
|
||||
|
|
@ -183,20 +266,129 @@ class SessionHandler(object):
|
|||
return self.sessions_from_player(player)
|
||||
return None
|
||||
|
||||
def session_from_suid(self, suid):
|
||||
"""
|
||||
Given a session id, retrieve the session (this is primarily
|
||||
intended to be called by web clients)
|
||||
"""
|
||||
return [sess for sess in self.get_sessions(include_unloggedin=True) if sess.suid and sess.suid == suid]
|
||||
|
||||
def announce_all(self, message):
|
||||
"""
|
||||
Send message to all connected sessions
|
||||
"""
|
||||
for sess in self.get_sessions(include_unloggedin=True):
|
||||
sess.msg(message)
|
||||
for sess in self.sessions.values():
|
||||
self.data_out(sess, message)
|
||||
|
||||
SESSIONS = SessionHandler()
|
||||
def data_out(self, session, string="", data=""):
|
||||
"""
|
||||
Sending data Server -> Portal
|
||||
"""
|
||||
self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid,
|
||||
msg=string,
|
||||
data=data)
|
||||
def data_in(self, sessid, string="", data=""):
|
||||
"""
|
||||
Data Portal -> Server
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
session.execute_cmd(string)
|
||||
|
||||
# ignore 'data' argument for now; this is otherwise the place
|
||||
# to put custom effects on the server due to data input, e.g.
|
||||
# from a custom client.
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Portal-SessionHandler class
|
||||
#------------------------------------------------------------
|
||||
|
||||
class PortalSessionHandler(SessionHandler):
|
||||
"""
|
||||
This object holds the sessions connected to the portal at any time.
|
||||
It is synced with the server's equivalent SessionHandler over the AMP
|
||||
connection.
|
||||
|
||||
Sessions register with the handler using the connect() method. This
|
||||
will assign a new unique sessionid to the session and send that sessid
|
||||
to the server using the AMP connection.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Init the handler
|
||||
"""
|
||||
self.portal = None
|
||||
self.sessions = {}
|
||||
self.latest_sessid = 0
|
||||
|
||||
def connect(self, session):
|
||||
"""
|
||||
Called by protocol at first connect. This adds a not-yet authenticated session
|
||||
using an ever-increasing counter for sessid.
|
||||
"""
|
||||
self.latest_sessid += 1
|
||||
sessid = self.latest_sessid
|
||||
session.sessid = sessid
|
||||
sessdata = session.get_sync_data()
|
||||
self.sessions[sessid] = session
|
||||
# sync with server-side
|
||||
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
|
||||
operation="PCONN",
|
||||
data=sessdata)
|
||||
def disconnect(self, session):
|
||||
"""
|
||||
Called from portal side when the connection is closed from the portal side.
|
||||
"""
|
||||
sessid = session.sessid
|
||||
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
|
||||
operation="PDISCONN")
|
||||
|
||||
def server_disconnect(self, sessid, reason=""):
|
||||
"""
|
||||
Called by server to force a disconnect by sessid
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
session.disconnect(reason)
|
||||
del session
|
||||
|
||||
def server_disconnect_all(self, reason=""):
|
||||
"""
|
||||
Called by server when forcing a clean disconnect for everyone.
|
||||
"""
|
||||
for session in self.sessions.values():
|
||||
session.disconnect(reason)
|
||||
del session
|
||||
|
||||
def session_from_suid(self, suid):
|
||||
"""
|
||||
Given a session id, retrieve the session (this is primarily
|
||||
intended to be called by web clients)
|
||||
"""
|
||||
return [sess for sess in self.get_sessions(include_unloggedin=True)
|
||||
if hasattr(sess, 'suid') and sess.suid == suid]
|
||||
|
||||
def data_in(self, session, string="", data=""):
|
||||
"""
|
||||
Called by portal sessions for relaying data coming
|
||||
in from the protocol to the server. data is
|
||||
serialized before passed on.
|
||||
"""
|
||||
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
|
||||
msg=string,
|
||||
data=data)
|
||||
def announce_all(self, message):
|
||||
"""
|
||||
Send message to all connection sessions
|
||||
"""
|
||||
for session in self.sessions.values():
|
||||
session.data_out(message)
|
||||
|
||||
def data_out(self, sessid, string="", data=""):
|
||||
"""
|
||||
Called by server for having the portal relay messages and data
|
||||
to the correct session protocol.
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
session.data_out(string, data=data)
|
||||
|
||||
SESSIONS = ServerSessionHandler()
|
||||
PORTAL_SESSIONS = PortalSessionHandler()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ This depends on a generic session module that implements
|
|||
the actual login procedure of the game, tracks
|
||||
sessions etc.
|
||||
|
||||
Using standard ssh client,
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
|
|
@ -25,7 +27,9 @@ from django.conf import settings
|
|||
from src.server import session
|
||||
from src.players.models import PlayerDB
|
||||
from src.utils import ansi, utils, logger
|
||||
#from src.commands.default.unloggedin import _login
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
|
||||
|
|
@ -40,13 +44,13 @@ class SshProtocol(Manhole, session.Session):
|
|||
them. All communication between game and player goes through
|
||||
here.
|
||||
"""
|
||||
def __init__(self, player):
|
||||
def __init__(self, starttuple):
|
||||
"""
|
||||
For setting up the player. If player is not None then we'll
|
||||
login automatically.
|
||||
"""
|
||||
self.player = player
|
||||
|
||||
self.authenticated_player = starttuple[0]
|
||||
self.cfactory = starttuple[1] # obs may not be called self.factory, it gets overwritten!
|
||||
|
||||
def terminalSize(self, width, height):
|
||||
"""
|
||||
|
|
@ -60,10 +64,14 @@ class SshProtocol(Manhole, session.Session):
|
|||
self.height = height
|
||||
|
||||
# initialize the session
|
||||
self.session_connect(self.getClientAddress())
|
||||
if self.player is not None:
|
||||
self.session_login(self.player)
|
||||
self.execute_cmd('look')
|
||||
client_address = self.getClientAddress()
|
||||
self.init_session("ssh", client_address, self.cfactory.sessionhandler)
|
||||
|
||||
# since we might have authenticated already, we might set this here.
|
||||
if self.authenticated_player:
|
||||
self.logged_in = True
|
||||
self.uid = self.authenticated_player.user.id
|
||||
self.sessionhandler.connect(self)
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
|
|
@ -74,8 +82,9 @@ class SshProtocol(Manhole, session.Session):
|
|||
self.keyHandlers[CTRL_C] = self.handle_INT
|
||||
self.keyHandlers[CTRL_D] = self.handle_EOF
|
||||
self.keyHandlers[CTRL_L] = self.handle_FF
|
||||
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
|
||||
|
||||
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
|
||||
|
||||
# initalize
|
||||
|
||||
def handle_INT(self):
|
||||
"""
|
||||
|
|
@ -116,32 +125,22 @@ class SshProtocol(Manhole, session.Session):
|
|||
self.terminal.loseConnection()
|
||||
|
||||
|
||||
def connectionLost(self, reason=None, step=1):
|
||||
def connectionLost(self, reason=None):
|
||||
"""
|
||||
This is executed when the connection is lost for
|
||||
whatever reason.
|
||||
whatever reason. It can also be called directly,
|
||||
from the disconnect method.
|
||||
|
||||
Closing the connection takes two steps
|
||||
|
||||
step 1 - is the default and is used when this method is
|
||||
called automatically. The method should then call self.session_disconnect().
|
||||
Step 2 - means this method is called from at_disconnect(). At this point
|
||||
the sessions are assumed to have been handled, and so the transport can close
|
||||
without further ado.
|
||||
"""
|
||||
insults.TerminalProtocol.connectionLost(self, reason)
|
||||
if step == 1:
|
||||
self.session_disconnect()
|
||||
else:
|
||||
self.terminal.loseConnection()
|
||||
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.terminal.loseConnection()
|
||||
|
||||
def getClientAddress(self):
|
||||
"""
|
||||
Returns the client's address and port in a tuple. For example
|
||||
('127.0.0.1', 41917)
|
||||
"""
|
||||
|
||||
return self.terminal.transport.getPeer()
|
||||
|
||||
|
||||
|
|
@ -152,7 +151,7 @@ class SshProtocol(Manhole, session.Session):
|
|||
command for the purpose of the MUD. So we take the user input
|
||||
and pass it on to the game engine.
|
||||
"""
|
||||
self.at_data_in(string)
|
||||
self.sessionhandler.data_in(self, string)
|
||||
|
||||
def lineSend(self, string):
|
||||
"""
|
||||
|
|
@ -166,35 +165,18 @@ class SshProtocol(Manhole, session.Session):
|
|||
self.terminal.write(line) #this is the telnet-specific method for sending
|
||||
self.terminal.nextLine()
|
||||
|
||||
|
||||
# session-general method hooks
|
||||
|
||||
def at_connect(self):
|
||||
"""
|
||||
Show the banner screen.
|
||||
"""
|
||||
self.telnet_markup = True
|
||||
# show connection screen
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
Called after authentication. self.logged_in=True at this point.
|
||||
"""
|
||||
if player.has_attribute('telnet_markup'):
|
||||
self.telnet_markup = player.get_attribute("telnet_markup")
|
||||
else:
|
||||
self.telnet_markup = True
|
||||
|
||||
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
|
||||
def disconnect(self, reason="Connection closed. Goodbye for now."):
|
||||
"""
|
||||
Disconnect from server
|
||||
"""
|
||||
char = self.get_character()
|
||||
if char:
|
||||
char.at_disconnect()
|
||||
self.at_data_out(reason)
|
||||
self.connectionLost(step=2)
|
||||
if reason:
|
||||
self.data_out(reason)
|
||||
self.connectionLost(reason)
|
||||
|
||||
def at_data_out(self, string, data=None):
|
||||
def data_out(self, string, data=None):
|
||||
"""
|
||||
Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings.
|
||||
"""
|
||||
|
|
@ -203,31 +185,18 @@ class SshProtocol(Manhole, session.Session):
|
|||
except Exception, e:
|
||||
self.lineSend(str(e))
|
||||
return
|
||||
nomarkup = not self.telnet_markup
|
||||
raw = False
|
||||
nomarkup = False
|
||||
raw = False
|
||||
if type(data) == dict:
|
||||
# check if we want escape codes to go through unparsed.
|
||||
raw = data.get("raw", self.telnet_markup)
|
||||
raw = data.get("raw", False)
|
||||
# check if we want to remove all markup
|
||||
nomarkup = data.get("nomarkup", not self.telnet_markup)
|
||||
nomarkup = data.get("nomarkup", False)
|
||||
if raw:
|
||||
self.lineSend(string)
|
||||
else:
|
||||
self.lineSend(ansi.parse_ansi(string, strip_ansi=nomarkup))
|
||||
|
||||
def at_data_in(self, string, data=None):
|
||||
"""
|
||||
Line from Player -> Evennia. 'data' argument is not used.
|
||||
|
||||
"""
|
||||
try:
|
||||
string = utils.to_unicode(string, encoding=self.encoding)
|
||||
self.execute_cmd(string)
|
||||
return
|
||||
except Exception, e:
|
||||
logger.log_errmsg(str(e))
|
||||
|
||||
|
||||
|
||||
class ExtraInfoAuthServer(SSHUserAuthServer):
|
||||
def auth_password(self, packet):
|
||||
|
|
@ -251,15 +220,19 @@ class PlayerDBPasswordChecker(object):
|
|||
"""
|
||||
credentialInterfaces = (credentials.IUsernamePassword,)
|
||||
|
||||
def __init__(self, factory):
|
||||
self.factory = factory
|
||||
super(PlayerDBPasswordChecker, self).__init__()
|
||||
|
||||
def requestAvatarId(self, c):
|
||||
"Generic credentials"
|
||||
up = credentials.IUsernamePassword(c, None)
|
||||
username = up.username
|
||||
password = up.password
|
||||
player = PlayerDB.objects.get_player_from_name(username)
|
||||
res = None
|
||||
res = (None, self.factory)
|
||||
if player and player.user.check_password(password):
|
||||
res = player
|
||||
res = (player, self.factory)
|
||||
return defer.succeed(res)
|
||||
|
||||
class PassAvatarIdTerminalRealm(TerminalRealm):
|
||||
|
|
@ -322,7 +295,7 @@ def getKeyPair(pubkeyfile, privkeyfile):
|
|||
|
||||
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
|
||||
# No keypair exists. Generate a new RSA keypair
|
||||
print " Generating SSH RSA keypair ...",
|
||||
print _(" Generating SSH RSA keypair ..."),
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
KEY_LENGTH = 1024
|
||||
|
|
@ -359,6 +332,7 @@ def makeFactory(configdict):
|
|||
rlm.transportFactory = TerminalSessionTransport_getPeer
|
||||
rlm.chainedProtocolFactory = chainProtocolFactory
|
||||
factory = ConchFactory(Portal(rlm))
|
||||
factory.sessionhandler = configdict['sessions']
|
||||
|
||||
try:
|
||||
# create/get RSA keypair
|
||||
|
|
@ -366,12 +340,12 @@ def makeFactory(configdict):
|
|||
factory.publicKeys = {'ssh-rsa': publicKey}
|
||||
factory.privateKeys = {'ssh-rsa': privateKey}
|
||||
except Exception, e:
|
||||
print " getKeyPair error: %s\n WARNING: Evennia could not auto-generate SSH keypair. Using conch default keys instead." % e
|
||||
print " If this error persists, create game/%s and game/%s yourself using third-party tools." % (pubkeyfile, privkeyfile)
|
||||
print _(" getKeyPair error: %(e)s\n WARNING: Evennia could not auto-generate SSH keypair. Using conch default keys instead.") % {'e': e}
|
||||
print _(" If this error persists, create game/%(pub)s and game/%(priv)s yourself using third-party tools.") % {'pub': pubkeyfile, 'priv': privkeyfile}
|
||||
|
||||
factory.services = factory.services.copy()
|
||||
factory.services['ssh-userauth'] = ExtraInfoAuthServer
|
||||
|
||||
factory.portal.registerChecker(PlayerDBPasswordChecker())
|
||||
factory.portal.registerChecker(PlayerDBPasswordChecker(factory))
|
||||
|
||||
return factory
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ from twisted.internet import ssl as twisted_ssl
|
|||
try:
|
||||
import OpenSSL
|
||||
except ImportError:
|
||||
print " SSL_ENABLED requires PyOpenSSL."
|
||||
sys.exit()
|
||||
print _(" SSL_ENABLED requires PyOpenSSL.")
|
||||
sys.exit(5)
|
||||
|
||||
from src.server.telnet import TelnetProtocol
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ def verify_SSL_key_and_cert(keyfile, certfile):
|
|||
from Crypto.PublicKey import RSA
|
||||
from twisted.conch.ssh.keys import Key
|
||||
|
||||
print " Creating SSL key and certificate ... ",
|
||||
print _(" Creating SSL key and certificate ... "),
|
||||
|
||||
try:
|
||||
# create the RSA key and store it.
|
||||
|
|
@ -42,9 +42,9 @@ def verify_SSL_key_and_cert(keyfile, certfile):
|
|||
keyString = rsaKey.toString(type="OPENSSH")
|
||||
file(keyfile, 'w+b').write(keyString)
|
||||
except Exception,e:
|
||||
print "rsaKey error: %s\n WARNING: Evennia could not auto-generate SSL private key." % e
|
||||
print "If this error persists, create game/%s yourself using third-party tools." % keyfile
|
||||
sys.exit()
|
||||
print _("rsaKey error: %(e)s\n WARNING: Evennia could not auto-generate SSL private key.") % {'e': e}
|
||||
print _("If this error persists, create game/%(keyfile)s yourself using third-party tools.") % {'keyfile': keyfile}
|
||||
sys.exit(5)
|
||||
|
||||
# try to create the certificate
|
||||
CERT_EXPIRE = 365 * 20 # twenty years validity
|
||||
|
|
@ -56,12 +56,12 @@ def verify_SSL_key_and_cert(keyfile, certfile):
|
|||
err = subprocess.call(exestring)#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
except OSError, e:
|
||||
print " %s\n" % e
|
||||
print " Evennia's SSL context factory could not automatically create an SSL certificate game/%s." % certfile
|
||||
print " A private key 'ssl.key' was already created. Please create %s manually using the commands valid " % certfile
|
||||
print " for your operating system."
|
||||
print " Example (linux, using the openssl program): "
|
||||
print " %s" % exestring
|
||||
sys.exit()
|
||||
print _(" Evennia's SSL context factory could not automatically create an SSL certificate game/%(cert)s.") % {'cert': certfile}
|
||||
print _(" A private key 'ssl.key' was already created. Please create %(cert)s manually using the commands valid") % {'cert': certfile}
|
||||
print _(" for your operating system.")
|
||||
print _(" Example (linux, using the openssl program): ")
|
||||
print " %s" % exestring
|
||||
sys.exit(5)
|
||||
print "done."
|
||||
|
||||
def getSSLContext():
|
||||
|
|
|
|||
|
|
@ -8,130 +8,73 @@ sessions etc.
|
|||
"""
|
||||
|
||||
from twisted.conch.telnet import StatefulTelnetProtocol
|
||||
from django.conf import settings
|
||||
from src.server import session
|
||||
from src.utils import ansi, utils, logger
|
||||
from src.server.session import Session
|
||||
from src.utils import utils, ansi
|
||||
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
|
||||
class TelnetProtocol(StatefulTelnetProtocol, session.Session):
|
||||
class TelnetProtocol(StatefulTelnetProtocol, Session):
|
||||
"""
|
||||
Each player connecting over telnet (ie using most traditional mud
|
||||
clients) gets a telnet protocol instance assigned to them. All
|
||||
communication between game and player goes through here.
|
||||
"""
|
||||
|
||||
# telnet-specific hooks
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
This is called when the connection is first
|
||||
established.
|
||||
"""
|
||||
# initialize the session
|
||||
self.session_connect(self.getClientAddress())
|
||||
client_address = self.transport.client
|
||||
self.init_session("telnet", client_address, self.factory.sessionhandler)
|
||||
# add us to sessionhandler
|
||||
self.sessionhandler.connect(self)
|
||||
|
||||
def connectionLost(self, reason=None, step=1):
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
This is executed when the connection is lost for
|
||||
whatever reason.
|
||||
whatever reason. It can also be called directly, from
|
||||
the disconnect method
|
||||
"""
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.transport.loseConnection()
|
||||
|
||||
Closing the connection takes two steps
|
||||
|
||||
step 1 - is the default and is used when this method is
|
||||
called automatically. The method should then call self.session_disconnect().
|
||||
Step 2 - means this method is called from at_disconnect(). At this point
|
||||
the sessions are assumed to have been handled, and so the transport can close
|
||||
without further ado.
|
||||
"""
|
||||
if step == 1:
|
||||
self.session_disconnect()
|
||||
else:
|
||||
self.transport.loseConnection()
|
||||
|
||||
def getClientAddress(self):
|
||||
"""
|
||||
Returns the client's address and port in a tuple. For example
|
||||
('127.0.0.1', 41917)
|
||||
"""
|
||||
return self.transport.client
|
||||
|
||||
def lineReceived(self, string):
|
||||
"""
|
||||
Communication Player -> Evennia. Any line return indicates a
|
||||
command for the purpose of the MUD. So we take the user input
|
||||
and pass it on to the game engine.
|
||||
Telnet method called when data is coming in over the telnet
|
||||
connection. We pass it on to the game engine directly.
|
||||
"""
|
||||
self.at_data_in(string)
|
||||
self.sessionhandler.data_in(self, string)
|
||||
|
||||
def lineSend(self, string):
|
||||
"""
|
||||
Communication Evennia -> Player
|
||||
Any string sent should already have been
|
||||
properly formatted and processed
|
||||
before reaching this point.
|
||||
# Session hooks
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
self.sendLine(string) #this is the telnet-specific method for sending
|
||||
generic hook for the engine to call in order to
|
||||
disconnect this protocol.
|
||||
"""
|
||||
if reason:
|
||||
self.data_out(reason)
|
||||
self.connectionLost(reason)
|
||||
|
||||
# session-general method hooks
|
||||
|
||||
def at_connect(self):
|
||||
def data_out(self, string, data=None):
|
||||
"""
|
||||
Show the banner screen.
|
||||
generic hook method for engine to call in order to send data
|
||||
through the telnet connection.
|
||||
Data Evennia -> Player. 'data' argument is not used
|
||||
"""
|
||||
self.telnet_markup = True
|
||||
# show connection screen
|
||||
self.execute_cmd('look')
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
Called after authentication. self.logged_in=True at this point.
|
||||
"""
|
||||
if player.has_attribute('telnet_markup'):
|
||||
self.telnet_markup = player.get_attribute("telnet_markup")
|
||||
else:
|
||||
self.telnet_markup = True
|
||||
|
||||
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
|
||||
"""
|
||||
Disconnect from server
|
||||
"""
|
||||
char = self.get_character()
|
||||
if char:
|
||||
char.at_disconnect()
|
||||
self.at_data_out(reason)
|
||||
self.connectionLost(step=2)
|
||||
|
||||
def at_data_out(self, string, data=None):
|
||||
"""
|
||||
Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings.
|
||||
"""
|
||||
try:
|
||||
string = utils.to_str(string, encoding=self.encoding)
|
||||
except Exception, e:
|
||||
self.lineSend(str(e))
|
||||
return
|
||||
nomarkup = not self.telnet_markup
|
||||
raw = False
|
||||
if type(data) == dict:
|
||||
try:
|
||||
string = utils.to_str(string, encoding=self.encoding)
|
||||
except Exception, e:
|
||||
self.sendLine(str(e))
|
||||
return
|
||||
nomarkup = False
|
||||
raw = False
|
||||
if type(data) == dict:
|
||||
# check if we want escape codes to go through unparsed.
|
||||
raw = data.get("raw", self.telnet_markup)
|
||||
raw = data.get("raw", False)
|
||||
# check if we want to remove all markup
|
||||
nomarkup = data.get("nomarkup", not self.telnet_markup)
|
||||
if raw:
|
||||
self.lineSend(string)
|
||||
else:
|
||||
self.lineSend(ansi.parse_ansi(string, strip_ansi=nomarkup))
|
||||
|
||||
def at_data_in(self, string, data=None):
|
||||
"""
|
||||
Line from Player -> Evennia. 'data' argument is not used.
|
||||
|
||||
"""
|
||||
try:
|
||||
string = utils.to_unicode(string, encoding=self.encoding)
|
||||
self.execute_cmd(string)
|
||||
return
|
||||
except Exception, e:
|
||||
logger.log_errmsg(str(e))
|
||||
nomarkup = data.get("nomarkup", False)
|
||||
if raw:
|
||||
self.sendLine(string)
|
||||
else:
|
||||
self.sendLine(ansi.parse_ansi(string, strip_ansi=nomarkup))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ from django.conf import settings
|
|||
from src.utils import utils, logger, ansi
|
||||
from src.utils.text2html import parse_html
|
||||
from src.server import session
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
SERVERNAME = settings.SERVERNAME
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
|
|
@ -55,7 +54,7 @@ def jsonify(obj):
|
|||
|
||||
class WebClient(resource.Resource):
|
||||
"""
|
||||
An ajax/comet long-polling transport protocol for
|
||||
An ajax/comet long-polling transport
|
||||
"""
|
||||
isLeaf = True
|
||||
allowedMethods = ('POST',)
|
||||
|
|
@ -95,24 +94,16 @@ class WebClient(resource.Resource):
|
|||
dataentries.append(jsonify({'msg':string, 'data':data}))
|
||||
self.databuffer[suid] = dataentries
|
||||
|
||||
def disconnect(self, suid, step=1):
|
||||
def client_disconnect(self, suid):
|
||||
"""
|
||||
Disconnect session with given suid.
|
||||
|
||||
step 1 : call session_disconnect()
|
||||
step 2 : finalize disconnection
|
||||
"""
|
||||
|
||||
if step == 1:
|
||||
sess = SESSIONS.session_from_suid(suid)
|
||||
sess[0].session_disconnect()
|
||||
else:
|
||||
if self.requests.has_key(suid):
|
||||
for request in self.requests.get(suid, []):
|
||||
request.finish()
|
||||
del self.requests[suid]
|
||||
if self.databuffer.has_key(suid):
|
||||
del self.databuffer[suid]
|
||||
if self.requests.has_key(suid):
|
||||
for request in self.requests.get(suid, []):
|
||||
request.finish()
|
||||
del self.requests[suid]
|
||||
if self.databuffer.has_key(suid):
|
||||
del self.databuffer[suid]
|
||||
|
||||
def mode_init(self, request):
|
||||
"""
|
||||
|
|
@ -133,7 +124,9 @@ class WebClient(resource.Resource):
|
|||
|
||||
sess = WebClientSession()
|
||||
sess.client = self
|
||||
sess.session_connect(remote_addr, suid)
|
||||
sess.init_session("comet", remote_addr, self.sessionhandler)
|
||||
sess.suid = suid
|
||||
sess.sessionhandler.connect(sess)
|
||||
return jsonify({'msg':host_string, 'suid':suid})
|
||||
|
||||
def mode_input(self, request):
|
||||
|
|
@ -144,11 +137,12 @@ class WebClient(resource.Resource):
|
|||
suid = request.args.get('suid', ['0'])[0]
|
||||
if suid == '0':
|
||||
return ''
|
||||
sess = SESSIONS.session_from_suid(suid)
|
||||
sess = self.sessionhandler.session_from_suid(suid)
|
||||
if sess:
|
||||
sess = sess[0]
|
||||
string = request.args.get('msg', [''])[0]
|
||||
data = request.args.get('data', [None])[0]
|
||||
sess[0].at_data_in(string, data)
|
||||
sess.sessionhandler.data_in(sess, string, data)
|
||||
return ''
|
||||
|
||||
def mode_receive(self, request):
|
||||
|
|
@ -179,7 +173,7 @@ class WebClient(resource.Resource):
|
|||
"""
|
||||
suid = request.args.get('suid', ['0'])[0]
|
||||
if suid == '0':
|
||||
self.disconnect(suid)
|
||||
self.client_disconnect(suid)
|
||||
return ''
|
||||
|
||||
def render_POST(self, request):
|
||||
|
|
@ -217,35 +211,16 @@ class WebClientSession(session.Session):
|
|||
"""
|
||||
This represents a session running in a webclient.
|
||||
"""
|
||||
|
||||
def at_connect(self):
|
||||
"""
|
||||
Show the banner screen.
|
||||
"""
|
||||
# show screen
|
||||
self.telnet_markup = True
|
||||
self.execute_cmd('look')
|
||||
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
Called after authentication. self.logged_in=True at this point.
|
||||
"""
|
||||
if player.has_attribute('telnet_markup'):
|
||||
self.telnet_markup = player.get_attribute("telnet_markup")
|
||||
|
||||
def at_disconnect(self, reason=None):
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
Disconnect from server
|
||||
"""
|
||||
if reason:
|
||||
self.lineSend(self.suid, reason)
|
||||
char = self.get_character()
|
||||
if char:
|
||||
char.at_disconnect()
|
||||
self.client.disconnect(self.suid, step=2)
|
||||
self.client.client_disconnect(self.suid)
|
||||
|
||||
def at_data_out(self, string='', data=None):
|
||||
def data_out(self, string='', data=None):
|
||||
"""
|
||||
Data Evennia -> Player access hook.
|
||||
|
||||
|
|
@ -261,13 +236,13 @@ class WebClientSession(session.Session):
|
|||
try:
|
||||
string = utils.to_str(string, encoding=self.encoding)
|
||||
|
||||
nomarkup = not self.telnet_markup
|
||||
nomarkup = False
|
||||
raw = False
|
||||
if type(data) == dict:
|
||||
# check if we want escape codes to go through unparsed.
|
||||
raw = data.get("raw", self.telnet_markup)
|
||||
raw = data.get("raw", False)
|
||||
# check if we want to remove all markup
|
||||
nomarkup = data.get("nomarkup", not self.telnet_markup)
|
||||
nomarkup = data.get("nomarkup", False)
|
||||
if raw:
|
||||
self.client.lineSend(self.suid, string)
|
||||
else:
|
||||
|
|
@ -275,21 +250,3 @@ class WebClientSession(session.Session):
|
|||
return
|
||||
except Exception, e:
|
||||
logger.log_trace()
|
||||
|
||||
def at_data_in(self, string, data=None):
|
||||
"""
|
||||
Input from Player -> Evennia (called by client protocol).
|
||||
Use of 'data' is up to the client - server implementation.
|
||||
"""
|
||||
|
||||
# treat data?
|
||||
if data:
|
||||
pass
|
||||
|
||||
# the string part is identical to telnet
|
||||
try:
|
||||
string = utils.to_unicode(string, encoding=self.encoding)
|
||||
self.execute_cmd(string)
|
||||
return
|
||||
except Exception, e:
|
||||
logger.log_trace()
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ SRC_DIR = os.path.join(BASE_PATH, 'src')
|
|||
GAME_DIR = os.path.join(BASE_PATH, 'game')
|
||||
# Place to put log files
|
||||
LOG_DIR = os.path.join(GAME_DIR, 'logs')
|
||||
DEFAULT_LOG_FILE = os.path.join(LOG_DIR, 'evennia.log')
|
||||
SERVER_LOG_FILE = os.path.join(LOG_DIR, 'server.log')
|
||||
PORTAL_LOG_FILE = os.path.join(LOG_DIR, 'portal.log')
|
||||
# Where to log server requests to the web server. This is VERY spammy, so this
|
||||
# file should be removed at regular intervals.
|
||||
HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log')
|
||||
|
|
@ -111,7 +112,12 @@ IDLE_COMMAND = "idle"
|
|||
# Add sets for languages/regions your players are likely to use.
|
||||
# (see http://en.wikipedia.org/wiki/Character_encoding)
|
||||
ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
|
||||
|
||||
# The game server opens an AMP port so that the portal can
|
||||
# communicate with it. This is an internal functionality of Evennia, usually
|
||||
# operating between the two processes on the same machine. Don't change unless
|
||||
# you know what you are doing.
|
||||
AMP_HOST = 'localhost'
|
||||
AMP_PORT = 5000
|
||||
|
||||
###################################################
|
||||
# Evennia Database config
|
||||
|
|
@ -340,7 +346,7 @@ SESSION_COOKIE_NAME = 'sessionid'
|
|||
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = False
|
||||
USE_I18N = True
|
||||
# This should be turned off unless you want to do tests with Django's
|
||||
# development webserver (normally Evennia runs its own server)
|
||||
SERVE_MEDIA = False
|
||||
|
|
|
|||
|
|
@ -714,8 +714,9 @@ class TypedObject(SharedMemoryModel):
|
|||
infochan = Channel.objects.get_channel(infochan[0])
|
||||
if infochan:
|
||||
cname = infochan.key
|
||||
cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')])
|
||||
infochan.msg(message)
|
||||
cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n') if line])
|
||||
cmessage = cmessage.strip()
|
||||
infochan.msg(cmessage)
|
||||
else:
|
||||
# no mudinfo channel is found. Log instead.
|
||||
cmessage = "\n".join(["[NO MUDINFO CHANNEL]: %s" % line for line in message.split('\n')])
|
||||
|
|
@ -1102,8 +1103,7 @@ class TypedObject(SharedMemoryModel):
|
|||
"Stop accidental deletion."
|
||||
raise Exception("Cannot delete the ndb object!")
|
||||
ndb = property(ndb_get, ndb_set, ndb_del)
|
||||
|
||||
|
||||
|
||||
# Lock / permission methods
|
||||
|
||||
def access(self, accessing_obj, access_type='read', default=False):
|
||||
|
|
|
|||
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