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:
Griatch 2011-09-03 10:22:19 +00:00
parent 14dae44a46
commit f13e8cdf7c
50 changed files with 3175 additions and 2565 deletions

View file

@ -39,13 +39,13 @@ Since pylint cannot catch dynamically created variables used in commands and
elsewhere in Evennia, one needs to reduce some checks to avoid false errors and
warnings. For best results, run pylint like this:
> pylint --disable-msg=E1101,E0102,F0401,W0232,R0903 filename.py
> pylint --disable=E1101,E0102,F0401,W0232,R0903 filename.py
To avoid entering the options every time, you can auto-create a pylintrc file by
using the option --generate-rcfile. You need to dump this output into a
file .pylintrc, for example like this (linux):
> pylint --disable-msg=E1101,E0102,F0401,W0232,R0903 --generate-rcfile > ~/.pylintrc
> pylint --disable=E1101,E0102,F0401,W0232,R0903 --generate-rcfile > ~/.pylintrc
From now on you can then just run

31
INSTALL
View file

@ -13,20 +13,20 @@ As far as operating systems go, any system with Python support should work.
* Windows (2000, XP, Vista, Win7)
* Mac OSX (>=10.5 recommended)
Of these, only Linux/Unix, Windows XP, and Windows 7 have actually been run by devs and shown to work at this time. Let us know.
Of these, Linux/Unix, Windows XP, and Windows 7 have been tested. If you use something else, let us know.
You'll need the following packages and minimum versions in order to run Evennia:
* Python (http://www.python.org)
o Version 2.5+ strongly recommended, although 2.3 or 2.4 may work. Obs- Python3.x is not supported yet.
o The default database system SQLite3 only comes as part of Python2.5 and later.
o Python is available in all modern operating systems (Linux, Mac, etc).
o Windows users are recommended to use ActivePython (http://www.activestate.com/activepython)
o Default database system SQLite3 only comes as part of Python 2.5 and later.
o Windows only: 2.7+ required for full server restart functionality.
o Windows only: Optionally use ActivePython instad (http://www.activestate.com/activepython)
* Twisted (http://twistedmatrix.com)
o Version 10.0+
o Twisted also requires:
+ ZopeInterface 3.0+ (http://www.zope.org/Products/ZopeInterface)
+ For Windows only: pywin32 (http://sourceforge.net/projects/pywin32)
+ Windows only: pywin32 (http://sourceforge.net/projects/pywin32)
* Django (http://www.djangoproject.com)
o Version 1.2.1+ or latest subversion trunk highly recommended.
o PIL library (http://www.pythonware.com/products/pil)
@ -44,8 +44,8 @@ Optional packages:
o Optional. Used for database migrations.
* Apache2 (http://httpd.apache.org)
o Optional. Most likely you'll not need to bother with this since Evennia
runs its own threaded web server based on Twisted. Other equivalent web servers with a Python interpreter
module can also be used.
runs its own threaded web server based on Twisted. Other equivalent web servers
with a Python interpreter module can also be used.
Installation and Quickstart
@ -84,18 +84,25 @@ Installation and Quickstart
* Run
> python evennia.py -i start
> python evennia.py
Note: Using -i starts the server in 'interactive mode' - it will print
This will launch a menu with options. You normally want option 1 for production
servers, whereas options 2-4 offers more or less debug output to the screen.
You can also start the server directly from the command line, e.g. with
> pythong evennia.py -i start
Note: Using -i starts the server and portal in 'interactive mode' - it will print
messages to standard output and you can shut it down with (on most systems)
Ctrl-C. To start the server as a background process (suitable for production
environments), just skip the -i flag. A server running as a process is
instead stopped with 'python evennia.py stop'.
instead stopped with 'python evennia.py stop'.
* Start up your MUD client of choice and point it to your server and port 4000.
If you are just running locally the server name is most likely 'localhost'.
If you are just running locally the server name is 'localhost'.
* Alternatively, ou can find the web interface and webclient by
* Alternatively, you can find the web interface and webclient by
pointing your web browser to http://localhost:8000.
* Login with the email address and password you provided to the syncdb script.

29
README
View file

@ -1,11 +1,12 @@
Evennia README http://evennia.com
Evennia README (http://evennia.com)
--------------
- < 2010 (earlier revisions)
- May 2010 - merged ABOUT and README. Added Current status /Griatch
- Aug 2010 - evennia devel merged into trunk /Griatch
- Aug 2011 - split evennia into portal + server for better reload /Griatch
- May 2011 - all commands implemented, web client, contribs /Griatch
- Aug 2010 - evennia devel merged into trunk /Griatch
- May 2010 - merged ABOUT and README. Added Current status /Griatch
- < 2010 (earlier revisions)
Contents:
---------
@ -58,6 +59,15 @@ See the INSTALL file for help on setting up and running Evennia.
Current Status
--------------
Aug 2011:
Split Evennia into two processes: Portal and Server. After a lot of
work trying to get in-memory code-reloading to work, it's clear this
is not Python's forte - it's impossible to catch all exceptions,
especially in asynchronous code like this. Trying to do so results in
hackish, flakey and unstable code. With the Portal-Server split, the
Server can simply be rebooted while players connected to the Portal
remain connected. The two communicates over twisted's AMP protocol.
May 2011:
The new version of Evennia, originally hitting trunk in Aug2010, is
maturing. All commands from the pre-Aug version, including IRC/IMC2
@ -66,10 +76,11 @@ including moving Evennia to be its own webserver (no more need for
Apache or django-testserver). Contrib-folder added.
Aug 2010:
Evennia-griatch-branch is ready for merging with trunk. This marks
a rather big change in the inner workings of the server, but should
hopefully bring everything together into one consistent package as
code development continues.
Evennia-griatch-branch is ready for merging with trunk. This marks a
rather big change in the inner workings of the server, such as the
introduction of TypeClasses and Scripts (as compared to the old
ScriptParents and Events) but should hopefully bring everything
together into one consistent package as code development continues.
May 2010:
Evennia is currently being heavily revised and cleaned from
@ -80,6 +91,8 @@ parts of Evennia's innards, from the way Objects are handled
to Events, Commands and Permissions.
Contact, Support and Development
-----------------------
This is still alpha software, but we try to give support best we can

View file

@ -9,7 +9,7 @@ simple menu-driven conversation. Create it by
creating an object of typeclass contrib.talking_npc.TalkingNPC,
For example using @create:
@create John : contrib.talking_npc.TalkingNCP
@create John : contrib.talking_npc.TalkingNPC
Walk up to it and give the talk command
to strike up a conversation. If there are many

File diff suppressed because it is too large Load diff

View file

@ -91,7 +91,7 @@ class AttackTimer(Script):
"This sets up the script"
self.key = "AttackTimer"
self.desc = "Drives an Enemy's combat."
self.interval = random.randint(10, 15) # how fast the Enemy acts
self.interval = random.randint(2, 3) # how fast the Enemy acts
self.start_delay = True # wait self.interval before first call
self.persistent = True

View file

@ -231,15 +231,22 @@ class StateLightSourceOn(Script):
self.start_delay = True # only fire after self.interval s.
self.repeats = 1 # only run once.
self.persistent = True # survive a server reboot.
def at_start(self):
"Called at script start - this can also happen if server is restarted."
self.interval = self.obj.db.burntime
self.db.script_started = time.time()
def at_repeat(self):
# this is only called when torch has burnt out
self.obj.db.burntime = -1
self.obj.reset()
def at_stop(self):
"""
Since this script stops after only 1 "repeat", we can use this hook
instead of at_repeat(). Since the user may also turn off the light
prematurely, this hook will also be called in that case.
Since the user may also turn off the light
prematurely, this hook will store the current
burntime.
"""
# calculate remaining burntime
try:
@ -247,12 +254,9 @@ class StateLightSourceOn(Script):
except TypeError:
# can happen if script_started is not defined
time_burnt = self.interval
burntime = self.interval - time_burnt
self.obj.db.burntime = burntime
if burntime <= 0:
# no burntime left. Reset the object.
self.obj.reset()
def is_valid(self):
"This script is only valid as long as the lightsource burns."
return self.obj.db.is_active
@ -339,7 +343,7 @@ class LightSource(TutorialObject):
"""
Can be called by tutorial world runner, or by the script when the lightsource
has burned out.
"""
"""
if self.db.burntime <= 0:
# light burned out. Since the lightsources's "location" should be
# a character, notify them this way.
@ -357,7 +361,7 @@ class LightSource(TutorialObject):
try:
self.location.scripts.validate()
except AttributeError,e:
pass
pass
self.delete()
#------------------------------------------------------------

View file

@ -258,7 +258,7 @@ class DarkState(Script):
"Someone turned on a light. This state dies. Switch to LightState."
for char in [char for char in self.obj.contents if char.has_player]:
char.cmdset.delete(DarkCmdSet)
self.obj.db.is_dark = False
self.obj.db.is_dark = False
self.obj.scripts.add(LightState)
class LightState(Script):

View file

@ -2,12 +2,15 @@
"""
EVENNIA SERVER STARTUP SCRIPT
This is the start point for running Evennia.
Sets the appropriate environmental variables and launches the server
process. Run the script with the -h flag to see usage information.
and portal through the runner. Run without arguments to get a
menu. Run the script with the -h flag to see usage information.
"""
import os
import sys
import signal
import os
import sys, signal
from optparse import OptionParser
from subprocess import Popen, call
@ -15,189 +18,425 @@ from subprocess import Popen, call
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
# i18n
from django.utils.translation import ugettext as _
SIG = signal.SIGINT
HELPENTRY = \
_("""
(version %s)
This program launches Evennia with various options. You can access all
this functionality directly from the command line; for example option
five (restart server) would be "evennia.py restart server". Use
"evennia.py -h" for command line options.
Evennia consists of two separate programs that both must be running
for the game to work as it should:
Portal - the connection to the outside world (via telnet, web, ssh
etc). This is normally running as a daemon and don't need to
be reloaded unless you are debugging a new connection
protocol. As long as this is running, players won't loose
their connection to your game. Only one instance of Portal
will be started, more will be ignored.
Server - the game server itself. This will often need to be reloaded
as you develop your game. The Portal will auto-connect to the
Server whenever the Server activates. We will also make sure
to automatically restart this whenever it is shut down (from
here or from inside the game or via task manager etc). Only
one instance of Server will be started, more will be ignored.
In a production environment you will want to run with the default
option (1), which runs as much as possible as a background
process. When developing your game it is however convenient to
directly see tracebacks on standard output, so starting with options
2-4 may be a good bet. As you make changes to your code, reload the
server (option 5) to make it available to users.
Reload and stop is not well supported in Windows. If you have issues, log
into the game to stop or restart the server instead.
""")
MENU = \
_("""
+---------------------------------------------------------------------------+
| |
| Welcome to the Evennia launcher! |
| |
| Pick an option below. Use 'h' to get help. |
| |
+--- Starting (will not restart already running processes) -----------------+
| |
| 1) (default): Start Server and Portal. Portal starts in daemon mode.|
| All output is to logfiles. |
| 2) (game debug): Start Server and Portal. Portal starts in daemon mode.|
| Server outputs to stdout instead of logfile. |
| 3) (portal debug): Start Server and Portal. Portal starts in non-daemon |
| mode (can be reloaded) and logs to stdout. |
| 4) (full debug): Start Server and Portal. Portal starts in non-daemon |
| mode (can be reloaded). Both log to stdout. |
| |
+--- Restarting (must first be started) ------------------------------------+
| |
| 5) Restart/reload the Server |
| 6) Restart/reload the Portal (only works in non-daemon mode. If running |
| in daemon mode, Portal needs to be restarted manually (option 1-4)) |
| |
+--- Stopping (must first be started) --------------------------------------+
| |
| 7) Stopping both Portal and Server. Server will not restart. |
| 8) Stopping only Server. Server will not restart. |
| 9) Stopping only Portal. |
| |
+---------------------------------------------------------------------------+
| h) Help |
| q) Quit |
+---------------------------------------------------------------------------+
""")
#
# System Configuration and setup
#
SERVER_PIDFILE = "server.pid"
PORTAL_PIDFILE = "portal.pid"
SERVER_RESTART = "server.restart"
PORTAL_RESTART = "portal.restart"
if not os.path.exists('settings.py'):
# make sure we have a settings.py file.
print " No settings.py file found. Launching manage.py ..."
# make sure we have a settings.py file.
print _(" No settings.py file found. launching manage.py ...")
import game.manage
import game.manage
print """
Now configure Evennia by editing your new settings.py file.
If you haven't already, you should also create/configure the
database with 'python manage.py syncdb' before continuing.
print _("""
... A new settings file was created. Edit this file to configure
Evennia as desired by copy&pasting options from
src/settings_default.py.
When you are ready, run this program again to start the server."""
You should then also create/configure the database using
python manage.py syncdb
Make sure to create a new admin user when prompted -- this will be
user #1 in-game. If you use django-south, you'll see mentions of
migrating things in the above run. You then also have to run
python manage.py migrate
If you use default sqlite3 database, you will find a file
evennia.db appearing. This is the database file. Just delete this
and repeat the above manage.py steps to start with a fresh
database.
When you are set up, run evennia.py again to start the server.""")
sys.exit()
# Get the settings
from django.conf import settings
# Setup the launch of twisted depending on which operating system we use
if os.name == 'nt':
from src.utils.utils import get_evennia_version
EVENNIA_VERSION = get_evennia_version()
# Setup access of the evennia server itself
SERVER_PY_FILE = os.path.join(settings.SRC_DIR, 'server/server.py')
PORTAL_PY_FILE = os.path.join(settings.SRC_DIR, 'server/portal.py')
# Get logfile names
SERVER_LOGFILE = settings.SERVER_LOG_FILE
PORTAL_LOGFILE = settings.PORTAL_LOG_FILE
# Check so a database exists and is accessible
from django.db import DatabaseError
from src.objects.models import ObjectDB
try:
test = ObjectDB.objects.get(id=1)
except ObjectDB.DoesNotExist:
pass # this is fine at this point
except DatabaseError:
print _("""
Your database does not seem to be set up correctly.
Please run:
python manage.py syncdb
(make sure to create an admin user when prompted). If you use
pyhon-south you will get mentions of migrating in the above
run. You then need to also run
python manage.py migrate
When you have a database set up, rerun evennia.py.
""")
sys.exit()
# Add this to the environmental variable for the 'twistd' command.
currpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if 'PYTHONPATH' in os.environ:
os.environ['PYTHONPATH'] += (":%s" % currpath)
else:
os.environ['PYTHONPATH'] = currpath
TWISTED_BINARY = 'twistd'
if os.name == 'nt':
# Windows needs more work to get the correct binary
try:
# Test for for win32api
import win32api
except ImportError:
print """
ERROR: Unable to import win32api, which Twisted requires to run.
print _("""
ERROR: Unable to import win32api, which Twisted requires to run.
You may download it from:
http://sourceforge.net/projects/pywin32
or
http://starship.python.net/crew/mhammond/win32/Downloads.html"""
http://sourceforge.net/projects/pywin32
or
http://starship.python.net/crew/mhammond/win32/Downloads.html""")
sys.exit()
if not os.path.exists('twistd.bat'):
# Test for executable twisted batch file. This calls the twistd.py
# Test for executable twisted batch file. This calls the twistd.py
# executable that is usually not found on the path in Windows.
# It's not enough to locate scripts.twistd, what we want is the
# executable script C:\PythonXX/Scripts/twistd.py. Alas we cannot
# hardcode this location since we don't know if user has Python
# in a non-standard location, so we try to figure it out.
# It's not enough to locate scripts.twistd, what we want is the
# executable script C:\PythonXX/Scripts/twistd.py. Alas we cannot
# hardcode this location since we don't know if user has Python
# in a non-standard location, so we try to figure it out.
from twisted.scripts import twistd
twistd_path = os.path.abspath(
os.path.join(os.path.dirname(twistd.__file__),
os.pardir, os.pardir, os.pardir, os.pardir,
'scripts', 'twistd.py'))
os.path.join(os.path.dirname(twistd.__file__),
os.pardir, os.pardir, os.pardir, os.pardir,
'scripts', 'twistd.py'))
bat_file = open('twistd.bat','w')
bat_file.write("@\"%s\" \"%s\" %%*" % (sys.executable, twistd_path))
bat_file.close()
print """
print _("""
INFO: Since you are running Windows, a file 'twistd.bat' was
created for you. This is a simple batch file that tries to call
the twisted executable. Evennia determined this to be:
%s
%{twistd_path}s
If you run into errors at startup you might need to edit
twistd.bat to point to the actual location of the Twisted
executable (usually called twistd.py) on your machine.
This procedure is only done once. Run evennia.py again when you
This procedure is only done once. Run evennia.py again when you
are ready to start the server.
""" % twistd_path
""") % {'twistd_path': twistd_path}
sys.exit()
TWISTED_BINARY = 'twistd.bat'
else:
TWISTED_BINARY = 'twistd'
# Setup access of the evennia server itself
SERVER_PY_FILE = os.path.join(settings.SRC_DIR, 'server/server.py')
# Add this to the environmental variable for the 'twistd' command.
thispath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if 'PYTHONPATH' in os.environ:
os.environ['PYTHONPATH'] += (":%s" % thispath)
else:
os.environ['PYTHONPATH'] = thispath
def cycle_logfile():
"""
Move the old log file to evennia.log.old (by default).
"""
logfile = settings.DEFAULT_LOG_FILE.strip()
logfile_old = logfile + '.old'
if os.path.exists(logfile):
# Cycle the old logfiles to *.old
if os.path.exists(logfile_old):
# E.g. Windows don't support rename-replace
os.remove(logfile_old)
os.rename(logfile, logfile_old)
logfile = settings.HTTP_LOG_FILE.strip()
logfile_old = logfile + '.old'
if os.path.exists(logfile):
# Cycle the old logfiles to *.old
if os.path.exists(logfile_old):
# E.g. Windows don't support rename-replace
os.remove(logfile_old)
os.rename(logfile, logfile_old)
def start_daemon(parser, options, args):
"""
Start the server in daemon mode. This means that all logging output will
be directed to logs/evennia.log by default, and the process will be
backgrounded.
"""
if os.path.exists('twistd.pid'):
print "A twistd.pid file exists in the current directory, which suggests that the server is already running."
sys.exit()
# Functions
print '\nStarting Evennia server in daemon mode ...'
print 'Logging to: %s.' % settings.DEFAULT_LOG_FILE
# Move the old evennia.log file out of the way.
cycle_logfile()
# Start it up
Popen([TWISTED_BINARY,
'--logfile=%s' % settings.DEFAULT_LOG_FILE,
'--python=%s' % SERVER_PY_FILE])
def start_interactive(parser, options, args):
def get_pid(pidfile):
"""
Start in interactive mode, which means the process is foregrounded and
all logging output is directed to stdout.
Get the PID (Process ID) by trying to access
an PID file.
"""
print '\nStarting Evennia server in interactive mode (stop with keyboard interrupt) ...'
print 'Logging to: Standard output.'
pid = None
if os.path.exists(pidfile):
f = open(pidfile, 'r')
pid = f.read()
return pid
# we cycle logfiles (this will at most put all files to *.old)
# to handle html request logging files.
cycle_logfile()
try:
call([TWISTED_BINARY,
'-n',
'--python=%s' % SERVER_PY_FILE])
except KeyboardInterrupt:
pass
def del_pid(pidfile):
"""
The pidfile should normally be removed after a process has finished, but
when sending certain signals they remain, so we need to clean them manually.
"""
if os.path.exists(pidfile):
os.remove(pidfile)
def stop_server(parser, options, args):
def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART, restart=True):
"""
Gracefully stop the server process.
Send a kill signal to a process based on PID. A customized success/error
message will be returned. If clean=True, the system will attempt to manually
remove the pid file.
"""
if os.name == 'posix':
if os.path.exists('twistd.pid'):
print 'Stopping the Evennia server...'
f = open('twistd.pid', 'r')
pid = f.read()
os.kill(int(pid), signal.SIGINT)
print 'Server stopped.'
pid = get_pid(pidfile)
if pid:
if os.name == 'nt':
if sys.version < "2.7":
print _("Windows requires Python 2.7 or higher for this operation.")
return
os.remove(pidfile)
# set restart/norestart flag
f = open(restart_file, 'w')
f.write(str(restart))
f.close()
try:
os.kill(int(pid), signal)
except OSError:
print _("Process %(pid)s could not be signalled. The PID file '%(pidfile)s' seems stale. Try removing it.") % {'pid': pid, 'pidfile': pidfile}
return
print "Evennia:", succmsg
return
print "Evennia:", errmsg
def run_menu():
"""
This launches an interactive menu.
"""
cmdstr = ["python", "runner.py"]
while True:
# menu loop
print MENU
inp = raw_input(_(" option > "))
# quitting and help
if inp.lower() == 'q':
sys.exit()
elif inp.lower() == 'h':
print HELPENTRY % EVENNIA_VERSION
raw_input(_("press <return> to continue ..."))
continue
# options
try:
inp = int(inp)
except ValueError:
print _("Not a valid option.")
continue
errmsg = _("The %s does not seem to be running.")
if inp < 5:
if inp == 1:
pass # default operation
elif inp == 2:
cmdstr.extend(['--iserver'])
elif inp == 3:
cmdstr.extend(['--iportal'])
elif inp == 4:
cmdstr.extend(['--iserver', '--iportal'])
return cmdstr
elif inp < 10:
if inp == 5:
if os.name == 'nt':
print _("This operation is not supported under Windows. Log into the game to restart/reload the server.")
return
kill(SERVER_PIDFILE, SIG, _("Server restarted."), errmsg % "Server")
elif inp == 6:
if os.name == 'nt':
print _("This operation is not supported under Windows.")
return
kill(PORTAL_PIDFILE, SIG, _("Portal restarted (or stopped if in daemon mode)."), errmsg % "Portal")
elif inp == 7:
kill(SERVER_PIDFILE, SIG, _("Stopped Portal."), errmsg % "Portal", PORTAL_RESTART, restart=False)
kill(PORTAL_PIDFILE, SIG, _("Stopped Server."), errmsg % "Server", restart=False)
elif inp == 8:
kill(PORTAL_PIDFILE, SIG, _("Stopped Server."), errmsg % "Server", restart=False)
elif inp == 9:
kill(SERVER_PIDFILE, SIG, _("Stopped Portal."), errmsg % "Portal", PORTAL_RESTART, restart=False)
return
else:
print "No twistd.pid file exists, the server doesn't appear to be running."
elif os.name == 'nt':
print '\n\rStopping cannot be done safely under this operating system.'
print 'Kill server using the task manager or shut it down from inside the game.'
else:
print '\n\rUnknown OS detected, can not stop. '
print _("Not a valid option.")
return None
def handle_args(options, mode, service):
"""
Handle argument options given on the command line.
options - parsed object for command line
mode - str; start/stop etc
service - str; server, portal or all
"""
inter = options.interactive
cmdstr = ["python", "runner.py"]
errmsg = _("The %s does not seem to be running.")
if mode == 'start':
# starting one or many services
if service == 'server':
if inter:
cmdstr.append('--iserver')
cmdstr.append('--noportal')
elif service == 'portal':
if inter:
cmdstr.append('--iportal')
cmdstr.append('--noserver')
else: # all
# for convenience we don't start logging of portal, only of server with this command.
if inter:
cmdstr.extend(['--iserver'])
return cmdstr
elif mode == 'restart':
# restarting services
if os.name == 'nt':
print _("Restarting from command line is not supported under Windows. Log into the game to restart.")
return
if service == 'server':
kill(SERVER_PIDFILE, SIG, _("Server restarted."), errmsg % 'Server')
elif service == 'portal':
print _("Note: Portal usually don't need to be restarted unless you are debugging in interactive mode.")
print _("If Portal was running in default Daemon mode, it cannot be restarted. In that case you have ")
print _("to restart it manually with 'evennia.py start portal'")
kill(PORTAL_PIDFILE, SIG, _("Portal restarted (or stopped, if it was in daemon mode)."), errmsg % 'Portal', PORTAL_RESTART)
else: # all
# default mode, only restart server
kill(SERVER_PIDFILE, SIG, _("Server restarted."), errmsg % 'Server')
elif mode == 'stop':
# stop processes, avoiding reload
if service == 'server':
kill(SERVER_PIDFILE, SIG, _("Server stopped."), errmsg % 'Server', restart=False)
elif service == 'portal':
kill(PORTAL_PIDFILE, SIG, _("Portal stopped."), errmsg % 'Portal', PORTAL_RESTART, restart=False)
else:
kill(PORTAL_PIDFILE, SIG, _("Portal stopped."), errmsg % 'Portal', PORTAL_RESTART, restart=False)
kill(SERVER_PIDFILE, SIG, _("Server stopped."), errmsg % 'Server', restart=False)
return None
def main():
"""
Beginning of the program logic.
This handles command line input.
"""
parser = OptionParser(usage="%prog [options] <start|stop>",
description="This command starts or stops the Evennia game server. Note that you have to setup the database by running 'manage.py syncdb' before starting the server for the first time.")
parser.add_option('-i', '--interactive', action='store_true',
dest='interactive', default=False,
help='Start in interactive mode')
parser.add_option('-d', '--daemon', action='store_false',
dest='interactive',
help='Start in daemon mode (default)')
(options, args) = parser.parse_args()
if "start" in args:
if options.interactive:
start_interactive(parser, options, args)
else:
start_daemon(parser, options, args)
elif "stop" in args:
stop_server(parser, options, args)
parser = OptionParser(usage="%prog [-i] [menu|start|restart|stop [server|portal|all]]",
description=_("""This is the main Evennia launcher. It handles the Portal and Server, the two services making up Evennia. Default is to operate on both services. Use --interactive together with start to launch services as 'interactive'. Note that when launching 'all' services with the --interactive flag, both services will be started, but only Server will actually be started in interactive mode. This is simply because this is the most commonly useful state. To activate interactive mode also for Portal, launch the two services explicitly as two separate calls to this program. You can also use the menu."""))
parser.add_option('-i', '--interactive', action='store_true', dest='interactive', default=False, help=_("Start given processes in interactive mode (log to stdout, don't start as a daemon)."))
options, args = parser.parse_args()
inter = options.interactive
if not args:
mode = "menu"
service = 'all'
if args:
mode = args[0]
service = "all"
if len(args) > 1:
service = args[1]
if mode not in ['menu', 'start', 'restart', 'stop']:
print _("mode should be none or one of 'menu', 'start', 'restart' or 'stop'.")
sys.exit()
if service not in ['server', 'portal', 'all']:
print _("service should be none or 'server', 'portal' or 'all'.")
sys.exit()
if mode == 'menu':
# launch menu
cmdstr = run_menu()
else:
parser.print_help()
# handle command-line arguments
cmdstr = handle_args(options, mode, service)
if cmdstr:
# call the runner.
cmdstr.append('start')
Popen(cmdstr)
if __name__ == '__main__':
from src.utils.utils import check_evennia_dependencies
if check_evennia_dependencies():

View file

@ -40,6 +40,7 @@ class DefaultCmdSet(cmdset_default.DefaultCmdSet):
"""
Populates the cmdset
"""
# calling setup in src.commands.default.cmdset_default
super(DefaultCmdSet, self).at_cmdset_creation()
#
@ -48,6 +49,7 @@ class DefaultCmdSet(cmdset_default.DefaultCmdSet):
#self.add(menusystem.CmdMenuTest())
#self.add(lineeditor.CmdEditor())
class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet):
"""
This is an example of how to overload the command set of the
@ -65,6 +67,7 @@ class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet):
"""
Populates the cmdset
"""
# calling setup in src.commands.default.cmdset_unloggedin
super(UnloggedinCmdSet, self).at_cmdset_creation()
#
@ -83,6 +86,7 @@ class OOCCmdSet(cmdset_ooc.OOCCmdSet):
"""
Populates the cmdset
"""
# calling setup in src.commands.default.cmdset_ooc
super(OOCCmdSet, self).at_cmdset_creation()
#

View file

@ -89,10 +89,14 @@ class Object(BaseObject):
at_get(getter) - called after object has been picked up. Does not stop pickup.
at_drop(dropper) - called when this object has been dropped.
at_say(speaker, message) - by default, called if an object inside this object speaks
at_cache() - called when this typeclass is instantiated and cached
at_server_reload() - called when server is reloading
at_server_shutdown() - called when server is resetting/shutting down
"""
pass
class Character(BaseCharacter):
"""
This is the default object created for a new user connecting - the

View file

@ -25,7 +25,7 @@ class BodyFunctions(Script):
self.interval = 20 # seconds
#self.repeats = 5 # repeat only a certain number of times
self.start_delay = True # wait self.interval until first call
self.persistent = True
#self.persistent = True
def at_repeat(self):
"""

View file

@ -8,6 +8,9 @@ the database.
import sys
import os
# i18n
from django.utils.translation import ugettext as _
# Tack on the root evennia directory to the python path.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@ -97,9 +100,9 @@ from src.settings_default import *
settings_file.write(string)
settings_file.close()
print """
Welcome to Evennia (version %s)!
We created a fresh settings.py file for you.""" % VERSION
print _("""
Welcome to Evennia (version %(version)s)!
We created a fresh settings.py file for you.""") % {'version': VERSION}
#------------------------------------------------------------
# Test the import of the settings file
@ -109,14 +112,14 @@ try:
except Exception:
import traceback
string = "\n" + traceback.format_exc()
string += """\n
string += _("""\n
Error: Couldn't import the file 'settings.py' in the directory
containing %r. There can be two reasons for this:
containing %(file)r. There can be two reasons for this:
1) You moved your settings.py elsewhere. In that case move it back or
create a link to it from this folder.
2) The settings module is where it's supposed to be, but contains errors.
Review the traceback above to resolve the problem, then try again.
""" % __file__
""") % {'file': __file__}
print string
sys.exit(1)
@ -129,11 +132,11 @@ if __name__ == "__main__":
# checks if the settings file was created this run
if _CREATED_SETTINGS:
print """
print _("""
Edit your new settings.py file as needed, then run
'python manage syncdb' and follow the prompts to
create the database and your superuser account.
"""
""")
sys.exit()
# run the standard django manager, if dependencies match

282
game/runner.py Normal file
View file

@ -0,0 +1,282 @@
#!/usr/bin/env python
"""
This runner is controlled by evennia.py and should normally not be
launched directly. It manages the two main Evennia processes (Server
and Portal) and most importanly runs a passive, threaded loop that
makes sure to restart Server whenever it shuts down.
Since twistd does not allow for returning an optional exit code we
need to handle the current reload state for server and portal with
flag-files instead. The files, one each for server and portal either
contains True or False indicating if the process should be restarted
upon returning, or not. A process returning != 0 will always stop, no
matter the value of this file.
"""
import os
import sys
from optparse import OptionParser
from subprocess import Popen, call
import Queue, thread, subprocess
#
# System Configuration
#
SERVER_PIDFILE = "server.pid"
PORTAL_PIDFILE = "portal.pid"
SERVER_RESTART = "server.restart"
PORTAL_RESTART = "portal.restart"
# Set the Python path up so we can get to settings.py from here.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
# i18n
from django.utils.translation import ugettext as _
if not os.path.exists('settings.py'):
print _("No settings.py file found. Run evennia.py to create it.")
sys.exit()
# Get the settings
from django.conf import settings
# Setup access of the evennia server itself
SERVER_PY_FILE = os.path.join(settings.SRC_DIR, 'server/server.py')
PORTAL_PY_FILE = os.path.join(settings.SRC_DIR, 'server/portal.py')
# Get logfile names
SERVER_LOGFILE = settings.SERVER_LOG_FILE
PORTAL_LOGFILE = settings.PORTAL_LOG_FILE
# Add this to the environmental variable for the 'twistd' command.
currpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if 'PYTHONPATH' in os.environ:
os.environ['PYTHONPATH'] += (":%s" % currpath)
else:
os.environ['PYTHONPATH'] = currpath
TWISTED_BINARY = 'twistd'
if os.name == 'nt':
TWISTED_BINARY = 'twistd.bat'
err = False
try:
import win32api # Test for for win32api
except ImportError:
err = True
if not os.path.exists(TWISTED_BINARY):
err = True
if err:
print _("Twisted binary for Windows is not ready to use. Please run evennia.py.")
sys.exit()
# Functions
def set_restart_mode(restart_file, flag=True):
"""
This sets a flag file for the restart mode.
"""
f = open(restart_file, 'w')
f.write(str(flag))
f.close()
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
def get_pid(pidfile):
"""
Get the PID (Process ID) by trying to access
an PID file.
"""
pid = None
if os.path.exists(pidfile):
f = open(pidfile, 'r')
pid = f.read()
return pid
def cycle_logfile(logfile):
"""
Move the old log files to <filename>.old
"""
logfile_old = logfile + '.old'
if os.path.exists(logfile):
# Cycle the old logfiles to *.old
if os.path.exists(logfile_old):
# E.g. Windows don't support rename-replace
os.remove(logfile_old)
os.rename(logfile, logfile_old)
logfile = settings.HTTP_LOG_FILE.strip()
logfile_old = logfile + '.old'
if os.path.exists(logfile):
# Cycle the old logfiles to *.old
if os.path.exists(logfile_old):
# E.g. Windows don't support rename-replace
os.remove(logfile_old)
os.rename(logfile, logfile_old)
# Start program management
SERVER = None
PORTAL = None
def start_services(server_argv, portal_argv):
"""
This calls a threaded loop that launces the Portal and Server
and then restarts them when they finish.
"""
global SERVER, PORTAL
processes = Queue.Queue()
def server_waiter(queue):
try:
rc = Popen(server_argv).wait()
except Exception, e:
print _("Server process error: %(e)s") % {'e': e}
queue.put(("server_stopped", rc)) # this signals the controller that the program finished
def portal_waiter(queue):
try:
rc = Popen(portal_argv).wait()
except Exception, e:
print _("Portal process error: %(e)s") % {'e': e}
queue.put(("portal_stopped", rc)) # this signals the controller that the program finished
if server_argv:
# start server as a reloadable thread
SERVER = thread.start_new_thread(server_waiter, (processes, ))
if portal_argv:
if get_restart_mode(PORTAL_RESTART):
# start portal as interactive, reloadable thread
PORTAL = thread.start_new_thread(portal_waiter, (processes, ))
else:
# normal operation: start portal as a daemon; we don't care to monitor it for restart
PORTAL = Popen(portal_argv)
if not SERVER:
# if portal is daemon and no server is running, we have no reason to continue to the loop.
return
# Reload loop
while True:
# this blocks until something is actually returned.
message, rc = processes.get()
# restart only if process stopped cleanly
if message == "server_stopped" and int(rc) == 0 and get_restart_mode(SERVER_RESTART):
print _("Evennia Server stopped. Restarting ...")
SERVER = thread.start_new_thread(server_waiter, (processes, ))
continue
# normally the portal is not reloaded since it's run as a daemon.
if message == "portal_stopped" and int(rc) == 0 and get_restart_mode(PORTAL_RESTART):
print _("Evennia Portal stopped in interactive mode. Restarting ...")
PORTAL = thread.start_new_thread(portal_waiter, (processes, ))
continue
break
# Setup signal handling
def main():
"""
This handles the command line input of the runner (it's most often called by evennia.py)
"""
parser = OptionParser(usage="%prog [options] start",
description=_("This runner should normally *not* be called directly - it is called automatically from the evennia.py main program. It manages the Evennia game server and portal processes an hosts a threaded loop to restart the Server whenever it is stopped (this constitues Evennia's reload mechanism)."))
parser.add_option('-s', '--noserver', action='store_true',
dest='noserver', default=False,
help=_('Do not start Server process'))
parser.add_option('-p', '--noportal', action='store_true',
dest='noportal', default=False,
help=_('Do not start Portal process'))
parser.add_option('-i', '--iserver', action='store_true',
dest='iserver', default=False,
help=_('output server log to stdout instead of logfile'))
parser.add_option('-d', '--iportal', action='store_true',
dest='iportal', default=False,
help=_('output portal log to stdout. Does not make portal a daemon.'))
options, args = parser.parse_args()
if not args or args[0] != 'start':
# this is so as to not be accidentally launched.
parser.print_help()
sys.exit()
# set up default project calls
server_argv = [TWISTED_BINARY,
'--nodaemon',
'--logfile=%s' % SERVER_LOGFILE,
'--pidfile=%s' % SERVER_PIDFILE,
'--python=%s' % SERVER_PY_FILE]
portal_argv = [TWISTED_BINARY,
'--logfile=%s' % PORTAL_LOGFILE,
'--pidfile=%s' % PORTAL_PIDFILE,
'--python=%s' % PORTAL_PY_FILE]
# Server
pid = get_pid(SERVER_PIDFILE)
if pid and not options.noserver:
print _("\nEvennia Server is already running as process %(pid)s. Not restarted.") % {'pid': pid}
options.noserver = True
if options.noserver:
server_argv = None
else:
set_restart_mode(SERVER_RESTART, True)
if options.iserver:
# don't log to server logfile
del server_argv[2]
print _("\nStarting Evennia Server (output to stdout).")
else:
print _("\nStarting Evennia Server (output to server logfile).")
cycle_logfile(SERVER_LOGFILE)
# Portal
pid = get_pid(PORTAL_PIDFILE)
if pid and not options.noportal:
print _("\nEvennia Portal is already running as process %(pid)s. Not restarted.") % {'pid': pid}
options.noportal = True
if options.noportal:
portal_argv = None
else:
if options.iportal:
# make portal interactive
portal_argv[1] = '--nodaemon'
PORTAL_INTERACTIVE = True
set_restart_mode(PORTAL_RESTART, True)
print _("\nStarting Evennia Portal in non-Daemon mode (output to stdout).")
else:
set_restart_mode(PORTAL_RESTART, False)
print _("\nStarting Evennia Portal in Daemon mode (output to portal logfile).")
cycle_logfile(PORTAL_LOGFILE)
# Windows fixes (Windows don't support pidfiles natively)
if os.name == 'nt':
if server_argv:
del server_argv[-2]
if portal_argv:
del portal_argv[-2]
# Start processes
start_services(server_argv, portal_argv)
if __name__ == '__main__':
from src.utils.utils import check_evennia_dependencies
if check_evennia_dependencies():
main()

