mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Trunk: Merged griatch-branch. This implements a new reload mechanism - splitting Evennia into two processes: Server and Portal with different tasks. Also cleans and fixes several bugs in script systems as well as introduces i18n (courtesy of raydeejay).
This commit is contained in:
parent
14dae44a46
commit
f13e8cdf7c
50 changed files with 3175 additions and 2565 deletions
|
|
@ -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
31
INSTALL
|
|
@ -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
29
README
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
#------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
515
game/evennia.py
515
game/evennia.py
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
282
game/runner.py
Normal 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
32
locale/README
Normal 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.
|
||||
|
|
@ -87,7 +87,7 @@ class CmdBoot(MuxCommand):
|
|||
feedback += "\nReason given: %s" % reason
|
||||
|
||||
for session in boot_list:
|
||||
name = session.name
|
||||
name = session.uname
|
||||
session.msg(feedback)
|
||||
session.disconnect()
|
||||
caller.msg("You booted %s." % name)
|
||||
|
|
|
|||
|
|
@ -408,7 +408,8 @@ class CmdCreate(ObjManipCommand):
|
|||
if caller.location:
|
||||
obj.home = caller.location
|
||||
obj.move_to(caller.location, quiet=True)
|
||||
caller.msg(string)
|
||||
if string:
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class CmdDebug(MuxCommand):
|
||||
|
|
@ -1077,7 +1078,7 @@ class CmdOpen(ObjManipCommand):
|
|||
exit_obj.destination = destination
|
||||
string = "Created new Exit '%s' from %s to %s (aliases: %s)." % (exit_name,location.name,
|
||||
destination.name,
|
||||
exit_aliases)
|
||||
", ".join([str(e) for e in exit_aliases]))
|
||||
else:
|
||||
string = "Error: Exit '%s' not created." % (exit_name)
|
||||
# emit results
|
||||
|
|
@ -1824,17 +1825,19 @@ class CmdScript(MuxCommand):
|
|||
attach scripts
|
||||
|
||||
Usage:
|
||||
@script[/switch] <obj> = <script.path or scriptkey>
|
||||
@script[/switch] <obj> [= <script.path or scriptkey>]
|
||||
|
||||
Switches:
|
||||
start - start a previously added script
|
||||
stop - stop a previously added script
|
||||
|
||||
Attaches the given script to the object and starts it. Script path can be given
|
||||
from the base location for scripts as given in settings.
|
||||
If stopping/starting an already existing script, the script's key
|
||||
can be given instead (if giving a path, *all* scripts with this path
|
||||
on <obj> will be affected).
|
||||
Attaches the given script to the object and starts it. Script path
|
||||
can be given from the base location for scripts as given in
|
||||
settings. If stopping/starting an already existing script, the
|
||||
script's key can be given instead (if giving a path, *all* scripts
|
||||
with this path on <obj> will be affected). If no script name is given,
|
||||
all scripts on the object is affected (or displayed if no start/stop
|
||||
switch is set).
|
||||
"""
|
||||
|
||||
key = "@script"
|
||||
|
|
@ -1847,8 +1850,8 @@ class CmdScript(MuxCommand):
|
|||
|
||||
caller = self.caller
|
||||
|
||||
if not self.rhs:
|
||||
string = "Usage: @script[/switch] <obj> = <script.path or script key>"
|
||||
if not self.args:
|
||||
string = "Usage: @script[/switch] <obj> [= <script.path or script key>]"
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
|
|
@ -1857,33 +1860,52 @@ class CmdScript(MuxCommand):
|
|||
return
|
||||
|
||||
string = ""
|
||||
if not self.switches:
|
||||
# adding a new script, and starting it
|
||||
ok = obj.scripts.add(self.rhs, autostart=True)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be added and/or started." % self.rhs
|
||||
if not self.rhs:
|
||||
# no rhs means we want to operate on all scripts
|
||||
scripts = obj.scripts.all()
|
||||
if not scripts:
|
||||
string += "No scripts defined on %s." % obj.key
|
||||
elif not self.switches:
|
||||
# view all scripts
|
||||
from src.commands.default.system import format_script_list
|
||||
string += format_script_list(scripts)
|
||||
elif "start" in self.switches:
|
||||
num = sum([obj.scripts.start(script.key) for script in scripts])
|
||||
string += "%s scripts started on %s." % num
|
||||
elif "stop" in self.switches:
|
||||
for script in scripts:
|
||||
string += "Stopping script %s." % script.key
|
||||
script.stop()
|
||||
string = string.strip()
|
||||
obj.scripts.validate()
|
||||
else: # rhs exists
|
||||
if not self.switches:
|
||||
# adding a new script, and starting it
|
||||
ok = obj.scripts.add(self.rhs, autostart=True)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be added and/or started." % self.rhs
|
||||
else:
|
||||
string = "Script successfully added and started."
|
||||
|
||||
else:
|
||||
string = "Script successfully added and started."
|
||||
|
||||
else:
|
||||
paths = [self.rhs] + ["%s.%s" % (prefix, self.rhs)
|
||||
for prefix in settings.SCRIPT_TYPECLASS_PATHS]
|
||||
if "stop" in self.switches:
|
||||
# we are stopping an already existing script
|
||||
for path in paths:
|
||||
ok = obj.scripts.stop(path)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be stopped. Does it exist?" % path
|
||||
else:
|
||||
string = "Script stopped and removed from object."
|
||||
break
|
||||
if "start" in self.switches:
|
||||
# we are starting an already existing script
|
||||
for path in paths:
|
||||
ok = obj.scripts.start(path)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be (re)started." % path
|
||||
else:
|
||||
string = "Script started successfully."
|
||||
break
|
||||
paths = [self.rhs] + ["%s.%s" % (prefix, self.rhs)
|
||||
for prefix in settings.SCRIPT_TYPECLASS_PATHS]
|
||||
if "stop" in self.switches:
|
||||
# we are stopping an already existing script
|
||||
for path in paths:
|
||||
ok = obj.scripts.stop(path)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be stopped. Does it exist?" % path
|
||||
else:
|
||||
string = "Script stopped and removed from object."
|
||||
break
|
||||
if "start" in self.switches:
|
||||
# we are starting an already existing script
|
||||
for path in paths:
|
||||
ok = obj.scripts.start(path)
|
||||
if not ok:
|
||||
string += "\nScript %s could not be (re)started." % path
|
||||
else:
|
||||
string = "Script started successfully."
|
||||
break
|
||||
caller.msg(string.strip())
|
||||
|
|
|
|||
|
|
@ -34,11 +34,12 @@ class DefaultCmdSet(CmdSet):
|
|||
|
||||
# System commands
|
||||
self.add(system.CmdReload())
|
||||
self.add(system.CmdReset())
|
||||
self.add(system.CmdShutdown())
|
||||
self.add(system.CmdPy())
|
||||
self.add(system.CmdScripts())
|
||||
self.add(system.CmdObjects())
|
||||
self.add(system.CmdService())
|
||||
self.add(system.CmdShutdown())
|
||||
self.add(system.CmdVersion())
|
||||
self.add(system.CmdTime())
|
||||
self.add(system.CmdServerLoad())
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from src.comms import irc, imc2
|
|||
from src.comms.channelhandler import CHANNELHANDLER
|
||||
from src.utils import create, utils
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
def find_channel(caller, channelname, silent=False, noaliases=False):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ class CmdLook(MuxCommand):
|
|||
"""
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
|
||||
if args:
|
||||
# Use search to handle duplicate/nonexistant results.
|
||||
looking_at_obj = caller.search(args, use_nicks=True)
|
||||
|
|
@ -345,7 +344,7 @@ class CmdQuit(MuxCommand):
|
|||
def func(self):
|
||||
"hook function"
|
||||
for session in self.caller.sessions:
|
||||
session.msg("Quitting. Hope to see you soon again.")
|
||||
session.msg("{RQuitting{n. Hope to see you soon again.")
|
||||
session.session_disconnect()
|
||||
|
||||
class CmdWho(MuxCommand):
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from src.scripts.models import ScriptDB
|
|||
from src.objects.models import ObjectDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.server.models import ServerConfig
|
||||
from src.utils import reloads, create, logger, utils, gametime
|
||||
from src.utils import create, logger, utils, gametime
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
|
||||
|
||||
|
|
@ -26,8 +26,9 @@ class CmdReload(MuxCommand):
|
|||
Usage:
|
||||
@reload
|
||||
|
||||
This reloads the system modules and
|
||||
re-validates all scripts.
|
||||
This restarts the server. The Portal is not
|
||||
affected. Non-persistent scripts will survive a @reload (use
|
||||
@reset to purge) and at_reload() hooks will be called.
|
||||
"""
|
||||
key = "@reload"
|
||||
locks = "cmd:perm(reload) or perm(Immortals)"
|
||||
|
|
@ -37,7 +38,62 @@ class CmdReload(MuxCommand):
|
|||
"""
|
||||
Reload the system.
|
||||
"""
|
||||
reloads.start_reload_loop()
|
||||
SESSIONS.announce_all(" Server restarting ...")
|
||||
SESSIONS.server.shutdown(mode='reload')
|
||||
|
||||
class CmdReset(MuxCommand):
|
||||
"""
|
||||
Reset and reboot the system
|
||||
|
||||
Usage:
|
||||
@reset
|
||||
|
||||
A cold reboot. This works like a mixture of @reload and @shutdown,
|
||||
- all shutdown hooks will be called and non-persistent scrips will
|
||||
be purged. But the Portal will not be affected and the server will
|
||||
automatically restart again.
|
||||
"""
|
||||
key = "@reset"
|
||||
aliases = ['@reboot']
|
||||
locks = "cmd:perm(reload) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Reload the system.
|
||||
"""
|
||||
SESSIONS.announce_all(" Server restarting ...")
|
||||
SESSIONS.server.shutdown(mode='reset')
|
||||
|
||||
|
||||
class CmdShutdown(MuxCommand):
|
||||
|
||||
"""
|
||||
@shutdown
|
||||
|
||||
Usage:
|
||||
@shutdown [announcement]
|
||||
|
||||
Gracefully shut down both Server and Portal.
|
||||
"""
|
||||
key = "@shutdown"
|
||||
locks = "cmd:perm(shutdown) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Define function"
|
||||
try:
|
||||
session = self.caller.sessions[0]
|
||||
except Exception:
|
||||
return
|
||||
self.caller.msg('Shutting down server ...')
|
||||
announcement = "\nServer is being SHUT DOWN!\n"
|
||||
if self.args:
|
||||
announcement += "%s\n" % self.args
|
||||
logger.log_infomsg('Server shutdown by %s.' % self.caller.name)
|
||||
SESSIONS.announce_all(announcement)
|
||||
SESSIONS.portal_shutdown()
|
||||
SESSIONS.server.shutdown(mode='shutdown')
|
||||
|
||||
class CmdPy(MuxCommand):
|
||||
"""
|
||||
|
|
@ -115,6 +171,58 @@ class CmdPy(MuxCommand):
|
|||
except AssertionError: # this is a strange thing; the script looses its id somehow..?
|
||||
pass
|
||||
|
||||
|
||||
# helper function. Kept outside so it can be imported and run
|
||||
# by other commands.
|
||||
|
||||
def format_script_list(scripts):
|
||||
"Takes a list of scripts and formats the output."
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
|
||||
table = [["id"], ["obj"], ["key"], ["intval"], ["next"], ["rept"], ["db"], ["typeclass"], ["desc"]]
|
||||
for script in scripts:
|
||||
|
||||
table[0].append(script.id)
|
||||
if not hasattr(script, 'obj') or not script.obj:
|
||||
table[1].append("<Global>")
|
||||
else:
|
||||
table[1].append(script.obj.key)
|
||||
table[2].append(script.key)
|
||||
if not hasattr(script, 'interval') or script.interval < 0:
|
||||
table[3].append("--")
|
||||
else:
|
||||
table[3].append("%ss" % script.interval)
|
||||
next = script.time_until_next_repeat()
|
||||
if not next:
|
||||
table[4].append("--")
|
||||
else:
|
||||
table[4].append("%ss" % next)
|
||||
|
||||
if not hasattr(script, 'repeats') or not script.repeats:
|
||||
table[5].append("--")
|
||||
else:
|
||||
table[5].append("%s" % script.repeats)
|
||||
if script.persistent:
|
||||
table[6].append("*")
|
||||
else:
|
||||
table[6].append("-")
|
||||
typeclass_path = script.typeclass_path.rsplit('.', 1)
|
||||
table[7].append("%s" % typeclass_path[-1])
|
||||
table[8].append(script.desc)
|
||||
|
||||
ftable = utils.format_table(table)
|
||||
string = ""
|
||||
for irow, row in enumerate(ftable):
|
||||
if irow == 0:
|
||||
srow = "\n" + "".join(row)
|
||||
srow = "{w%s{n" % srow.rstrip()
|
||||
else:
|
||||
srow = "\n" + "{w%s{n" % row[0] + "".join(row[1:])
|
||||
string += srow.rstrip()
|
||||
return string.strip()
|
||||
|
||||
|
||||
class CmdScripts(MuxCommand):
|
||||
"""
|
||||
Operate on scripts.
|
||||
|
|
@ -137,54 +245,7 @@ class CmdScripts(MuxCommand):
|
|||
aliases = "@listscripts"
|
||||
locks = "cmd:perm(listscripts) or perm(Wizards)"
|
||||
help_category = "System"
|
||||
|
||||
def format_script_list(self, scripts):
|
||||
"Takes a list of scripts and formats the output."
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
|
||||
table = [["id"], ["obj"], ["key"], ["intval"], ["next"], ["rept"], ["db"], ["typeclass"], ["desc"]]
|
||||
for script in scripts:
|
||||
|
||||
table[0].append(script.id)
|
||||
if not hasattr(script, 'obj') or not script.obj:
|
||||
table[1].append("<Global>")
|
||||
else:
|
||||
table[1].append(script.obj.key)
|
||||
table[2].append(script.key)
|
||||
if not hasattr(script, 'interval') or script.interval < 0:
|
||||
table[3].append("--")
|
||||
else:
|
||||
table[3].append("%ss" % script.interval)
|
||||
next = script.time_until_next_repeat()
|
||||
if not next:
|
||||
table[4].append("--")
|
||||
else:
|
||||
table[4].append("%ss" % next)
|
||||
|
||||
if not hasattr(script, 'repeats') or not script.repeats:
|
||||
table[5].append("--")
|
||||
else:
|
||||
table[5].append("%s" % script.repeats)
|
||||
if script.persistent:
|
||||
table[6].append("*")
|
||||
else:
|
||||
table[6].append("-")
|
||||
typeclass_path = script.typeclass_path.rsplit('.', 1)
|
||||
table[7].append("%s" % typeclass_path[-1])
|
||||
table[8].append(script.desc)
|
||||
|
||||
ftable = utils.format_table(table)
|
||||
string = ""
|
||||
for irow, row in enumerate(ftable):
|
||||
if irow == 0:
|
||||
srow = "\n" + "".join(row)
|
||||
srow = "{w%s{n" % srow.rstrip()
|
||||
else:
|
||||
srow = "\n" + "{w%s{n" % row[0] + "".join(row[1:])
|
||||
string += srow.rstrip()
|
||||
return string.strip()
|
||||
|
||||
|
||||
def func(self):
|
||||
"implement method"
|
||||
|
||||
|
|
@ -232,7 +293,7 @@ class CmdScripts(MuxCommand):
|
|||
else:
|
||||
# multiple matches.
|
||||
string = "Multiple script matches. Please refine your search:\n"
|
||||
string += self.format_script_list(scripts)
|
||||
string += format_script_list(scripts)
|
||||
elif self.switches and self.switches[0] in ("validate", "valid", "val"):
|
||||
# run validation on all found scripts
|
||||
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts)
|
||||
|
|
@ -240,7 +301,7 @@ class CmdScripts(MuxCommand):
|
|||
string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped)
|
||||
else:
|
||||
# No stopping or validation. We just want to view things.
|
||||
string = self.format_script_list(scripts)
|
||||
string = format_script_list(scripts)
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -411,34 +472,6 @@ class CmdService(MuxCommand):
|
|||
caller.msg("Starting service '%s'." % self.args)
|
||||
service.startService()
|
||||
|
||||
class CmdShutdown(MuxCommand):
|
||||
|
||||
"""
|
||||
@shutdown
|
||||
|
||||
Usage:
|
||||
@shutdown [announcement]
|
||||
|
||||
Shut the game server down gracefully.
|
||||
"""
|
||||
key = "@shutdown"
|
||||
locks = "cmd:perm(shutdown) or perm(Immortals)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"Define function"
|
||||
try:
|
||||
session = self.caller.sessions[0]
|
||||
except Exception:
|
||||
return
|
||||
self.caller.msg('Shutting down server ...')
|
||||
announcement = "\nServer is being SHUT DOWN!\n"
|
||||
if self.args:
|
||||
announcement += "%s\n" % self.args
|
||||
logger.log_infomsg('Server shutdown by %s.' % self.caller.name)
|
||||
SESSIONS.announce_all(announcement)
|
||||
SESSIONS.server.shutdown()
|
||||
|
||||
class CmdVersion(MuxCommand):
|
||||
"""
|
||||
@version - game version
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ except ImportError:
|
|||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
from src.utils import create, ansi
|
||||
from src.server import session, sessionhandler
|
||||
from src.server import serversession, sessionhandler
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.server.models import ServerConfig
|
||||
from src.comms.models import Channel, Msg, PlayerChannelConnection, ExternalChannelConnection
|
||||
|
|
@ -46,15 +46,37 @@ def cleanup():
|
|||
ExternalChannelConnection.objects.all().delete()
|
||||
ServerConfig.objects.all().delete()
|
||||
|
||||
class FakeSession(session.Session):
|
||||
class FakeSessionHandler(sessionhandler.ServerSessionHandler):
|
||||
"""
|
||||
Fake sessionhandler, without an amp connection
|
||||
"""
|
||||
def portal_shutdown(self):
|
||||
pass
|
||||
def disconnect(self, session, reason=""):
|
||||
pass
|
||||
def login(self, session):
|
||||
pass
|
||||
def session_sync(self):
|
||||
pass
|
||||
def data_out(self, session, string="", data=""):
|
||||
return string
|
||||
|
||||
SESSIONS = FakeSessionHandler()
|
||||
|
||||
class FakeSession(serversession.ServerSession):
|
||||
"""
|
||||
A fake session that
|
||||
implements dummy versions of the real thing; this is needed to
|
||||
mimic a logged-in player.
|
||||
"""
|
||||
protocol_key = "TestProtocol"
|
||||
sessdict = {'protocol_key':'telnet', 'address':('0.0.0.0','5000'), 'sessid':2, 'uid':2, 'uname':None,
|
||||
'logged_in':False, 'cid':None, 'ndb':{}, 'encoding':'utf-8',
|
||||
'conn_time':time.time(), 'cmd_last':time.time(), 'cmd_last_visible':time.time(), 'cmd_total':1}
|
||||
|
||||
def connectionMade(self):
|
||||
self.session_connect('0,0,0,0')
|
||||
self.load_sync_data(self.sessdict)
|
||||
self.sessionhandler = SESSIONS
|
||||
def disconnectClient(self):
|
||||
pass
|
||||
def lineReceived(self, raw_string):
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ class CmdQuit(MuxCommand):
|
|||
"Simply close the connection."
|
||||
session = self.caller
|
||||
session.msg("Good bye! Disconnecting ...")
|
||||
session.at_disconnect()
|
||||
session.session_disconnect()
|
||||
|
||||
class CmdUnconnectedLook(MuxCommand):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -387,8 +387,8 @@ def start_scripts(validate=False):
|
|||
"""
|
||||
|
||||
if validate:
|
||||
from src.utils import reloads
|
||||
reloads.reload_scripts()
|
||||
from src.scripts.models import ScriptDB
|
||||
ScriptDB.objects.validate()
|
||||
return
|
||||
if not search.scripts("IMC2_Send_IsAlive"):
|
||||
create.create_script(Send_IsAlive)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ be able to delete connections on the fly).
|
|||
|
||||
from django.db import models
|
||||
from src.utils.idmapper.models import SharedMemoryModel
|
||||
#from src.server.sessionhandler import SESSIONS
|
||||
from src.comms import managers
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.utils import logger
|
||||
|
|
|
|||
|
|
@ -724,7 +724,7 @@ class ObjectDB(TypedObject):
|
|||
Destroys all of the exits and any exits pointing to this
|
||||
object as a destination.
|
||||
"""
|
||||
for out_exit in self.exits:
|
||||
for out_exit in [exi for exi in ObjectDB.objects.get_contents(self) if exi.db_destination]:
|
||||
out_exit.delete()
|
||||
for in_exit in ObjectDB.objects.filter(db_destination=self):
|
||||
in_exit.delete()
|
||||
|
|
@ -779,6 +779,7 @@ class ObjectDB(TypedObject):
|
|||
new_key = "%s_copy" % self.key
|
||||
return ObjectDB.objects.copy_object(self, new_key=new_key)
|
||||
|
||||
delete_iter = 0
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this object.
|
||||
|
|
@ -786,13 +787,20 @@ class ObjectDB(TypedObject):
|
|||
objects to their respective home locations, as well as clean
|
||||
up all exits to/from the object.
|
||||
"""
|
||||
if self.delete_iter > 0:
|
||||
# make sure to only call delete once on this object
|
||||
# (avoid recursive loops)
|
||||
return False
|
||||
|
||||
if not self.at_object_delete():
|
||||
# this is an extra pre-check
|
||||
# run before deletion mechanism
|
||||
# is kicked into gear.
|
||||
self.delete_iter == 0
|
||||
return False
|
||||
|
||||
self.delete_iter += 1
|
||||
|
||||
# See if we need to kick the player off.
|
||||
|
||||
for session in self.sessions:
|
||||
|
|
|
|||
|
|
@ -75,6 +75,17 @@ class Object(TypeClass):
|
|||
"""
|
||||
pass
|
||||
|
||||
def at_init(self):
|
||||
"""
|
||||
This is always called whenever this
|
||||
object initiated -- both when the object
|
||||
is first created as well as after each restart.
|
||||
It is also called after each server reload, so
|
||||
if something should survive a warm-reboot (rebooting
|
||||
the server without the players logging out), put it here.
|
||||
"""
|
||||
pass
|
||||
|
||||
def basetype_posthook_setup(self):
|
||||
"""
|
||||
Called once, after basetype_setup and at_object_creation. This should generally not be overloaded unless
|
||||
|
|
@ -87,9 +98,26 @@ class Object(TypeClass):
|
|||
def at_cache(self):
|
||||
"""
|
||||
Called whenever this object is cached to the idmapper backend.
|
||||
This is the place to put eventual reloads of non-persistent attributes
|
||||
you saved in the at_server_reload() below.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_reload(self):
|
||||
"""
|
||||
This hook is called whenever the server is shutting down for restart/reboot.
|
||||
If you want to, for example, save non-persistent properties across a restart,
|
||||
this is the place to do it.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_shutdown(self):
|
||||
"""
|
||||
This hook is called whenever the server is shutting down fully (i.e. not for
|
||||
a restart).
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_cmdset_get(self):
|
||||
"""
|
||||
Called just before cmdsets on this object are requested by the
|
||||
|
|
@ -384,9 +412,9 @@ class Character(Object):
|
|||
the script is permanently stored to this object (the permanent
|
||||
keyword creates a script to do this), we should never need to
|
||||
do this again for as long as this object exists.
|
||||
pass
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def at_after_move(self, source_location):
|
||||
"Default is to look around after a move."
|
||||
self.execute_cmd('look')
|
||||
|
|
@ -512,7 +540,7 @@ class Exit(Object):
|
|||
self.locks.add("traverse:all()") # who can pass through exit by default
|
||||
self.locks.add("get:false()") # noone can pick up the exit
|
||||
|
||||
# an exit should have a destination (this is replaced at creation time)
|
||||
# an exit should have a destination (this is replaced at creation time)
|
||||
if self.dbobj.location:
|
||||
self.destination = self.dbobj.location
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,14 @@ class PlayerManager(TypedObjectManager):
|
|||
"""
|
||||
return User.objects.filter(email__iexact=uemail)
|
||||
|
||||
@returns_typeclass
|
||||
@returns_player
|
||||
def get_player_from_uid(self, uid):
|
||||
"""
|
||||
Returns a player object based on User id.
|
||||
"""
|
||||
return User.objects.get(id=uid)
|
||||
|
||||
@returns_typeclass
|
||||
def get_player_from_name(self, uname):
|
||||
"Get player object based on name"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,17 @@ class Player(TypeClass):
|
|||
pass
|
||||
|
||||
|
||||
def at_init(self):
|
||||
"""
|
||||
This is always called whenever this
|
||||
object initiated -- both when the object
|
||||
is first created as well as after each restart.
|
||||
It is also called after each server reload, so
|
||||
if something should survive a warm-reboot (rebooting
|
||||
the server without the players logging out), put it here.
|
||||
"""
|
||||
pass
|
||||
|
||||
# Note that the hooks below also exist
|
||||
# in the character object's typeclass. You
|
||||
# can often ignore these and rely on the
|
||||
|
|
@ -101,3 +112,18 @@ class Player(TypeClass):
|
|||
itself as a sender in the msg() call.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_reload(self):
|
||||
"""
|
||||
This hook is called whenever the server is shutting down for restart/reboot.
|
||||
If you want to, for example, save non-persistent properties across a restart,
|
||||
this is the place to do it.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_shutdown(self):
|
||||
"""
|
||||
This hook is called whenever the server is shutting down fully (i.e. not for
|
||||
a restart).
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -88,10 +88,12 @@ class ScriptManager(TypedObjectManager):
|
|||
key = validate only scripts with a particular key
|
||||
dbref = validate only the single script with this particular id.
|
||||
|
||||
init_mode - This is used during server upstart. It causes non-persistent
|
||||
scripts to be removed and persistent scripts to be
|
||||
force-restarted.
|
||||
|
||||
init_mode - This is used during server upstart and can have
|
||||
three values:
|
||||
False (no init mode). Called during run.
|
||||
"reset" - server reboot. Kill non-persistent scripts
|
||||
"reload" - server reload. Keep non-persistent scripts.
|
||||
|
||||
This method also makes sure start any scripts it validates,
|
||||
this should be harmless, since already-active scripts
|
||||
have the property 'is_running' set and will be skipped.
|
||||
|
|
@ -100,6 +102,7 @@ class ScriptManager(TypedObjectManager):
|
|||
# we store a variable that tracks if we are calling a
|
||||
# validation from within another validation (avoids
|
||||
# loops).
|
||||
|
||||
global VALIDATE_ITERATION
|
||||
if VALIDATE_ITERATION > 0:
|
||||
# we are in a nested validation. Exit.
|
||||
|
|
@ -113,14 +116,15 @@ class ScriptManager(TypedObjectManager):
|
|||
nr_stopped = 0
|
||||
|
||||
if init_mode:
|
||||
# special mode when server starts or object logs in.
|
||||
# This deletes all non-persistent scripts from database
|
||||
nr_stopped += self.remove_non_persistent(obj=obj)
|
||||
if init_mode == 'reset':
|
||||
# special mode when server starts or object logs in.
|
||||
# This deletes all non-persistent scripts from database
|
||||
nr_stopped += self.remove_non_persistent(obj=obj)
|
||||
# turn off the activity flag for all remaining scripts
|
||||
scripts = self.get_all_scripts()
|
||||
for script in scripts:
|
||||
script.dbobj.is_active = False
|
||||
|
||||
|
||||
elif not scripts:
|
||||
# normal operation
|
||||
if dbref and self.dbref(dbref):
|
||||
|
|
@ -137,8 +141,8 @@ class ScriptManager(TypedObjectManager):
|
|||
|
||||
#print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts))
|
||||
for script in scripts:
|
||||
if script.is_valid():
|
||||
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script.dbobj), init_mode)
|
||||
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script.dbobj), init_mode)
|
||||
if script.is_valid():
|
||||
nr_started += script.start(force_restart=init_mode)
|
||||
#print "back from start. nr_started=", nr_started
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -265,3 +265,10 @@ class ScriptDB(TypedObject):
|
|||
# By setting is_active=True, we trick the script not to run "again".
|
||||
self.is_active = True
|
||||
return super(ScriptDB, self).at_typeclass_error()
|
||||
|
||||
delete_iter = 0
|
||||
def delete(self):
|
||||
if self.delete_iter > 0:
|
||||
return
|
||||
self.delete_iter += 1
|
||||
super(ScriptDB, self).delete()
|
||||
|
|
|
|||
|
|
@ -35,17 +35,26 @@ class ScriptClass(TypeClass):
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
def _start_task(self):
|
||||
def _start_task(self, start_now=True):
|
||||
"start task runner"
|
||||
#print "_start_task: self.interval:", self.key, self.interval, self.dbobj.db_interval
|
||||
|
||||
self.ndb.twisted_task = LoopingCall(self._step_task)
|
||||
self.ndb.twisted_task.start(self.interval, now=not self.start_delay)
|
||||
self.ndb.time_last_called = int(time())
|
||||
if self.ndb._paused_time:
|
||||
# we had paused the script, restarting
|
||||
#print " start with paused time:", self.key, self.ndb._paused_time
|
||||
self.ndb.twisted_task.start(self.ndb._paused_time, now=False)
|
||||
else:
|
||||
# starting script anew.
|
||||
#print "_start_task: self.interval:", self.key, self.dbobj.interval
|
||||
self.ndb.twisted_task.start(self.dbobj.interval, now=start_now and not self.start_delay)
|
||||
self.ndb.time_last_called = int(time())
|
||||
|
||||
def _stop_task(self):
|
||||
"stop task runner"
|
||||
try:
|
||||
#print "stopping twisted task:", id(self.ndb.twisted_task), self.obj
|
||||
self.ndb.twisted_task.stop()
|
||||
if self.ndb.twisted_task and not self.ndb.twisted_task.running:
|
||||
self.ndb.twisted_task.stop()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
def _step_err_callback(self, e):
|
||||
|
|
@ -73,6 +82,16 @@ class ScriptClass(TypeClass):
|
|||
self.dbobj.db_repeats -= 1
|
||||
self.ndb.time_last_called = int(time())
|
||||
self.save()
|
||||
|
||||
if self.ndb._paused_time:
|
||||
# this means we were running an unpaused script, for the time remaining
|
||||
# after the pause. Now we start a normal-running timer again.
|
||||
#print "switching to normal run:", self.key
|
||||
del self.ndb._paused_time
|
||||
self._stop_task()
|
||||
self._start_task(start_now=False)
|
||||
|
||||
|
||||
def _step_task(self):
|
||||
"step task"
|
||||
try:
|
||||
|
|
@ -92,7 +111,10 @@ class ScriptClass(TypeClass):
|
|||
check in on their scripts and when they will next be run.
|
||||
"""
|
||||
try:
|
||||
return max(0, (self.ndb.time_last_called + self.dbobj.db_interval) - int(time()))
|
||||
if self.ndb._paused_time:
|
||||
return max(0, (self.ndb.time_last_called + self.ndb._paused_time) - int(time()))
|
||||
else:
|
||||
return max(0, (self.ndb.time_last_called + self.dbobj.db_interval) - int(time()))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
|
@ -122,7 +144,12 @@ class ScriptClass(TypeClass):
|
|||
# this means the object is not initialized.
|
||||
self.dbobj.is_active = False
|
||||
return 0
|
||||
# try to start the script
|
||||
|
||||
# try to restart a paused script
|
||||
if self.unpause():
|
||||
return 1
|
||||
|
||||
# try to start the script from scratch
|
||||
try:
|
||||
self.dbobj.is_active = True
|
||||
self.at_start()
|
||||
|
|
@ -162,6 +189,37 @@ class ScriptClass(TypeClass):
|
|||
return 0
|
||||
return 1
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
This stops a running script and stores its active state.
|
||||
"""
|
||||
#print "pausing", self.key, self.time_until_next_repeat()
|
||||
dt = self.time_until_next_repeat()
|
||||
if dt == None:
|
||||
return
|
||||
self.db._paused_time = dt
|
||||
self._stop_task()
|
||||
|
||||
def unpause(self):
|
||||
"""
|
||||
Restart a paused script. This WILL call at_start().
|
||||
"""
|
||||
#print "unpausing", self.key, self.db._paused_time
|
||||
dt = self.db._paused_time
|
||||
if dt == None:
|
||||
return False
|
||||
try:
|
||||
self.dbobj.is_active = True
|
||||
self.at_start()
|
||||
self.ndb._paused_time = dt
|
||||
self._start_task(start_now=False)
|
||||
del self.db._paused_time
|
||||
except Exception, e:
|
||||
logger.log_trace()
|
||||
self.dbobj.is_active = False
|
||||
return False
|
||||
return True
|
||||
|
||||
# hooks
|
||||
def at_script_creation(self):
|
||||
"placeholder"
|
||||
|
|
@ -178,154 +236,7 @@ class ScriptClass(TypeClass):
|
|||
def at_repeat(self):
|
||||
"placeholder"
|
||||
pass
|
||||
|
||||
|
||||
# class ScriptClass(TypeClass):
|
||||
# """
|
||||
# Base class for all Scripts.
|
||||
# """
|
||||
|
||||
# # private methods for handling timers.
|
||||
|
||||
# def __eq__(self, other):
|
||||
# """
|
||||
# This has to be located at this level, having it in the
|
||||
# parent doesn't work.
|
||||
# """
|
||||
# if other:
|
||||
# return other.id == self.id
|
||||
# return False
|
||||
|
||||
# def _start_task(self):
|
||||
# "start the task runner."
|
||||
# print "self_interval:", self.interval
|
||||
# if self.interval > 0:
|
||||
# #print "Starting task runner"
|
||||
# start_now = not self.start_delay
|
||||
# self.ndb.twisted_task = task.LoopingCall(self._step_task)
|
||||
# self.ndb.twisted_task.start(self.interval, now=start_now)
|
||||
# self.ndb.time_last_called = int(time())
|
||||
# #self.save()
|
||||
# def _stop_task(self):
|
||||
# "stop the task runner"
|
||||
# if hasattr(self.ndb, "twisted_task"):
|
||||
# self.ndb.twisted_task.stop()
|
||||
# def _step_task(self):
|
||||
# "perform one repeat step of the script"
|
||||
# #print "Stepping task runner (obj %s)" % id(self)
|
||||
# #print "Has dbobj: %s" % hasattr(self, 'dbobj')
|
||||
# if not self.is_valid():
|
||||
# #the script is not valid anymore. Abort.
|
||||
# self.stop()
|
||||
# return
|
||||
# try:
|
||||
# self.at_repeat()
|
||||
# if self.repeats:
|
||||
# if self.repeats <= 1:
|
||||
# self.stop()
|
||||
# return
|
||||
# else:
|
||||
# self.repeats -= 1
|
||||
# self.ndb.time_last_called = int(time())
|
||||
# self.save()
|
||||
# except Exception:
|
||||
# logger.log_trace()
|
||||
# self._stop_task()
|
||||
|
||||
# def time_until_next_repeat(self):
|
||||
# """
|
||||
# Returns the time in seconds until the script will be
|
||||
# run again. If this is not a stepping script, returns None.
|
||||
# This is not used in any way by the script's stepping
|
||||
# system; it's only here for the user to be able to
|
||||
# check in on their scripts and when they will next be run.
|
||||
# """
|
||||
# if self.interval and hasattr(self.ndb, 'time_last_called'):
|
||||
# return max(0, (self.ndb.time_last_called + self.interval) - int(time()))
|
||||
# else:
|
||||
# return None
|
||||
|
||||
# def start(self, force_restart=False):
|
||||
# """
|
||||
# Called every time the script is started (for
|
||||
# persistent scripts, this is usually once every server start)
|
||||
|
||||
# force_restart - if True, will always restart the script, regardless
|
||||
# of if it has started before.
|
||||
# """
|
||||
# #print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj),
|
||||
# # self.is_active, force_restart)
|
||||
# if force_restart:
|
||||
# self.is_active = False
|
||||
|
||||
# should_start = True
|
||||
# if self.obj:
|
||||
# try:
|
||||
# #print "checking cmdset ... for obj", self.obj
|
||||
# dummy = object.__getattribute__(self.obj, 'cmdset')
|
||||
# #print "... checked cmdset"
|
||||
# except AttributeError:
|
||||
# #print "self.obj.cmdset not found. Setting is_active=False."
|
||||
# self.is_active = False
|
||||
# should_start = False
|
||||
# if self.is_active and not force_restart:
|
||||
# should_start = False
|
||||
|
||||
# if should_start:
|
||||
# #print "... starting."
|
||||
# try:
|
||||
# self.is_active = True
|
||||
# self.at_start()
|
||||
# self._start_task()
|
||||
# return 1
|
||||
# except Exception:
|
||||
# #print ".. error when starting"
|
||||
# logger.log_trace()
|
||||
# self.is_active = False
|
||||
# return 0
|
||||
# else:
|
||||
# # avoid starting over.
|
||||
# #print "... Start cancelled (invalid start or already running)."
|
||||
# return 0 # this is used by validate() for counting started scripts
|
||||
|
||||
# def stop(self, kill=False):
|
||||
# """
|
||||
# Called to stop the script from running.
|
||||
# This also deletes the script.
|
||||
|
||||
# kill - don't call finishing hooks.
|
||||
# """
|
||||
# #print "stopping script %s" % self.key
|
||||
# if not kill:
|
||||
# try:
|
||||
# self.at_stop()
|
||||
# except Exception:
|
||||
# logger.log_trace()
|
||||
# if self.interval:
|
||||
# try:
|
||||
# self._stop_task()
|
||||
# except Exception:
|
||||
# pass
|
||||
# self.is_running = False
|
||||
# try:
|
||||
# self.delete()
|
||||
# except AssertionError:
|
||||
# return 0
|
||||
# return 1
|
||||
|
||||
# def is_valid(self):
|
||||
# "placeholder"
|
||||
# pass
|
||||
# def at_start(self):
|
||||
# "placeholder."
|
||||
# pass
|
||||
# def at_stop(self):
|
||||
# "placeholder"
|
||||
# pass
|
||||
# def at_repeat(self):
|
||||
# "placeholder"
|
||||
# pass
|
||||
|
||||
|
||||
#
|
||||
# Base Script - inherit from this
|
||||
|
|
@ -359,7 +270,8 @@ class Script(ScriptClass):
|
|||
def at_start(self):
|
||||
"""
|
||||
Called whenever the script is started, which for persistent
|
||||
scripts is at least once every server start.
|
||||
scripts is at least once every server start. It will also be called
|
||||
when starting again after a pause (such as after a server reload)
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
@ -377,6 +289,20 @@ class Script(ScriptClass):
|
|||
"""
|
||||
pass
|
||||
|
||||
def at_server_reload(self):
|
||||
"""
|
||||
This hook is called whenever the server is shutting down for restart/reboot.
|
||||
If you want to, for example, save non-persistent properties across a restart,
|
||||
this is the place to do it.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_shutdown(self):
|
||||
"""
|
||||
This hook is called whenever the server is shutting down fully (i.e. not for
|
||||
a restart).
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
394
src/server/amp.py
Normal file
394
src/server/amp.py
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
"""
|
||||
Contains the protocols, commands, and client factory needed for the server
|
||||
to service the MUD portal proxy.
|
||||
|
||||
The separation works like this:
|
||||
|
||||
Portal - (AMP client) handles protocols. It contains a list of connected sessions in a
|
||||
dictionary for identifying the respective player connected. If it looses the AMP connection
|
||||
it will automatically try to reconnect.
|
||||
|
||||
Server - (AMP server) Handles all mud operations. The server holds its own list
|
||||
of sessions tied to player objects. This is synced against the portal at startup
|
||||
and when a session connects/disconnects
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
from twisted.protocols import amp
|
||||
from twisted.internet import protocol, defer, reactor
|
||||
from django.conf import settings
|
||||
from src.utils import utils
|
||||
from src.server.models import ServerConfig
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.server.serversession import ServerSession
|
||||
|
||||
PORTAL_RESTART = os.path.join(settings.GAME_DIR, "portal.restart")
|
||||
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server.restart")
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
# Signals
|
||||
|
||||
|
||||
|
||||
|
||||
def get_restart_mode(restart_file):
|
||||
"""
|
||||
Parse the server/portal restart status
|
||||
"""
|
||||
if os.path.exists(restart_file):
|
||||
flag = open(restart_file, 'r').read()
|
||||
return flag == "True"
|
||||
return False
|
||||
|
||||
class AmpServerFactory(protocol.ServerFactory):
|
||||
"""
|
||||
This factory creates new AMPProtocol protocol instances to use for accepting
|
||||
connections from TCPServer.
|
||||
"""
|
||||
def __init__(self, server):
|
||||
"""
|
||||
server: The Evennia server service instance
|
||||
protocol: The protocol the factory creates instances of.
|
||||
"""
|
||||
self.server = server
|
||||
self.protocol = AMPProtocol
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Start a new connection, and store it on the service object
|
||||
"""
|
||||
#print "Evennia Server connected to Portal at %s." % addr
|
||||
self.server.amp_protocol = AMPProtocol()
|
||||
self.server.amp_protocol.factory = self
|
||||
return self.server.amp_protocol
|
||||
|
||||
|
||||
|
||||
class AmpClientFactory(protocol.ReconnectingClientFactory):
|
||||
"""
|
||||
This factory creates new AMPProtocol protocol instances to use to connect
|
||||
to the MUD server. It also maintains the portal attribute
|
||||
on the ProxyService instance, which is used for piping input
|
||||
from Telnet to the MUD server.
|
||||
"""
|
||||
# Initial reconnect delay in seconds.
|
||||
initialDelay = 1
|
||||
#factor = 1.5
|
||||
maxDelay = 1
|
||||
|
||||
def __init__(self, portal):
|
||||
self.portal = portal
|
||||
self.protocol = AMPProtocol
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
"""
|
||||
Called when starting to try to connect to the MUD server.
|
||||
"""
|
||||
pass
|
||||
#print 'AMP started to connect:', connector
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Creates an AMPProtocol instance when connecting to the server.
|
||||
"""
|
||||
#print "Portal connected to Evennia server at %s." % addr
|
||||
self.resetDelay()
|
||||
self.portal.amp_protocol = AMPProtocol()
|
||||
self.portal.amp_protocol.factory = self
|
||||
return self.portal.amp_protocol
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
"""
|
||||
Called when the AMP connection to the MUD server is lost.
|
||||
"""
|
||||
if not get_restart_mode(SERVER_RESTART):
|
||||
self.portal.sessions.announce_all(_(" Portal lost connection to Server."))
|
||||
protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
"""
|
||||
Called when an AMP connection attempt to the MUD server fails.
|
||||
"""
|
||||
self.portal.sessions.announce_all(" ...")
|
||||
protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
|
||||
|
||||
|
||||
class MsgPortal2Server(amp.Command):
|
||||
"""
|
||||
Message portal -> server
|
||||
"""
|
||||
arguments = [('sessid', amp.Integer()),
|
||||
('msg', amp.String()),
|
||||
('data', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = []
|
||||
|
||||
class MsgServer2Portal(amp.Command):
|
||||
"""
|
||||
Message server -> portal
|
||||
"""
|
||||
arguments = [('sessid', amp.Integer()),
|
||||
('msg', amp.String()),
|
||||
('data', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = []
|
||||
|
||||
class ServerAdmin(amp.Command):
|
||||
"""
|
||||
Portal -> Server
|
||||
|
||||
Sent when the portal needs to perform admin
|
||||
operations on the server, such as when a new
|
||||
session connects or resyncs
|
||||
"""
|
||||
arguments = [('sessid', amp.Integer()),
|
||||
('operation', amp.String()),
|
||||
('data', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = []
|
||||
|
||||
class PortalAdmin(amp.Command):
|
||||
"""
|
||||
Server -> Portal
|
||||
|
||||
Sent when the server needs to perform admin
|
||||
operations on the portal.
|
||||
"""
|
||||
arguments = [('sessid', amp.Integer()),
|
||||
('operation', amp.String()),
|
||||
('data', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = []
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Core AMP protocol for communication Server <-> Portal
|
||||
#------------------------------------------------------------
|
||||
|
||||
class AMPProtocol(amp.AMP):
|
||||
"""
|
||||
This is the protocol that the MUD server and the proxy server
|
||||
communicate to each other with. AMP is a bi-directional protocol, so
|
||||
both the proxy and the MUD use the same commands and protocol.
|
||||
|
||||
AMP specifies responder methods here and connect them to amp.Command
|
||||
subclasses that specify the datatypes of the input/output of these methods.
|
||||
"""
|
||||
|
||||
# helper methods
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
This is called when a connection is established
|
||||
between server and portal. It is called on both sides,
|
||||
so we need to make sure to only trigger resync from the
|
||||
server side.
|
||||
"""
|
||||
if hasattr(self.factory, "portal"):
|
||||
sessdata = self.factory.portal.sessions.get_all_sync_data()
|
||||
self.call_remote_ServerAdmin(0,
|
||||
"PSYNC",
|
||||
data=sessdata)
|
||||
if get_restart_mode(SERVER_RESTART):
|
||||
msg = _(" ... Server restarted.")
|
||||
self.factory.portal.sessions.announce_all(msg)
|
||||
|
||||
|
||||
# Error handling
|
||||
|
||||
def errback(self, e, info):
|
||||
"error handler, to avoid dropping connections on server tracebacks."
|
||||
e.trap(Exception)
|
||||
print _("AMP Error for %(info)s: %(e)s") % {'info': info, 'e': e.getErrorMessage()}
|
||||
|
||||
|
||||
# Message definition + helper methods to call/create each message type
|
||||
|
||||
# Portal -> Server Msg
|
||||
|
||||
def amp_msg_portal2server(self, sessid, msg, data):
|
||||
"""
|
||||
Relays message to server. This method is executed on the Server.
|
||||
"""
|
||||
#print "msg portal -> server (server side):", sessid, msg
|
||||
self.factory.server.sessions.data_in(sessid, msg, pickle.loads(utils.to_str(data)))
|
||||
return {}
|
||||
MsgPortal2Server.responder(amp_msg_portal2server)
|
||||
|
||||
def call_remote_MsgPortal2Server(self, sessid, msg, data=""):
|
||||
"""
|
||||
Access method called by the Portal and executed on the Portal.
|
||||
"""
|
||||
#print "msg portal->server (portal side):", sessid, msg
|
||||
self.callRemote(MsgPortal2Server,
|
||||
sessid=sessid,
|
||||
msg=msg,
|
||||
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgPortal2Server")
|
||||
|
||||
# Server -> Portal message
|
||||
|
||||
def amp_msg_server2portal(self, sessid, msg, data):
|
||||
"""
|
||||
Relays message to Portal. This method is executed on the Portal.
|
||||
"""
|
||||
#print "msg server->portal (portal side):", sessid, msg
|
||||
self.factory.portal.sessions.data_out(sessid, msg, pickle.loads(utils.to_str(data)))
|
||||
return {}
|
||||
MsgServer2Portal.responder(amp_msg_server2portal)
|
||||
|
||||
def call_remote_MsgServer2Portal(self, sessid, msg, data=""):
|
||||
"""
|
||||
Access method called by the Server and executed on the Server.
|
||||
"""
|
||||
#print "msg server->portal (server side):", sessid, msg, data
|
||||
self.callRemote(MsgServer2Portal,
|
||||
sessid=sessid,
|
||||
msg=utils.to_str(msg),
|
||||
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgServer2Portal")
|
||||
|
||||
|
||||
# Server administration from the Portal side
|
||||
|
||||
def amp_server_admin(self, sessid, operation, data):
|
||||
"""
|
||||
This allows the portal to perform admin
|
||||
operations on the server. This is executed on the Server.
|
||||
|
||||
"""
|
||||
data = pickle.loads(utils.to_str(data))
|
||||
|
||||
#print "serveradmin (server side):", sessid, operation, data
|
||||
|
||||
if operation == 'PCONN': #portal_session_connect
|
||||
# create a new, session and sync it
|
||||
sess = ServerSession()
|
||||
sess.sessionhandler = self.factory.server.sessions
|
||||
sess.load_sync_data(data)
|
||||
if sess.logged_in and sess.uid:
|
||||
# this can happen in the case of auto-authenticating protocols like SSH
|
||||
|
||||
sess.player = PlayerDB.objects.get_player_from_uid(sess.uid)
|
||||
sess.at_sync() # this runs initialization without acr
|
||||
|
||||
self.factory.server.sessions.portal_connect(sessid, sess)
|
||||
|
||||
elif operation == 'PDISCONN': #'portal_session_disconnect'
|
||||
# session closed from portal side
|
||||
self.factory.server.sessions.portal_disconnect(sessid)
|
||||
|
||||
elif operation == 'PSYNC': #'portal_session_sync'
|
||||
# force a resync of sessions when portal reconnects to server (e.g. after a server reboot)
|
||||
# the data kwarg contains a dict {sessid: {arg1:val1,...}} representing the attributes
|
||||
# to sync for each session.
|
||||
sesslist = []
|
||||
server_sessionhandler = self.factory.server.sessions
|
||||
for sessid, sessdict in data.items():
|
||||
sess = ServerSession()
|
||||
sess.sessionhandler = server_sessionhandler
|
||||
sess.load_sync_data(sessdict)
|
||||
if sess.uid:
|
||||
sess.player = PlayerDB.objects.get_player_from_uid(sess.uid)
|
||||
sess.at_sync()
|
||||
sesslist.append(sess)
|
||||
# replace sessions on server
|
||||
server_sessionhandler.portal_session_sync(sesslist)
|
||||
|
||||
# after sync is complete we force-validate all scripts (this starts everthing)
|
||||
init_mode = ServerConfig.objects.conf("server_restart_mode", default=None)
|
||||
ScriptDB.objects.validate(init_mode=init_mode)
|
||||
ServerConfig.objects.conf("server_restart_mode", delete=True)
|
||||
|
||||
else:
|
||||
raise Exception(_("operation %(op)s not recognized.") % {'op': operation})
|
||||
|
||||
|
||||
return {}
|
||||
ServerAdmin.responder(amp_server_admin)
|
||||
|
||||
def call_remote_ServerAdmin(self, sessid, operation="", data=""):
|
||||
"""
|
||||
Access method called by the Portal and Executed on the Portal.
|
||||
"""
|
||||
#print "serveradmin (portal side):", sessid, operation, data
|
||||
data = utils.to_str(pickle.dumps(data))
|
||||
|
||||
self.callRemote(ServerAdmin,
|
||||
sessid=sessid,
|
||||
operation=operation,
|
||||
data=data).addErrback(self.errback, "ServerAdmin")
|
||||
|
||||
# Portal administraton from the Server side
|
||||
|
||||
def amp_portal_admin(self, sessid, operation, data):
|
||||
"""
|
||||
This allows the server to perform admin
|
||||
operations on the portal. This is executed on the Portal.
|
||||
"""
|
||||
data = pickle.loads(utils.to_str(data))
|
||||
|
||||
#print "portaladmin (portal side):", sessid, operation, data
|
||||
if operation == 'SLOGIN': # 'server_session_login'
|
||||
# a session has authenticated; sync it.
|
||||
sess = self.factory.portal.sessions.get_session(sessid)
|
||||
sess.load_sync_data(data)
|
||||
|
||||
elif operation == 'SDISCONN': #'server_session_disconnect'
|
||||
# the server is ordering to disconnect the session
|
||||
self.factory.portal.sessions.server_disconnect(sessid, reason=data)
|
||||
|
||||
elif operation == 'SDISCONNALL': #'server_session_disconnect_all'
|
||||
# server orders all sessions to disconnect
|
||||
self.factory.portal.sessions.server_disconnect_all(reason=data)
|
||||
|
||||
elif operation == 'SSHUTD': #server_shutdown'
|
||||
# the server orders the portal to shut down
|
||||
self.factory.portal.shutdown(restart=False)
|
||||
|
||||
elif operation == 'SSYNC': #'server_session_sync'
|
||||
# server wants to save session data to the portal, maybe because
|
||||
# it's about to shut down. We don't overwrite any sessions,
|
||||
# just update data on them and remove eventual ones that are
|
||||
# out of sync (shouldn't happen normally).
|
||||
|
||||
portal_sessionhandler = self.factory.portal.sessions.sessions
|
||||
|
||||
to_save = [sessid for sessid in data if sessid in portal_sessionhandler.sessions]
|
||||
to_delete = [sessid for sessid in data if sessid not in to_save]
|
||||
|
||||
# save protocols
|
||||
for sessid in to_save:
|
||||
portal_sessionhandler.sessions[sessid].load_sync_data(data[sessid])
|
||||
# disconnect missing protocols
|
||||
for sessid in to_delete:
|
||||
portal_sessionhandler.server_disconnect(sessid)
|
||||
else:
|
||||
raise Exception(_("operation %(op)s not recognized.") % {'op': operation})
|
||||
return {}
|
||||
PortalAdmin.responder(amp_portal_admin)
|
||||
|
||||
def call_remote_PortalAdmin(self, sessid, operation="", data=""):
|
||||
"""
|
||||
Access method called by the server side.
|
||||
"""
|
||||
#print "portaladmin (server side):", sessid, operation, data
|
||||
data = utils.to_str(pickle.dumps(data))
|
||||
|
||||
self.callRemote(PortalAdmin,
|
||||
sessid=sessid,
|
||||
operation=operation,
|
||||
data=data).addErrback(self.errback, "PortalAdmin")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -13,6 +13,9 @@ from src.server.models import ServerConfig
|
|||
from src.help.models import HelpEntry
|
||||
from src.utils import create
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
def create_config_values():
|
||||
"""
|
||||
Creates the initial config values.
|
||||
|
|
@ -31,7 +34,7 @@ def create_objects():
|
|||
Creates the #1 player and Limbo room.
|
||||
"""
|
||||
|
||||
print " Creating objects (Player #1 and Limbo room) ..."
|
||||
print _(" Creating objects (Player #1 and Limbo room) ...")
|
||||
|
||||
# Set the initial User's account object's username on the #1 object.
|
||||
# This object is pure django and only holds name, email and password.
|
||||
|
|
@ -55,7 +58,7 @@ def create_objects():
|
|||
typeclass=character_typeclass,
|
||||
user=god_user)
|
||||
god_character.id = 1
|
||||
god_character.db.desc = 'This is User #1.'
|
||||
god_character.db.desc = _('This is User #1.')
|
||||
god_character.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all();puppet:false()")
|
||||
|
||||
god_character.save()
|
||||
|
|
@ -63,12 +66,13 @@ def create_objects():
|
|||
# Limbo is the initial starting room.
|
||||
|
||||
room_typeclass = settings.BASE_ROOM_TYPECLASS
|
||||
limbo_obj = create.create_object(room_typeclass, 'Limbo')
|
||||
limbo_obj = create.create_object(room_typeclass, _('Limbo'))
|
||||
limbo_obj.id = 2
|
||||
string = "Welcome to your new %chEvennia%cn-based game."
|
||||
string = " Welcome to your new {wEvennia{n-based game."
|
||||
string += " From here you are ready to begin development."
|
||||
string += " If you should need help or would like to participate"
|
||||
string += " in community discussions, visit http://evennia.com."
|
||||
string = _(string)
|
||||
limbo_obj.db.desc = string
|
||||
limbo_obj.save()
|
||||
|
||||
|
|
@ -80,7 +84,7 @@ def create_channels():
|
|||
"""
|
||||
Creates some sensible default channels.
|
||||
"""
|
||||
print " Creating default channels ..."
|
||||
print _(" Creating default channels ...")
|
||||
|
||||
# public channel
|
||||
key, aliases, desc, locks = settings.CHANNEL_PUBLIC
|
||||
|
|
@ -103,12 +107,12 @@ def import_MUX_help_files():
|
|||
"""
|
||||
Imports the MUX help files.
|
||||
"""
|
||||
print " Importing MUX help database (devel reference only) ..."
|
||||
print _(" Importing MUX help database (devel reference only) ...")
|
||||
management.call_command('loaddata', '../src/help/mux_help_db.json', verbosity=0)
|
||||
# categorize the MUX help files into its own category.
|
||||
default_category = "MUX"
|
||||
print " Moving imported help db to help category '%s'." \
|
||||
% default_category
|
||||
print _(" Moving imported help db to help category '%(default)s'." \
|
||||
% {'default': default_category})
|
||||
HelpEntry.objects.all_to_category(default_category)
|
||||
|
||||
def create_system_scripts():
|
||||
|
|
@ -118,7 +122,7 @@ def create_system_scripts():
|
|||
"""
|
||||
from src.scripts import scripts
|
||||
|
||||
print " Creating and starting global scripts ..."
|
||||
print _(" Creating and starting global scripts ...")
|
||||
|
||||
# check so that all sessions are alive.
|
||||
script1 = create.create_script(scripts.CheckSessions)
|
||||
|
|
@ -127,7 +131,7 @@ def create_system_scripts():
|
|||
# update the channel handler to make sure it's in sync
|
||||
script3 = create.create_script(scripts.ValidateChannelHandler)
|
||||
if not script1 or not script2 or not script3:
|
||||
print " Error creating system scripts."
|
||||
print _(" Error creating system scripts.")
|
||||
|
||||
def start_game_time():
|
||||
"""
|
||||
|
|
@ -136,7 +140,7 @@ def start_game_time():
|
|||
the total run time of the server as well as its current uptime
|
||||
(the uptime can also be found directly from the server though).
|
||||
"""
|
||||
print " Starting in-game time ..."
|
||||
print _(" Starting in-game time ...")
|
||||
from src.utils import gametime
|
||||
gametime.init_gametime()
|
||||
|
||||
|
|
@ -155,17 +159,17 @@ def create_admin_media_links():
|
|||
dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
|
||||
apath = os.path.join(settings.ADMIN_MEDIA_ROOT)
|
||||
if os.path.isdir(apath):
|
||||
print " ADMIN_MEDIA_ROOT already exists. Ignored."
|
||||
print _(" ADMIN_MEDIA_ROOT already exists. Ignored.")
|
||||
return
|
||||
if os.name == 'nt':
|
||||
print " Admin-media files copied to ADMIN_MEDIA_ROOT (Windows mode)."
|
||||
print _(" Admin-media files copied to ADMIN_MEDIA_ROOT (Windows mode).")
|
||||
os.mkdir(apath)
|
||||
os.system('xcopy "%s" "%s" /e /q /c' % (dpath, apath))
|
||||
if os.name == 'posix':
|
||||
os.symlink(dpath, apath)
|
||||
print " Admin-media symlinked to ADMIN_MEDIA_ROOT."
|
||||
print _(" Admin-media symlinked to ADMIN_MEDIA_ROOT.")
|
||||
else:
|
||||
print " Admin-media files should be copied manually to ADMIN_MEDIA_ROOT."
|
||||
print _(" Admin-media files should be copied manually to ADMIN_MEDIA_ROOT.")
|
||||
|
||||
def handle_setup(last_step):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ from src.utils.idmapper.models import SharedMemoryModel
|
|||
from src.utils import logger, utils
|
||||
from src.server.manager import ServerConfigManager
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# ServerConfig
|
||||
|
|
@ -82,7 +85,7 @@ class ServerConfig(SharedMemoryModel):
|
|||
"Setter. Allows for self.value = value"
|
||||
if utils.has_parent('django.db.models.base.Model', value):
|
||||
# we have to protect against storing db objects.
|
||||
logger.log_errmsg("ServerConfig cannot store db objects! (%s)" % value)
|
||||
logger.log_errmsg(_("ServerConfig cannot store db objects! (%s)" % value))
|
||||
return
|
||||
self.db_value = pickle.dumps(value)
|
||||
self.save()
|
||||
|
|
|
|||
304
src/server/portal.py
Normal file
304
src/server/portal.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
"""
|
||||
This module implements the main Evennia server process, the core of
|
||||
the game engine.
|
||||
|
||||
This module should be started with the 'twistd' executable since it
|
||||
sets up all the networking features. (this is done automatically
|
||||
by game/evennia.py).
|
||||
|
||||
"""
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
if os.name == 'nt':
|
||||
# For Windows batchfile we need an extra path insertion here.
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from twisted.application import internet, service
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.web import server, static
|
||||
from django.conf import settings
|
||||
from src.utils.utils import get_evennia_version
|
||||
from src.server.sessionhandler import PORTAL_SESSIONS
|
||||
|
||||
if os.name == 'nt':
|
||||
# For Windows we need to handle pid files manually.
|
||||
PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, 'portal.pid')
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Evennia Portal settings
|
||||
#------------------------------------------------------------
|
||||
|
||||
VERSION = get_evennia_version()
|
||||
|
||||
SERVERNAME = settings.SERVERNAME
|
||||
|
||||
PORTAL_RESTART = os.path.join(settings.GAME_DIR, 'portal.restart')
|
||||
|
||||
TELNET_PORTS = settings.TELNET_PORTS
|
||||
SSL_PORTS = settings.SSL_PORTS
|
||||
SSH_PORTS = settings.SSH_PORTS
|
||||
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
|
||||
|
||||
TELNET_INTERFACES = settings.TELNET_INTERFACES
|
||||
SSL_INTERFACES = settings.SSL_INTERFACES
|
||||
SSH_INTERFACES = settings.SSH_INTERFACES
|
||||
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
|
||||
|
||||
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
|
||||
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
|
||||
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
|
||||
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
|
||||
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||
IMC2_ENABLED = settings.IMC2_ENABLED
|
||||
IRC_ENABLED = settings.IRC_ENABLED
|
||||
|
||||
AMP_HOST = settings.AMP_HOST
|
||||
AMP_PORT = settings.AMP_PORT
|
||||
AMP_ENABLED = AMP_HOST and AMP_PORT
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Portal Service object
|
||||
#------------------------------------------------------------
|
||||
class Portal(object):
|
||||
|
||||
"""
|
||||
The main Portal server handler. This object sets up the database and
|
||||
tracks and interlinks all the twisted network services that make up
|
||||
Portal.
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
"""
|
||||
Setup the server.
|
||||
|
||||
application - an instantiated Twisted application
|
||||
|
||||
"""
|
||||
sys.path.append('.')
|
||||
|
||||
# create a store of services
|
||||
self.services = service.IServiceCollection(application)
|
||||
self.amp_protocol = None # set by amp factory
|
||||
self.sessions = PORTAL_SESSIONS
|
||||
self.sessions.portal = self
|
||||
|
||||
print '\n' + '-'*50
|
||||
|
||||
# Make info output to the terminal.
|
||||
self.terminal_output()
|
||||
|
||||
print '-'*50
|
||||
|
||||
# set a callback if the server is killed abruptly,
|
||||
# by Ctrl-C, reboot etc.
|
||||
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _abrupt=True)
|
||||
|
||||
self.game_running = False
|
||||
|
||||
def terminal_output(self):
|
||||
"""
|
||||
Outputs server startup info to the terminal.
|
||||
"""
|
||||
print _(' %(servername)s Portal (%(version)s) started.') % {'servername': SERVERNAME, 'version': VERSION}
|
||||
if AMP_ENABLED:
|
||||
print " amp (Server): %s" % AMP_PORT
|
||||
if TELNET_ENABLED:
|
||||
ports = ", ".join([str(port) for port in TELNET_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in TELNET_INTERFACES if iface != '0.0.0.0'])
|
||||
print " telnet%s: %s" % (ifaces, ports)
|
||||
if SSH_ENABLED:
|
||||
ports = ", ".join([str(port) for port in SSH_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in SSH_INTERFACES if iface != '0.0.0.0'])
|
||||
print " ssh%s: %s" % (ifaces, ports)
|
||||
if SSL_ENABLED:
|
||||
ports = ", ".join([str(port) for port in SSL_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in SSL_INTERFACES if iface != '0.0.0.0'])
|
||||
print " ssl%s: %s" % (ifaces, ports)
|
||||
if WEBSERVER_ENABLED:
|
||||
clientstring = ""
|
||||
if WEBCLIENT_ENABLED:
|
||||
clientstring = '/client'
|
||||
ports = ", ".join([str(port) for port in WEBSERVER_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in WEBSERVER_INTERFACES if iface != '0.0.0.0'])
|
||||
print " webserver%s%s: %s" % (clientstring, ifaces, ports)
|
||||
|
||||
def set_restart_mode(self, mode=None):
|
||||
"""
|
||||
This manages the flag file that tells the runner if the server should
|
||||
be restarted or is shutting down. Valid modes are True/False and None.
|
||||
If mode is None, no change will be done to the flag file.
|
||||
"""
|
||||
if mode == None:
|
||||
return
|
||||
f = open(PORTAL_RESTART, 'w')
|
||||
print _("writing mode=%(mode)s to %(portal_restart)s") % {'mode': mode, 'portal_restart': PORTAL_RESTART}
|
||||
f.write(str(mode))
|
||||
f.close()
|
||||
|
||||
def shutdown(self, restart=None, _abrupt=False):
|
||||
"""
|
||||
Shuts down the server from inside it.
|
||||
|
||||
restart - True/False sets the flags so the server will be
|
||||
restarted or not. If None, the current flag setting
|
||||
(set at initialization or previous runs) is used.
|
||||
_abrupt - this is set if server is stopped by a kill command,
|
||||
in which case the reactor is dead anyway.
|
||||
|
||||
Note that restarting (regardless of the setting) will not work
|
||||
if the Portal is currently running in daemon mode. In that
|
||||
case it always needs to be restarted manually.
|
||||
"""
|
||||
self.set_restart_mode(restart)
|
||||
if not _abrupt:
|
||||
reactor.callLater(0, reactor.stop)
|
||||
if os.name == 'nt' and os.path.exists(PORTAL_PIDFILE):
|
||||
# for Windows we need to remove pid files manually
|
||||
os.remove(PORTAL_PIDFILE)
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Start the Portal proxy server and add all active services
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
# twistd requires us to define the variable 'application' so it knows
|
||||
# what to execute from.
|
||||
application = service.Application('Portal')
|
||||
|
||||
# The main Portal server program. This sets up the database
|
||||
# and is where we store all the other services.
|
||||
PORTAL = Portal(application)
|
||||
|
||||
if AMP_ENABLED:
|
||||
|
||||
# The AMP protocol handles the communication between
|
||||
# the portal and the mud server. Only reason to ever deactivate
|
||||
# it would be during testing and debugging.
|
||||
|
||||
from src.server import amp
|
||||
|
||||
factory = amp.AmpClientFactory(PORTAL)
|
||||
amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
|
||||
amp_client.setName('evennia_amp')
|
||||
PORTAL.services.addService(amp_client)
|
||||
|
||||
# We group all the various services under the same twisted app.
|
||||
# These will gradually be started as they are initialized below.
|
||||
|
||||
if TELNET_ENABLED:
|
||||
|
||||
# Start telnet game connections
|
||||
|
||||
from src.server import telnet
|
||||
|
||||
for interface in TELNET_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(TELNET_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in TELNET_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = telnet.TelnetProtocol
|
||||
factory.sessionhandler = PORTAL_SESSIONS
|
||||
telnet_service = internet.TCPServer(port, factory, interface=interface)
|
||||
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
||||
PORTAL.services.addService(telnet_service)
|
||||
|
||||
if SSL_ENABLED:
|
||||
|
||||
# Start SSL game connection (requires PyOpenSSL).
|
||||
|
||||
from src.server import ssl
|
||||
|
||||
for interface in SSL_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(SSL_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSL_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.sessionhandler = PORTAL_SESSIONS
|
||||
factory.protocol = ssl.SSLProtocol
|
||||
ssl_service = internet.SSLServer(port, factory, ssl.getSSLContext(), interface=interface)
|
||||
ssl_service.setName('EvenniaSSL%s' % pstring)
|
||||
PORTAL.services.addService(ssl_service)
|
||||
|
||||
if SSH_ENABLED:
|
||||
|
||||
# Start SSH game connections. Will create a keypair in evennia/game if necessary.
|
||||
|
||||
from src.server import ssh
|
||||
|
||||
for interface in SSH_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(SSH_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSH_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
|
||||
'protocolArgs':(),
|
||||
'sessions':PORTAL_SESSIONS})
|
||||
ssh_service = internet.TCPServer(port, factory, interface=interface)
|
||||
ssh_service.setName('EvenniaSSH%s' % pstring)
|
||||
PORTAL.services.addService(ssh_service)
|
||||
|
||||
if WEBSERVER_ENABLED:
|
||||
|
||||
# Start a django-compatible webserver.
|
||||
|
||||
from twisted.python import threadpool
|
||||
from src.server.webserver import DjangoWebRoot, WSGIWebServer
|
||||
|
||||
# start a thread pool and define the root url (/) as a wsgi resource
|
||||
# recognized by Django
|
||||
threads = threadpool.ThreadPool()
|
||||
web_root = DjangoWebRoot(threads)
|
||||
# point our media resources to url /media
|
||||
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
|
||||
|
||||
if WEBCLIENT_ENABLED:
|
||||
# create ajax client processes at /webclientdata
|
||||
from src.server.webclient import WebClient
|
||||
webclient = WebClient()
|
||||
webclient.sessionhandler = PORTAL_SESSIONS
|
||||
web_root.putChild("webclientdata", webclient)
|
||||
|
||||
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||
|
||||
for interface in WEBSERVER_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(WEBSERVER_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in WEBSERVER_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
# create the webserver
|
||||
webserver = WSGIWebServer(threads, port, web_site, interface=interface)
|
||||
webserver.setName('EvenniaWebServer%s' % pstring)
|
||||
PORTAL.services.addService(webserver)
|
||||
|
||||
if IRC_ENABLED:
|
||||
|
||||
# IRC channel connections
|
||||
|
||||
from src.comms import irc
|
||||
irc.connect_all()
|
||||
|
||||
if IMC2_ENABLED:
|
||||
|
||||
# IMC2 channel connections
|
||||
|
||||
from src.comms import imc2
|
||||
imc2.connect_all()
|
||||
|
||||
if os.name == 'nt':
|
||||
# Windows only: Set PID file manually
|
||||
f = open(os.path.join(settings.GAME_DIR, 'portal.pid'), 'w')
|
||||
f.write(str(os.getpid()))
|
||||
f.close()
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
This module implements the main Evennia server process, the core of
|
||||
the game engine. Don't import this module directly! If you need to
|
||||
access the server processes from code, instead go via the session-
|
||||
handler: src.sessionhandler.SESSIONS.server
|
||||
the game engine.
|
||||
|
||||
This module should be started with the 'twistd' executable since it
|
||||
sets up all the networking features. (this is done automatically
|
||||
|
|
@ -12,6 +10,7 @@ by game/evennia.py).
|
|||
import time
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
if os.name == 'nt':
|
||||
# For Windows batchfile we need an extra path insertion here.
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||||
|
|
@ -22,14 +21,23 @@ from twisted.internet import protocol, reactor, defer
|
|||
from twisted.web import server, static
|
||||
from django.db import connection
|
||||
from django.conf import settings
|
||||
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.server.models import ServerConfig
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.server import initial_setup
|
||||
|
||||
from src.utils.utils import get_evennia_version
|
||||
from src.comms import channelhandler
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
if os.name == 'nt':
|
||||
# For Windows we need to handle pid files manually.
|
||||
SERVER_PIDFILE = os.path.join(settings.GAME_DIR, 'server.pid')
|
||||
|
||||
SERVER_RESTART = os.path.join(settings.GAME_DIR, 'server.restart')
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Evennia Server settings
|
||||
|
|
@ -38,23 +46,10 @@ from src.comms import channelhandler
|
|||
SERVERNAME = settings.SERVERNAME
|
||||
VERSION = get_evennia_version()
|
||||
|
||||
TELNET_PORTS = settings.TELNET_PORTS
|
||||
SSL_PORTS = settings.SSL_PORTS
|
||||
SSH_PORTS = settings.SSH_PORTS
|
||||
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
|
||||
AMP_ENABLED = True
|
||||
AMP_HOST = settings.AMP_HOST
|
||||
AMP_PORT = settings.AMP_PORT
|
||||
|
||||
TELNET_INTERFACES = settings.TELNET_INTERFACES
|
||||
SSL_INTERFACES = settings.SSL_INTERFACES
|
||||
SSH_INTERFACES = settings.SSH_INTERFACES
|
||||
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
|
||||
|
||||
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
|
||||
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
|
||||
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
|
||||
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
|
||||
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||
IMC2_ENABLED = settings.IMC2_ENABLED
|
||||
IRC_ENABLED = settings.IRC_ENABLED
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Evennia Main Server object
|
||||
|
|
@ -75,10 +70,13 @@ class Evennia(object):
|
|||
|
||||
"""
|
||||
sys.path.append('.')
|
||||
|
||||
|
||||
# create a store of services
|
||||
self.services = service.IServiceCollection(application)
|
||||
|
||||
self.amp_protocol = None # set by amp factory
|
||||
self.sessions = SESSIONS
|
||||
self.sessions.server = self
|
||||
|
||||
print '\n' + '-'*50
|
||||
|
||||
# Database-specific startup optimizations.
|
||||
|
|
@ -87,20 +85,11 @@ class Evennia(object):
|
|||
# Run the initial setup if needed
|
||||
self.run_initial_setup()
|
||||
|
||||
# we have to null this here.
|
||||
SESSIONS.session_count(0)
|
||||
# we link ourself to the sessionhandler so other modules don't have to
|
||||
# re-import the server module itself (which would re-initialize it).
|
||||
SESSIONS.server = self
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
# initialize channelhandler
|
||||
channelhandler.CHANNELHANDLER.update()
|
||||
|
||||
# init all global scripts
|
||||
ScriptDB.objects.validate(init_mode=True)
|
||||
|
||||
|
||||
# Make info output to the terminal.
|
||||
self.terminal_output()
|
||||
|
||||
|
|
@ -138,7 +127,7 @@ class Evennia(object):
|
|||
if not last_initial_setup_step:
|
||||
# None is only returned if the config does not exist,
|
||||
# i.e. this is an empty DB that needs populating.
|
||||
print ' Server started for the first time. Setting defaults.'
|
||||
print _(' Server started for the first time. Setting defaults.')
|
||||
initial_setup.handle_setup(0)
|
||||
print '-'*50
|
||||
elif int(last_initial_setup_step) >= 0:
|
||||
|
|
@ -146,51 +135,86 @@ class Evennia(object):
|
|||
# modules and setup will resume from this step, retrying
|
||||
# the last failed module. When all are finished, the step
|
||||
# is set to -1 to show it does not need to be run again.
|
||||
print ' Resuming initial setup from step %s.' % \
|
||||
last_initial_setup_step
|
||||
print _(' Resuming initial setup from step %(last)s.' % \
|
||||
{'last': last_initial_setup_step})
|
||||
initial_setup.handle_setup(int(last_initial_setup_step))
|
||||
print '-'*50
|
||||
|
||||
|
||||
def terminal_output(self):
|
||||
"""
|
||||
Outputs server startup info to the terminal.
|
||||
"""
|
||||
print ' %s (%s) started on port(s):' % (SERVERNAME, VERSION)
|
||||
if TELNET_ENABLED:
|
||||
ports = ", ".join([str(port) for port in TELNET_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in TELNET_INTERFACES if iface != '0.0.0.0'])
|
||||
print " telnet%s: %s" % (ifaces, ports)
|
||||
if SSH_ENABLED:
|
||||
ports = ", ".join([str(port) for port in SSH_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in SSH_INTERFACES if iface != '0.0.0.0'])
|
||||
print " ssh%s: %s" % (ifaces, ports)
|
||||
if SSL_ENABLED:
|
||||
ports = ", ".join([str(port) for port in SSL_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in SSL_INTERFACES if iface != '0.0.0.0'])
|
||||
print " ssl%s: %s" % (ifaces, ports)
|
||||
if WEBSERVER_ENABLED:
|
||||
clientstring = ""
|
||||
if WEBCLIENT_ENABLED:
|
||||
clientstring = '/client'
|
||||
ports = ", ".join([str(port) for port in WEBSERVER_PORTS])
|
||||
ifaces = ",".join([" %s" % iface for iface in WEBSERVER_INTERFACES if iface != '0.0.0.0'])
|
||||
print " webserver%s%s: %s" % (clientstring, ifaces, ports)
|
||||
print _(' %(servername)s Portal (%(version)s) started.') % {'servername': SERVERNAME, 'version': VERSION}
|
||||
print ' amp (Portal): %s' % AMP_PORT
|
||||
|
||||
def shutdown(self, message="{rThe server has been shutdown. Disconnecting.{n", _abrupt=False):
|
||||
def set_restart_mode(self, mode=None):
|
||||
"""
|
||||
If called directly, this disconnects everyone cleanly and shuts down the
|
||||
reactor. If the server is killed by other means (Ctrl-C, reboot etc), this
|
||||
might be called as a callback, at which point the reactor is already dead
|
||||
and should not be tried to stop again (_abrupt=True).
|
||||
This manages the flag file that tells the runner if the server is
|
||||
reloading, resetting or shutting down. Valid modes are
|
||||
'reload', 'reset', 'shutdown' and None.
|
||||
If mode is None, no change will be done to the flag file.
|
||||
|
||||
message - message to send to all connected sessions
|
||||
_abrupt - only to be used by internal callback_mechanism.
|
||||
Either way, the active restart setting (Restart=True/False) is
|
||||
returned so the server knows which more it's in.
|
||||
"""
|
||||
if mode == None:
|
||||
if os.path.exists(SERVER_RESTART) and 'True' == open(SERVER_RESTART, 'r').read():
|
||||
mode = 'reload'
|
||||
else:
|
||||
mode = 'shutdown'
|
||||
else:
|
||||
restart = mode in ('reload', 'reset')
|
||||
f = open(SERVER_RESTART, 'w')
|
||||
f.write(str(restart))
|
||||
f.close()
|
||||
return mode
|
||||
|
||||
def shutdown(self, mode=None, _abrupt=False):
|
||||
"""
|
||||
SESSIONS.disconnect_all_sessions(reason=message)
|
||||
Shuts down the server from inside it.
|
||||
|
||||
mode - sets the server restart mode.
|
||||
'reload' - server restarts, no "persistent" scripts are stopped, at_reload hooks called.
|
||||
'reset' - server restarts, non-persistent scripts stopped, at_shutdown hooks called.
|
||||
'shutdown' - like reset, but server will not auto-restart.
|
||||
None - keep currently set flag from flag file.
|
||||
_abrupt - this is set if server is stopped by a kill command,
|
||||
in which case the reactor is dead anyway.
|
||||
"""
|
||||
mode = self.set_restart_mode(mode)
|
||||
|
||||
# call shutdown hooks on all cached objects
|
||||
|
||||
from src.objects.models import ObjectDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.server.models import ServerConfig
|
||||
|
||||
if mode == 'reload':
|
||||
# call restart hooks
|
||||
[(o.typeclass(o), o.at_server_reload()) for o in ObjectDB.get_all_cached_instances()]
|
||||
[(p.typeclass(p), p.at_server_reload()) for p in PlayerDB.get_all_cached_instances()]
|
||||
[(s.typeclass(s), s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()]
|
||||
|
||||
ServerConfig.objects.conf("server_restart_mode", "reload")
|
||||
|
||||
else:
|
||||
if mode == 'reset':
|
||||
# don't call disconnect hooks on reset
|
||||
[(o.typeclass(o), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
|
||||
else: # shutdown
|
||||
[(o.typeclass(o), o.at_disconnect(), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
|
||||
[(p.typeclass(p), p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()]
|
||||
[(s.typeclass(s), s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()]
|
||||
|
||||
ServerConfig.objects.conf("server_restart_mode", "reset")
|
||||
|
||||
if not _abrupt:
|
||||
reactor.callLater(0, reactor.stop)
|
||||
|
||||
|
||||
if os.name == 'nt' and os.path.exists(SERVER_PIDFILE):
|
||||
# for Windows we need to remove pid files manually
|
||||
os.remove(SERVER_PIDFILE)
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Start the Evennia game server and add all active services
|
||||
|
|
@ -208,108 +232,24 @@ application = service.Application('Evennia')
|
|||
# and is where we store all the other services.
|
||||
EVENNIA = Evennia(application)
|
||||
|
||||
# We group all the various services under the same twisted app.
|
||||
# These will gradually be started as they are initialized below.
|
||||
# The AMP protocol handles the communication between
|
||||
# the portal and the mud server. Only reason to ever deactivate
|
||||
# it would be during testing and debugging.
|
||||
|
||||
if TELNET_ENABLED:
|
||||
if AMP_ENABLED:
|
||||
|
||||
# Start telnet game connections
|
||||
from src.server import amp
|
||||
|
||||
from src.server import telnet
|
||||
|
||||
for interface in TELNET_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(TELNET_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in TELNET_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = telnet.TelnetProtocol
|
||||
telnet_service = internet.TCPServer(port, factory, interface=interface)
|
||||
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
||||
EVENNIA.services.addService(telnet_service)
|
||||
|
||||
if SSL_ENABLED:
|
||||
|
||||
# Start SSL game connection (requires PyOpenSSL).
|
||||
|
||||
from src.server import ssl
|
||||
|
||||
for interface in SSL_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(SSL_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSL_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = ssl.SSLProtocol
|
||||
ssl_service = internet.SSLServer(port, factory, ssl.getSSLContext(), interface=interface)
|
||||
ssl_service.setName('EvenniaSSL%s' % pstring)
|
||||
EVENNIA.services.addService(ssl_service)
|
||||
|
||||
if SSH_ENABLED:
|
||||
|
||||
# Start SSH game connections. Will create a keypair in evennia/game if necessary.
|
||||
|
||||
from src.server import ssh
|
||||
|
||||
for interface in SSH_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(SSH_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSH_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
|
||||
'protocolArgs':()})
|
||||
ssh_service = internet.TCPServer(port, factory, interface=interface)
|
||||
ssh_service.setName('EvenniaSSH%s' % pstring)
|
||||
EVENNIA.services.addService(ssh_service)
|
||||
|
||||
if WEBSERVER_ENABLED:
|
||||
|
||||
# Start a django-compatible webserver.
|
||||
|
||||
from twisted.python import threadpool
|
||||
from src.server.webserver import DjangoWebRoot, WSGIWebServer
|
||||
|
||||
# start a thread pool and define the root url (/) as a wsgi resource
|
||||
# recognized by Django
|
||||
threads = threadpool.ThreadPool()
|
||||
web_root = DjangoWebRoot(threads)
|
||||
# point our media resources to url /media
|
||||
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
|
||||
|
||||
if WEBCLIENT_ENABLED:
|
||||
# create ajax client processes at /webclientdata
|
||||
from src.server.webclient import WebClient
|
||||
web_root.putChild("webclientdata", WebClient())
|
||||
|
||||
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||
|
||||
for interface in WEBSERVER_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface != '0.0.0.0' or len(WEBSERVER_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in WEBSERVER_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
# create the webserver
|
||||
webserver = WSGIWebServer(threads, port, web_site, interface=interface)
|
||||
webserver.setName('EvenniaWebServer%s' % pstring)
|
||||
EVENNIA.services.addService(webserver)
|
||||
|
||||
if IRC_ENABLED:
|
||||
|
||||
# IRC channel connections
|
||||
|
||||
from src.comms import irc
|
||||
irc.connect_all()
|
||||
|
||||
if IMC2_ENABLED:
|
||||
|
||||
# IMC2 channel connections
|
||||
|
||||
from src.comms import imc2
|
||||
imc2.connect_all()
|
||||
factory = amp.AmpServerFactory(EVENNIA)
|
||||
amp_service = internet.TCPServer(AMP_PORT, factory)
|
||||
amp_service.setName("EvenniaPortal")
|
||||
EVENNIA.services.addService(amp_service)
|
||||
|
||||
# clear server startup mode
|
||||
ServerConfig.objects.conf("server_starting_mode", delete=True)
|
||||
|
||||
if os.name == 'nt':
|
||||
# Windows only: Set PID file manually
|
||||
f = open(os.path.join(settings.GAME_DIR, 'server.pid'), 'w')
|
||||
f.write(str(os.getpid()))
|
||||
f.close()
|
||||
|
|
|
|||
247
src/server/serversession.py
Normal file
247
src/server/serversession.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""
|
||||
This defines a the Server's generic session object. This object represents
|
||||
a connection to the outside world but don't know any details about how the
|
||||
connection actually happens (so it's the same for telnet, web, ssh etc).
|
||||
|
||||
It is stored on the Server side (as opposed to protocol-specific sessions which
|
||||
are stored on the Portal side)
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.comms.models import Channel
|
||||
from src.utils import logger
|
||||
from src.commands import cmdhandler
|
||||
|
||||
IDLE_COMMAND = settings.IDLE_COMMAND
|
||||
|
||||
from src.server.session import Session
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Server Session
|
||||
#------------------------------------------------------------
|
||||
|
||||
class ServerSession(Session):
|
||||
"""
|
||||
This class represents a player's session and is a template for
|
||||
individual protocols to communicate with Evennia.
|
||||
|
||||
Each player gets a session assigned to them whenever they connect
|
||||
to the game server. All communication between game and player goes
|
||||
through their session.
|
||||
|
||||
"""
|
||||
|
||||
def at_sync(self):
|
||||
"""
|
||||
This is called whenever a session has been resynced with the portal.
|
||||
At this point all relevant attributes have already been set and self.player
|
||||
been assigned (if applicable).
|
||||
|
||||
Since this is often called after a server restart we need to set up
|
||||
the session as it was.
|
||||
"""
|
||||
if not self.logged_in:
|
||||
return
|
||||
|
||||
player = self.get_player()
|
||||
character = self.get_character()
|
||||
if player:
|
||||
player.at_init()
|
||||
if character:
|
||||
character.at_init()
|
||||
# start (persistent) scripts on this object
|
||||
ScriptDB.objects.validate(obj=character)
|
||||
|
||||
def session_login(self, player):
|
||||
"""
|
||||
Startup mechanisms that need to run at login. This is called
|
||||
by the login command (which need to have handled authentication
|
||||
already before calling this method)
|
||||
|
||||
player - the connected player
|
||||
"""
|
||||
|
||||
# actually do the login by assigning session data
|
||||
|
||||
self.player = player
|
||||
self.user = player.user
|
||||
self.uid = self.user.id
|
||||
self.uname = self.user.username
|
||||
self.logged_in = True
|
||||
self.conn_time = time.time()
|
||||
|
||||
# Update account's last login time.
|
||||
self.user.last_login = datetime.now()
|
||||
self.user.save()
|
||||
|
||||
# player init
|
||||
player.at_init()
|
||||
|
||||
# Check if this is the first time the *player* logs in
|
||||
if player.db.FIRST_LOGIN:
|
||||
player.at_first_login()
|
||||
del player.db.FIRST_LOGIN
|
||||
player.at_pre_login()
|
||||
|
||||
character = player.character
|
||||
character.at_init()
|
||||
if character:
|
||||
# this player has a character. Check if it's the
|
||||
# first time *this character* logs in
|
||||
if character.db.FIRST_LOGIN:
|
||||
character.at_first_login()
|
||||
del character.db.FIRST_LOGIN
|
||||
# run character login hook
|
||||
character.at_pre_login()
|
||||
|
||||
# this is always called first
|
||||
player.at_init()
|
||||
|
||||
self.log(_('Logged in: %(self)s') % {'self': self})
|
||||
|
||||
# start (persistent) scripts on this object
|
||||
ScriptDB.objects.validate(obj=self.player.character)
|
||||
|
||||
#add session to connected list
|
||||
self.sessionhandler.login(self)
|
||||
|
||||
# post-login hooks
|
||||
player.at_post_login()
|
||||
if character:
|
||||
character.at_post_login()
|
||||
|
||||
def session_disconnect(self):
|
||||
"""
|
||||
Clean up the session, removing it from the game and doing some
|
||||
accounting. This method is used also for non-loggedin
|
||||
accounts.
|
||||
"""
|
||||
if self.logged_in:
|
||||
player = self.get_player()
|
||||
character = self.get_character()
|
||||
if character:
|
||||
character.at_disconnect()
|
||||
uaccount = player.user
|
||||
uaccount.last_login = datetime.now()
|
||||
uaccount.save()
|
||||
self.logged_in = False
|
||||
self.sessionhandler.disconnect(self)
|
||||
|
||||
def get_player(self):
|
||||
"""
|
||||
Get the player associated with this session
|
||||
"""
|
||||
if self.logged_in:
|
||||
return self.player
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_character(self):
|
||||
"""
|
||||
Returns the in-game character associated with this session.
|
||||
This returns the typeclass of the object.
|
||||
"""
|
||||
player = self.get_player()
|
||||
if player:
|
||||
return player.character
|
||||
return None
|
||||
|
||||
def log(self, message, channel=True):
|
||||
"""
|
||||
Emits session info to the appropriate outputs and info channels.
|
||||
"""
|
||||
if channel:
|
||||
try:
|
||||
cchan = settings.CHANNEL_CONNECTINFO
|
||||
cchan = Channel.objects.get_channel(cchan[0])
|
||||
cchan.msg("[%s]: %s" % (cchan.key, message))
|
||||
except Exception:
|
||||
pass
|
||||
logger.log_infomsg(message)
|
||||
|
||||
def update_session_counters(self, idle=False):
|
||||
"""
|
||||
Hit this when the user enters a command in order to update idle timers
|
||||
and command counters.
|
||||
"""
|
||||
# Store the timestamp of the user's last command.
|
||||
self.cmd_last = time.time()
|
||||
if not idle:
|
||||
# Increment the user's command counter.
|
||||
self.cmd_total += 1
|
||||
# Player-visible idle time, not used in idle timeout calcs.
|
||||
self.cmd_last_visible = time.time()
|
||||
|
||||
def execute_cmd(self, command_string):
|
||||
"""
|
||||
Execute a command string on the server.
|
||||
"""
|
||||
# handle the 'idle' command
|
||||
if str(command_string).strip() == IDLE_COMMAND:
|
||||
self.update_session_counters(idle=True)
|
||||
return
|
||||
|
||||
# all other inputs, including empty inputs
|
||||
character = self.get_character()
|
||||
|
||||
|
||||
if character:
|
||||
character.execute_cmd(command_string)
|
||||
else:
|
||||
if self.logged_in:
|
||||
# there is no character, but we are logged in. Use player instead.
|
||||
self.get_player().execute_cmd(command_string)
|
||||
else:
|
||||
# we are not logged in. Use special unlogged-in call.
|
||||
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
|
||||
self.update_session_counters()
|
||||
|
||||
def data_out(self, msg, data=None):
|
||||
"""
|
||||
Send Evennia -> Player
|
||||
"""
|
||||
self.sessionhandler.data_out(self, msg, data)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.address == other.address
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
String representation of the user session class. We use
|
||||
this a lot in the server logs.
|
||||
"""
|
||||
if self.logged_in:
|
||||
symbol = '#'
|
||||
else:
|
||||
symbol = '?'
|
||||
try:
|
||||
address = ":".join([str(part) for part in self.address])
|
||||
except Exception:
|
||||
address = self.address
|
||||
return "<%s> %s@%s" % (symbol, self.uname, address)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Unicode representation
|
||||
"""
|
||||
return u"%s" % str(self)
|
||||
|
||||
|
||||
# easy-access functions
|
||||
|
||||
def login(self, player):
|
||||
"alias for at_login"
|
||||
self.session_login(player)
|
||||
def disconnect(self):
|
||||
"alias for session_disconnect"
|
||||
self.session_disconnect()
|
||||
def msg(self, string='', data=None):
|
||||
"alias for at_data_out"
|
||||
self.data_out(string, data=data)
|
||||
|
|
@ -1,412 +1,125 @@
|
|||
"""
|
||||
This defines a generic session class.
|
||||
|
||||
All protocols should implement this class and its hook methods.
|
||||
|
||||
|
||||
The process of first connect:
|
||||
|
||||
- The custom connection-handler for the respective
|
||||
protocol should be called by the transport connection itself.
|
||||
- The connect-handler handles whatever internal settings are needed
|
||||
- The connection-handler calls session_connect()
|
||||
- session_connect() setups sessions then calls session.at_connect()
|
||||
|
||||
Disconnecting is a bit more complex in order to avoid circular calls
|
||||
depending on if the disconnect happens automatically or manually from
|
||||
a command.
|
||||
|
||||
The process at automatic disconnect:
|
||||
- The custom disconnect-handler for the respective protocol
|
||||
should be called by the transport connection itself. This handler
|
||||
should be defined with a keyword argument 'step' defaulting to 1.
|
||||
- since step=1, the disconnect-handler calls session_disconnect()
|
||||
- session_disconnect() removes session, then calls session.at_disconnect()
|
||||
- session.at_disconnect() calls the custom disconnect-handler with
|
||||
step=2 as argument
|
||||
- since step=2, the disconnect-handler closes the connection and
|
||||
performs all needed protocol cleanup.
|
||||
|
||||
The process of manual disconnect:
|
||||
- The command/outside function calls session.session_disconnect().
|
||||
- from here the process proceeds as the automatic disconnect above.
|
||||
This defines a generic session class. All connection instances (both
|
||||
on Portal and Server side) should inherit from this class.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
#from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
#from src.objects.models import ObjectDB
|
||||
from src.comms.models import Channel
|
||||
from src.utils import logger, reloads
|
||||
from src.commands import cmdhandler
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||
IDLE_COMMAND = settings.IDLE_COMMAND
|
||||
|
||||
|
||||
|
||||
class IOdata(object):
|
||||
"""
|
||||
A simple storage object that allows for storing
|
||||
new attributes on it at creation.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
"Give keyword arguments to store as new arguments on the object."
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
|
||||
def _login(session, player):
|
||||
"""
|
||||
For logging a player in. Removed this from CmdConnect because ssh
|
||||
wanted to call it for autologin.
|
||||
"""
|
||||
# We are logging in, get/setup the player object controlled by player
|
||||
|
||||
# Check if this is the first time the
|
||||
# *player* connects (should be set by the
|
||||
if player.db.FIRST_LOGIN:
|
||||
player.at_first_login()
|
||||
del player.db.FIRST_LOGIN
|
||||
player.at_pre_login()
|
||||
|
||||
character = player.character
|
||||
if character:
|
||||
# this player has a character. Check if it's the
|
||||
# first time *this character* logs in (this should be
|
||||
# set by the initial create command)
|
||||
if character.db.FIRST_LOGIN:
|
||||
character.at_first_login()
|
||||
del character.db.FIRST_LOGIN
|
||||
# run character login hook
|
||||
character.at_pre_login()
|
||||
|
||||
# actually do the login
|
||||
session.session_login(player)
|
||||
|
||||
# post-login hooks
|
||||
player.at_post_login()
|
||||
if character:
|
||||
character.at_post_login()
|
||||
character.execute_cmd('look')
|
||||
else:
|
||||
player.execute_cmd('look')
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# SessionBase class
|
||||
# Server Session
|
||||
#------------------------------------------------------------
|
||||
|
||||
class SessionBase(object):
|
||||
class Session(object):
|
||||
"""
|
||||
This class represents a player's session and is a template for
|
||||
individual protocols to communicate with Evennia.
|
||||
This class represents a player's session and is a template for
|
||||
both portal- and server-side sessions.
|
||||
|
||||
Each player gets a session assigned to them whenever they connect
|
||||
to the game server. All communication between game and player goes
|
||||
through their session.
|
||||
Each connection will see two session instances created:
|
||||
|
||||
1) A Portal session. This is customized for the respective connection
|
||||
protocols that Evennia supports, like Telnet, SSH etc. The Portal session
|
||||
must call init_session() as part of its initialization. The respective
|
||||
hook methods should be connected to the methods unique for the respective
|
||||
protocol so that there is a unified interface to Evennia.
|
||||
2) A Server session. This is the same for all connected players, regardless
|
||||
of how they connect.
|
||||
|
||||
The Portal and Server have their own respective sessionhandlers. These are synced
|
||||
whenever new connections happen or the Server restarts etc, which means much of the
|
||||
same information must be stored in both places e.g. the portal can re-sync with the
|
||||
server when the server reboots.
|
||||
|
||||
"""
|
||||
|
||||
# use this to uniquely identify the protocol name, e.g. "telnet" or "comet"
|
||||
protocol_key = "BaseProtocol"
|
||||
# names of attributes that should be affected by syncing.
|
||||
_attrs_to_sync = ['protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname',
|
||||
'logged_in', 'cid', 'encoding',
|
||||
'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total']
|
||||
|
||||
def session_connect(self, address, suid=None):
|
||||
def init_session(self, protocol_key, address, sessionhandler):
|
||||
"""
|
||||
The setup of the session. An address (usually an IP address) on any form is required.
|
||||
|
||||
This should be called by the protocol at connection time.
|
||||
|
||||
suid = this is a session id. Needed by some transport protocols.
|
||||
Initialize the Session. This should be called by the protocol when
|
||||
a new session is established.
|
||||
protocol_key - telnet, ssh, ssl or web
|
||||
address - client address
|
||||
sessionhandler - reference to the sessionhandler instance
|
||||
"""
|
||||
# This is currently 'telnet', 'ssh', 'ssl' or 'web'
|
||||
self.protocol_key = protocol_key
|
||||
# Protocol address tied to this session
|
||||
self.address = address
|
||||
|
||||
# user setup
|
||||
self.name = None
|
||||
# suid is used by some protocols, it's a hex key.
|
||||
self.suid = None
|
||||
|
||||
# unique id for this session
|
||||
self.sessid = 0 # no sessid yet
|
||||
# database id for the user connected to this session
|
||||
self.uid = None
|
||||
self.suid = suid
|
||||
# user name, for easier tracking of sessions
|
||||
self.uname = None
|
||||
# if user has authenticated already or not
|
||||
self.logged_in = False
|
||||
|
||||
# database id of character/object connected to this player session (if any)
|
||||
self.cid = None
|
||||
self.encoding = "utf-8"
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
# The time the user last issued a command.
|
||||
self.cmd_last = current_time
|
||||
# Player-visible idle time, excluding the IDLE command.
|
||||
self.cmd_last_visible = current_time
|
||||
# The time when the user connected.
|
||||
self.conn_time = current_time
|
||||
# Total number of commands issued.
|
||||
self.cmd_total = 0
|
||||
#self.channels_subscribed = {}
|
||||
SESSIONS.add_unloggedin_session(self)
|
||||
# calling hook
|
||||
self.at_connect()
|
||||
|
||||
def session_login(self, player):
|
||||
"""
|
||||
Startup mechanisms that need to run at login
|
||||
|
||||
player - the connected player
|
||||
"""
|
||||
# Check if this is the first time the *player* logs in
|
||||
if player.db.FIRST_LOGIN:
|
||||
player.at_first_login()
|
||||
del player.db.FIRST_LOGIN
|
||||
player.at_pre_login()
|
||||
|
||||
character = player.character
|
||||
if character:
|
||||
# this player has a character. Check if it's the
|
||||
# first time *this character* logs in
|
||||
if character.db.FIRST_LOGIN:
|
||||
character.at_first_login()
|
||||
del character.db.FIRST_LOGIN
|
||||
# run character login hook
|
||||
character.at_pre_login()
|
||||
|
||||
# actually do the login by assigning session data
|
||||
|
||||
self.player = player
|
||||
self.user = player.user
|
||||
self.uid = self.user.id
|
||||
self.name = self.user.username
|
||||
self.logged_in = True
|
||||
# session time statistics
|
||||
self.conn_time = time.time()
|
||||
self.cmd_last_visible = self.conn_time
|
||||
self.cmd_last = self.conn_time
|
||||
self.cmd_total = 0
|
||||
|
||||
# a back-reference to the relevant sessionhandler this
|
||||
# session is stored in.
|
||||
self.sessionhandler = sessionhandler
|
||||
|
||||
def get_sync_data(self):
|
||||
"""
|
||||
Return all data relevant to sync the session
|
||||
"""
|
||||
sessdata = {}
|
||||
for attrname in self._attrs_to_sync:
|
||||
sessdata[attrname] = self.__dict__.get(attrname, None)
|
||||
return sessdata
|
||||
|
||||
def load_sync_data(self, sessdata):
|
||||
"""
|
||||
Takes a session dictionary, as created by get_sync_data,
|
||||
and loads it into the correct attributes of the session.
|
||||
"""
|
||||
for attrname, value in sessdata.items():
|
||||
self.__dict__[attrname] = value
|
||||
|
||||
# Update account's last login time.
|
||||
self.user.last_login = datetime.now()
|
||||
self.user.save()
|
||||
self.log('Logged in: %s' % self)
|
||||
|
||||
# start (persistent) scripts on this object
|
||||
reloads.reload_scripts(obj=self.player.character)
|
||||
|
||||
#add session to connected list
|
||||
SESSIONS.add_loggedin_session(self)
|
||||
|
||||
#call login hook
|
||||
self.at_login(player)
|
||||
|
||||
# post-login hooks
|
||||
player.at_post_login()
|
||||
if character:
|
||||
character.at_post_login()
|
||||
|
||||
def session_disconnect(self):
|
||||
def at_sync(self):
|
||||
"""
|
||||
Clean up the session, removing it from the game and doing some
|
||||
accounting. This method is used also for non-loggedin
|
||||
accounts.
|
||||
|
||||
Note that this methods does not close the connection - this is protocol-dependent
|
||||
and have to be done right after this function!
|
||||
"""
|
||||
if self.logged_in:
|
||||
player = self.get_player()
|
||||
uaccount = player.user
|
||||
uaccount.last_login = datetime.now()
|
||||
uaccount.save()
|
||||
self.at_disconnect()
|
||||
self.logged_in = False
|
||||
SESSIONS.remove_session(self)
|
||||
|
||||
def session_validate(self):
|
||||
"""
|
||||
Validate the session to make sure they have not been idle for too long
|
||||
"""
|
||||
if IDLE_TIMEOUT > 0 and (time.time() - self.cmd_last) > IDLE_TIMEOUT:
|
||||
self.msg("Idle timeout exceeded, disconnecting.")
|
||||
self.session_disconnect()
|
||||
|
||||
def get_player(self):
|
||||
"""
|
||||
Get the player associated with this session
|
||||
"""
|
||||
if self.logged_in:
|
||||
return self.player
|
||||
else:
|
||||
return None
|
||||
|
||||
# if self.logged_in:
|
||||
# character = ObjectDB.objects.get_object_with_user(self.uid)
|
||||
# if not character:
|
||||
# string = "No player match for session uid: %s" % self.uid
|
||||
# logger.log_errmsg(string)
|
||||
# return None
|
||||
# return character.player
|
||||
# return None
|
||||
|
||||
def get_character(self):
|
||||
"""
|
||||
Returns the in-game character associated with a session.
|
||||
This returns the typeclass of the object.
|
||||
"""
|
||||
player = self.get_player()
|
||||
if player:
|
||||
return player.character
|
||||
return None
|
||||
|
||||
def log(self, message, channel=True):
|
||||
"""
|
||||
Emits session info to the appropriate outputs and info channels.
|
||||
"""
|
||||
if channel:
|
||||
try:
|
||||
cchan = settings.CHANNEL_CONNECTINFO
|
||||
cchan = Channel.objects.get_channel(cchan[0])
|
||||
cchan.msg("[%s]: %s" % (cchan.key, message))
|
||||
except Exception:
|
||||
pass
|
||||
logger.log_infomsg(message)
|
||||
|
||||
def update_session_counters(self, idle=False):
|
||||
"""
|
||||
Hit this when the user enters a command in order to update idle timers
|
||||
and command counters.
|
||||
"""
|
||||
# Store the timestamp of the user's last command.
|
||||
self.cmd_last = time.time()
|
||||
if not idle:
|
||||
# Increment the user's command counter.
|
||||
self.cmd_total += 1
|
||||
# Player-visible idle time, not used in idle timeout calcs.
|
||||
self.cmd_last_visible = time.time()
|
||||
|
||||
def execute_cmd(self, command_string):
|
||||
"""
|
||||
Execute a command string.
|
||||
"""
|
||||
|
||||
# handle the 'idle' command
|
||||
if str(command_string).strip() == IDLE_COMMAND:
|
||||
self.update_session_counters(idle=True)
|
||||
return
|
||||
|
||||
# all other inputs, including empty inputs
|
||||
character = self.get_character()
|
||||
|
||||
if character:
|
||||
# normal operation.
|
||||
character.execute_cmd(command_string)
|
||||
#import cProfile
|
||||
#cProfile.runctx("character.execute_cmd(command_string)",
|
||||
# {"command_string":command_string,"character":character}, {}, "execute_cmd.profile")
|
||||
else:
|
||||
if self.logged_in:
|
||||
# there is no character, but we are logged in. Use player instead.
|
||||
self.get_player().execute_cmd(command_string)
|
||||
else:
|
||||
# we are not logged in. Use special unlogged-in call.
|
||||
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
|
||||
self.update_session_counters()
|
||||
|
||||
def get_data_obj(self, **kwargs):
|
||||
"""
|
||||
Create a data object, storing keyword arguments on itself as arguments.
|
||||
"""
|
||||
return IOdata(**kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.address == other.address
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
String representation of the user session class. We use
|
||||
this a lot in the server logs.
|
||||
"""
|
||||
if self.logged_in:
|
||||
symbol = '#'
|
||||
else:
|
||||
symbol = '?'
|
||||
return "<%s> %s@%s" % (symbol, self.name, self.address,)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Unicode representation
|
||||
"""
|
||||
return u"%s" % str(self)
|
||||
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Session class - inherit from this
|
||||
#------------------------------------------------------------
|
||||
|
||||
class Session(SessionBase):
|
||||
"""
|
||||
The main class to inherit from. Overload the methods here.
|
||||
"""
|
||||
|
||||
# exchange this for a unique name you can use to identify the
|
||||
# protocol type this session uses
|
||||
protocol_key = "TemplateProtocol"
|
||||
|
||||
#
|
||||
# Hook methods
|
||||
#
|
||||
|
||||
def at_connect(self):
|
||||
"""
|
||||
This method is called by the connection mechanic after
|
||||
connection has been made. The session is added to the
|
||||
sessionhandler and basic accounting has been made at this
|
||||
point.
|
||||
|
||||
This is the place to put e.g. welcome screens specific to the
|
||||
protocol.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
This method is called by the login mechanic whenever the user
|
||||
has finished authenticating. The user has been moved to the
|
||||
right sessionhandler list and basic book keeping has been
|
||||
done at this point (so logged_in=True).
|
||||
Called after a session has been fully synced (including
|
||||
secondary operations such as setting self.player based
|
||||
on uid etc).
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_disconnect(self):
|
||||
"""
|
||||
This method is called just before cleaning up the session
|
||||
(so still logged_in=True at this point).
|
||||
|
||||
This method should not be called from commands, instead it
|
||||
is called automatically by session_disconnect() as part of
|
||||
the cleanup.
|
||||
|
||||
This method MUST call the protocol-dependant disconnect-handler
|
||||
with step=2 to finalize the closing of the connection!
|
||||
"""
|
||||
# self.my-disconnect-handler(step=2)
|
||||
pass
|
||||
# access hooks
|
||||
|
||||
def at_data_in(self, string="", data=None):
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
Player -> Evennia
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_data_out(self, string="", data=None):
|
||||
"""
|
||||
Evennia -> Player
|
||||
|
||||
string - an string of any form to send to the player
|
||||
data - a data structure of any form
|
||||
|
||||
generic hook called from the outside to disconnect this session
|
||||
should be connected to the protocols actual disconnect mechanism.
|
||||
"""
|
||||
pass
|
||||
|
||||
# easy-access functions
|
||||
def login(self, player):
|
||||
"alias for at_login"
|
||||
self.at_login(player)
|
||||
def disconnect(self):
|
||||
"alias for session_disconnect"
|
||||
self.session_disconnect()
|
||||
def msg(self, string='', data=None):
|
||||
"alias for at_data_out"
|
||||
self.at_data_out(string, data=data)
|
||||
def data_out(self, msg, data=None):
|
||||
"""
|
||||
generic hook for sending data out through the protocol. Server
|
||||
protocols can use this right away. Portal sessions
|
||||
should overload this to format/handle the outgoing data as needed.
|
||||
"""
|
||||
pass
|
||||
|
||||
def data_in(self, msg, data=None):
|
||||
"""
|
||||
hook for protocols to send incoming data to the engine.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,72 @@
|
|||
"""
|
||||
This module handles sessions of users connecting
|
||||
to the server.
|
||||
This module defines handlers for storing sessions when handles
|
||||
sessions of users connecting to the server.
|
||||
|
||||
Since Evennia supports several different connection
|
||||
protocols, it is important to have a joint place
|
||||
to store session info. It also makes it easier
|
||||
to dispatch data.
|
||||
|
||||
Whereas server.py handles all setup of the server
|
||||
and database itself, this file handles all that
|
||||
comes after initial startup.
|
||||
|
||||
All new sessions (of whatever protocol) are responsible for
|
||||
registering themselves with this module.
|
||||
There are two similar but separate stores of sessions:
|
||||
ServerSessionHandler - this stores generic game sessions
|
||||
for the game. These sessions has no knowledge about
|
||||
how they are connected to the world.
|
||||
PortalSessionHandler - this stores sessions created by
|
||||
twisted protocols. These are dumb connectors that
|
||||
handle network communication but holds no game info.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from src.server.models import ServerConfig
|
||||
from src.utils import utils
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION
|
||||
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||
|
||||
#------------------------------------------------------------
|
||||
# SessionHandler class
|
||||
#------------------------------------------------------------
|
||||
|
||||
class SessionHandler(object):
|
||||
"""
|
||||
This handler holds a stack of sessions.
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Init the handler.
|
||||
"""
|
||||
self.sessions = {}
|
||||
|
||||
def get_sessions(self, include_unloggedin=False):
|
||||
"""
|
||||
Returns the connected session objects.
|
||||
"""
|
||||
if include_unloggedin:
|
||||
return self.sessions.values()
|
||||
else:
|
||||
return [session for session in self.sessions.values() if session.logged_in]
|
||||
|
||||
def get_session(self, sessid):
|
||||
"""
|
||||
Get session by sessid
|
||||
"""
|
||||
return self.sessions.get(sessid, None)
|
||||
|
||||
def get_all_sync_data(self):
|
||||
"""
|
||||
Create a dictionary of sessdata dicts representing all
|
||||
sessions in store.
|
||||
"""
|
||||
sessdict = {}
|
||||
for sess in self.sessions.values():
|
||||
# copy all relevant data from all sessions
|
||||
sessdict[sess.sessid] = sess.get_sync_data()
|
||||
return sessdict
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Server-SessionHandler class
|
||||
#------------------------------------------------------------
|
||||
|
||||
class ServerSessionHandler(SessionHandler):
|
||||
"""
|
||||
This object holds the stack of sessions active in the game at
|
||||
any time.
|
||||
|
|
@ -38,102 +79,144 @@ class SessionHandler(object):
|
|||
|
||||
"""
|
||||
|
||||
# AMP communication methods
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Init the handler. We track two types of sessions, those
|
||||
who have just connected (unloggedin) and those who have
|
||||
logged in (authenticated).
|
||||
Init the handler.
|
||||
"""
|
||||
self.unloggedin = []
|
||||
self.loggedin = []
|
||||
|
||||
# we keep a link to the server here, for the rest of the game to access.
|
||||
self.sessions = {}
|
||||
self.server = None
|
||||
|
||||
def add_unloggedin_session(self, session):
|
||||
def portal_connect(self, sessid, session):
|
||||
"""
|
||||
Call at first connect. This adds a not-yet authenticated session.
|
||||
"""
|
||||
self.unloggedin.insert(0, session)
|
||||
Called by Portal when a new session has connected.
|
||||
Creates a new, unlogged-in game session.
|
||||
"""
|
||||
self.sessions[sessid] = session
|
||||
session.execute_cmd('look')
|
||||
|
||||
def portal_disconnect(self, sessid):
|
||||
"""
|
||||
Called by Portal when portal reports a closing of a session
|
||||
from the portal side.
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
del self.sessions[session.sessid]
|
||||
self.session_count(-1)
|
||||
|
||||
def portal_session_sync(self, sesslist):
|
||||
"""
|
||||
Syncing all session ids of the portal with the ones of the server. This is instantiated
|
||||
by the portal when reconnecting.
|
||||
|
||||
def add_loggedin_session(self, session):
|
||||
sesslist is a complete list of (sessid, session) pairs, matching the list on the portal.
|
||||
if session was logged in, the amp handler will have logged them in before this point.
|
||||
"""
|
||||
for sess in self.sessions.values():
|
||||
# we delete the old session to make sure to catch eventual lingering references.
|
||||
del sess
|
||||
for sess in sesslist:
|
||||
self.sessions[sess.sessid] = sess
|
||||
sess.at_sync()
|
||||
|
||||
def portal_shutdown(self):
|
||||
"""
|
||||
Called by server when shutting down the portal.
|
||||
"""
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(0,
|
||||
operation='SSHUTD',
|
||||
data="")
|
||||
# server-side access methods
|
||||
|
||||
def disconnect(self, session, reason=""):
|
||||
"""
|
||||
Called from server side to remove session and inform portal
|
||||
of this fact.
|
||||
"""
|
||||
session = self.sessions.get(session.sessid, None)
|
||||
if session:
|
||||
sessid = session.sessid
|
||||
del self.sessions[sessid]
|
||||
# inform portal that session should be closed.
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(sessid,
|
||||
operation='SDISCONN',
|
||||
data=reason)
|
||||
self.session_count(-1)
|
||||
|
||||
|
||||
def login(self, session):
|
||||
"""
|
||||
Log in the previously unloggedin session and the player we by
|
||||
now should know is connected to it. After this point we
|
||||
assume the session to be logged in one way or another.
|
||||
"""
|
||||
# prep the session with player/user info
|
||||
|
||||
|
||||
|
||||
if not ALLOW_MULTISESSION:
|
||||
# disconnect previous sessions.
|
||||
self.disconnect_duplicate_sessions(session)
|
||||
|
||||
# store/move the session to the right list
|
||||
try:
|
||||
self.unloggedin.remove(session)
|
||||
except ValueError:
|
||||
pass
|
||||
self.loggedin.insert(0, session)
|
||||
session.logged_in = True
|
||||
self.session_count(1)
|
||||
# sync the portal to this session
|
||||
sessdata = session.get_sync_data()
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(session.sessid,
|
||||
operation='SLOGIN',
|
||||
data=sessdata)
|
||||
|
||||
def session_sync(self):
|
||||
"""
|
||||
This is called by the server when it reboots. It syncs all session data
|
||||
to the portal.
|
||||
"""
|
||||
sessdata = self.get_all_sync_data()
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(0,
|
||||
'SSYNC',
|
||||
data=sessdata)
|
||||
|
||||
def remove_session(self, session):
|
||||
"""
|
||||
Remove session from the handler
|
||||
"""
|
||||
removed = False
|
||||
try:
|
||||
self.unloggedin.remove(session)
|
||||
except Exception:
|
||||
try:
|
||||
self.loggedin.remove(session)
|
||||
except Exception:
|
||||
return
|
||||
self.session_count(-1)
|
||||
|
||||
def get_sessions(self, include_unloggedin=False):
|
||||
"""
|
||||
Returns the connected session objects.
|
||||
"""
|
||||
if include_unloggedin:
|
||||
return self.loggedin + self.unloggedin
|
||||
else:
|
||||
return self.loggedin
|
||||
|
||||
def disconnect_all_sessions(self, reason="You have been disconnected."):
|
||||
"""
|
||||
Cleanly disconnect all of the connected sessions.
|
||||
"""
|
||||
sessions = self.get_sessions(include_unloggedin=True)
|
||||
for session in sessions:
|
||||
session.at_data_out(reason)
|
||||
session.session_disconnect()
|
||||
|
||||
for session in self.sessions:
|
||||
del session
|
||||
self.session_count(0)
|
||||
# tell portal to disconnect all sessions
|
||||
self.server.amp_protocol.call_remote_PortalAdmin(0,
|
||||
operation='SDISCONNALL',
|
||||
data=reason)
|
||||
|
||||
def disconnect_duplicate_sessions(self, curr_session):
|
||||
"""
|
||||
Disconnects any existing sessions with the same game object.
|
||||
"""
|
||||
reason = "Your account has been logged in from elsewhere. Disconnecting."
|
||||
curr_char = curr_session.get_character()
|
||||
doublet_sessions = [sess for sess in self.get_sessions()
|
||||
if sess.get_character() == curr_char and sess != curr_session]
|
||||
logged_out = 0
|
||||
for session in doublet_sessions:
|
||||
session.msg(reason)
|
||||
self.remove_session(session)
|
||||
logged_out += 1
|
||||
self.session_count(-logged_out)
|
||||
return logged_out
|
||||
doublet_sessions = [sess for sess in self.sessions
|
||||
if sess.logged_in
|
||||
and sess.get_character() == curr_char
|
||||
and sess != curr_session]
|
||||
reason = _("Logged in from elsewhere. Disconnecting.")
|
||||
for sessid in doublet_sessions:
|
||||
self.disconnect(session, reason)
|
||||
self.session_count(-1)
|
||||
|
||||
|
||||
def validate_sessions(self):
|
||||
"""
|
||||
Check all currently connected sessions (logged in and not)
|
||||
and see if any are dead.
|
||||
"""
|
||||
for session in self.get_sessions(include_unloggedin=True):
|
||||
session.session_validate()
|
||||
|
||||
tcurr = time.time()
|
||||
invalid_sessions = [session for session in self.sessions.values()
|
||||
if session.logged_in and IDLE_TIMEOUT > 0
|
||||
and (tcurr - session.cmd_last) > IDLE_TIMEOUT]
|
||||
for session in invalid_sessions:
|
||||
self.disconnect(session, reason=_("Idle timeout exceeded, disconnecting."))
|
||||
self.session_count(-1)
|
||||
|
||||
def session_count(self, num=None):
|
||||
"""
|
||||
Count up/down the number of connected, authenticated users.
|
||||
|
|
@ -160,7 +243,7 @@ class SessionHandler(object):
|
|||
may have more than one session connected if ALLOW_MULTISESSION is True)
|
||||
Only logged-in players are counted here.
|
||||
"""
|
||||
return len(set(sess.uid for sess in self.get_sessions()))
|
||||
return len(set(session.uid for session in self.sessions.values() if session.logged_in))
|
||||
|
||||
def sessions_from_player(self, player):
|
||||
"""
|
||||
|
|
@ -172,7 +255,7 @@ class SessionHandler(object):
|
|||
except User.DoesNotExist:
|
||||
return None
|
||||
uid = uobj.id
|
||||
return [session for session in self.loggedin if session.uid == uid]
|
||||
return [session for session in self.sessions.values() if session.logged_in and session.uid == uid]
|
||||
|
||||
def sessions_from_character(self, character):
|
||||
"""
|
||||
|
|
@ -183,20 +266,129 @@ class SessionHandler(object):
|
|||
return self.sessions_from_player(player)
|
||||
return None
|
||||
|
||||
def session_from_suid(self, suid):
|
||||
"""
|
||||
Given a session id, retrieve the session (this is primarily
|
||||
intended to be called by web clients)
|
||||
"""
|
||||
return [sess for sess in self.get_sessions(include_unloggedin=True) if sess.suid and sess.suid == suid]
|
||||
|
||||
def announce_all(self, message):
|
||||
"""
|
||||
Send message to all connected sessions
|
||||
"""
|
||||
for sess in self.get_sessions(include_unloggedin=True):
|
||||
sess.msg(message)
|
||||
for sess in self.sessions.values():
|
||||
self.data_out(sess, message)
|
||||
|
||||
SESSIONS = SessionHandler()
|
||||
def data_out(self, session, string="", data=""):
|
||||
"""
|
||||
Sending data Server -> Portal
|
||||
"""
|
||||
self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid,
|
||||
msg=string,
|
||||
data=data)
|
||||
def data_in(self, sessid, string="", data=""):
|
||||
"""
|
||||
Data Portal -> Server
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
session.execute_cmd(string)
|
||||
|
||||
# ignore 'data' argument for now; this is otherwise the place
|
||||
# to put custom effects on the server due to data input, e.g.
|
||||
# from a custom client.
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Portal-SessionHandler class
|
||||
#------------------------------------------------------------
|
||||
|
||||
class PortalSessionHandler(SessionHandler):
|
||||
"""
|
||||
This object holds the sessions connected to the portal at any time.
|
||||
It is synced with the server's equivalent SessionHandler over the AMP
|
||||
connection.
|
||||
|
||||
Sessions register with the handler using the connect() method. This
|
||||
will assign a new unique sessionid to the session and send that sessid
|
||||
to the server using the AMP connection.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Init the handler
|
||||
"""
|
||||
self.portal = None
|
||||
self.sessions = {}
|
||||
self.latest_sessid = 0
|
||||
|
||||
def connect(self, session):
|
||||
"""
|
||||
Called by protocol at first connect. This adds a not-yet authenticated session
|
||||
using an ever-increasing counter for sessid.
|
||||
"""
|
||||
self.latest_sessid += 1
|
||||
sessid = self.latest_sessid
|
||||
session.sessid = sessid
|
||||
sessdata = session.get_sync_data()
|
||||
self.sessions[sessid] = session
|
||||
# sync with server-side
|
||||
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
|
||||
operation="PCONN",
|
||||
data=sessdata)
|
||||
def disconnect(self, session):
|
||||
"""
|
||||
Called from portal side when the connection is closed from the portal side.
|
||||
"""
|
||||
sessid = session.sessid
|
||||
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
|
||||
operation="PDISCONN")
|
||||
|
||||
def server_disconnect(self, sessid, reason=""):
|
||||
"""
|
||||
Called by server to force a disconnect by sessid
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
session.disconnect(reason)
|
||||
del session
|
||||
|
||||
def server_disconnect_all(self, reason=""):
|
||||
"""
|
||||
Called by server when forcing a clean disconnect for everyone.
|
||||
"""
|
||||
for session in self.sessions.values():
|
||||
session.disconnect(reason)
|
||||
del session
|
||||
|
||||
def session_from_suid(self, suid):
|
||||
"""
|
||||
Given a session id, retrieve the session (this is primarily
|
||||
intended to be called by web clients)
|
||||
"""
|
||||
return [sess for sess in self.get_sessions(include_unloggedin=True)
|
||||
if hasattr(sess, 'suid') and sess.suid == suid]
|
||||
|
||||
def data_in(self, session, string="", data=""):
|
||||
"""
|
||||
Called by portal sessions for relaying data coming
|
||||
in from the protocol to the server. data is
|
||||
serialized before passed on.
|
||||
"""
|
||||
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
|
||||
msg=string,
|
||||
data=data)
|
||||
def announce_all(self, message):
|
||||
"""
|
||||
Send message to all connection sessions
|
||||
"""
|
||||
for session in self.sessions.values():
|
||||
session.data_out(message)
|
||||
|
||||
def data_out(self, sessid, string="", data=""):
|
||||
"""
|
||||
Called by server for having the portal relay messages and data
|
||||
to the correct session protocol.
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
session.data_out(string, data=data)
|
||||
|
||||
SESSIONS = ServerSessionHandler()
|
||||
PORTAL_SESSIONS = PortalSessionHandler()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ This depends on a generic session module that implements
|
|||
the actual login procedure of the game, tracks
|
||||
sessions etc.
|
||||
|
||||
Using standard ssh client,
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
|
|
@ -25,7 +27,9 @@ from django.conf import settings
|
|||
from src.server import session
|
||||
from src.players.models import PlayerDB
|
||||
from src.utils import ansi, utils, logger
|
||||
#from src.commands.default.unloggedin import _login
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
|
||||
|
|
@ -40,13 +44,13 @@ class SshProtocol(Manhole, session.Session):
|
|||
them. All communication between game and player goes through
|
||||
here.
|
||||
"""
|
||||
def __init__(self, player):
|
||||
def __init__(self, starttuple):
|
||||
"""
|
||||
For setting up the player. If player is not None then we'll
|
||||
login automatically.
|
||||
"""
|
||||
self.player = player
|
||||
|
||||
self.authenticated_player = starttuple[0]
|
||||
self.cfactory = starttuple[1] # obs may not be called self.factory, it gets overwritten!
|
||||
|
||||
def terminalSize(self, width, height):
|
||||
"""
|
||||
|
|
@ -60,10 +64,14 @@ class SshProtocol(Manhole, session.Session):
|
|||
self.height = height
|
||||
|
||||
# initialize the session
|
||||
self.session_connect(self.getClientAddress())
|
||||
if self.player is not None:
|
||||
self.session_login(self.player)
|
||||
self.execute_cmd('look')
|
||||
client_address = self.getClientAddress()
|
||||
self.init_session("ssh", client_address, self.cfactory.sessionhandler)
|
||||
|
||||
# since we might have authenticated already, we might set this here.
|
||||
if self.authenticated_player:
|
||||
self.logged_in = True
|
||||
self.uid = self.authenticated_player.user.id
|
||||
self.sessionhandler.connect(self)
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
|
|
@ -74,8 +82,9 @@ class SshProtocol(Manhole, session.Session):
|
|||
self.keyHandlers[CTRL_C] = self.handle_INT
|
||||
self.keyHandlers[CTRL_D] = self.handle_EOF
|
||||
self.keyHandlers[CTRL_L] = self.handle_FF
|
||||
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
|
||||
|
||||
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
|
||||
|
||||
# initalize
|
||||
|
||||
def handle_INT(self):
|
||||
"""
|
||||
|
|
@ -116,32 +125,22 @@ class SshProtocol(Manhole, session.Session):
|
|||
self.terminal.loseConnection()
|
||||
|
||||
|
||||
def connectionLost(self, reason=None, step=1):
|
||||
def connectionLost(self, reason=None):
|
||||
"""
|
||||
This is executed when the connection is lost for
|
||||
whatever reason.
|
||||
whatever reason. It can also be called directly,
|
||||
from the disconnect method.
|
||||
|
||||
Closing the connection takes two steps
|
||||
|
||||
step 1 - is the default and is used when this method is
|
||||
called automatically. The method should then call self.session_disconnect().
|
||||
Step 2 - means this method is called from at_disconnect(). At this point
|
||||
the sessions are assumed to have been handled, and so the transport can close
|
||||
without further ado.
|
||||
"""
|
||||
insults.TerminalProtocol.connectionLost(self, reason)
|
||||
if step == 1:
|
||||
self.session_disconnect()
|
||||
else:
|
||||
self.terminal.loseConnection()
|
||||
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.terminal.loseConnection()
|
||||
|
||||
def getClientAddress(self):
|
||||
"""
|
||||
Returns the client's address and port in a tuple. For example
|
||||
('127.0.0.1', 41917)
|
||||
"""
|
||||
|
||||
return self.terminal.transport.getPeer()
|
||||
|
||||
|
||||
|
|
@ -152,7 +151,7 @@ class SshProtocol(Manhole, session.Session):
|
|||
command for the purpose of the MUD. So we take the user input
|
||||
and pass it on to the game engine.
|
||||
"""
|
||||
self.at_data_in(string)
|
||||
self.sessionhandler.data_in(self, string)
|
||||
|
||||
def lineSend(self, string):
|
||||
"""
|
||||
|
|
@ -166,35 +165,18 @@ class SshProtocol(Manhole, session.Session):
|
|||
self.terminal.write(line) #this is the telnet-specific method for sending
|
||||
self.terminal.nextLine()
|
||||
|
||||
|
||||
# session-general method hooks
|
||||
|
||||
def at_connect(self):
|
||||
"""
|
||||
Show the banner screen.
|
||||
"""
|
||||
self.telnet_markup = True
|
||||
# show connection screen
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
Called after authentication. self.logged_in=True at this point.
|
||||
"""
|
||||
if player.has_attribute('telnet_markup'):
|
||||
self.telnet_markup = player.get_attribute("telnet_markup")
|
||||
else:
|
||||
self.telnet_markup = True
|
||||
|
||||
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
|
||||
def disconnect(self, reason="Connection closed. Goodbye for now."):
|
||||
"""
|
||||
Disconnect from server
|
||||
"""
|
||||
char = self.get_character()
|
||||
if char:
|
||||
char.at_disconnect()
|
||||
self.at_data_out(reason)
|
||||
self.connectionLost(step=2)
|
||||
if reason:
|
||||
self.data_out(reason)
|
||||
self.connectionLost(reason)
|
||||
|
||||
def at_data_out(self, string, data=None):
|
||||
def data_out(self, string, data=None):
|
||||
"""
|
||||
Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings.
|
||||
"""
|
||||
|
|
@ -203,31 +185,18 @@ class SshProtocol(Manhole, session.Session):
|
|||
except Exception, e:
|
||||
self.lineSend(str(e))
|
||||
return
|
||||
nomarkup = not self.telnet_markup
|
||||
raw = False
|
||||
nomarkup = False
|
||||
raw = False
|
||||
if type(data) == dict:
|
||||
# check if we want escape codes to go through unparsed.
|
||||
raw = data.get("raw", self.telnet_markup)
|
||||
raw = data.get("raw", False)
|
||||
# check if we want to remove all markup
|
||||
nomarkup = data.get("nomarkup", not self.telnet_markup)
|
||||
nomarkup = data.get("nomarkup", False)
|
||||
if raw:
|
||||
self.lineSend(string)
|
||||
else:
|
||||
self.lineSend(ansi.parse_ansi(string, strip_ansi=nomarkup))
|
||||
|
||||
def at_data_in(self, string, data=None):
|
||||
"""
|
||||
Line from Player -> Evennia. 'data' argument is not used.
|
||||
|
||||
"""
|
||||
try:
|
||||
string = utils.to_unicode(string, encoding=self.encoding)
|
||||
self.execute_cmd(string)
|
||||
return
|
||||
except Exception, e:
|
||||
logger.log_errmsg(str(e))
|
||||
|
||||
|
||||
|
||||
class ExtraInfoAuthServer(SSHUserAuthServer):
|
||||
def auth_password(self, packet):
|
||||
|
|
@ -251,15 +220,19 @@ class PlayerDBPasswordChecker(object):
|
|||
"""
|
||||
credentialInterfaces = (credentials.IUsernamePassword,)
|
||||
|
||||
def __init__(self, factory):
|
||||
self.factory = factory
|
||||
super(PlayerDBPasswordChecker, self).__init__()
|
||||
|
||||
def requestAvatarId(self, c):
|
||||
"Generic credentials"
|
||||
up = credentials.IUsernamePassword(c, None)
|
||||
username = up.username
|
||||
password = up.password
|
||||
player = PlayerDB.objects.get_player_from_name(username)
|
||||
res = None
|
||||
res = (None, self.factory)
|
||||
if player and player.user.check_password(password):
|
||||
res = player
|
||||
res = (player, self.factory)
|
||||
return defer.succeed(res)
|
||||
|
||||
class PassAvatarIdTerminalRealm(TerminalRealm):
|
||||
|
|
@ -322,7 +295,7 @@ def getKeyPair(pubkeyfile, privkeyfile):
|
|||
|
||||
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
|
||||
# No keypair exists. Generate a new RSA keypair
|
||||
print " Generating SSH RSA keypair ...",
|
||||
print _(" Generating SSH RSA keypair ..."),
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
KEY_LENGTH = 1024
|
||||
|
|
@ -359,6 +332,7 @@ def makeFactory(configdict):
|
|||
rlm.transportFactory = TerminalSessionTransport_getPeer
|
||||
rlm.chainedProtocolFactory = chainProtocolFactory
|
||||
factory = ConchFactory(Portal(rlm))
|
||||
factory.sessionhandler = configdict['sessions']
|
||||
|
||||
try:
|
||||
# create/get RSA keypair
|
||||
|
|
@ -366,12 +340,12 @@ def makeFactory(configdict):
|
|||
factory.publicKeys = {'ssh-rsa': publicKey}
|
||||
factory.privateKeys = {'ssh-rsa': privateKey}
|
||||
except Exception, e:
|
||||
print " getKeyPair error: %s\n WARNING: Evennia could not auto-generate SSH keypair. Using conch default keys instead." % e
|
||||
print " If this error persists, create game/%s and game/%s yourself using third-party tools." % (pubkeyfile, privkeyfile)
|
||||
print _(" getKeyPair error: %(e)s\n WARNING: Evennia could not auto-generate SSH keypair. Using conch default keys instead.") % {'e': e}
|
||||
print _(" If this error persists, create game/%(pub)s and game/%(priv)s yourself using third-party tools.") % {'pub': pubkeyfile, 'priv': privkeyfile}
|
||||
|
||||
factory.services = factory.services.copy()
|
||||
factory.services['ssh-userauth'] = ExtraInfoAuthServer
|
||||
|
||||
factory.portal.registerChecker(PlayerDBPasswordChecker())
|
||||
factory.portal.registerChecker(PlayerDBPasswordChecker(factory))
|
||||
|
||||
return factory
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ from twisted.internet import ssl as twisted_ssl
|
|||
try:
|
||||
import OpenSSL
|
||||
except ImportError:
|
||||
print " SSL_ENABLED requires PyOpenSSL."
|
||||
sys.exit()
|
||||
print _(" SSL_ENABLED requires PyOpenSSL.")
|
||||
sys.exit(5)
|
||||
|
||||
from src.server.telnet import TelnetProtocol
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ def verify_SSL_key_and_cert(keyfile, certfile):
|
|||
from Crypto.PublicKey import RSA
|
||||
from twisted.conch.ssh.keys import Key
|
||||
|
||||
print " Creating SSL key and certificate ... ",
|
||||
print _(" Creating SSL key and certificate ... "),
|
||||
|
||||
try:
|
||||
# create the RSA key and store it.
|
||||
|
|
@ -42,9 +42,9 @@ def verify_SSL_key_and_cert(keyfile, certfile):
|
|||
keyString = rsaKey.toString(type="OPENSSH")
|
||||
file(keyfile, 'w+b').write(keyString)
|
||||
except Exception,e:
|
||||
print "rsaKey error: %s\n WARNING: Evennia could not auto-generate SSL private key." % e
|
||||
print "If this error persists, create game/%s yourself using third-party tools." % keyfile
|
||||
sys.exit()
|
||||
print _("rsaKey error: %(e)s\n WARNING: Evennia could not auto-generate SSL private key.") % {'e': e}
|
||||
print _("If this error persists, create game/%(keyfile)s yourself using third-party tools.") % {'keyfile': keyfile}
|
||||
sys.exit(5)
|
||||
|
||||
# try to create the certificate
|
||||
CERT_EXPIRE = 365 * 20 # twenty years validity
|
||||
|
|
@ -56,12 +56,12 @@ def verify_SSL_key_and_cert(keyfile, certfile):
|
|||
err = subprocess.call(exestring)#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
except OSError, e:
|
||||
print " %s\n" % e
|
||||
print " Evennia's SSL context factory could not automatically create an SSL certificate game/%s." % certfile
|
||||
print " A private key 'ssl.key' was already created. Please create %s manually using the commands valid " % certfile
|
||||
print " for your operating system."
|
||||
print " Example (linux, using the openssl program): "
|
||||
print " %s" % exestring
|
||||
sys.exit()
|
||||
print _(" Evennia's SSL context factory could not automatically create an SSL certificate game/%(cert)s.") % {'cert': certfile}
|
||||
print _(" A private key 'ssl.key' was already created. Please create %(cert)s manually using the commands valid") % {'cert': certfile}
|
||||
print _(" for your operating system.")
|
||||
print _(" Example (linux, using the openssl program): ")
|
||||
print " %s" % exestring
|
||||
sys.exit(5)
|
||||
print "done."
|
||||
|
||||
def getSSLContext():
|
||||
|
|
|
|||
|
|
@ -8,130 +8,73 @@ sessions etc.
|
|||
"""
|
||||
|
||||
from twisted.conch.telnet import StatefulTelnetProtocol
|
||||
from django.conf import settings
|
||||
from src.server import session
|
||||
from src.utils import ansi, utils, logger
|
||||
from src.server.session import Session
|
||||
from src.utils import utils, ansi
|
||||
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
|
||||
class TelnetProtocol(StatefulTelnetProtocol, session.Session):
|
||||
class TelnetProtocol(StatefulTelnetProtocol, Session):
|
||||
"""
|
||||
Each player connecting over telnet (ie using most traditional mud
|
||||
clients) gets a telnet protocol instance assigned to them. All
|
||||
communication between game and player goes through here.
|
||||
"""
|
||||
|
||||
# telnet-specific hooks
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
This is called when the connection is first
|
||||
established.
|
||||
"""
|
||||
# initialize the session
|
||||
self.session_connect(self.getClientAddress())
|
||||
client_address = self.transport.client
|
||||
self.init_session("telnet", client_address, self.factory.sessionhandler)
|
||||
# add us to sessionhandler
|
||||
self.sessionhandler.connect(self)
|
||||
|
||||
def connectionLost(self, reason=None, step=1):
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
This is executed when the connection is lost for
|
||||
whatever reason.
|
||||
whatever reason. It can also be called directly, from
|
||||
the disconnect method
|
||||
"""
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.transport.loseConnection()
|
||||
|
||||
Closing the connection takes two steps
|
||||
|
||||
step 1 - is the default and is used when this method is
|
||||
called automatically. The method should then call self.session_disconnect().
|
||||
Step 2 - means this method is called from at_disconnect(). At this point
|
||||
the sessions are assumed to have been handled, and so the transport can close
|
||||
without further ado.
|
||||
"""
|
||||
if step == 1:
|
||||
self.session_disconnect()
|
||||
else:
|
||||
self.transport.loseConnection()
|
||||
|
||||
def getClientAddress(self):
|
||||
"""
|
||||
Returns the client's address and port in a tuple. For example
|
||||
('127.0.0.1', 41917)
|
||||
"""
|
||||
return self.transport.client
|
||||
|
||||
def lineReceived(self, string):
|
||||
"""
|
||||
Communication Player -> Evennia. Any line return indicates a
|
||||
command for the purpose of the MUD. So we take the user input
|
||||
and pass it on to the game engine.
|
||||
Telnet method called when data is coming in over the telnet
|
||||
connection. We pass it on to the game engine directly.
|
||||
"""
|
||||
self.at_data_in(string)
|
||||
self.sessionhandler.data_in(self, string)
|
||||
|
||||
def lineSend(self, string):
|
||||
"""
|
||||
Communication Evennia -> Player
|
||||
Any string sent should already have been
|
||||
properly formatted and processed
|
||||
before reaching this point.
|
||||
# Session hooks
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
self.sendLine(string) #this is the telnet-specific method for sending
|
||||
generic hook for the engine to call in order to
|
||||
disconnect this protocol.
|
||||
"""
|
||||
if reason:
|
||||
self.data_out(reason)
|
||||
self.connectionLost(reason)
|
||||
|
||||
# session-general method hooks
|
||||
|
||||
def at_connect(self):
|
||||
def data_out(self, string, data=None):
|
||||
"""
|
||||
Show the banner screen.
|
||||
generic hook method for engine to call in order to send data
|
||||
through the telnet connection.
|
||||
Data Evennia -> Player. 'data' argument is not used
|
||||
"""
|
||||
self.telnet_markup = True
|
||||
# show connection screen
|
||||
self.execute_cmd('look')
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
Called after authentication. self.logged_in=True at this point.
|
||||
"""
|
||||
if player.has_attribute('telnet_markup'):
|
||||
self.telnet_markup = player.get_attribute("telnet_markup")
|
||||
else:
|
||||
self.telnet_markup = True
|
||||
|
||||
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
|
||||
"""
|
||||
Disconnect from server
|
||||
"""
|
||||
char = self.get_character()
|
||||
if char:
|
||||
char.at_disconnect()
|
||||
self.at_data_out(reason)
|
||||
self.connectionLost(step=2)
|
||||
|
||||
def at_data_out(self, string, data=None):
|
||||
"""
|
||||
Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings.
|
||||
"""
|
||||
try:
|
||||
string = utils.to_str(string, encoding=self.encoding)
|
||||
except Exception, e:
|
||||
self.lineSend(str(e))
|
||||
return
|
||||
nomarkup = not self.telnet_markup
|
||||
raw = False
|
||||
if type(data) == dict:
|
||||
try:
|
||||
string = utils.to_str(string, encoding=self.encoding)
|
||||
except Exception, e:
|
||||
self.sendLine(str(e))
|
||||
return
|
||||
nomarkup = False
|
||||
raw = False
|
||||
if type(data) == dict:
|
||||
# check if we want escape codes to go through unparsed.
|
||||
raw = data.get("raw", self.telnet_markup)
|
||||
raw = data.get("raw", False)
|
||||
# check if we want to remove all markup
|
||||
nomarkup = data.get("nomarkup", not self.telnet_markup)
|
||||
if raw:
|
||||
self.lineSend(string)
|
||||
else:
|
||||
self.lineSend(ansi.parse_ansi(string, strip_ansi=nomarkup))
|
||||
|
||||
def at_data_in(self, string, data=None):
|
||||
"""
|
||||
Line from Player -> Evennia. 'data' argument is not used.
|
||||
|
||||
"""
|
||||
try:
|
||||
string = utils.to_unicode(string, encoding=self.encoding)
|
||||
self.execute_cmd(string)
|
||||
return
|
||||
except Exception, e:
|
||||
logger.log_errmsg(str(e))
|
||||
nomarkup = data.get("nomarkup", False)
|
||||
if raw:
|
||||
self.sendLine(string)
|
||||
else:
|
||||
self.sendLine(ansi.parse_ansi(string, strip_ansi=nomarkup))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ from django.conf import settings
|
|||
from src.utils import utils, logger, ansi
|
||||
from src.utils.text2html import parse_html
|
||||
from src.server import session
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
|
||||
SERVERNAME = settings.SERVERNAME
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
|
|
@ -55,7 +54,7 @@ def jsonify(obj):
|
|||
|
||||
class WebClient(resource.Resource):
|
||||
"""
|
||||
An ajax/comet long-polling transport protocol for
|
||||
An ajax/comet long-polling transport
|
||||
"""
|
||||
isLeaf = True
|
||||
allowedMethods = ('POST',)
|
||||
|
|
@ -95,24 +94,16 @@ class WebClient(resource.Resource):
|
|||
dataentries.append(jsonify({'msg':string, 'data':data}))
|
||||
self.databuffer[suid] = dataentries
|
||||
|
||||
def disconnect(self, suid, step=1):
|
||||
def client_disconnect(self, suid):
|
||||
"""
|
||||
Disconnect session with given suid.
|
||||
|
||||
step 1 : call session_disconnect()
|
||||
step 2 : finalize disconnection
|
||||
"""
|
||||
|
||||
if step == 1:
|
||||
sess = SESSIONS.session_from_suid(suid)
|
||||
sess[0].session_disconnect()
|
||||
else:
|
||||
if self.requests.has_key(suid):
|
||||
for request in self.requests.get(suid, []):
|
||||
request.finish()
|
||||
del self.requests[suid]
|
||||
if self.databuffer.has_key(suid):
|
||||
del self.databuffer[suid]
|
||||
if self.requests.has_key(suid):
|
||||
for request in self.requests.get(suid, []):
|
||||
request.finish()
|
||||
del self.requests[suid]
|
||||
if self.databuffer.has_key(suid):
|
||||
del self.databuffer[suid]
|
||||
|
||||
def mode_init(self, request):
|
||||
"""
|
||||
|
|
@ -133,7 +124,9 @@ class WebClient(resource.Resource):
|
|||
|
||||
sess = WebClientSession()
|
||||
sess.client = self
|
||||
sess.session_connect(remote_addr, suid)
|
||||
sess.init_session("comet", remote_addr, self.sessionhandler)
|
||||
sess.suid = suid
|
||||
sess.sessionhandler.connect(sess)
|
||||
return jsonify({'msg':host_string, 'suid':suid})
|
||||
|
||||
def mode_input(self, request):
|
||||
|
|
@ -144,11 +137,12 @@ class WebClient(resource.Resource):
|
|||
suid = request.args.get('suid', ['0'])[0]
|
||||
if suid == '0':
|
||||
return ''
|
||||
sess = SESSIONS.session_from_suid(suid)
|
||||
sess = self.sessionhandler.session_from_suid(suid)
|
||||
if sess:
|
||||
sess = sess[0]
|
||||
string = request.args.get('msg', [''])[0]
|
||||
data = request.args.get('data', [None])[0]
|
||||
sess[0].at_data_in(string, data)
|
||||
sess.sessionhandler.data_in(sess, string, data)
|
||||
return ''
|
||||
|
||||
def mode_receive(self, request):
|
||||
|
|
@ -179,7 +173,7 @@ class WebClient(resource.Resource):
|
|||
"""
|
||||
suid = request.args.get('suid', ['0'])[0]
|
||||
if suid == '0':
|
||||
self.disconnect(suid)
|
||||
self.client_disconnect(suid)
|
||||
return ''
|
||||
|
||||
def render_POST(self, request):
|
||||
|
|
@ -217,35 +211,16 @@ class WebClientSession(session.Session):
|
|||
"""
|
||||
This represents a session running in a webclient.
|
||||
"""
|
||||
|
||||
def at_connect(self):
|
||||
"""
|
||||
Show the banner screen.
|
||||
"""
|
||||
# show screen
|
||||
self.telnet_markup = True
|
||||
self.execute_cmd('look')
|
||||
|
||||
|
||||
def at_login(self, player):
|
||||
"""
|
||||
Called after authentication. self.logged_in=True at this point.
|
||||
"""
|
||||
if player.has_attribute('telnet_markup'):
|
||||
self.telnet_markup = player.get_attribute("telnet_markup")
|
||||
|
||||
def at_disconnect(self, reason=None):
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
Disconnect from server
|
||||
"""
|
||||
if reason:
|
||||
self.lineSend(self.suid, reason)
|
||||
char = self.get_character()
|
||||
if char:
|
||||
char.at_disconnect()
|
||||
self.client.disconnect(self.suid, step=2)
|
||||
self.client.client_disconnect(self.suid)
|
||||
|
||||
def at_data_out(self, string='', data=None):
|
||||
def data_out(self, string='', data=None):
|
||||
"""
|
||||
Data Evennia -> Player access hook.
|
||||
|
||||
|
|
@ -261,13 +236,13 @@ class WebClientSession(session.Session):
|
|||
try:
|
||||
string = utils.to_str(string, encoding=self.encoding)
|
||||
|
||||
nomarkup = not self.telnet_markup
|
||||
nomarkup = False
|
||||
raw = False
|
||||
if type(data) == dict:
|
||||
# check if we want escape codes to go through unparsed.
|
||||
raw = data.get("raw", self.telnet_markup)
|
||||
raw = data.get("raw", False)
|
||||
# check if we want to remove all markup
|
||||
nomarkup = data.get("nomarkup", not self.telnet_markup)
|
||||
nomarkup = data.get("nomarkup", False)
|
||||
if raw:
|
||||
self.client.lineSend(self.suid, string)
|
||||
else:
|
||||
|
|
@ -275,21 +250,3 @@ class WebClientSession(session.Session):
|
|||
return
|
||||
except Exception, e:
|
||||
logger.log_trace()
|
||||
|
||||
def at_data_in(self, string, data=None):
|
||||
"""
|
||||
Input from Player -> Evennia (called by client protocol).
|
||||
Use of 'data' is up to the client - server implementation.
|
||||
"""
|
||||
|
||||
# treat data?
|
||||
if data:
|
||||
pass
|
||||
|
||||
# the string part is identical to telnet
|
||||
try:
|
||||
string = utils.to_unicode(string, encoding=self.encoding)
|
||||
self.execute_cmd(string)
|
||||
return
|
||||
except Exception, e:
|
||||
logger.log_trace()
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ SRC_DIR = os.path.join(BASE_PATH, 'src')
|
|||
GAME_DIR = os.path.join(BASE_PATH, 'game')
|
||||
# Place to put log files
|
||||
LOG_DIR = os.path.join(GAME_DIR, 'logs')
|
||||
DEFAULT_LOG_FILE = os.path.join(LOG_DIR, 'evennia.log')
|
||||
SERVER_LOG_FILE = os.path.join(LOG_DIR, 'server.log')
|
||||
PORTAL_LOG_FILE = os.path.join(LOG_DIR, 'portal.log')
|
||||
# Where to log server requests to the web server. This is VERY spammy, so this
|
||||
# file should be removed at regular intervals.
|
||||
HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log')
|
||||
|
|
@ -111,7 +112,12 @@ IDLE_COMMAND = "idle"
|
|||
# Add sets for languages/regions your players are likely to use.
|
||||
# (see http://en.wikipedia.org/wiki/Character_encoding)
|
||||
ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
|
||||
|
||||
# The game server opens an AMP port so that the portal can
|
||||
# communicate with it. This is an internal functionality of Evennia, usually
|
||||
# operating between the two processes on the same machine. Don't change unless
|
||||
# you know what you are doing.
|
||||
AMP_HOST = 'localhost'
|
||||
AMP_PORT = 5000
|
||||
|
||||
###################################################
|
||||
# Evennia Database config
|
||||
|
|
@ -340,7 +346,7 @@ SESSION_COOKIE_NAME = 'sessionid'
|
|||
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = False
|
||||
USE_I18N = True
|
||||
# This should be turned off unless you want to do tests with Django's
|
||||
# development webserver (normally Evennia runs its own server)
|
||||
SERVE_MEDIA = False
|
||||
|
|
|
|||
|
|
@ -714,8 +714,9 @@ class TypedObject(SharedMemoryModel):
|
|||
infochan = Channel.objects.get_channel(infochan[0])
|
||||
if infochan:
|
||||
cname = infochan.key
|
||||
cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')])
|
||||
infochan.msg(message)
|
||||
cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n') if line])
|
||||
cmessage = cmessage.strip()
|
||||
infochan.msg(cmessage)
|
||||
else:
|
||||
# no mudinfo channel is found. Log instead.
|
||||
cmessage = "\n".join(["[NO MUDINFO CHANNEL]: %s" % line for line in message.split('\n')])
|
||||
|
|
@ -1102,8 +1103,7 @@ class TypedObject(SharedMemoryModel):
|
|||
"Stop accidental deletion."
|
||||
raise Exception("Cannot delete the ndb object!")
|
||||
ndb = property(ndb_get, ndb_set, ndb_del)
|
||||
|
||||
|
||||
|
||||
# Lock / permission methods
|
||||
|
||||
def access(self, accessing_obj, access_type='read', default=False):
|
||||
|
|
|
|||
81
src/utils/evennia-mode.el
Normal file
81
src/utils/evennia-mode.el
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
;
|
||||
; Emacs major mode for editing Evennia batch-command files (*.ev files).
|
||||
; Griatch 2011-09. Tested with GNU Emacs 23. Released under same license as Evennia.
|
||||
;
|
||||
; For batch-code files it's better to simply use the normal Python mode.
|
||||
;
|
||||
; Features:
|
||||
; Syntax hilighting
|
||||
; Auto-indenting properly when pressing <tab>.
|
||||
;
|
||||
; Installation:
|
||||
; - Copy this file, evennia-mode.el, to a location where emacs looks for plugins
|
||||
; (usually .emacs.d/ at least under Linux)
|
||||
; - If you don't have that directory, either look on the web for how to find it
|
||||
; or create it yourself - create a new directory .emacs.d/ some place and add
|
||||
; the following to emacs' configuration file (.emacs):
|
||||
; (add-to-list 'load-path "<PATH>/.emacs.d/")
|
||||
; where PATH is the place you created the directory. Now Emacs will know to
|
||||
; look here for plugins. Copy this file there.
|
||||
; - In emacs config file (.emacs), next add the following line:
|
||||
; (require 'evennia-mode)
|
||||
; - (re)start emacs
|
||||
; - Open a batch file with the ending *.ev. The mode will start automatically
|
||||
; (otherwise you can manually start it with M-x evennia-mode).
|
||||
;
|
||||
; Report bugs to evennia's issue tracker.
|
||||
;
|
||||
|
||||
(defvar evennia-mode-hook nil)
|
||||
|
||||
; Add keyboard shortcuts (not used)
|
||||
(defvar evennia-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map "\C-j" 'newline-and-indent)
|
||||
map)
|
||||
"Keymap for evennia major mode")
|
||||
|
||||
; Autoload this when .ev file opens.
|
||||
(add-to-list 'auto-mode-alist '("\\.ev\\'" . evennia-mode))
|
||||
|
||||
; Syntax hilighting
|
||||
(defconst evennia-font-lock-keywords
|
||||
(list
|
||||
'("^ *#.*" . font-lock-comment-face)
|
||||
'("^[^ |^#]*" . font-lock-variable-name-face))
|
||||
;'("^[^ #].*" . font-lock-variable-name-face)) ; more extreme hilight
|
||||
"Minimal highlighting for evennia ev files."
|
||||
)
|
||||
|
||||
; Auto-indentation
|
||||
(defun evennia-indent-line ()
|
||||
"Indent current line as batch-code"
|
||||
(interactive)
|
||||
(beginning-of-line)
|
||||
(if (looking-at "^ *#") ; a comment line
|
||||
(indent-line-to 0)
|
||||
(progn
|
||||
(forward-line -1) ; back up one line
|
||||
(if (looking-at "^ *#") ; previous line was comment
|
||||
(progn
|
||||
(forward-line)
|
||||
(indent-line-to 0))
|
||||
(progn
|
||||
(forward-line)
|
||||
(indent-line-to 1)))))
|
||||
)
|
||||
|
||||
; Register with Emacs system
|
||||
(defun evennia-mode ()
|
||||
"Major mode for editing Evennia batch-command files."
|
||||
(interactive)
|
||||
(kill-all-local-variables)
|
||||
(use-local-map evennia-mode-map)
|
||||
(set (make-local-variable 'indent-line-function) 'evennia-indent-line)
|
||||
(set (make-local-variable 'font-lock-defaults) '(evennia-font-lock-keywords))
|
||||
(setq major-mode 'evennia-mode)
|
||||
(setq mode-name "evennia")
|
||||
(run-hooks 'evennia-mode-hook)
|
||||
)
|
||||
|
||||
(provide 'evennia-mode)
|
||||
|
|
@ -1,658 +0,0 @@
|
|||
# MIT Licensed
|
||||
# Copyright (c) 2009-2010 Peter Shinners <pete@shinners.org>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without
|
||||
# restriction, including without limitation the rights to use,
|
||||
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the
|
||||
# Software is furnished to do so, subject to the following
|
||||
# conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""
|
||||
This module intends to be a full featured replacement for Python's reload
|
||||
function. It is targeted towards making a reload that works for Python
|
||||
plugins and extensions used by longer running applications.
|
||||
|
||||
Reimport currently supports Python 2.4 through 2.6.
|
||||
|
||||
By its very nature, this is not a completely solvable problem. The goal of
|
||||
this module is to make the most common sorts of updates work well. It also
|
||||
allows individual modules and package to assist in the process. A more
|
||||
detailed description of what happens is at
|
||||
http://code.google.com/p/reimport .
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ["reimport", "modified"]
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import gc
|
||||
import inspect
|
||||
import weakref
|
||||
import traceback
|
||||
import time
|
||||
|
||||
|
||||
|
||||
__version__ = "1.2"
|
||||
__author__ = "Peter Shinners <pete@shinners.org>"
|
||||
__license__ = "MIT"
|
||||
__url__ = "http://code.google.com/p/reimport"
|
||||
|
||||
|
||||
|
||||
_previous_scan_time = time.time() - 1.0
|
||||
_module_timestamps = {}
|
||||
|
||||
|
||||
# find the 'instance' old style type
|
||||
class _OldClass: pass
|
||||
_InstanceType = type(_OldClass())
|
||||
del _OldClass
|
||||
|
||||
|
||||
|
||||
def reimport(*modules):
|
||||
"""Reimport python modules. Multiple modules can be passed either by
|
||||
name or by reference. Only pure python modules can be reimported.
|
||||
|
||||
For advanced control, global variables can be placed in modules
|
||||
that allows finer control of the reimport process.
|
||||
|
||||
If a package module has a true value for "__package_reimport__"
|
||||
then that entire package will be reimported when any of its children
|
||||
packages or modules are reimported.
|
||||
|
||||
If a package module defines __reimported__ it must be a callable
|
||||
function that accepts one argument and returns a bool. The argument
|
||||
is the reference to the old version of that module before any
|
||||
cleanup has happend. The function should normally return True to
|
||||
allow the standard reimport cleanup. If the function returns false
|
||||
then cleanup will be disabled for only that module. Any exceptions
|
||||
raised during the callback will be handled by traceback.print_exc,
|
||||
similar to what happens with tracebacks in the __del__ method.
|
||||
"""
|
||||
__internal_swaprefs_ignore__ = "reimport"
|
||||
reloadSet = set()
|
||||
|
||||
if not modules:
|
||||
return
|
||||
|
||||
# Get names of all modules being reloaded
|
||||
for module in modules:
|
||||
name, target = _find_exact_target(module)
|
||||
if not target:
|
||||
raise ValueError("Module %r not found" % module)
|
||||
if not _is_code_module(target):
|
||||
raise ValueError("Cannot reimport extension, %r" % name)
|
||||
|
||||
reloadSet.update(_find_reloading_modules(name))
|
||||
|
||||
# Sort module names
|
||||
reloadNames = _package_depth_sort(reloadSet, False)
|
||||
|
||||
# Check for SyntaxErrors ahead of time. This won't catch all
|
||||
# possible SyntaxErrors or any other ImportErrors. But these
|
||||
# should be the most common problems, and now is the cleanest
|
||||
# time to abort.
|
||||
# I know this gets compiled again anyways. It could be
|
||||
# avoided with py_compile, but I will not be the creator
|
||||
# of messy .pyc files!
|
||||
for name in reloadNames:
|
||||
filename = getattr(sys.modules[name], "__file__", None)
|
||||
if not filename:
|
||||
continue
|
||||
pyname = os.path.splitext(filename)[0] + ".py"
|
||||
try:
|
||||
data = open(pyname, "rU").read() + "\n"
|
||||
except (IOError, OSError):
|
||||
continue
|
||||
|
||||
compile(data, pyname, "exec", 0, False) # Let this raise exceptions
|
||||
|
||||
# Move modules out of sys
|
||||
oldModules = {}
|
||||
for name in reloadNames:
|
||||
oldModules[name] = sys.modules.pop(name)
|
||||
ignores = (id(oldModules), id(__builtins__))
|
||||
prevNames = set(sys.modules)
|
||||
|
||||
# Python will munge the parent package on import. Remember original value
|
||||
parentPackageName = name.rsplit(".", 1)
|
||||
parentPackage = None
|
||||
parentPackageDeleted = lambda: None
|
||||
if len(parentPackageName) == 2:
|
||||
parentPackage = sys.modules.get(parentPackageName[0], None)
|
||||
parentValue = getattr(parentPackage, parentPackageName[1], parentPackageDeleted)
|
||||
|
||||
# Reimport modules, trying to rollback on exceptions
|
||||
try:
|
||||
for name in reloadNames:
|
||||
if name not in sys.modules:
|
||||
__import__(name)
|
||||
|
||||
except StandardError:
|
||||
# Try to dissolve any newly import modules and revive the old ones
|
||||
newNames = set(sys.modules) - prevNames
|
||||
newNames = _package_depth_sort(newNames, True)
|
||||
for name in newNames:
|
||||
_unimport_module(sys.modules[name], ignores)
|
||||
assert name not in sys.modules
|
||||
|
||||
sys.modules.update(oldModules)
|
||||
raise
|
||||
|
||||
newNames = set(sys.modules) - prevNames
|
||||
newNames = _package_depth_sort(newNames, True)
|
||||
|
||||
# Update timestamps for loaded time
|
||||
now = time.time() - 1.0
|
||||
for name in newNames:
|
||||
_module_timestamps[name] = (now, True)
|
||||
|
||||
# Fix Python automatically shoving of children into parent packages
|
||||
if parentPackage and parentValue:
|
||||
if parentValue == parentPackageDeleted:
|
||||
delattr(parentPackage, parentPackageName[1])
|
||||
else:
|
||||
setattr(parentPackage, parentPackageName[1], parentValue)
|
||||
parentValue = parentPackage = parentPackageDeleted = None
|
||||
|
||||
# Push exported namespaces into parent packages
|
||||
pushSymbols = {}
|
||||
for name in newNames:
|
||||
oldModule = oldModules.get(name)
|
||||
if not oldModule:
|
||||
continue
|
||||
parents = _find_parent_importers(name, oldModule, newNames)
|
||||
pushSymbols[name] = parents
|
||||
for name, parents in pushSymbols.iteritems():
|
||||
for parent in parents:
|
||||
oldModule = oldModules[name]
|
||||
newModule = sys.modules[name]
|
||||
_push_imported_symbols(newModule, oldModule, parent)
|
||||
# Rejigger the universe
|
||||
for name in newNames:
|
||||
old = oldModules.get(name)
|
||||
if not old:
|
||||
continue
|
||||
new = sys.modules[name]
|
||||
rejigger = True
|
||||
reimported = getattr(new, "__reimported__", None)
|
||||
if reimported:
|
||||
try:
|
||||
rejigger = reimported(old)
|
||||
except StandardError:
|
||||
# What else can we do? the callbacks must go on
|
||||
# Note, this is same as __del__ behaviour. /shrug
|
||||
traceback.print_exc()
|
||||
|
||||
if rejigger:
|
||||
_rejigger_module(old, new, ignores)
|
||||
else:
|
||||
_unimport_module(new, ignores)
|
||||
|
||||
|
||||
|
||||
def modified(path=None):
|
||||
"""Find loaded modules that have changed on disk under the given path.
|
||||
If no path is given then all modules are searched.
|
||||
"""
|
||||
global _previous_scan_time
|
||||
modules = []
|
||||
|
||||
if path:
|
||||
path = os.path.normpath(path) + os.sep
|
||||
|
||||
defaultTime = (_previous_scan_time, False)
|
||||
pycExt = __debug__ and ".pyc" or ".pyo"
|
||||
|
||||
for name, module in sys.modules.items():
|
||||
filename = _is_code_module(module)
|
||||
if not filename:
|
||||
continue
|
||||
|
||||
filename = os.path.normpath(filename)
|
||||
prevTime, prevScan = _module_timestamps.setdefault(name, defaultTime)
|
||||
if path and not filename.startswith(path):
|
||||
continue
|
||||
|
||||
# Get timestamp of .pyc if this is first time checking this module
|
||||
if not prevScan:
|
||||
pycName = os.path.splitext(filename)[0] + pycExt
|
||||
if pycName != filename:
|
||||
try:
|
||||
prevTime = os.path.getmtime(pycName)
|
||||
except OSError:
|
||||
pass
|
||||
_module_timestamps[name] = (prevTime, True)
|
||||
|
||||
# Get timestamp of source file
|
||||
try:
|
||||
diskTime = os.path.getmtime(filename)
|
||||
except OSError:
|
||||
diskTime = None
|
||||
|
||||
if diskTime is not None and prevTime < diskTime:
|
||||
modules.append(name)
|
||||
|
||||
_previous_scan_time = time.time()
|
||||
return modules
|
||||
|
||||
|
||||
|
||||
def _is_code_module(module):
|
||||
"""Determine if a module comes from python code"""
|
||||
# getsourcefile will not return "bare" pyc modules. we can reload those?
|
||||
try:
|
||||
return inspect.getsourcefile(module) or ""
|
||||
except TypeError:
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
def _find_exact_target(module):
|
||||
"""Given a module name or object, find the
|
||||
base module where reimport will happen."""
|
||||
# Given a name or a module, find both the name and the module
|
||||
actualModule = sys.modules.get(module)
|
||||
if actualModule is not None:
|
||||
name = module
|
||||
else:
|
||||
for name, mod in sys.modules.iteritems():
|
||||
if mod is module:
|
||||
actualModule = module
|
||||
break
|
||||
else:
|
||||
return "", None
|
||||
|
||||
# Find highest level parent package that has package_reimport magic
|
||||
parentName = name
|
||||
while True:
|
||||
splitName = parentName.rsplit(".", 1)
|
||||
if len(splitName) <= 1:
|
||||
return name, actualModule
|
||||
parentName = splitName[0]
|
||||
|
||||
parentModule = sys.modules.get(parentName)
|
||||
if getattr(parentModule, "__package_reimport__", None):
|
||||
name = parentName
|
||||
actualModule = parentModule
|
||||
|
||||
|
||||
|
||||
def _find_reloading_modules(name):
|
||||
"""Find all modules that will be reloaded from given name"""
|
||||
modules = [name]
|
||||
childNames = name + "."
|
||||
for name in sys.modules.keys():
|
||||
if name.startswith(childNames) and _is_code_module(sys.modules[name]):
|
||||
modules.append(name)
|
||||
return modules
|
||||
|
||||
|
||||
|
||||
def _package_depth_sort(names, reverse):
|
||||
"""Sort a list of module names by their package depth"""
|
||||
def packageDepth(name):
|
||||
return name.count(".")
|
||||
return sorted(names, key=packageDepth, reverse=reverse)
|
||||
|
||||
|
||||
|
||||
def _find_module_exports(module):
|
||||
allNames = getattr(module, "__all__", ())
|
||||
if not allNames:
|
||||
allNames = [n for n in dir(module) if n[0] != "_"]
|
||||
return set(allNames)
|
||||
|
||||
|
||||
|
||||
def _find_parent_importers(name, oldModule, newNames):
|
||||
"""Find parents of reimported module that have all exported symbols"""
|
||||
parents = []
|
||||
|
||||
# Get exported symbols
|
||||
exports = _find_module_exports(oldModule)
|
||||
if not exports:
|
||||
return parents
|
||||
|
||||
# Find non-reimported parents that have all old symbols
|
||||
parent = name
|
||||
while True:
|
||||
names = parent.rsplit(".", 1)
|
||||
if len(names) <= 1:
|
||||
break
|
||||
parent = names[0]
|
||||
if parent in newNames:
|
||||
continue
|
||||
parentModule = sys.modules[parent]
|
||||
if not exports - set(dir(parentModule)):
|
||||
parents.append(parentModule)
|
||||
|
||||
return parents
|
||||
|
||||
|
||||
def _push_imported_symbols(newModule, oldModule, parent):
|
||||
"""Transfer changes symbols from a child module to a parent package"""
|
||||
# This assumes everything in oldModule is already found in parent
|
||||
oldExports = _find_module_exports(oldModule)
|
||||
newExports = _find_module_exports(newModule)
|
||||
|
||||
# Delete missing symbols
|
||||
for name in oldExports - newExports:
|
||||
delattr(parent, name)
|
||||
|
||||
# Add new symbols
|
||||
for name in newExports - oldExports:
|
||||
setattr(parent, name, getattr(newModule, name))
|
||||
|
||||
# Update existing symbols
|
||||
for name in newExports & oldExports:
|
||||
oldValue = getattr(oldModule, name)
|
||||
if getattr(parent, name) is oldValue:
|
||||
setattr(parent, name, getattr(newModule, name))
|
||||
|
||||
|
||||
|
||||
# To rejigger is to copy internal values from new to old
|
||||
# and then to swap external references from old to new
|
||||
|
||||
|
||||
def _rejigger_module(old, new, ignores):
|
||||
"""Mighty morphin power modules"""
|
||||
__internal_swaprefs_ignore__ = "rejigger_module"
|
||||
oldVars = vars(old)
|
||||
newVars = vars(new)
|
||||
ignores += (id(oldVars),)
|
||||
old.__doc__ = new.__doc__
|
||||
|
||||
# Get filename used by python code
|
||||
filename = new.__file__
|
||||
|
||||
for name, value in newVars.iteritems():
|
||||
if name in oldVars:
|
||||
oldValue = oldVars[name]
|
||||
if oldValue is value:
|
||||
continue
|
||||
|
||||
if _from_file(filename, value):
|
||||
if inspect.isclass(value):
|
||||
_rejigger_class(oldValue, value, ignores)
|
||||
|
||||
elif inspect.isfunction(value):
|
||||
_rejigger_func(oldValue, value, ignores)
|
||||
|
||||
setattr(old, name, value)
|
||||
|
||||
for name in oldVars.keys():
|
||||
if name not in newVars:
|
||||
value = getattr(old, name)
|
||||
delattr(old, name)
|
||||
if _from_file(filename, value):
|
||||
if inspect.isclass(value) or inspect.isfunction(value):
|
||||
_remove_refs(value, ignores)
|
||||
|
||||
_swap_refs(old, new, ignores)
|
||||
|
||||
|
||||
|
||||
def _from_file(filename, value):
|
||||
"""Test if object came from a filename, works for pyc/py confusion"""
|
||||
try:
|
||||
objfile = inspect.getsourcefile(value)
|
||||
except TypeError:
|
||||
return False
|
||||
return bool(objfile) and objfile.startswith(filename)
|
||||
|
||||
|
||||
|
||||
def _rejigger_class(old, new, ignores):
|
||||
"""Mighty morphin power classes"""
|
||||
__internal_swaprefs_ignore__ = "rejigger_class"
|
||||
oldVars = vars(old)
|
||||
newVars = vars(new)
|
||||
ignores += (id(oldVars),)
|
||||
|
||||
for name, value in newVars.iteritems():
|
||||
if name in ("__dict__", "__doc__", "__weakref__"):
|
||||
continue
|
||||
|
||||
if name in oldVars:
|
||||
oldValue = oldVars[name]
|
||||
if oldValue is value:
|
||||
continue
|
||||
|
||||
if inspect.isclass(value) and value.__module__ == new.__module__:
|
||||
_rejigger_class(oldValue, value, ignores)
|
||||
|
||||
elif inspect.isfunction(value):
|
||||
_rejigger_func(oldValue, value, ignores)
|
||||
|
||||
setattr(old, name, value)
|
||||
|
||||
for name in oldVars.keys():
|
||||
if name not in newVars:
|
||||
value = getattr(old, name)
|
||||
delattr(old, name)
|
||||
_remove_refs(value, ignores)
|
||||
|
||||
_swap_refs(old, new, ignores)
|
||||
|
||||
|
||||
|
||||
def _rejigger_func(old, new, ignores):
|
||||
"""Mighty morphin power functions"""
|
||||
__internal_swaprefs_ignore__ = "rejigger_func"
|
||||
old.func_code = new.func_code
|
||||
old.func_doc = new.func_doc
|
||||
old.func_defaults = new.func_defaults
|
||||
old.func_dict = new.func_dict
|
||||
_swap_refs(old, new, ignores)
|
||||
|
||||
|
||||
|
||||
def _unimport_module(old, ignores):
|
||||
"""Remove traces of a module"""
|
||||
__internal_swaprefs_ignore__ = "unimport_module"
|
||||
oldValues = vars(old).values()
|
||||
ignores += (id(oldValues),)
|
||||
|
||||
# Get filename used by python code
|
||||
filename = old.__file__
|
||||
fileext = os.path.splitext(filename)
|
||||
if fileext in (".pyo", ".pyc", ".pyw"):
|
||||
filename = filename[:-1]
|
||||
|
||||
for value in oldValues:
|
||||
try: objfile = inspect.getsourcefile(value)
|
||||
except TypeError: objfile = ""
|
||||
|
||||
if objfile == filename:
|
||||
if inspect.isclass(value):
|
||||
_unimport_class(value, ignores)
|
||||
|
||||
elif inspect.isfunction(value):
|
||||
_remove_refs(value, ignores)
|
||||
|
||||
_remove_refs(old, ignores)
|
||||
|
||||
|
||||
|
||||
def _unimport_class(old, ignores):
|
||||
"""Remove traces of a class"""
|
||||
__internal_swaprefs_ignore__ = "unimport_class"
|
||||
oldItems = vars(old).items()
|
||||
ignores += (id(oldItems),)
|
||||
|
||||
for name, value in oldItems:
|
||||
if name in ("__dict__", "__doc__", "__weakref__"):
|
||||
continue
|
||||
|
||||
if inspect.isclass(value) and value.__module__ == old.__module__:
|
||||
_unimport_class(value, ignores)
|
||||
|
||||
elif inspect.isfunction(value):
|
||||
_remove_refs(value, ignores)
|
||||
|
||||
_remove_refs(old, ignores)
|
||||
|
||||
|
||||
|
||||
_recursive_tuple_swap = set()
|
||||
|
||||
|
||||
def _bonus_containers():
|
||||
"""Find additional container types, if they are loaded. Returns
|
||||
(deque, defaultdict).
|
||||
Any of these will be None if not loaded.
|
||||
"""
|
||||
deque = defaultdict = None
|
||||
collections = sys.modules.get("collections", None)
|
||||
if collections:
|
||||
deque = getattr(collections, "collections", None)
|
||||
defaultdict = getattr(collections, "defaultdict", None)
|
||||
return deque, defaultdict
|
||||
|
||||
|
||||
|
||||
def _find_sequence_indices(container, value):
|
||||
"""Find indices of value in container. The indices will
|
||||
be in reverse order, to allow safe editing.
|
||||
"""
|
||||
indices = []
|
||||
for i in range(len(container)-1, -1, -1):
|
||||
if container[i] is value:
|
||||
indices.append(i)
|
||||
return indices
|
||||
|
||||
|
||||
def _swap_refs(old, new, ignores):
|
||||
"""Swap references from one object to another"""
|
||||
__internal_swaprefs_ignore__ = "swap_refs"
|
||||
# Swap weak references
|
||||
refs = weakref.getweakrefs(old)
|
||||
if refs:
|
||||
try:
|
||||
newRef = weakref.ref(new)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
for oldRef in refs:
|
||||
_swap_refs(oldRef, newRef, ignores + (id(refs),))
|
||||
del refs
|
||||
|
||||
deque, defaultdict = _bonus_containers()
|
||||
|
||||
# Swap through garbage collector
|
||||
referrers = gc.get_referrers(old)
|
||||
for container in referrers:
|
||||
if id(container) in ignores:
|
||||
continue
|
||||
containerType = type(container)
|
||||
|
||||
if containerType is list or containerType is deque:
|
||||
for index in _find_sequence_indices(container, old):
|
||||
container[index] = new
|
||||
|
||||
elif containerType is tuple:
|
||||
# protect from recursive tuples
|
||||
orig = container
|
||||
if id(orig) in _recursive_tuple_swap:
|
||||
continue
|
||||
_recursive_tuple_swap.add(id(orig))
|
||||
try:
|
||||
container = list(container)
|
||||
for index in _find_sequence_indices(container, old):
|
||||
container[index] = new
|
||||
container = tuple(container)
|
||||
_swap_refs(orig, container, ignores + (id(referrers),))
|
||||
finally:
|
||||
_recursive_tuple_swap.remove(id(orig))
|
||||
|
||||
elif containerType is dict or containerType is defaultdict:
|
||||
if "__internal_swaprefs_ignore__" not in container:
|
||||
try:
|
||||
if old in container:
|
||||
container[new] = container.pop(old)
|
||||
except TypeError: # Unhashable old value
|
||||
pass
|
||||
for k,v in container.iteritems():
|
||||
if v is old:
|
||||
container[k] = new
|
||||
|
||||
elif containerType is set:
|
||||
container.remove(old)
|
||||
container.add(new)
|
||||
|
||||
elif containerType is type:
|
||||
if old in container.__bases__:
|
||||
bases = list(container.__bases__)
|
||||
bases[bases.index(old)] = new
|
||||
container.__bases__ = tuple(bases)
|
||||
|
||||
elif type(container) is old:
|
||||
container.__class__ = new
|
||||
|
||||
elif containerType is _InstanceType:
|
||||
if container.__class__ is old:
|
||||
container.__class__ = new
|
||||
|
||||
|
||||
|
||||
def _remove_refs(old, ignores):
|
||||
"""Remove references to a discontinued object"""
|
||||
__internal_swaprefs_ignore__ = "remove_refs"
|
||||
|
||||
# Ignore builtin immutables that keep no other references
|
||||
if old is None or isinstance(old, (int, basestring, float, complex)):
|
||||
return
|
||||
|
||||
deque, defaultdict = _bonus_containers()
|
||||
|
||||
# Remove through garbage collector
|
||||
for container in gc.get_referrers(old):
|
||||
if id(container) in ignores:
|
||||
continue
|
||||
containerType = type(container)
|
||||
|
||||
if containerType is list or containerType is deque:
|
||||
for index in _find_sequence_indices(container, old):
|
||||
del container[index]
|
||||
|
||||
elif containerType is tuple:
|
||||
orig = container
|
||||
container = list(container)
|
||||
for index in _find_sequence_indices(container, old):
|
||||
del container[index]
|
||||
container = tuple(container)
|
||||
_swap_refs(orig, container, ignores)
|
||||
|
||||
elif containerType is dict or containerType is defaultdict:
|
||||
if "__internal_swaprefs_ignore__" not in container:
|
||||
try:
|
||||
container.pop(old, None)
|
||||
except TypeError: # Unhashable old value
|
||||
pass
|
||||
for k,v in container.items():
|
||||
if v is old:
|
||||
del container[k]
|
||||
|
||||
elif containerType is set:
|
||||
container.remove(old)
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
"""
|
||||
This holds the mechanism for reloading the game modules on the
|
||||
fly. It's in this separate module since it's not a good idea to
|
||||
keep it in server.py since it messes with importing, and it's
|
||||
also not good to tie such important functionality to a user-definable
|
||||
command class.
|
||||
"""
|
||||
|
||||
import time
|
||||
from django.db.models.loading import AppCache
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.conf import settings
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.objects.models import ObjectDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.comms.models import Channel, Msg
|
||||
from src.help.models import HelpEntry
|
||||
|
||||
from src.typeclasses import models as typeclassmodels
|
||||
from src.comms import channelhandler
|
||||
from src.comms.models import Channel
|
||||
from src.utils import reimport, utils, logger
|
||||
|
||||
def start_reload_loop():
|
||||
"""
|
||||
This starts the asynchronous reset loop. While
|
||||
important that it runs asynchronously (to not block the
|
||||
mud while its running), the order at which things are
|
||||
updated does matter.
|
||||
"""
|
||||
|
||||
def run_loop():
|
||||
""
|
||||
cemit_info('-'*50)
|
||||
cemit_info(" Starting asynchronous server reload.")
|
||||
reload_modules()
|
||||
reload_scripts()
|
||||
reload_commands()
|
||||
reset_loop()
|
||||
|
||||
def at_return(r):
|
||||
"default callback"
|
||||
cemit_info(" Asynchronous server reload finished.\n" + '-'*50)
|
||||
def at_err(e):
|
||||
"error callback"
|
||||
string = " Reload: Asynchronous reset exited with an error:\n {r%s{n" % e.getErrorMessage()
|
||||
cemit_info(string)
|
||||
|
||||
utils.run_async(run_loop, at_return, at_err)
|
||||
|
||||
|
||||
def reload_modules():
|
||||
"""
|
||||
Reload modules that don't have any variables that can be reset.
|
||||
Note that python reloading is a tricky art and strange things have
|
||||
been known to happen if debugging and reloading a lot. A server
|
||||
cold reboot is often needed eventually.
|
||||
|
||||
"""
|
||||
# We protect e.g. src/ from reload since reloading it in a running
|
||||
# server can create unexpected results (and besides, non-evennia devs
|
||||
# should never need to do that anyway). Updating src requires a server
|
||||
# reboot. Modules in except_dirs are considered ok to reload despite being
|
||||
# inside src/
|
||||
protected_dirs = ('src.', 'django.', 'twisted.') # note that these MUST be tuples!
|
||||
except_dirs = ('src.commands.default.',) # "
|
||||
|
||||
# flag 'dangerous' typeclasses (those which retain a memory
|
||||
# reference, notably Scripts with a timer component) for
|
||||
# non-reload, since these cannot be safely cleaned from memory
|
||||
# without causing havoc. A server reboot is required for updating
|
||||
# these (or killing all running, timed scripts).
|
||||
unsafe_modules = []
|
||||
for scriptobj in ScriptDB.objects.get_all_scripts():
|
||||
if (scriptobj.interval > -1) and scriptobj.typeclass_path:
|
||||
unsafe_modules.append(scriptobj.typeclass_path)
|
||||
unsafe_modules = list(set(unsafe_modules))
|
||||
|
||||
def safe_dir_to_reload(modpath):
|
||||
"Check so modpath is not a subdir of a protected dir, and not an ok exception"
|
||||
return not any(modpath.startswith(pdir) and not any(modpath.startswith(edir) for edir in except_dirs) for pdir in protected_dirs)
|
||||
def safe_mod_to_reload(modpath):
|
||||
"Check so modpath is not in an unsafe module"
|
||||
return not any(mpath.startswith(modpath) for mpath in unsafe_modules)
|
||||
|
||||
#cemit_info(" Cleaning module caches ...")
|
||||
|
||||
# clean as much of the caches as we can
|
||||
cache = AppCache()
|
||||
cache.app_store = SortedDict()
|
||||
#cache.app_models = SortedDict() # cannot clean this, it resets ContentTypes!
|
||||
cache.app_errors = {}
|
||||
cache.handled = {}
|
||||
cache.loaded = False
|
||||
|
||||
# find modified modules
|
||||
modified = reimport.modified()
|
||||
safe_dir_modified = [mod for mod in modified if safe_dir_to_reload(mod)]
|
||||
unsafe_dir_modified = [mod for mod in modified if mod not in safe_dir_modified]
|
||||
safe_modified = [mod for mod in safe_dir_modified if safe_mod_to_reload(mod)]
|
||||
unsafe_mod_modified = [mod for mod in safe_dir_modified if mod not in safe_modified]
|
||||
|
||||
string = ""
|
||||
if unsafe_dir_modified or unsafe_mod_modified:
|
||||
|
||||
if unsafe_mod_modified:
|
||||
string += "\n {rModules containing Script classes with a timer component{n"
|
||||
string += "\n {rand which has already spawned instances cannot be reloaded safely.{n"
|
||||
string += "\n {rThese module(s) can only be reloaded by server reboot:{n\n %s\n"
|
||||
string = string % ", ".join(unsafe_dir_modified + unsafe_mod_modified)
|
||||
|
||||
if string:
|
||||
cemit_info(string)
|
||||
|
||||
if safe_modified:
|
||||
cemit_info(" Reloading safe module(s):{n\n %s" % "\n ".join(safe_modified))
|
||||
reimport.reimport(*safe_modified)
|
||||
wait_time = 5
|
||||
cemit_info(" Waiting %s secs to give modules time to re-cache ..." % wait_time)
|
||||
time.sleep(wait_time)
|
||||
cemit_info(" ... all safe modules reloaded.")
|
||||
else:
|
||||
cemit_info(" No modules reloaded.")
|
||||
|
||||
# clean out cache dictionary of typeclasses, exits and channels
|
||||
channelhandler.CHANNELHANDLER.update()
|
||||
|
||||
# run through all objects in database, forcing re-caching.
|
||||
|
||||
|
||||
def reload_scripts(scripts=None, obj=None, key=None, dbref=None):
|
||||
"""
|
||||
Run a validation of the script database.
|
||||
obj - only validate scripts on this object
|
||||
key - only validate scripts with this key
|
||||
dbref - only validate the script with this unique idref
|
||||
emit_to_obj - which object to receive error message
|
||||
|
||||
"""
|
||||
|
||||
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts,
|
||||
obj=obj, key=key,
|
||||
dbref=dbref,
|
||||
init_mode=False)
|
||||
if nr_started or nr_stopped:
|
||||
string = " Started %s script(s). Stopped %s invalid script(s)." % \
|
||||
(nr_started, nr_stopped)
|
||||
cemit_info(string)
|
||||
|
||||
def reload_commands():
|
||||
from src.commands import cmdsethandler
|
||||
cmdsethandler.CACHED_CMDSETS = {}
|
||||
#cemit_info(" Cleaned cmdset cache.")
|
||||
|
||||
def reset_loop():
|
||||
"Reload and restart all entities that can be reloaded."
|
||||
# run the reset loop on all objects
|
||||
cemit_info(" Resetting all cached database entities ...")
|
||||
t1 = time.time()
|
||||
[h.locks.reset() for h in HelpEntry.objects.all()]
|
||||
[m.locks.reset() for m in Msg.objects.all()]
|
||||
[c.locks.reset() for c in Channel.objects.all()]
|
||||
[s.locks.reset() for s in ScriptDB.objects.all()]
|
||||
[(o.typeclass(o), o.cmdset.reset(), o.locks.reset(), o.at_cache()) for o in ObjectDB.get_all_cached_instances()]
|
||||
[(p.typeclass(p), p.cmdset.reset(), p.locks.reset()) for p in PlayerDB.get_all_cached_instances()]
|
||||
|
||||
t2 = time.time()
|
||||
cemit_info(" ... Reset finished in %g seconds." % (t2-t1))
|
||||
|
||||
def cemit_info(message):
|
||||
"""
|
||||
Sends the info to a pre-set channel. This channel is
|
||||
set by CHANNEL_MUDINFO in settings.
|
||||
"""
|
||||
|
||||
logger.log_infomsg(message)
|
||||
infochan = None
|
||||
try:
|
||||
infochan = settings.CHANNEL_MUDINFO
|
||||
infochan = Channel.objects.get_channel(infochan[0])
|
||||
except Exception:
|
||||
pass
|
||||
if infochan:
|
||||
cname = infochan.key
|
||||
cmessage = "\n".join(["[%s][reload]: %s" % (cname, line) for line in message.split('\n')])
|
||||
infochan.msg(cmessage)
|
||||
else:
|
||||
cmessage = "\n".join(["[MUDINFO][reload] %s" % line for line in message.split('\n')])
|
||||
logger.log_infomsg(cmessage)
|
||||
|
|
@ -3,6 +3,8 @@ General helper functions that don't fit neatly under any given category.
|
|||
|
||||
They provide some useful string and conversion methods that might
|
||||
be of use when designing your own game.
|
||||
|
||||
|
||||
"""
|
||||
import os, sys, imp
|
||||
import textwrap
|
||||
|
|
@ -475,6 +477,7 @@ def check_evennia_dependencies():
|
|||
twisted_min = '10.0'
|
||||
django_min = '1.2'
|
||||
south_min = '0.7'
|
||||
nt_stop_python_min = '2.7'
|
||||
|
||||
errstring = ""
|
||||
no_error = True
|
||||
|
|
@ -483,7 +486,9 @@ def check_evennia_dependencies():
|
|||
pversion = ".".join([str(num) for num in sys.version_info if type(num) == int])
|
||||
if pversion < python_min:
|
||||
errstring += "\n WARNING: Python %s used. Evennia recommends version %s or higher (but not 3.x)." % (pversion, python_min)
|
||||
no_error = False
|
||||
if os.name == 'nt' and pversion < nt_stop_python_min:
|
||||
errstring += "\n WARNING: Windows requires Python %s or higher in order to restart/stop the server from the command line."
|
||||
errstring += "\n (You need to restart/stop from inside the game.)" % nt_stop_python_min
|
||||
# Twisted
|
||||
try:
|
||||
import twisted
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue