Reshuffling the Evennia package into the new template paradigm.

This commit is contained in:
Griatch 2015-01-06 14:53:45 +01:00
parent 2846e64833
commit 2b3a32e447
371 changed files with 17250 additions and 304 deletions

747
bin/evennia.py Executable file
View file

@ -0,0 +1,747 @@
#!/usr/bin/env python
"""
EVENNIA SERVER STARTUP SCRIPT
This is the start point for running Evennia.
Sets the appropriate environmental variables and launches the server
and portal through the runner. Run without arguments to get a
menu. Run the script with the -h flag to see usage information.
Usage:
evennia init <path> - creates a new game location, sets up a custom
settings file and copies all templates to <path>
evennia [settings][options] - handles server start/stop/restart if called
from the game folder. Can be called outside
the game folder if called with the path
to the settings file.
"""
import os
import sys
import signal
import shutil
import importlib
import django
from argparse import ArgumentParser
from subprocess import Popen
from django.core import management
# Signal processing
SIG = signal.SIGINT
# Set up the main python paths to Evennia
EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
EVENNIA_BIN = os.path.join(EVENNIA_ROOT, "bin")
EVENNIA_LIB = os.path.join(EVENNIA_ROOT, "lib")
EVENNIA_RUNNER = os.path.join(EVENNIA_BIN, "runner.py")
EVENNIA_TEMPLATE = os.path.join(EVENNIA_ROOT, "game_template")
EVENNIA_VERSION = "Unknown"
TWISTED_BINARY = "twistd"
# Game directory structure
SETTINGFILE = "settings.py"
SERVERDIR = "server"
CONFDIR = os.path.join(SERVERDIR, "conf")
SETTINGS_PATH = os.path.join(CONFDIR, SETTINGFILE)
SETTINGS_DOTPATH = "server.conf.settings"
CURRENT_DIR = os.getcwd()
GAMEDIR = CURRENT_DIR
# Operational setup
SERVER_LOGFILE = None
PORTAL_LOGFILE = None
SERVER_PIDFILE = None
PORTAL_PIDFILE = None
SERVER_RESTART = None
PORTAL_RESTART = None
SERVER_PY_FILE = None
PORTAL_PY_FILE = None
# add Evennia root and bin dir to PYTHONPATH
sys.path.insert(0, EVENNIA_ROOT)
#------------------------------------------------------------
#
# Messages
#
#------------------------------------------------------------
WELCOME_MESSAGE = \
"""
Welcome to Evennia!
No previous setting file was found so we created a fresh
settings.py file for you. No database was created. You may edit
the settings file now if you like, but you don't have to touch
anything if you just want to quickly get started.
Once you are ready to continue, run evennia.py again to
initialize the database and a third time to start the server.
The first time the server starts it will set things up for you.
Make sure to create a superuser when asked. The superuser's
email-address does not have to exist.
"""
WARNING_RUNSERVER = \
"""
WARNING: There is no need to run the Django development
webserver to test out Evennia web features (the web client
will in fact not work since the Django test server knows
nothing about MUDs). Instead, just start Evennia with the
webserver component active (this is the default).
"""
ERROR_SETTINGS = \
"""
ERROR: Could not import the file {settingsfile} from {settingspath}.
There are usually two reasons for this:
1) The settings file is a normal Python module. It may contain a syntax error.
Resolve the problem and try again.
2) Django is not correctly installed. This usually shows by errors involving
'DJANGO_SETTINGS_MODULE'. If you run a virtual machine, it might be worth to restart it
to see if this resolves the issue.
""".format(settingsfile=SETTINGFILE, settingspath=SETTINGS_PATH)
ERROR_DATABASE = \
"""
Your database does not seem to be set up correctly.
(error was '{traceback}')
Try to run
python evennia.py
to initialize the database according to your settings.
"""
ERROR_WINDOWS_WIN32API = \
"""
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
"""
INFO_WINDOWS_BATFILE = \
"""
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:
%(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
are ready to start the server.
"""
CMDLINE_HELP = \
"""
Main Evennia launcher. When starting in interactive (-i) mode, only
the Server will do so since this is the most commonly useful setup. To
activate interactive mode also for the Portal, use the menu or launch
the two services one after the other as two separate calls to this
program.
"""
VERSION_INFO = \
"""
Evennia {version}
{about}
OS: {os}
Python: {python}
Twisted: {twisted}
Django: {django}
"""
ABOUT_INFO= \
"""
Evennia MUD/MUX/MU* development system
Licence: BSD 3-Clause Licence
Web: http://www.evennia.com
Irc: #evennia on FreeNode
Forum: http://www.evennia.com/discussions
Maintainer (2010-): Griatch (griatch AT gmail DOT com)
Maintainer (2006-10): Greg Taylor
"""
HELP_ENTRY = \
"""
See python evennia.py -h for controlling Evennia directly from
the command line.
Evennia has two parts that both must run:
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.
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.
Use option (1) in a production environment. During development (2) is
usually enough, portal debugging is usually only useful if you are
adding new protocols or are debugging an Evennia bug.
Reload with (5) to update the server with your changes without
disconnecting any players.
Reload and stop are sometimes poorly supported in Windows. If you have
issues, log into the game to stop or restart the server instead.
"""
MENU = \
"""
+----Evennia Launcher-------------------------------------------------------+
| |
+--- Starting --------------------------------------------------------------+
| |
| 1) (default): All output to logfiles. |
| 2) (game debug): Server outputs to terminal instead of to logfile. |
| 3) (portal debug): Portal outputs to terminal instead of to logfile. |
| 4) (full debug): Both Server and Portal output to terminal |
| |
+--- Restarting ------------------------------------------------------------+
| |
| 5) Reload the Server |
| 6) Reload the Portal (only works with portal/full debug) |
| |
+--- Stopping --------------------------------------------------------------+
| |
| 7) Stopping both Portal and Server. |
| 8) Stopping only Server. |
| 9) Stopping only Portal. |
| |
+---------------------------------------------------------------------------+
| h) Help i) About info q) Abort |
+---------------------------------------------------------------------------+
"""
#------------------------------------------------------------
#
# Functions
#
#------------------------------------------------------------
def evennia_version():
"""
Get the Evennia version info from the main package.
"""
version = "Unknown"
with open(os.path.join(EVENNIA_ROOT, "VERSION.txt"), 'r') as f:
version = f.read().strip()
try:
version = "%s(GIT %s)" % (version, os.popen("git rev-parse --short HEAD").read().strip())
except IOError:
pass
return version
def init_game_directory(path):
"""
Try to analyze the given path to find settings.py - this defines
the game directory and also sets PYTHONPATH as well as the
django path.
"""
global GAMEDIR
if os.path.exists(os.path.join(path, SETTINGFILE)):
# path given to server/conf/
GAMEDIR = os.path.dirname(os.path.dirname(os.path.dirname(path)))
elif os.path.exists(SETTINGS_PATH):
# path given to somewhere else in gamedir
GAMEDIR = os.path.dirname(os.path.dirname(path))
else:
# Assume path given to root game dir
GAMEDIR = path
# set pythonpath to gamedir
sys.path.insert(0, GAMEDIR)
# set the settings location
os.environ['DJANGO_SETTINGS_MODULE'] = SETTINGS_DOTPATH
# required since django1.7.
django.setup()
# check all dependencies
from evennia.utils.utils import check_evennia_dependencies
if not check_evennia_
# test existence of settings module
try:
settings = importlib.import_module(SETTINGS_DOTPATH)
except Exception:
import traceback
print "\n" + traceback.format_exc()
print ERROR_SETTINGS
sys.exit()
# set up the Evennia executables and log file locations
global SERVER_PY_FILE, PORTAL_PY_FILE
global SERVER_LOGFILE, PORTAL_LOGFILE
global SERVER_PIDFILE, PORTAL_PIDFILE
global SERVER_RESTART, PORTAL_RESTART
global EVENNIA_VERSION
SERVER_PY_FILE = os.path.join(settings.LIB_DIR, "server/server.py")
PORTAL_PY_FILE = os.path.join(settings.LIB_DIR, "portal/server.py")
SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid")
PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid")
SERVER_RESTART = os.path.join(GAMEDIR, SERVERDIR, "server.restart")
PORTAL_RESTART = os.path.join(GAMEDIR, SERVERDIR, "portal.restart")
SERVER_LOGFILE = settings.SERVER_LOG_FILE
PORTAL_LOGFILE = settings.PORTAL_LOG_FILE
# This also tests the library access
from evennia.utils.utils import get_evennia_version
EVENNIA_VERSION = get_evennia_version()
# set up twisted
if os.name == 'nt':
# We need to handle Windows twisted separately. We create a
# batchfile in game/server, linking to the actual binary
global TWISTED_BINARY
TWISTED_BINARY = "twistd.bat"
# add path so system can find the batfile
sys.path.insert(0, os.path.join(GAMEDIR, SERVERDIR))
try:
importlib.import_module("win32api")
except ImportError:
print ERROR_WINDOWS_WIN32API
sys.exit()
if not os.path.exists(os.path.join(EVENNIA_BIN, TWISTED_BINARY)):
# 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.
twistd = importlib.import_module("twisted.scripts.twistd")
twistd_dir = os.path.dirname(twistd.__file__)
# note that we hope the twistd package won't change here, since we
# try to get to the executable by relative path.
twistd_path = os.path.abspath(os.path.join(twistd_dir,
os.pardir, os.pardir, os.pardir, os.pardir,
'scripts', 'twistd.py'))
with open('twistd.bat', 'w') as bat_file:
# build a custom bat file for windows
bat_file.write("@\"%s\" \"%s\" %%*" % (sys.executable, twistd_path))
print INFO_WINDOWS_BATFILE.format(twistd_path=twistd_path)
#
# Check/Create settings
#
def create_secret_key():
"""
Randomly create the secret key for the settings file
"""
import random
import string
secret_key = list((string.letters +
string.digits + string.punctuation).replace("\\", "").replace("'", '"'))
random.shuffle(secret_key)
secret_key = "".join(secret_key[:40])
return secret_key
def create_settings_file():
"""
Uses the template settings file to build a working
settings file.
"""
settings_path = os.path.join(GAMEDIR, "server", "conf", "settings.py")
with open(settings_path, 'r') as f:
settings_string = f.read()
# tweak the settings
setting_dict = {"servername":"Evennia",
"secret_key":create_secret_key}
# modify the settings
settings_string.format(**setting_dict)
with open(settings_path, 'w') as f:
f.write(settingsj_string)
# Functions
def create_game_directory(dirname):
"""
Initialize a new game directory named dirname
at the current path. This means copying the
template directory from evennia's root.
"""
global GAMEDIR
GAMEDIR = os.abspath(os.path.join(CURRENT_DIR, dirname))
if os.path.exists(GAMEDIR):
print "Cannot create new Evennia game dir: '%s' already exists." % dirname
sys.exit()
# copy template directory
shutil.copytree(EVENNIA_TEMPLATE, GAMEDIR)
# pre-build settings file in the new GAMEDIR
create_settings_file()
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 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 kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART, restart="reload"):
"""
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.
"""
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
if restart == 'reload':
management.call_command('collectstatic', interactive=False, verbosity=0)
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 show_version_info(about=False):
"""
Display version info
"""
import os, sys
import twisted
import django
return VERSION_INFO.format(version=EVENNIA_VERSION,
about=ABOUT_INFO if about else "",
os=os.name, python=sys.version.split()[0],
twisted=twisted.version.short(),
django=django.get_version())
def run_menu():
"""
This launches an interactive menu.
"""
cmdstr = [sys.executable, EVENNIA_RUNNER]
while True:
# menu loop
print MENU
inp = raw_input(" option > ")
# quitting and help
if inp.lower() == 'q':
sys.exit()
elif inp.lower() == 'h':
print HELP_ENTRY % EVENNIA_VERSION
raw_input("press <return> to continue ...")
continue
elif inp.lower() in ('v', 'i', 'a'):
print show_version_info(about=True)
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'])
# start server
cmdstr.append("start")
Popen(cmdstr)
return
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 reloaded.", errmsg % "Server", restart="reload")
elif inp == 6:
if os.name == 'nt':
print "This operation is not supported under Windows."
return
kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped if in daemon mode).", errmsg % "Portal", restart=True)
elif inp == 7:
kill(SERVER_PIDFILE, SIG, "Stopped Portal.", errmsg % "Portal", PORTAL_RESTART, restart=False)
kill(PORTAL_PIDFILE, SIG, "Stopped Server.", errmsg % "Server", restart="shutdown")
elif inp == 8:
kill(PORTAL_PIDFILE, SIG, "Stopped Server.", errmsg % "Server", restart="shutdown")
elif inp == 9:
kill(SERVER_PIDFILE, SIG, "Stopped Portal.", errmsg % "Portal", PORTAL_RESTART, restart=False)
return
else:
print "Not a valid option."
def server_operation(mode, service, interactive):
"""
Handle argument options given on the command line.
mode - str; start/stop etc
service - str; server, portal or all
interactive - bool; use interactive mode or daemon
"""
cmdstr = [sys.executable, EVENNIA_RUNNER]
errmsg = "The %s does not seem to be running."
if mode == 'start':
# launch the error checker. Best to catch the errors already here.
error_check_python_modules()
# starting one or many services
if service == 'server':
if interactive:
cmdstr.append('--iserver')
cmdstr.append('--noportal')
elif service == 'portal':
if interactive:
cmdstr.append('--iportal')
cmdstr.append('--noserver')
management.call_command('collectstatic', verbosity=1, interactive=False)
else: # all
# for convenience we don't start logging of
# portal, only of server with this command.
if interactive:
cmdstr.extend(['--iserver'])
management.call_command('collectstatic', verbosity=1, interactive=False)
# start the server
cmdstr.append("start")
Popen(cmdstr)
elif mode == 'reload':
# 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 reloaded.", errmsg % 'Server', restart="reload")
elif service == 'portal':
print """
Note: Portal usually don't need to be reloaded unless you are debugging in interactive mode.
If Portal was running in default Daemon mode, it cannot be restarted. In that case you have
to restart it manually with 'evennia.py start portal'
"""
kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped, if it was in daemon mode).", errmsg % 'Portal', PORTAL_RESTART)
else: # all
# default mode, only restart server
kill(SERVER_PIDFILE, SIG, "Server reload.", errmsg % 'Server', restart="reload")
elif mode == 'stop':
# stop processes, avoiding reload
if service == 'server':
kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', restart="shutdown")
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="shutdown")
def error_check_python_modules():
"""
Import settings modules in settings. This will raise exceptions on
pure python-syntax issues which are hard to catch gracefully
with exceptions in the engine (since they are formatting errors in
the python source files themselves). Best they fail already here
before we get any further.
"""
from django.conf import settings
# core modules
importlib.import_module(settings.COMMAND_PARSER)
importlib.import_module(settings.SEARCH_AT_RESULT)
importlib.import_module(settings.SEARCH_AT_MULTIMATCH_INPUT)
importlib.import_module(settings.CONNECTION_SCREEN_MODULE, split=False)
#imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False)
for path in settings.LOCK_FUNC_MODULES:
importlib.import_module(path, split=False)
# cmdsets
deprstring = "settings.%s should be renamed to %s. If defaults are used, " \
"their path/classname must be updated (see src/settings_default.py)."
if hasattr(settings, "CMDSET_DEFAULT"):
raise DeprecationWarning(deprstring % ("CMDSET_DEFAULT", "CMDSET_CHARACTER"))
if hasattr(settings, "CMDSET_OOC"):
raise DeprecationWarning(deprstring % ("CMDSET_OOC", "CMDSET_PLAYER"))
if settings.WEBSERVER_ENABLED and not isinstance(settings.WEBSERVER_PORTS[0], tuple):
raise DeprecationWarning("settings.WEBSERVER_PORTS must be on the form [(proxyport, serverport), ...]")
if hasattr(settings, "BASE_COMM_TYPECLASS"):
raise DeprecationWarning(deprstring % ("BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS"))
if hasattr(settings, "COMM_TYPECLASS_PATHS"):
raise DeprecationWarning(deprstring % ("COMM_TYPECLASS_PATHS", "CHANNEL_TYPECLASS_PATHS"))
if hasattr(settings, "CHARACTER_DEFAULT_HOME"):
raise DeprecationWarning("settings.CHARACTER_DEFAULT_HOME should be renamed to DEFAULT_HOME. " \
"See also settings.START_LOCATION (see src/settings_default.py).")
from src.commands import cmdsethandler
if not cmdsethandler.import_cmdset(settings.CMDSET_UNLOGGEDIN, None): print "Warning: CMDSET_UNLOGGED failed to load!"
if not cmdsethandler.import_cmdset(settings.CMDSET_CHARACTER, None): print "Warning: CMDSET_CHARACTER failed to load"
if not cmdsethandler.import_cmdset(settings.CMDSET_PLAYER, None): print "Warning: CMDSET_PLAYER failed to load"
# typeclasses
importlib.import_module(settings.BASE_PLAYER_TYPECLASS)
importlib.import_module(settings.BASE_OBJECT_TYPECLASS)
importlib.import_module(settings.BASE_CHARACTER_TYPECLASS)
importlib.import_module(settings.BASE_ROOM_TYPECLASS)
importlib.import_module(settings.BASE_EXIT_TYPECLASS)
importlib.import_module(settings.BASE_SCRIPT_TYPECLASS)
def create_database():
from django.core.management import call_command
print "\nCreating a database ...\n"
call_command("migrate", interactive=False)
print "\n ... database initialized.\n"
def create_superuser():
from django.core.management import call_command
print "\nCreate a superuser below. The superuser is Player #1, the 'owner' account of the server.\n"
call_command("createsuperuser", interactive=True)
def check_database(automigrate=False):
# Check so a database exists and is accessible
from django.db import DatabaseError
from src.players.models import PlayerDB
try:
PlayerDB.objects.get(id=1)
except DatabaseError, e:
if automigrate:
create_database()
create_superuser()
else:
print ERROR_DATABASE.format(traceback=e)
sys.exit()
except PlayerDB.DoesNotExist:
# no superuser yet. We need to create it.
create_superuser()
def main():
"""
Run the evennia main program.
"""
# set up argument parser
parser = ArgumentParser(#usage="%prog [-i] start|stop|reload|menu [server|portal]|manager args",
description=CMDLINE_HELP)
parser.add_argument('-i', '--interactive', action='store_true',
dest='interactive', default=False,
help="Start given processes in interactive mode.")
parser.add_argument('-v', '--version', action='store_true',
dest='show_version', default=False,
help="Show version info.")
parser.add_argument('--init', action='store', dest="init", metavar="dirname")
parser.add_argument('-c', '--config', action='store', dest="config", default=None)
parser.add_argument("mode", default="menu")
parser.add_argument("service", choices=["all", "server", "portal"], default="all")
args = parser.parse_args()
# handle arguments
if args.show_version:
print show_version_info()
mode, service = args.mode, args.service
if args.init:
create_game_directory(args.init)
sys.exit()
# this must be done first - it sets up all the global properties
# and initializes django for the game directory
init_game_directory(CURRENT_DIR)
if mode == 'menu':
# launch menu for operation
check_database(True)
run_menu()
elif mode in ('start', 'reload', 'stop'):
# operate the server directly
if mode != "stop":
check_database(False)
server_operation(mode, service, args.interactive)
else:
# pass-through to django manager
from django.core.management import call_command
call_command(mode)
if __name__ == '__main__':
# start Evennia from the command line
if check_evennia_dependencies():
if len(sys.argv) > 1 and sys.argv[1] in ('runserver', 'testserver'):
print WARNING_RUNSERVER
main()

320
bin/runner.py Normal file
View file

@ -0,0 +1,320 @@
#!/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
import Queue, thread
import django
try:
# check if launched with pypy
import __pypy__ as is_pypy
except ImportError:
is_pypy = False
#
# 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'
if not os.path.exists('settings.py'):
print "No settings.py file found. Run evennia.py to create it."
sys.exit()
django.setup()
# 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/portal.py')
# Get logfile names
SERVER_LOGFILE = settings.SERVER_LOG_FILE
PORTAL_LOGFILE = settings.PORTAL_LOG_FILE
HTTP_LOGFILE = settings.HTTP_LOG_FILE.strip()
CYCLE_LOGFILES = settings.CYCLE_LOGFILES
# 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="reload"):
"""
This sets a flag file for the restart mode.
"""
with open(restart_file, 'w') as f:
f.write(str(flag))
def get_restart_mode(restart_file):
"""
Parse the server/portal restart status
"""
if os.path.exists(restart_file):
with open(restart_file, 'r') as f:
return f.read()
return "shutdown"
def get_pid(pidfile):
"""
Get the PID (Process ID) by trying to access
an PID file.
"""
pid = None
if os.path.exists(pidfile):
with open(pidfile, 'r') as f:
pid = f.read()
return pid
def cycle_logfile(logfile):
"""
Rotate 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)
# 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}
return
# this signals the controller that the program finished
queue.put(("server_stopped", rc))
def portal_waiter(queue):
try:
rc = Popen(portal_argv).wait()
except Exception, e:
print "Portal process error: %(e)s" % {'e': e}
return
# this signals the controller that the program finished
queue.put(("portal_stopped", rc))
if portal_argv:
try:
if get_restart_mode(PORTAL_RESTART) == "True":
# 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)
except IOError, e:
print "Portal IOError: %s\nA possible explanation for this is that 'twistd' is not found." % e
return
try:
if server_argv:
# start server as a reloadable thread
SERVER = thread.start_new_thread(server_waiter, (processes, ))
except IOError, e:
print "Server IOError: %s\nA possible explanation for this is that 'twistd' is not found." % e
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) in ("True", "reload", "reset")):
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) == "True"):
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.')
parser.add_option('-S', '--profile-server', action='store_true',
dest='sprof', default=False,
help='run server under cProfile')
parser.add_option('-P', '--profile-portal', action='store_true',
dest='pprof', default=False,
help='run portal under cProfile')
options, args = parser.parse_args()
if not args or args[0] != 'start':
# this is so as to avoid runner.py be accidentally launched manually.
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]
# Profiling settings (read file from python shell e.g with
# p = pstats.Stats('server.prof')
sprof_argv = ['--savestats',
'--profiler=cprofile',
'--profile=server.prof']
pprof_argv = ['--savestats',
'--profiler=cprofile',
'--profile=portal.prof']
# 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, "shutdown")
if options.iserver:
# don't log to server logfile
del server_argv[2]
print "\nStarting Evennia Server (output to stdout)."
else:
if CYCLE_LOGFILES:
cycle_logfile(SERVER_LOGFILE)
print "\nStarting Evennia Server (output to server logfile)."
if options.sprof:
server_argv.extend(sprof_argv)
print "\nRunning Evennia Server under cProfile."
# 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'
set_restart_mode(PORTAL_RESTART, True)
print "\nStarting Evennia Portal in non-Daemon mode (output to stdout)."
else:
if CYCLE_LOGFILES:
cycle_logfile(PORTAL_LOGFILE)
cycle_logfile(HTTP_LOGFILE)
set_restart_mode(PORTAL_RESTART, False)
print "\nStarting Evennia Portal in Daemon mode (output to portal logfile)."
if options.pprof:
portal_argv.extend(pprof_argv)
print "\nRunning Evennia Portal under cProfile."
# 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()