32
locale/README Normal file
View file

@ -0,0 +1,32 @@
Evennia locales
---------------
* Changing server language
Edit your settings file:
USE_I18N = True
LANGUAGE_CODE = 'en'
Change 'en' to a translated language (see locale/).
Restart Server and Portal.
* Translating
This folder contains translation strings for the core server.
First look in locale/ to see if there are already data files for
your language available to improve upon. If not, you can
start translate for a new language by placing yourself in
Evennia's root directory and run
django-admin makemessages -l <language code>
where <language code> is the two-letter locale code for the language
you want, like "sv" for Swedish, "es" for Spanish and so on. Next
go to locale/<language code>/LC_MESSAGES/and edit the *.po file
you find there by translating each given English string to the equivalent in
the other language. Editing the raw file manually is not necessary -- search
the web and you'll find many open-source graphical .po editors available.

View file

@ -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)

View file

@ -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())

View file

@ -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())

View file

@ -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):
"""

View file

@ -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):

View file

@ -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

View file

@ -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):

View file

@ -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):
"""

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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:

View file

@ -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()

View file

@ -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
View 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")

View file

@ -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):
"""

View file

@ -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
View 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()

View file

@ -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
View 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)

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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():

View file

@ -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))

View file

@ -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()

View file

@ -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

View file

@ -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
View 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)

View file

@ -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)

View file

@ -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)

View file

@ -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