Refactored the devel/bin structure to hopefully make the system work better on Windows.

This commit is contained in:
Griatch 2015-02-02 20:14:28 +01:00
parent 6323ba965b
commit f780f469b1
12 changed files with 32 additions and 21 deletions

View file

@ -1,905 +0,0 @@
#!/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 evennia_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 shutil
import importlib
from argparse import ArgumentParser
from subprocess import Popen, check_output, call, CalledProcessError, STDOUT
import django
# 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")
import evennia
EVENNIA_LIB = os.path.join(os.path.dirname(os.path.abspath(evennia.__file__)))
EVENNIA_RUNNER = os.path.join(EVENNIA_BIN, "evennia_runner.py")
EVENNIA_TEMPLATE = os.path.join(EVENNIA_LIB, "game_template")
EVENNIA_BINTESTING = os.path.join(EVENNIA_BIN, "testing")
EVENNIA_DUMMYRUNNER = os.path.join(EVENNIA_BINTESTING, "dummyrunner.py")
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
HTTP_LOGFILE = None
SERVER_PIDFILE = None
PORTAL_PIDFILE = None
SERVER_RESTART = None
PORTAL_RESTART = None
SERVER_PY_FILE = None
PORTAL_PY_FILE = None
PYTHON_MIN = '2.7'
TWISTED_MIN = '12.0'
DJANGO_MIN = '1.7'
DJANGO_REC = '1.7'
# add Evennia root to PYTHONPATH note that bin/evennia.py is
# automatically added to sys.modules and will be imported first, which
# is not what we want. So we remove it manually and set the path so
# the root/evennia package is found first instead.
del sys.modules["evennia"]
sys.path[0] = EVENNIA_ROOT
#------------------------------------------------------------
#
# Messages
#
#------------------------------------------------------------
CREATED_NEW_GAMEDIR = \
"""
Welcome to Evennia!
Created a new Evennia game directory '{gamedir}'.
You can now optionally edit your new settings file
at {settings_path}. If you don't, the defaults
will work out of the box. When ready to continue, 'cd' to your
game directory and run:
evennia migrate
This initializes the database. To start the server for the first
time, run:
evennia -i start
Make sure to create a superuser when asked for it. You should now
be able to (by default) connect to your server on server
'localhost', port 4000 using a telnet/mud client or
http://localhost:8000 using your web browser. If things don't
work, check so those ports are open.
"""
ERROR_NO_GAMEDIR = \
"""
No Evennia settings file was found. You must run this command from
inside a valid game directory first created with --init.
"""
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 = \
"""
There was an error importing Evennia's config file {settingspath}. There is usually
one of three reasons for this:
1) You are not running this command from your game directory.
Change directory to your game directory and try again (or
create a new game directory using evennia --init <dirname>)
2) The settings file contains a syntax error. If you see a
traceback above, review it, resolve the problem and try again.
3) Django is not correctly installed. This usually shows as
errors mentioning '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}')
Standing in your game directory, try to run
evennia migrate
to initialize/update 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 = \
"""
Starts or operates the Evennia MU* server. Also allows for
initializing a new game directory and managing the game's
database. You can also pass django manage.py arguments through
this launcher. If you need manage.py --options, use djangoadmin
directly instead.
"""
VERSION_INFO = \
"""
{about}
Evennia {version}
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
Use -h for command line options.
"""
HELP_ENTRY = \
"""
Enter 'evennia -h' for command-line options.
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 Evennia itself.
Reload with (5) to update the server with your changes without
disconnecting any players.
Note: 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) (normal): All output to logfiles |
| 2) (server devel): Server logs to terminal (-i option) |
| 3) (portal devel): Portal logs to terminal |
| 4) (full devel): Both Server and Portal logs 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 |
+---------------------------------------------------------------+
"""
ERROR_PYTHON_VERSION = \
"""
ERROR: Python {pversion} used. Evennia requires version
{python_min} or higher (but not 3.x).
"""
WARNING_TWISTED_VERSION = \
"""
WARNING: Twisted {tversion} found. Evennia recommends
v{twisted_min} or higher."
"""
ERROR_NOTWISTED = \
"""
ERROR: Twisted does not seem to be installed.
"""
ERROR_DJANGO_MIN = \
"""
ERROR: Django {dversion} found. Evennia requires version
{django_min} or higher.
"""
NOTE_DJANGO_MIN = \
"""
NOTE: Django {dversion} found. This will work, but v{django_rec}
is recommended for production.
"""
NOTE_DJANGO_NEW = \
"""
NOTE: Django {dversion} found. This is newer than Evennia's
recommended version (v{django_rec}). It will probably work, but
may be new enough not to be fully tested yet. Report any issues."
"""
ERROR_NODJANGO = \
"""
ERROR: Django does not seem to be installed.
"""
#------------------------------------------------------------
#
# Functions
#
#------------------------------------------------------------
def evennia_version():
"""
Get the Evennia version info from the main package.
"""
version = "Unknown"
try:
import evennia
version = evennia.__version__
except ImportError:
pass
try:
version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=EVENNIA_ROOT, stderr=STDOUT).strip())
except (IOError, CalledProcessError):
pass
return version
EVENNIA_VERSION = evennia_version()
def check_main_evennia_dependencies():
"""
Checks and imports the Evennia dependencies. This must be done
already before the paths are set up.
"""
error = False
# Python
pversion = ".".join(str(num) for num in sys.version_info if type(num) == int)
if pversion < PYTHON_MIN:
print ERROR_PYTHON_VERSION.format(pversion=pversion, python_min=PYTHON_MIN)
error = True
# Twisted
try:
import twisted
tversion = twisted.version.short()
if tversion < TWISTED_MIN:
print WARNING_TWISTED_VERSION.format(tversion=tversion, twisted_min=TWISTED_MIN)
except ImportError:
print ERROR_NOTWISTED
error = True
# Django
try:
dversion = ".".join(str(num) for num in django.VERSION if type(num) == int)
# only the main version (1.5, not 1.5.4.0)
dversion_main = ".".join(dversion.split(".")[:2])
if dversion < DJANGO_MIN:
print ERROR_DJANGO_MIN.format(dversion=dversion_main, django_min=DJANGO_MIN)
error = True
elif DJANGO_MIN <= dversion < DJANGO_REC:
print NOTE_DJANGO_MIN.format(dversion=dversion_main, django_rec=DJANGO_REC)
elif DJANGO_REC < dversion_main:
print NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_REC)
except ImportError:
print ERROR_NODJANGO
error = True
if error:
sys.exit()
def set_gamedir(path):
"""
Set GAMEDIR based on path, by figuring out where the setting file
is inside the directory tree.
"""
global GAMEDIR
if os.path.exists(os.path.join(path, SETTINGS_PATH)):
# path at root of game dir
GAMEDIR = os.path.abspath(path)
elif os.path.exists(os.path.join(path, os.path.pardir, SETTINGS_PATH)):
# path given to somewhere one level down
GAMEDIR = os.path.dirname(path)
elif os.path.exists(os.path.join(path, os.path.pardir, os.path.pardir, SETTINGS_PATH)):
# path given to somwhere two levels down
GAMEDIR = os.path.dirname(os.path.dirname(path))
elif os.path.exists(os.path.join(path, os.path.pardir, os.path. pardir, os.path.pardir, SETTINGS_PATH)):
# path given to somewhere three levels down (custom directories)
GAMEDIR = os.path.dirname(os.path.dirname(os.path.dirname(path)))
else:
# we don't look further down than this ...
print ERROR_NO_GAMEDIR
sys.exit()
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 = {"settings_default": os.path.join(EVENNIA_LIB, "settings_default.py"),
"servername":"\"%s\"" % GAMEDIR.rsplit(os.path.sep, 1)[1].capitalize(),
"game_dir":"\"%s\"" % GAMEDIR,
"secret_key":"\'%s\'" % create_secret_key()}
# modify the settings
settings_string = settings_string.format(**setting_dict)
with open(settings_path, 'w') as f:
f.write(settings_string)
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.path.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 create_superuser():
"Create the superuser player"
print "\nCreate a superuser below. The superuser is Player #1, the 'owner' account of the server.\n"
django.core.management.call_command("createsuperuser", interactive=True)
def check_database(exit_on_error=False):
"""
Check database exists
"""
# Check so a database exists and is accessible
from django.db import connection
tables = connection.introspection.get_table_list(connection.cursor())
if tables and u'players_playerdb' in tables:
# database exists and seems set up. Initialize evennia.
import evennia
evennia.init()
else:
if exit_on_error:
print ERROR_DATABASE.format(traceback=e)
sys.exit()
return False
return True
# Try to get Player#1
from evennia.players.models import PlayerDB
try:
PlayerDB.objects.get(id=1)
except PlayerDB.DoesNotExist:
# no superuser yet. We need to create it.
create_superuser()
return True
def getenv():
"""
Get current environment and add PYTHONPATH
"""
sep = ";" if os.name == 'nt' else ":"
env = os.environ.copy()
env['PYTHONPATH'] = sep.join(sys.path)
return env
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=False):
"""
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':
os.remove(pidfile)
# set restart/norestart flag
if restart:
django.core.management.call_command('collectstatic', interactive=False, verbosity=0)
with open(restart_file, 'w') as f:
f.write("reload")
else:
with open(restart_file, 'w') as f:
f.write("shutdown")
try:
os.kill(int(pid), signal)
except OSError:
print "Process %(pid)s cannot be stopped. "\
"The PID file 'server/%(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 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
def imp(path, split=True):
mod, fromlist = path, "None"
if split:
mod, fromlist = path.rsplit('.', 1)
__import__(mod, fromlist=[fromlist])
# core modules
imp(settings.COMMAND_PARSER)
imp(settings.SEARCH_AT_RESULT)
imp(settings.SEARCH_AT_MULTIMATCH_INPUT)
imp(settings.CONNECTION_SCREEN_MODULE)
#imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False)
for path in settings.LOCK_FUNC_MODULES:
imp(path, split=False)
# cmdsets
deprstring = "settings.%s should be renamed to %s. If defaults are used, " \
"their path/classname must be updated (see evennia/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 evennia/settings_default.py).")
from evennia.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
imp(settings.BASE_PLAYER_TYPECLASS)
imp(settings.BASE_OBJECT_TYPECLASS)
imp(settings.BASE_CHARACTER_TYPECLASS)
imp(settings.BASE_ROOM_TYPECLASS)
imp(settings.BASE_EXIT_TYPECLASS)
imp(settings.BASE_SCRIPT_TYPECLASS)
def init_game_directory(path, check_db=True):
"""
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.
"""
# set the GAMEDIR path
set_gamedir(path)
# Add gamedir to python path
sys.path.insert(1, GAMEDIR)
# Prepare django; set the settings location
os.environ['DJANGO_SETTINGS_MODULE'] = SETTINGS_DOTPATH
# required since django1.7
django.setup()
# test existence of the settings module
try:
from django.conf import settings
except Exception, ex:
if not str(ex).startswith("No module named"):
import traceback
print traceback.format_exc().strip()
print ERROR_SETTINGS
sys.exit()
# this will both check the database and initialize the evennia dir.
if check_db:
check_database()
# set up the Evennia executables and log file locations
global SERVER_PY_FILE, PORTAL_PY_FILE
global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE
global SERVER_PIDFILE, PORTAL_PIDFILE
global SERVER_RESTART, PORTAL_RESTART
global EVENNIA_VERSION
SERVER_PY_FILE = os.path.join(EVENNIA_LIB, "server", "server.py")
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "portal", "portal", "portal.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
HTTP_LOGFILE = settings.HTTP_LOG_FILE
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(1, 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)
def run_dummyrunner(number_of_dummies):
"""
Start an instance of the dummyrunner
The dummy players' behavior can be customized by adding a
dummyrunner_settings.py config file in the game's conf directory.
"""
number_of_dummies = str(int(number_of_dummies)) if number_of_dummies else 1
cmdstr = [sys.executable, EVENNIA_DUMMYRUNNER, "-N", number_of_dummies]
config_file = os.path.join(SETTINGS_PATH, "dummyrunner_settings.py")
if os.path.exists(config_file):
cmdstr.extend(["--config", config_file])
try:
call(cmdstr, env=getenv())
except KeyboardInterrupt:
pass
def run_menu():
"""
This launches an interactive menu.
"""
while True:
# menu loop
print MENU
inp = raw_input(" option > ")
# quitting and help
if inp.lower() == 'q':
return
elif inp.lower() == 'h':
print HELP_ENTRY
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
if inp == 1:
# start everything, log to log files
server_operation("start", "all", False, False)
elif inp == 2:
# start everything, server interactive start
server_operation("start", "all", True, False)
elif inp == 3:
# start everything, portal interactive start
server_operation("start", "server", False, False)
server_operation("start", "portal", True, False)
elif inp == 4:
# start both server and portal interactively
server_operation("start", "server", True, False)
server_operation("start", "portal", True, False)
elif inp == 5:
# reload the server
server_operation("reload", "server", None, None)
elif inp == 6:
# reload the portal
server_operation("reload", "portal", None, None)
elif inp == 7:
# stop server and portal
server_operation("stop", "all", None, None)
elif inp == 8:
# stop server
server_operation("stop", "server", None, None)
elif inp == 9:
# stop portal
server_operation("stop", "portal", None, None)
else:
print "Not a valid option."
continue
return
def server_operation(mode, service, interactive, profiler):
"""
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
profiler - run the service under the profiler
"""
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 profiler:
cmdstr.append('--pserver')
if interactive:
cmdstr.append('--iserver')
cmdstr.append('--noportal')
elif service == 'portal':
if profiler:
cmdstr.append('--pportal')
if interactive:
cmdstr.append('--iportal')
cmdstr.append('--noserver')
django.core.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 profiler:
cmdstr.append('--pserver') # this is the common case
if interactive:
cmdstr.append('--iserver')
django.core.management.call_command('collectstatic', verbosity=1, interactive=False)
cmdstr.extend([GAMEDIR, TWISTED_BINARY, SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE])
# start the server
Popen(cmdstr, env=getenv())
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', SERVER_RESTART, restart=True)
elif service == 'portal':
print """
Note: Portal usually doesnt'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, restart=True)
else: # all
# default mode, only restart server
kill(SERVER_PIDFILE, SIG, "Server reload.", errmsg % 'Server', SERVER_RESTART, restart=True)
elif mode == 'stop':
# stop processes, avoiding reload
if service == 'server':
kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', SERVER_RESTART)
elif service == 'portal':
kill(PORTAL_PIDFILE, SIG, "Portal stopped.", errmsg % 'Portal', PORTAL_RESTART)
else:
kill(PORTAL_PIDFILE, SIG, "Portal stopped.", errmsg % 'Portal', PORTAL_RESTART)
kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', SERVER_RESTART)
def main():
"""
Run the evennia main program.
"""
# set up argument parser
parser = ArgumentParser(description=CMDLINE_HELP)
parser.add_argument('-v', '--version', action='store_true',
dest='show_version', default=False,
help="Show version info.")
parser.add_argument('-i', '--interactive', action='store_true',
dest='interactive', default=False,
help="Start given processes in interactive mode.")
parser.add_argument('--init', action='store', dest="init", metavar="name",
help="Creates a new game directory 'name' at the current location.")
parser.add_argument('--profiler', action='store_true', dest='profiler', default=False,
help="Start given server component under the Python profiler.")
parser.add_argument('--dummyrunner', nargs=1, action='store', dest='dummyrunner', metavar="N",
help="Tests a running server by connecting N dummy players to it.")
parser.add_argument("mode", metavar="option", nargs='?', default="help",
help="Operational mode or management option. Commonly start, stop, reload, migrate, or menu (default).")
parser.add_argument("service", metavar="component", nargs='?', choices=["all", "server", "portal"], default="all",
help="Which server component to operate on. One of server, portal or all (default).")
args = parser.parse_args()
# handle arguments
mode, service = args.mode, args.service
check_main_evennia_dependencies()
if args.init:
create_game_directory(args.init)
print CREATED_NEW_GAMEDIR.format(gamedir=args.init,
settings_path=os.path.join(args.init, SETTINGS_PATH))
sys.exit()
if args.show_version:
print show_version_info(mode=="help")
sys.exit()
if mode == "help" and not args.dummyrunner:
print ABOUT_INFO
sys.exit()
check_db = not mode == "migrate"
# this must be done first - it sets up all the global properties
# and initializes django for the game directory
init_game_directory(CURRENT_DIR, check_db=check_db)
if args.dummyrunner:
# launch the dummy runner
run_dummyrunner(args.dummyrunner[0])
elif mode == 'menu':
# launch menu for operation
run_menu()
elif mode in ('start', 'reload', 'stop'):
# operate the server directly
server_operation(mode, service, args.interactive, args.profiler)
else:
# pass-through to django manager
if mode in ('runserver', 'testserver'):
print WARNING_RUNSERVER
django.core.management.call_command(mode)
if __name__ == '__main__':
# start Evennia from the command line
main()

View file

@ -1,330 +0,0 @@
#!/usr/bin/env python
"""
This runner is controlled by the evennia launcher 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 argparse import ArgumentParser
from subprocess import Popen
import Queue, thread
import evennia
try:
# check if launched with pypy
import __pypy__ as is_pypy
except ImportError:
is_pypy = False
SERVER = None
PORTAL = None
EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
EVENNIA_BIN = os.path.join(EVENNIA_ROOT, "bin")
EVENNIA_LIB = os.path.dirname(evennia.__file__)
SERVER_PY_FILE = os.path.join(EVENNIA_LIB,'server', 'server.py')
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, 'server', 'portal', 'portal.py')
GAMEDIR = None
SERVERDIR = "server"
SERVER_PIDFILE = None
PORTAL_PIDFILE = None
SERVER_RESTART = None
PORTAL_RESTART = None
SERVER_LOGFILE = None
PORTAL_LOGFILE = None
HTTP_LOGFILE = None
PPROFILER_LOGFILE = None
SPROFILER_LOGFILE = None
# messages
CMDLINE_HELP = \
"""
This program manages the running Evennia processes. It is called
by evennia and should not be started manually. Its main task is to
sit and watch the Server and restart it whenever the user reloads.
The runner depends on four files for its operation, two PID files
and two RESTART files for Server and Portal respectively; these
are stored in the game's server/ directory.
"""
PROCESS_ERROR = \
"""
{component} process error: {traceback}.
"""
PROCESS_IOERROR = \
"""
{component} IOError: {traceback}
One possible explanation is that 'twistd' was not found.
"""
PROCESS_RESTART = "{component} restarting ..."
# 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 getenv():
"""
Get current environment and add PYTHONPATH
"""
sep = ";" if os.name == "nt" else ":"
env = os.environ.copy()
env['PYTHONPATH'] = sep.join(sys.path)
return env
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
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, env=getenv()).wait()
except Exception, e:
print PROCESS_ERROR.format(component="Server", traceback=e)
return
# this signals the controller that the program finished
queue.put(("server_stopped", rc))
def portal_waiter(queue):
try:
rc = Popen(portal_argv, env=getenv()).wait()
except Exception, e:
print PROCESS_ERROR.format(component="Portal", traceback=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, env=getenv())
except IOError, e:
print PROCESS_IOERROR.format(component="Portal", traceback=e)
return
try:
if server_argv:
# start server as a reloadable thread
SERVER = thread.start_new_thread(server_waiter, (processes, ))
except IOError, e:
print PROCESS_IOERROR.format(component="Server", traceback=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 PROCESS_RESTART.format(component="Server")
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 PROCESS_RESTART.format(component="Portal")
PORTAL = thread.start_new_thread(portal_waiter, (processes, ))
continue
break
def main():
"""
This handles the command line input of the runner, usually created by
the evennia launcher
"""
parser = ArgumentParser(description=CMDLINE_HELP)
parser.add_argument('--noserver', action='store_true', dest='noserver',
default=False, help='Do not start Server process')
parser.add_argument('--noportal', action='store_true', dest='noportal',
default=False, help='Do not start Portal process')
parser.add_argument('--iserver', action='store_true', dest='iserver',
default=False, help='Server in interactive mode')
parser.add_argument('--iportal', action='store_true', dest='iportal',
default=False, help='Portal in interactive mode')
parser.add_argument('--pserver', action='store_true', dest='pserver',
default=False, help='Profile Server')
parser.add_argument('--pportal', action='store_true', dest='pportal',
default=False, help='Profile Portal')
parser.add_argument('--nologcycle', action='store_false', dest='nologcycle',
default=True, help='Do not cycle log files')
parser.add_argument('gamedir', help="path to game dir")
parser.add_argument('twistdbinary', help="path to twistd binary")
parser.add_argument('slogfile', help="path to server log file")
parser.add_argument('plogfile', help="path to portal log file")
parser.add_argument('hlogfile', help="path to http log file")
args = parser.parse_args()
global GAMEDIR
global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE
global SERVER_PIDFILE, PORTAL_PIDFILE
global SERVER_RESTART, PORTAL_RESTART
global SPROFILER_LOGFILE, PPROFILER_LOGFILE
GAMEDIR = args.gamedir
sys.path.insert(1, os.path.join(GAMEDIR, SERVERDIR))
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 = args.slogfile
PORTAL_LOGFILE = args.plogfile
HTTP_LOGFILE = args.hlogfile
TWISTED_BINARY = args.twistdbinary
SPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "server.prof")
PPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "portal.prof")
# 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')
pserver_argv = ['--savestats',
'--profiler=cprofile',
'--profile=%s' % SPROFILER_LOGFILE]
pportal_argv = ['--savestats',
'--profiler=cprofile',
'--profile=%s' % PPROFILER_LOGFILE]
# Server
pid = get_pid(SERVER_PIDFILE)
if pid and not args.noserver:
print "\nEvennia Server is already running as process %(pid)s. Not restarted." % {'pid': pid}
args.noserver = True
if args.noserver:
server_argv = None
else:
set_restart_mode(SERVER_RESTART, "shutdown")
if args.iserver:
# don't log to server logfile
del server_argv[2]
print "\nStarting Evennia Server (output to stdout)."
else:
if not args.nologcycle:
cycle_logfile(SERVER_LOGFILE)
print "\nStarting Evennia Server (output to server logfile)."
if args.pserver:
server_argv.extend(pserver_argv)
print "\nRunning Evennia Server under cProfile."
# Portal
pid = get_pid(PORTAL_PIDFILE)
if pid and not args.noportal:
print "\nEvennia Portal is already running as process %(pid)s. Not restarted." % {'pid': pid}
args.noportal = True
if args.noportal:
portal_argv = None
else:
if args.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 not args.nologcycle:
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 args.pportal:
portal_argv.extend(pportal_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__':
main()

View file

@ -1,6 +0,0 @@
Dummyrunner
This is a test system for stress-testing the server. It will launch numbers
of "dummy players" to connect to the server and do various sequences of actions.
See header of dummyrunner.py for usage.

View file

@ -1,310 +0,0 @@
"""
Dummy client runner
This module implements a stand-alone launcher for stress-testing
an Evennia game. It will launch any number of fake clients. These
clients will log into the server and start doing random operations.
Customizing and weighing these operations differently depends on
which type of game is tested. The module contains a testing module
for plain Evennia.
Please note that you shouldn't run this on a production server!
Launch the program without any arguments or options to see a
full step-by-step setup help.
Basically (for testing default Evennia):
- Use an empty/testing database.
- set PERMISSION_PLAYER_DEFAULT = "Builders"
- start server, eventually with profiling active
- launch this client runner
If you want to customize the runner's client actions
(because you changed the cmdset or needs to better
match your use cases or add more actions), you can
change which actions by adding a path to
DUMMYRUNNER_ACTIONS_MODULE = <path.to.your.module>
in your settings. See utils.dummyrunner_actions.py
for instructions on how to define this module.
"""
import time, random
from argparse import ArgumentParser
from twisted.conch import telnet
from twisted.internet import reactor, protocol
from twisted.internet.task import LoopingCall
from django.conf import settings
from evennia.utils import mod_import, time_format
# Load the dummyrunner settings module
DUMMYRUNNER_SETTINGS = mod_import(settings.DUMMYRUNNER_SETTINGS_MODULE)
DATESTRING = "%Y%m%d%H%M%S"
# Settings
# number of clients to launch if no input is given on command line
NCLIENTS = 1
# time between each 'tick', in seconds, if not set on command
# line. All launched clients will be called upon to possibly do an
# action with this frequency.
TIMESTEP = DUMMYRUNNER_SETTINGS.TIMESTEP
# chance of a client performing an action, per timestep. This helps to
# spread out usage randomly, like it would be in reality.
CHANCE_OF_ACTION = DUMMYRUNNER_SETTINGS.CHANCE_OF_ACTION
# spread out the login action separately, having many players create accounts
# and connect simultaneously is generally unlikely.
CHANCE_OF_LOGIN = DUMMYRUNNER_SETTINGS.CHANCE_OF_LOGIN
# Port to use, if not specified on command line
TELNET_PORT = DUMMYRUNNER_SETTINGS.TELNET_PORT or settings.TELNET_PORTS[0]
#
NLOGGED_IN = 0
# Messages
INFO_STARTING = \
"""
Dummyrunner starting using {N} dummy player(s). If you don't see
any connection messages, make sure that the Evennia server is
running. If you intend the dummies to work fully, set
settings.PERMISSION_PLAYER_DEFAULT appropriately (if so it is
recommended that you use a temporary testing database).
Use Ctrl-C to stop/disconnect clients.
"""
ERROR_FEW_ACTIONS = \
"""
Dummyrunner settings error: The ACTIONS tuple is too short: it must
contain at least login- and logout functions.
"""
HELPTEXT = """
DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE!
This stand-alone program launches dummy telnet clients against a
running Evennia server. The idea is to mimic real players logging in
and repeatedly doing resource-heavy commands so as to stress test the
game. It uses the default command set to log in and issue commands, so
if that was customized, some of the functionality will not be tested
(it will not fail, the commands will just not be recognized). The
running clients will create new objects and rooms all over the place
as part of their running, so using a clean/testing database is
strongly recommended.
Setup:
1) setup a fresh/clean database (if using sqlite, just safe-copy
away your real evennia.db3 file and create a new one with
manage.py)
2) in server/conf/settings.py, add
PERMISSION_PLAYER_DEFAULT="Builders"
This is so that the dummy players can test building operations.
You can also customize the dummyrunner by modifying a setting
file specified by DUMMYRUNNER_SETTINGS_MODULE
3) Start Evennia like normal, optionally with profiling (--profile)
4) Run this dummy runner via the evennia launcher:
evennia --dummyrunner <nr_of_clients>
5) Log on and determine if game remains responsive despite the
heavier load. Note that if you do profiling, there is an
additional overhead from the profiler too!j
6) If you use profiling, let the game run long enough to gather
data, then stop the server cleanly using evennia stop or @shutdown.
@shutdown. The profile appears as
server/logs/server.prof/portal.prof (see Python's manual on
cProfiler).
"""
#------------------------------------------------------------
# Helper functions
#------------------------------------------------------------
ICOUNT = 0
def idcounter():
"makes unique ids"
global ICOUNT
ICOUNT += 1
return str(ICOUNT)
GCOUNT = 0
def gidcounter():
"makes globally unique ids"
global GCOUNT
GCOUNT += 1
return "%s-%s" % (time.strftime(DATESTRING), GCOUNT)
def makeiter(obj):
"makes everything iterable"
if not hasattr(obj, '__iter__'):
return [obj]
return obj
#------------------------------------------------------------
# Client classes
#------------------------------------------------------------
class DummyClient(telnet.StatefulTelnetProtocol):
"""
Handles connection to a running Evennia server,
mimicking a real player by sending commands on
a timer.
"""
def connectionMade(self):
# public properties
self.cid = idcounter()
self.key = "Dummy-%s" % self.cid
self.gid = "%s-%s" % (time.strftime(DATESTRING), self.cid)
self.istep = 0
self.exits = [] # exit names created
self.objs = [] # obj names created
self._loggedin = False
self._logging_out = False
self._report = ""
self._cmdlist = [] # already stepping in a cmd definition
self._login = self.factory.actions[0]
self._logout = self.factory.actions[1]
self._actions = self.factory.actions[2:]
reactor.addSystemEventTrigger('before', 'shutdown', self.logout)
# start client tick
d = LoopingCall(self.step)
# dissipate exact step by up to +/- 0.5 second
timestep = TIMESTEP + (-0.5 + (random.random()*1.0))
d.start(timestep, now=True).addErrback(self.error)
def dataReceived(self, data):
"Echo incoming data to stdout"
pass
def connectionLost(self, reason):
"loosing the connection"
if not self._logging_out:
print "client %s(%s) lost connection (%s)" % (self.key, self.cid, reason)
def error(self, err):
"error callback"
print err
def counter(self):
"produces a unique id, also between clients"
return gidcounter()
def logout(self):
"Causes the client to log out of the server. Triggered by ctrl-c signal."
self._logging_out = True
cmd = self._logout(self)
print "client %s(%s) logout (%s actions)" % (self.key, self.cid, self.istep)
self.sendLine(cmd)
def step(self):
"""
Perform a step. This is called repeatedly by the runner
and causes the client to issue commands to the server.
This holds all "intelligence" of the dummy client.
"""
global NLOGGED_IN
rand = random.random()
if not self._cmdlist:
# no commands ready. Load some.
if not self._loggedin:
if rand < CHANCE_OF_LOGIN:
# get the login commands
self._cmdlist = list(makeiter(self._login(self)))
NLOGGED_IN += 1 # this is for book-keeping
print "connecting client %s (%i/%i)..." % (self.key, NLOGGED_IN, NCLIENTS)
self._loggedin = True
else:
# no login yet, so cmdlist not yet set
return
else:
# we always pick a cumulatively random function
crand = random.random()
cfunc = [func for (cprob, func) in self._actions if cprob >= crand][0]
self._cmdlist = list(makeiter(cfunc(self)))
# at this point we always have a list of commands
if rand < CHANCE_OF_ACTION:
# send to the game
self.sendLine(str(self._cmdlist.pop(0)))
self.istep += 1
class DummyFactory(protocol.ClientFactory):
protocol = DummyClient
def __init__(self, actions):
"Setup the factory base (shared by all clients)"
self.actions = actions
#------------------------------------------------------------
# Access method:
# Starts clients and connects them to a running server.
#------------------------------------------------------------
def start_all_dummy_clients(nclients):
global NCLIENTS
NCLIENTS = int(nclients)
actions = DUMMYRUNNER_SETTINGS.ACTIONS
if len(actions) < 2:
print ERROR_FEW_ACTIONS
return
# make sure the probabilities add up to 1
pratio = 1.0 / sum(tup[0] for tup in actions[2:])
flogin, flogout, probs, cfuncs = actions[0], actions[1], [tup[0] * pratio for tup in actions[2:]], [tup[1] for tup in actions[2:]]
# create cumulative probabilies for the random actions
cprobs = [sum(v for i,v in enumerate(probs) if i<=k) for k in range(len(probs))]
# rebuild a new, optimized action structure
actions = (flogin, flogout) + tuple(zip(cprobs, cfuncs))
# setting up all clients (they are automatically started)
factory = DummyFactory(actions)
for i in range(NCLIENTS):
reactor.connectTCP("localhost", TELNET_PORT, factory)
# start reactor
reactor.run()
#------------------------------------------------------------
# Command line interface
#------------------------------------------------------------
if __name__ == '__main__':
# parsing command line with default vals
parser = ArgumentParser(description=HELPTEXT)
parser.add_argument("-N", nargs=1, default=1, dest="nclients",
help="Number of clients to start")
args = parser.parse_args()
print INFO_STARTING.format(N=args.nclients[0])
# run the dummyrunner
t0 = time.time()
start_all_dummy_clients(nclients=args.nclients[0])
ttot = time.time() - t0
# output runtime
print "... dummy client runner stopped after %s." % time_format(ttot, style=3)

View file

@ -1,267 +0,0 @@
"""
Settings and actions for the dummyrunner
This module defines dummyrunner settings and sets up
the actions available to dummy players.
The settings are global variables:
TIMESTEP - time in seconds between each 'tick'
CHANCE_OF_ACTION - chance 0-1 of action happening
CHANCE_OF_LOGIN - chance 0-1 of login happening
TELNET_PORT - port to use, defaults to settings.TELNET_PORT
ACTIONS - see below
ACTIONS is a tuple
(login_func, logout_func, (0.3, func1), (0.1, func2) ... )
where the first entry is the function to call on first connect, with a
chance of occurring given by CHANCE_OF_LOGIN. This function is usually
responsible for logging in the player. The second entry is always
called when the dummyrunner disconnects from the server and should
thus issue a logout command. The other entries are tuples (chance,
func). They are picked randomly, their commonality based on the
cumulative chance given (the chance is normalized between all options
so if will still work also if the given chances don't add up to 1).
Since each function can return a list of game-command strings, each
function may result in multiple operations.
An action-function is called with a "client" argument which is a
reference to the dummy client currently performing the action. It
returns a string or a list of command strings to execute. Use the
client object for optionally saving data between actions.
The client object has the following relevant properties and methods:
key - an optional client key. This is only used for dummyrunner output.
Default is "Dummy-<cid>"
cid - client id
gid - globally unique id, hashed with time stamp
istep - the current step
exits - an empty list. Can be used to store exit names
objs - an empty list. Can be used to store object names
counter() - returns a unique increasing id, hashed with time stamp
to make it unique also between dummyrunner instances.
The return should either be a single command string or a tuple of
command strings. This list of commands will always be executed every
TIMESTEP with a chance given by CHANCE_OF_ACTION by in the order given
(no randomness) and allows for setting up a more complex chain of
commands (such as creating an account and logging in).
"""
# Dummy runner settings
# Time between each dummyrunner "tick", in seconds. Each dummy
# will be called with this frequency.
TIMESTEP = 2
# Chance of a dummy actually performing an action on a given tick.
# This spreads out usage randomly, like it would be in reality.
CHANCE_OF_ACTION = 0.20
# Chance of a currently unlogged-in dummy performing its login
# action every tick. This emulates not all players logging in
# at exactly the same time.
CHANCE_OF_LOGIN = 0.10
# Which telnet port to connect to. If set to None, uses the first
# default telnet port of the running server.
TELNET_PORT = None
# Setup actions tuple
# some convenient templates
DUMMY_NAME = "Dummy-%s"
DUMMY_PWD = "password-%s"
START_ROOM = "testing_room_start_%s"
ROOM_TEMPLATE = "testing_room_%s"
EXIT_TEMPLATE = "exit_%s"
OBJ_TEMPLATE = "testing_obj_%s"
TOBJ_TEMPLATE = "testing_button_%s"
TOBJ_TYPECLASS = "contrib.tutorial_examples.red_button.RedButton"
# action function definitions (pick and choose from
# these to build a client "usage profile"
# login/logout
def c_login(client):
"logins to the game"
# we always use a new client name
cname = DUMMY_NAME % client.gid
cpwd = DUMMY_PWD % client.gid
# set up for digging a first room (to move to and keep the
# login room clean)
roomname = ROOM_TEMPLATE % client.counter()
exitname1 = EXIT_TEMPLATE % client.counter()
exitname2 = EXIT_TEMPLATE % client.counter()
client.exits.extend([exitname1, exitname2])
cmds = ('create %s %s' % (cname, cpwd),
'connect %s %s' % (cname, cpwd),
'@dig %s' % START_ROOM % client.gid,
'@teleport %s' % START_ROOM % client.gid,
'@dig %s = %s, %s' % (roomname, exitname1, exitname2)
)
return cmds
def c_login_nodig(client):
"logins, don't dig its own room"
cname = DUMMY_NAME % client.gid
cpwd = DUMMY_PWD % client.gid
cmds = ('create %s %s' % (cname, cpwd),
'connect %s %s' % (cname, cpwd))
return cmds
def c_logout(client):
"logouts of the game"
return "@quit"
# random commands
def c_looks(client):
"looks at various objects"
cmds = ["look %s" % obj for obj in client.objs]
if not cmds:
cmds = ["look %s" % exi for exi in client.exits]
if not cmds:
cmds = "look"
return cmds
def c_examines(client):
"examines various objects"
cmds = ["examine %s" % obj for obj in client.objs]
if not cmds:
cmds = ["examine %s" % exi for exi in client.exits]
if not cmds:
cmds = "examine me"
return cmds
def c_help(client):
"reads help files"
cmds = ('help',
'help @teleport',
'help look',
'help @tunnel',
'help @dig')
return cmds
def c_digs(client):
"digs a new room, storing exit names on client"
roomname = ROOM_TEMPLATE % client.counter()
exitname1 = EXIT_TEMPLATE % client.counter()
exitname2 = EXIT_TEMPLATE % client.counter()
client.exits.extend([exitname1, exitname2])
return '@dig/tel %s = %s, %s' % (roomname, exitname1, exitname2)
def c_creates_obj(client):
"creates normal objects, storing their name on client"
objname = OBJ_TEMPLATE % client.counter()
client.objs.append(objname)
cmds = ('@create %s' % objname,
'@desc %s = "this is a test object' % objname,
'@set %s/testattr = this is a test attribute value.' % objname,
'@set %s/testattr2 = this is a second test attribute.' % objname)
return cmds
def c_creates_button(client):
"creates example button, storing name on client"
objname = TOBJ_TEMPLATE % client.counter()
client.objs.append(objname)
cmds = ('@create %s:%s' % (objname, TOBJ_TYPECLASS),
'@desc %s = test red button!' % objname)
return cmds
def c_socialize(client):
"socializechats on channel"
cmds = ('ooc Hello!',
'ooc Testing ...',
'ooc Testing ... times 2',
'say Yo!',
'emote stands looking around.')
return cmds
def c_moves(client):
"moves to a previously created room, using the stored exits"
cmds = client.exits # try all exits - finally one will work
return "look" if not cmds else cmds
def c_moves_n(client):
"move through north exit if available"
return "north"
def c_moves_s(client):
"move through south exit if available"
return "south"
# Action tuple (required)
#
# This is a tuple of client action functions. The first element is the
# function the client should use to log into the game and move to
# STARTROOM . The second element is the logout command, for cleanly
# exiting the mud. The following elements are 2-tuples of (probability,
# action_function). The probablities should normally sum up to 1,
# otherwise the system will normalize them.
#
## "normal builder" definitionj
#ACTIONS = ( c_login,
# c_logout,
# (0.5, c_looks),
# (0.08, c_examines),
# (0.1, c_help),
# (0.01, c_digs),
# (0.01, c_creates_obj),
# (0.3, c_moves))
## "heavy" builder definition
#ACTIONS = ( c_login,
# c_logout,
# (0.2, c_looks),
# (0.1, c_examines),
# (0.2, c_help),
# (0.1, c_digs),
# (0.1, c_creates_obj),
# #(0.01, c_creates_button),
# (0.2, c_moves))
## "passive player" definition
#ACTIONS = ( c_login,
# c_logout,
# (0.7, c_looks),
# #(0.1, c_examines),
# (0.3, c_help))
# #(0.1, c_digs),
# #(0.1, c_creates_obj),
# #(0.1, c_creates_button),
# #(0.4, c_moves))
## "normal player" definition
ACTIONS = ( c_login,
c_logout,
(0.01, c_digs),
(0.39, c_looks),
(0.2, c_help),
(0.4, c_moves))
# walking tester. This requires a pre-made
# "loop" of multiple rooms that ties back
# to limbo (using @tunnel and @open)
#ACTIONS = (c_login_nodig,
# c_logout,
# (1.0, c_moves_n))
## "socializing heavy builder" definition
#ACTIONS = (c_login,
# c_logout,
# (0.1, c_socialize),
# (0.1, c_looks),
# (0.2, c_help),
# (0.1, c_creates_obj),
# (0.2, c_digs),
# (0.3, c_moves))
## "heavy digger memory tester" definition
#ACTIONS = (c_login,
# c_logout,
# (1.0, c_digs))

View file

@ -1,101 +0,0 @@
"""
Script that saves memory and idmapper data over time.
Data will be saved to game/logs/memoryusage.log. Note that
the script will append to this file if it already exists.
Call this module directly to plot the log (requires matplotlib and numpy).
"""
import os, sys
import time
#TODO!
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
#os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
import ev
from evennia.utils.idmapper import base as _idmapper
LOGFILE = "logs/memoryusage.log"
INTERVAL = 30 # log every 30 seconds
class Memplot(ev.Script):
def at_script_creation(self):
self.key = "memplot"
self.desc = "Save server memory stats to file"
self.start_delay = False
self.persistent = True
self.interval = INTERVAL
self.db.starttime = time.time()
def at_repeat(self):
pid = os.getpid()
rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1000.0 # resident memory
vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1000.0 # virtual memory
total_num, cachedict = _idmapper.cache_size()
t0 = (time.time() - self.db.starttime) / 60.0 # save in minutes
with open(LOGFILE, "a") as f:
f.write("%s, %s, %s, %s\n" % (t0, rmem, vmem, int(total_num)))
if __name__ == "__main__":
# plot output from the file
from matplotlib import pyplot as pp
import numpy
data = numpy.genfromtxt("../../../game/" + LOGFILE, delimiter=",")
secs = data[:,0]
rmem = data[:,1]
vmem = data[:,2]
nobj = data[:,3]
# calculate derivative of obj creation
#oderiv = (0.5*(nobj[2:] - nobj[:-2]) / (secs[2:] - secs[:-2])).copy()
#oderiv = (0.5*(rmem[2:] - rmem[:-2]) / (secs[2:] - secs[:-2])).copy()
fig = pp.figure()
ax1 = fig.add_subplot(111)
ax1.set_title("1000 bots (normal players with light building)")
ax1.set_xlabel("Time (mins)")
ax1.set_ylabel("Memory usage (MB)")
ax1.plot(secs, rmem, "r", label="RMEM", lw=2)
ax1.plot(secs, vmem, "b", label="VMEM", lw=2)
ax1.legend(loc="upper left")
ax2 = ax1.twinx()
ax2.plot(secs, nobj, "g--", label="objs in cache", lw=2)
#ax2.plot(secs[:-2], oderiv/60.0, "g--", label="Objs/second", lw=2)
#ax2.plot(secs[:-2], oderiv, "g--", label="Objs/second", lw=2)
ax2.set_ylabel("Number of objects")
ax2.legend(loc="lower right")
ax2.annotate("First 500 bots\nconnecting", xy=(10, 4000))
ax2.annotate("Next 500 bots\nconnecting", xy=(350,10000))
#ax2.annotate("@reload", xy=(185,600))
# # plot mem vs cachesize
# nobj, rmem, vmem = nobj[:262].copy(), rmem[:262].copy(), vmem[:262].copy()
#
# fig = pp.figure()
# ax1 = fig.add_subplot(111)
# ax1.set_title("Memory usage per cache size")
# ax1.set_xlabel("Cache size (number of objects)")
# ax1.set_ylabel("Memory usage (MB)")
# ax1.plot(nobj, rmem, "r", label="RMEM", lw=2)
# ax1.plot(nobj, vmem, "b", label="VMEM", lw=2)
#
## # empirical estimate of memory usage: rmem = 35.0 + 0.0157 * Ncache
## # Ncache = int((rmem - 35.0) / 0.0157) (rmem in MB)
#
# rderiv_aver = 0.0157
# fig = pp.figure()
# ax1 = fig.add_subplot(111)
# ax1.set_title("Relation between memory and cache size")
# ax1.set_xlabel("Memory usage (MB)")
# ax1.set_ylabel("Idmapper Cache Size (number of objects)")
# rmem = numpy.linspace(35, 2000, 2000)
# nobjs = numpy.array([int((mem - 35.0) / 0.0157) for mem in rmem])
# ax1.plot(rmem, nobjs, "r", lw=2)
pp.show()

View file

@ -1,41 +0,0 @@
"""
This is a little routine for viewing the sql queries that are executed by a given
query as well as count them for optimization testing.
"""
import sys, os
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
#os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings"
from django.db import connection
def count_queries(exec_string, setup_string):
"""
Display queries done by exec_string. Use setup_string
to setup the environment to test.
"""
exec setup_string
num_queries_old = len(connection.queries)
exec exec_string
nqueries = len(connection.queries) - num_queries_old
for query in connection.queries[-nqueries if nqueries else 1:]:
print query["time"], query["sql"]
print "Number of queries: %s" % nqueries
if __name__ == "__main__":
# setup tests here
setup_string = \
"""
from evennia.objects.models import ObjectDB
g = ObjectDB.objects.get(db_key="Griatch")
"""
exec_string = \
"""
g.tags.all()
"""
count_queries(exec_string, setup_string)

11
bin/unix/evennia Executable file
View file

@ -0,0 +1,11 @@
#! /usr/bin/python2.7
"""
Linux launcher
"""
import os, sys
sys.path.insert(0, os.path.abspath(os.getcwd()))
from evennia.server.evennia_launcher import main
main()

1
bin/windows/evennia.bat Normal file
View file

@ -0,0 +1 @@
@"python" "evennia.py" %*

11
bin/windows/evennia.py Executable file
View file

@ -0,0 +1,11 @@
#! /usr/bin/python2.7
"""
Linux launcher
"""
import os, sys
sys.path.insert(0, os.path.abspath(os.getcwd()))
from evennia.server.evennia_launcher import main
main()