From 40a1923c850189f8c6f6984dec30702ea0096a8c Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 2 Feb 2015 17:57:01 +0100 Subject: [PATCH] More changes and variations, hopefully more applicable to windows. --- bin/evennia | 909 ++++++++++++++++++++++++++++++++++++++++++++++++- bin/evennia.py | 905 ------------------------------------------------ setup.py | 2 +- 3 files changed, 901 insertions(+), 915 deletions(-) delete mode 100644 bin/evennia.py diff --git a/bin/evennia b/bin/evennia index dd311e1d77..59b1e81864 100755 --- a/bin/evennia +++ b/bin/evennia @@ -1,14 +1,905 @@ -#! /usr/bin/python2.7 -""" -Linux evennia launcher wrapper. Windows (which doesn't support -shebangs but which associates .py files with the python launcher) will -use the evennia.py program directly. +#!/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 subprocess +import signal +import shutil +import importlib +from argparse import ArgumentParser +from subprocess import Popen, check_output, call, CalledProcessError, STDOUT +import django -# set up the path and launch evennia.py -evenniapath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "evennia.py") -subprocess.Popen([sys.executable] + [evenniapath] + sys.argv[1:]) +# 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 ) + 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 to continue ...") + continue + elif inp.lower() in ('v', 'i', 'a'): + print show_version_info(about=True) + raw_input("press 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() diff --git a/bin/evennia.py b/bin/evennia.py deleted file mode 100644 index 59b1e81864..0000000000 --- a/bin/evennia.py +++ /dev/null @@ -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 ) - 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 to continue ...") - continue - elif inp.lower() in ('v', 'i', 'a'): - print show_version_info(about=True) - raw_input("press 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() diff --git a/setup.py b/setup.py index e1a51ad30c..2658cfd2cd 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def get_scripts(): execlist = [] if os.name == "nt": # Windows - with open(os.path("bin", "evennia.bat"), "w") as bat_file: + with open(os.path.join("bin", "evennia.bat"), "w") as bat_file: bat_file.write("@\"%s\" \"%s\" %%*" % (sys.executable, os.path.join("bin/python.py"))) execlist.append("bin/evennia.bat") else: