#!/usr/bin/python
"""
Evennia launcher program
This is the start point for running Evennia.
Sets the appropriate environmental variables for managing an Evennia game. It will start and connect
to the Portal, through which the Server is also controlled. This pprogram
Run the script with the -h flag to see usage information.
"""
import argparse
import importlib
import os
import pickle
import re
import shutil
import signal
import sys
from argparse import ArgumentParser
from distutils.version import LooseVersion
from subprocess import STDOUT, CalledProcessError, Popen, call, check_output
import django
from django.core.management import execute_from_command_line
from django.db.utils import ProgrammingError
from twisted.internet import endpoints, reactor
from twisted.protocols import amp
# Signal processing
SIG = signal.SIGINT
CTRL_C_EVENT = 0 # Windows SIGINT-like signal
# Set up the main python paths to Evennia
EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import evennia # noqa
EVENNIA_LIB = os.path.join(EVENNIA_ROOT, "evennia")
EVENNIA_SERVER = os.path.join(EVENNIA_LIB, "server")
EVENNIA_TEMPLATE = os.path.join(EVENNIA_LIB, "game_template")
EVENNIA_PROFILING = os.path.join(EVENNIA_SERVER, "profiling")
EVENNIA_DUMMYRUNNER = os.path.join(EVENNIA_PROFILING, "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_PY_FILE = None
PORTAL_PY_FILE = None
SPROFILER_LOGFILE = None
PPROFILER_LOGFILE = None
TEST_MODE = False
ENFORCED_SETTING = False
REACTOR_RUN = False
NO_REACTOR_STOP = False
# communication constants
AMP_PORT = None
AMP_HOST = None
AMP_INTERFACE = None
AMP_CONNECTION = None
SRELOAD = chr(14) # server reloading (have portal start a new server)
SSTART = chr(15) # server start
PSHUTD = chr(16) # portal (+server) shutdown
SSHUTD = chr(17) # server-only shutdown
PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # shutdown server in reset mode
# live version requirement checks (from VERSION_REQS.txt file)
PYTHON_MIN = None
PYTHON_MAX_TESTED = None
TWISTED_MIN = None
DJANGO_MIN = None
DJANGO_MAX_TESTED = None
with open(os.path.join(EVENNIA_LIB, "VERSION_REQS.txt")) as fil:
for line in fil.readlines():
if line.startswith("#") or "=" not in line:
continue
key, *value = (part.strip() for part in line.split("=", 1))
if key == "PYTHON_MIN":
PYTHON_MIN = value[0] if value else "0"
elif key == "PYTHON_MAX_TESTED":
PYTHON_MAX_TESTED = value[0] if value else "100"
elif key == "TWISTED_MIN":
TWISTED_MIN = value[0] if value else "0"
elif key == "DJANGO_MIN":
DJANGO_MIN = value[0] if value else "0"
elif key == "DJANGO_MAX_TESTED":
DJANGO_MAX_TESTED = value[0] if value else "100"
try:
sys.path[1] = EVENNIA_ROOT
except IndexError:
sys.path.append(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 start
Make sure to create a superuser when asked for it (the email is optional)
You should now be able to connect to your server on 'localhost', port 4000
using a telnet/mud client or http://localhost:4001 using your web browser.
If things don't work, check the log with `evennia --log`. Also make sure
ports are open.
(Finally, why not run `evennia connections` and make the world aware of
your new Evennia project!)
"""
ERROR_INPUT = """
Command
{args} {kwargs}
raised an error: '{traceback}'.
"""
ERROR_NO_ALT_GAMEDIR = """
The path '{gamedir}' could not be found.
"""
ERROR_NO_GAMEDIR = """
ERROR: No Evennia settings file was found. Evennia looks for the
file in your game directory as ./server/conf/settings.py.
You must run this command from somewhere inside a valid game
directory first created with
evennia --init mygamename
If you are in a game directory but is missing a settings.py file,
it may be because you have git-cloned an existing game directory.
The settings.py file is not cloned by git (it's in .gitignore)
since it can contain sensitive and/or server-specific information.
You can create a new, empty settings file with
evennia --initsettings
If cloning the settings file is not a problem you could manually
copy over the old settings file or remove its entry in .gitignore
"""
WARNING_MOVING_SUPERUSER = """
WARNING: Evennia expects an Account superuser with id=1. No such
Account was found. However, another superuser ('{other_key}',
id={other_id}) was found in the database. If you just created this
superuser and still see this text it is probably due to the
database being flushed recently - in this case the database's
internal auto-counter might just start from some value higher than
one.
We will fix this by assigning the id 1 to Account '{other_key}'.
Please confirm this is acceptable before continuing.
"""
WARNING_RUNSERVER = """
WARNING: There is no need to run the Django development
webserver to test out Evennia web features (the web client
will in fact not work since the Django test server knows
nothing about MUDs). Instead, just start Evennia with the
webserver component active (this is the default).
"""
ERROR_SETTINGS = """
ERROR: 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(
settingspath=SETTINGS_PATH
)
ERROR_INITSETTINGS = """
u ERROR: 'evennia --initsettings' must be called from the root of
your game directory, since it tries to (re)create the new
settings.py file in a subfolder server/conf/.
"""
RECREATED_SETTINGS = """
(Re)created an empty settings file in server/conf/settings.py.
Note that if you were using an existing database, the password
salt of this new settings file will be different from the old one.
This means that any existing accounts may not be able to log in to
their accounts with their old passwords.
"""
ERROR_INITMISSING = """
ERROR: 'evennia --initmissing' must be called from the root of
your game directory, since it tries to create any missing files
in the server/ subfolder.
"""
RECREATED_MISSING = """
(Re)created any missing directories or files. Evennia should
be ready to run now!
"""
ERROR_DATABASE = """
ERROR: Your database does not exist or is not set up correctly.
(error was '{traceback}')
If you think your database should work, make sure you are running your
commands from inside your game directory. If this error persists, 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 with pip in your Python environment:
pip install --upgrade pywin32
"""
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}
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` again when you
are ready to start the server.
"""
CMDLINE_HELP = """Starts, initializes, manages and operates the Evennia MU* server.
Most standard django management commands are also accepted."""
VERSION_INFO = """
Evennia {version}
OS: {os}
Python: {python}
Twisted: {twisted}
Django: {django}{about}
"""
ABOUT_INFO = """
Evennia MUD/MUX/MU* development system
Licence: BSD 3-Clause Licence
Web: http://www.evennia.com
Chat: https://discord.gg/AJJpcRUhtF
Forum: http://www.evennia.com/discussions
Maintainer (2006-10): Greg Taylor
Maintainer (2010-): Griatch (griatch AT gmail DOT com)
Use -h for command line options.
"""
HELP_ENTRY = """
Evennia has two processes, the 'Server' and the 'Portal'.
External users connect to the Portal while the Server runs the
game/database. Restarting the Server will refresh code but not
disconnect users.
To start a new game, use 'evennia --init mygame'.
For more ways to operate and manage Evennia, see 'evennia -h'.
If you want to add unit tests to your game, see
https://github.com/evennia/evennia/wiki/Unit-Testing
Evennia's manual is found here:
https://github.com/evennia/evennia/wiki
"""
MENU = """
+----Evennia Launcher-------------------------------------------+
{gameinfo}
+--- Common operations -----------------------------------------+
| 1) Start (also restart stopped Server) |
| 2) Reload (stop/start Server in 'reload' mode) |
| 3) Stop (shutdown Portal and Server) |
| 4) Reboot (shutdown then restart) |
+--- Other operations ------------------------------------------+
| 5) Reset (stop/start Server in 'shutdown' mode) |
| 6) Stop Server only |
| 7) Kill Server only (send kill signal to process) |
| 8) Kill Portal + Server |
+--- Information -----------------------------------------------+
| 9) Tail log files (quickly see errors - Ctrl-C to exit) |
| 10) Status |
| 11) Port info |
+--- Testing ---------------------------------------------------+
| 12) Test gamedir (run gamedir test suite, if any) |
| 13) Test Evennia (run Evennia test suite) |
+---------------------------------------------------------------+
| h) Help i) About info q) Abort |
+---------------------------------------------------------------+"""
ERROR_AMP_UNCONFIGURED = """
Can't find server info for connecting. Either run this command from
the game dir (it will then use the game's settings file) or specify
the path to your game's settings file manually with the --settings
option.
"""
ERROR_LOGDIR_MISSING = """
ERROR: One or more log-file directory locations could not be
found:
{logfiles}
This is simple to fix: Just manually create the missing log
directory (or directories) and re-launch the server (the log files
will be created automatically).
(Explanation: Evennia creates the log directory automatically when
initializing a new game directory. This error usually happens if
you used git to clone a pre-created game directory - since log
files are in .gitignore they will not be cloned, which leads to
the log directory also not being created.)
"""
ERROR_PYTHON_VERSION = """
ERROR: Python {python_version} used. Evennia requires version
{python_min} or higher.
"""
WARNING_PYTHON_MAX_TESTED_VERSION = """
WARNING: Python {python_version} used. Evennia is only tested with Python
versions {python_min} to {python_max_tested}. If you see unexpected errors, try
reinstalling with a tested Python version instead.
"""
ERROR_TWISTED_VERSION = """
ERROR: Twisted {twisted_version} found. Evennia requires
version {twisted_min} or higher.
"""
ERROR_NOTWISTED = """
ERROR: Twisted does not seem to be installed.
"""
ERROR_DJANGO_MIN = """
ERROR: Django {django_version} found. Evennia supports Django
{django_min} - {django_max_tested}. Using an older version is not supported.
If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
`evennia` is the folder to where you cloned the Evennia library. If not
in a virtualenv you can install django with for example `pip install --upgrade django`
or with `pip install django=={django_min}` to get a specific version.
It's also a good idea to run `evennia migrate` after this upgrade. Ignore
any warnings and don't run `makemigrate` even if told to.
"""
NOTE_DJANGO_NEW = """
NOTE: Django {django_version} found. This is newer than Evennia's
recommended version ({django_max_tested}). It might work, but is new
enough to not be fully tested yet. Report any issues.
"""
ERROR_NODJANGO = """
ERROR: Django does not seem to be installed.
"""
NOTE_KEYBOARDINTERRUPT = """
STOP: Caught keyboard interrupt while in interactive mode.
"""
NOTE_TEST_DEFAULT = """
TESTING: Using Evennia's default settings file (evennia.settings_default).
(use 'evennia test --settings settings.py .' to run only your custom game tests)
"""
NOTE_TEST_CUSTOM = """
TESTING: Using specified settings file '{settings_dotpath}'.
OBS: Evennia's full test suite may not pass if the settings are very
different from the default (use 'evennia test evennia' to run core tests)
"""
PROCESS_ERROR = """
{component} process error: {traceback}.
"""
PORTAL_INFO = """{servername} Portal {version}
external ports:
{telnet}
{telnet_ssl}
{ssh}
{webserver_proxy}
{webclient}
internal_ports (to Server):
{webserver_internal}
{amp}
"""
SERVER_INFO = """{servername} Server {version}
internal ports (to Portal):
{webserver}
{amp}
{irc_rss}
{info}
{errors}"""
ARG_OPTIONS = """Actions on installed server. One of:
start - launch server+portal if not running
reload - restart server in 'reload' mode
stop - shutdown server+portal
reboot - shutdown server+portal, then start again
reset - restart server in 'shutdown' mode
istart - start server in foreground (until reload)
ipstart - start portal in foreground
sstop - stop only server
kill - send kill signal to portal+server (force)
skill - send kill signal only to server
status - show server and portal run state
info - show server and portal port info
menu - show a menu of options
connections - show connection wizard
Others, like migrate, test and shell is passed on to Django."""
# ------------------------------------------------------------
#
# Private helper functions
#
# ------------------------------------------------------------
def _is_windows():
return os.name == "nt"
def _file_names_compact(filepath1, filepath2):
"Compact the output of filenames with same base dir"
dirname1 = os.path.dirname(filepath1)
dirname2 = os.path.dirname(filepath2)
if dirname1 == dirname2:
name2 = os.path.basename(filepath2)
return "{} and {}".format(filepath1, name2)
else:
return "{} and {}".format(filepath1, filepath2)
def _print_info(portal_info_dict, server_info_dict):
"""
Format info dicts from the Portal/Server for display
"""
ind = " " * 8
def _prepare_dict(dct):
out = {}
for key, value in dct.items():
if isinstance(value, list):
value = "\n{}".format(ind).join(str(val) for val in value)
out[key] = value
return out
def _strip_empty_lines(string):
return "\n".join(line for line in string.split("\n") if line.strip())
pstr, sstr = "", ""
if portal_info_dict:
pdict = _prepare_dict(portal_info_dict)
pstr = _strip_empty_lines(PORTAL_INFO.format_map(pdict))
if server_info_dict:
sdict = _prepare_dict(server_info_dict)
sstr = _strip_empty_lines(SERVER_INFO.format_map(sdict))
info = pstr + ("\n\n" + sstr if sstr else "")
maxwidth = max(len(line) for line in info.split("\n"))
top_border = "-" * (maxwidth - 11) + " Evennia " + "---"
border = "-" * (maxwidth + 1)
print(top_border + "\n" + info + "\n" + border)
def _parse_status(response):
"Unpack the status information"
return pickle.loads(response["status"])
def _get_twistd_cmdline(pprofiler, sprofiler):
"""
Compile the command line for starting a Twisted application using the 'twistd' executable.
"""
portal_cmd = [
TWISTED_BINARY,
f"--python={PORTAL_PY_FILE}",
"--logger=evennia.utils.logger.GetPortalLogObserver",
]
server_cmd = [
TWISTED_BINARY,
f"--python={SERVER_PY_FILE}",
"--logger=evennia.utils.logger.GetServerLogObserver",
]
if os.name != "nt":
# PID files only for UNIX
portal_cmd.append("--pidfile={}".format(PORTAL_PIDFILE))
server_cmd.append("--pidfile={}".format(SERVER_PIDFILE))
if pprofiler:
portal_cmd.extend(
["--savestats", "--profiler=cprofile", "--profile={}".format(PPROFILER_LOGFILE)]
)
if sprofiler:
server_cmd.extend(
["--savestats", "--profiler=cprofile", "--profile={}".format(SPROFILER_LOGFILE)]
)
return portal_cmd, server_cmd
def _reactor_stop():
if not NO_REACTOR_STOP:
reactor.stop()
# ------------------------------------------------------------
#
# Protocol Evennia launcher - Portal/Server communication
#
# ------------------------------------------------------------
[docs]class MsgStatus(amp.Command):
"""
Ping between AMP services
"""
key = "MsgStatus"
arguments = [(b"status", amp.String())]
errors = {Exception: b"EXCEPTION"}
response = [(b"status", amp.String())]
[docs]class MsgLauncher2Portal(amp.Command):
"""
Message Launcher -> Portal
"""
key = "MsgLauncher2Portal"
arguments = [(b"operation", amp.String()), (b"arguments", amp.String())]
errors = {Exception: b"EXCEPTION"}
response = []
[docs]class AMPLauncherProtocol(amp.AMP):
"""
Defines callbacks to the launcher
"""
[docs] def __init__(self):
self.on_status = []
[docs] def wait_for_status(self, callback):
"""
Register a waiter for a status return.
"""
self.on_status.append(callback)
[docs] @MsgStatus.responder
def receive_status_from_portal(self, status):
"""
Get a status signal from portal - fire next queued
callback
"""
try:
callback = self.on_status.pop()
except IndexError:
pass
else:
status = pickle.loads(status)
callback(status)
return {"status": pickle.dumps(b"")}
[docs]def send_instruction(operation, arguments, callback=None, errback=None):
"""
Send instruction and handle the response.
"""
global AMP_CONNECTION, REACTOR_RUN
if None in (AMP_HOST, AMP_PORT, AMP_INTERFACE):
print(ERROR_AMP_UNCONFIGURED)
sys.exit()
def _callback(result):
if callback:
callback(result)
def _errback(fail):
if errback:
errback(fail)
def _on_connect(prot):
"""
This fires with the protocol when connection is established. We
immediately send off the instruction
"""
global AMP_CONNECTION
AMP_CONNECTION = prot
_send()
def _on_connect_fail(fail):
"This is called if portal is not reachable."
errback(fail)
def _send():
if operation == PSTATUS:
return AMP_CONNECTION.callRemote(MsgStatus, status=b"").addCallbacks(
_callback, _errback
)
else:
return AMP_CONNECTION.callRemote(
MsgLauncher2Portal,
operation=bytes(operation, "utf-8"),
arguments=pickle.dumps(arguments, pickle.HIGHEST_PROTOCOL),
).addCallbacks(_callback, _errback)
if AMP_CONNECTION:
# already connected - send right away
return _send()
else:
# we must connect first, send once connected
point = endpoints.TCP4ClientEndpoint(reactor, AMP_HOST, AMP_PORT)
deferred = endpoints.connectProtocol(point, AMPLauncherProtocol())
deferred.addCallbacks(_on_connect, _on_connect_fail)
REACTOR_RUN = True
return deferred
[docs]def query_status(callback=None):
"""
Send status ping to portal
"""
wmap = {True: "RUNNING", False: "NOT RUNNING"}
def _callback(response):
if callback:
callback(response)
else:
pstatus, sstatus, ppid, spid, pinfo, sinfo = _parse_status(response)
print(
"Portal: {}{}\nServer: {}{}".format(
wmap[pstatus],
" (pid {})".format(get_pid(PORTAL_PIDFILE, ppid)) if pstatus else "",
wmap[sstatus],
" (pid {})".format(get_pid(SERVER_PIDFILE, spid)) if sstatus else "",
)
)
_reactor_stop()
def _errback(fail):
pstatus, sstatus = False, False
print("Portal: {}\nServer: {}".format(wmap[pstatus], wmap[sstatus]))
_reactor_stop()
send_instruction(PSTATUS, None, _callback, _errback)
[docs]def wait_for_status_reply(callback):
"""
Wait for an explicit STATUS signal to be sent back from Evennia.
"""
if AMP_CONNECTION:
AMP_CONNECTION.wait_for_status(callback)
else:
print("No Evennia connection established.")
[docs]def wait_for_status(
portal_running=True, server_running=True, callback=None, errback=None, rate=0.5, retries=20
):
"""
Repeat the status ping until the desired state combination is achieved.
Args:
portal_running (bool or None): Desired portal run-state. If None, any state
is accepted.
server_running (bool or None): Desired server run-state. If None, any state
is accepted. The portal must be running.
callback (callable): Will be called with portal_state, server_state when
condition is fulfilled.
errback (callable): Will be called with portal_state, server_state if the
request is timed out.
rate (float): How often to retry.
retries (int): How many times to retry before timing out and calling `errback`.
"""
def _callback(response):
prun, srun, _, _, _, _ = _parse_status(response)
if (portal_running is None or prun == portal_running) and (
server_running is None or srun == server_running
):
# the correct state was achieved
if callback:
callback(prun, srun)
else:
_reactor_stop()
else:
if retries <= 0:
if errback:
errback(prun, srun)
else:
print("Connection to Evennia timed out. Try again.")
_reactor_stop()
else:
reactor.callLater(
rate,
wait_for_status,
portal_running,
server_running,
callback,
errback,
rate,
retries - 1,
)
def _errback(fail):
"""
Portal not running
"""
if not portal_running:
# this is what we want
if callback:
callback(portal_running, server_running)
else:
_reactor_stop()
else:
if retries <= 0:
if errback:
errback(portal_running, server_running)
else:
print("Connection to Evennia timed out. Try again.")
_reactor_stop()
else:
reactor.callLater(
rate,
wait_for_status,
portal_running,
server_running,
callback,
errback,
rate,
retries - 1,
)
return send_instruction(PSTATUS, None, _callback, _errback)
# ------------------------------------------------------------
#
# Operational functions
#
# ------------------------------------------------------------
[docs]def collectstatic():
"Run the collectstatic django command"
django.core.management.call_command("collectstatic", interactive=False, verbosity=0)
[docs]def start_evennia(pprofiler=False, sprofiler=False):
"""
This will start Evennia anew by launching the Evennia Portal (which in turn
will start the Server)
"""
portal_cmd, server_cmd = _get_twistd_cmdline(pprofiler, sprofiler)
def _fail(fail):
print(fail)
_reactor_stop()
def _server_started(response):
print("... Server started.\nEvennia running.")
if response:
_, _, _, _, pinfo, sinfo = response
_print_info(pinfo, sinfo)
_reactor_stop()
def _portal_started(*args):
print(
"... Portal started.\nServer starting {} ...".format(
"(under cProfile)" if sprofiler else ""
)
)
wait_for_status_reply(_server_started)
send_instruction(SSTART, server_cmd)
def _portal_running(response):
prun, srun, ppid, spid, _, _ = _parse_status(response)
print("Portal is already running as process {pid}. Not restarted.".format(pid=ppid))
if srun:
print("Server is already running as process {pid}. Not restarted.".format(pid=spid))
_reactor_stop()
else:
print("Server starting {}...".format("(under cProfile)" if sprofiler else ""))
send_instruction(SSTART, server_cmd, _server_started, _fail)
def _portal_not_running(fail):
print("Portal starting {}...".format("(under cProfile)" if pprofiler else ""))
try:
if _is_windows():
# Windows requires special care
create_no_window = 0x08000000
Popen(portal_cmd, env=getenv(), bufsize=-1, creationflags=create_no_window)
else:
Popen(portal_cmd, env=getenv(), bufsize=-1)
except Exception as e:
print(PROCESS_ERROR.format(component="Portal", traceback=e))
_reactor_stop()
wait_for_status(True, None, _portal_started)
collectstatic()
send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
[docs]def reload_evennia(sprofiler=False, reset=False):
"""
This will instruct the Portal to reboot the Server component. We
do this manually by telling the server to shutdown (in reload mode)
and wait for the portal to report back, at which point we start the
server again. This way we control the process exactly.
"""
_, server_cmd = _get_twistd_cmdline(False, sprofiler)
def _server_restarted(*args):
print("... Server re-started.")
_reactor_stop()
def _server_reloaded(status):
print("... Server {}.".format("reset" if reset else "reloaded"))
_reactor_stop()
def _server_stopped(status):
wait_for_status_reply(_server_reloaded)
send_instruction(SSTART, server_cmd)
def _portal_running(response):
_, srun, _, _, _, _ = _parse_status(response)
if srun:
print("Server {}...".format("resetting" if reset else "reloading"))
wait_for_status_reply(_server_stopped)
send_instruction(SRESET if reset else SRELOAD, {})
else:
print("Server down. Re-starting ...")
wait_for_status_reply(_server_restarted)
send_instruction(SSTART, server_cmd)
def _portal_not_running(fail):
print("Evennia not running. Starting ...")
start_evennia()
collectstatic()
send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
[docs]def stop_evennia():
"""
This instructs the Portal to stop the Server and then itself.
"""
def _portal_stopped(*args):
print("... Portal stopped.\nEvennia shut down.")
_reactor_stop()
def _server_stopped(*args):
print("... Server stopped.\nStopping Portal ...")
send_instruction(PSHUTD, {})
wait_for_status(False, None, _portal_stopped)
def _portal_running(response):
prun, srun, ppid, spid, _, _ = _parse_status(response)
if srun:
print("Server stopping ...")
send_instruction(SSHUTD, {})
wait_for_status_reply(_server_stopped)
else:
print("Server already stopped.\nStopping Portal ...")
send_instruction(PSHUTD, {})
wait_for_status(False, None, _portal_stopped)
def _portal_not_running(fail):
print("Evennia not running.")
_reactor_stop()
send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
[docs]def reboot_evennia(pprofiler=False, sprofiler=False):
"""
This is essentially an evennia stop && evennia start except we make sure
the system has successfully shut down before starting it again.
If evennia was not running, start it.
"""
global AMP_CONNECTION
def _portal_stopped(*args):
print("... Portal stopped. Evennia shut down. Rebooting ...")
global AMP_CONNECTION
AMP_CONNECTION = None
start_evennia(pprofiler, sprofiler)
def _server_stopped(*args):
print("... Server stopped.\nStopping Portal ...")
send_instruction(PSHUTD, {})
wait_for_status(False, None, _portal_stopped)
def _portal_running(response):
prun, srun, ppid, spid, _, _ = _parse_status(response)
if srun:
print("Server stopping ...")
send_instruction(SSHUTD, {})
wait_for_status_reply(_server_stopped)
else:
print("Server already stopped.\nStopping Portal ...")
send_instruction(PSHUTD, {})
wait_for_status(False, None, _portal_stopped)
def _portal_not_running(fail):
print("Evennia not running. Starting ...")
start_evennia()
collectstatic()
send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
[docs]def start_only_server():
"""
Tell portal to start server (debug)
"""
portal_cmd, server_cmd = _get_twistd_cmdline(False, False)
print("launcher: Sending to portal: SSTART + {}".format(server_cmd))
collectstatic()
send_instruction(SSTART, server_cmd)
[docs]def start_server_interactive():
"""
Start the Server under control of the launcher process (foreground)
"""
def _iserver():
_, server_twistd_cmd = _get_twistd_cmdline(False, False)
server_twistd_cmd.append("--nodaemon")
print("Starting Server in interactive mode (stop with Ctrl-C)...")
try:
Popen(server_twistd_cmd, env=getenv(), stderr=STDOUT).wait()
except KeyboardInterrupt:
print("... Stopped Server with Ctrl-C.")
else:
print("... Server stopped (leaving interactive mode).")
collectstatic()
stop_server_only(when_stopped=_iserver, interactive=True)
[docs]def start_portal_interactive():
"""
Start the Portal under control of the launcher process (foreground)
Notes:
In a normal start, the launcher waits for the Portal to start, then
tells it to start the Server. Since we can't do this here, we instead
start the Server first and then starts the Portal - the Server will
auto-reconnect to the Portal. To allow the Server to be reloaded, this
relies on a fixed server server-cmdline stored as a fallback on the
portal application in evennia/server/portal/portal.py.
"""
def _iportal(fail):
portal_twistd_cmd, server_twistd_cmd = _get_twistd_cmdline(False, False)
portal_twistd_cmd.append("--nodaemon")
# starting Server first - it will auto-connect once Portal comes up
if _is_windows():
# Windows requires special care
create_no_window = 0x08000000
Popen(server_twistd_cmd, env=getenv(), bufsize=-1, creationflags=create_no_window)
else:
Popen(server_twistd_cmd, env=getenv(), bufsize=-1)
print("Starting Portal in interactive mode (stop with Ctrl-C)...")
try:
Popen(portal_twistd_cmd, env=getenv(), stderr=STDOUT).wait()
except KeyboardInterrupt:
print("... Stopped Portal with Ctrl-C.")
else:
print("... Portal stopped (leaving interactive mode).")
def _portal_running(response):
print("Evennia must be shut down completely before running Portal in interactive mode.")
_reactor_stop()
send_instruction(PSTATUS, None, _portal_running, _iportal)
[docs]def stop_server_only(when_stopped=None, interactive=False):
"""
Only stop the Server-component of Evennia (this is not useful except for debug)
Args:
when_stopped (callable): This will be called with no arguments when Server has stopped (or
if it had already stopped when this is called).
interactive (bool, optional): Set if this is called as part of the interactive reload
mechanism.
"""
def _server_stopped(*args):
if when_stopped:
when_stopped()
else:
print("... Server stopped.")
_reactor_stop()
def _portal_running(response):
_, srun, _, _, _, _ = _parse_status(response)
if srun:
print("Server stopping ...")
wait_for_status_reply(_server_stopped)
if interactive:
send_instruction(SRELOAD, {})
else:
send_instruction(SSHUTD, {})
else:
if when_stopped:
when_stopped()
else:
print("Server is not running.")
_reactor_stop()
def _portal_not_running(fail):
print("Evennia is not running.")
if interactive:
print("Start Evennia normally first, then use `istart` to switch to interactive mode.")
_reactor_stop()
send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
[docs]def query_info():
"""
Display the info strings from the running Evennia
"""
def _got_status(status):
_, _, _, _, pinfo, sinfo = _parse_status(status)
_print_info(pinfo, sinfo)
_reactor_stop()
def _portal_running(response):
query_status(_got_status)
def _portal_not_running(fail):
print("Evennia is not running.")
send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
[docs]def tail_log_files(filename1, filename2, start_lines1=20, start_lines2=20, rate=1):
"""
Tail two logfiles interactively, combining their output to stdout
When first starting, this will display the tail of the log files. After
that it will poll the log files repeatedly and display changes.
Args:
filename1 (str): Path to first log file.
filename2 (str): Path to second log file.
start_lines1 (int): How many lines to show from existing first log.
start_lines2 (int): How many lines to show from existing second log.
rate (int, optional): How often to poll the log file.
"""
global REACTOR_RUN
def _file_changed(filename, prev_size):
"Get size of file in bytes, get diff compared with previous size"
try:
new_size = os.path.getsize(filename)
except FileNotFoundError:
return False, 0
return new_size != prev_size, new_size
def _get_new_lines(filehandle, old_linecount):
"count lines, get the ones not counted before"
def _block(filehandle, size=65536):
"File block generator for quick traversal"
while True:
dat = filehandle.read(size)
if not dat:
break
yield dat
# count number of lines in file
new_linecount = sum(blck.count("\n") for blck in _block(filehandle))
if new_linecount < old_linecount:
# this happens if the file was cycled or manually deleted/edited.
print(
" ** Log file {filename} has cycled or been edited. Restarting log. ".format(
filename=filehandle.name
)
)
new_linecount = 0
old_linecount = 0
lines_to_get = max(0, new_linecount - old_linecount)
if not lines_to_get:
return [], old_linecount
lines_found = []
buffer_size = 4098
block_count = -1
while len(lines_found) < lines_to_get:
try:
# scan backwards in file, starting from the end
filehandle.seek(block_count * buffer_size, os.SEEK_END)
except IOError:
# file too small for current seek, include entire file
filehandle.seek(0)
lines_found = filehandle.readlines()
break
lines_found = filehandle.readlines()
block_count -= 1
# only actually return the new lines
return lines_found[-lines_to_get:], new_linecount
def _tail_file(filename, file_size, line_count, max_lines=None):
"""This will cycle repeatedly, printing new lines"""
# poll for changes
has_changed, file_size = _file_changed(filename, file_size)
if has_changed:
try:
with open(filename, "r") as filehandle:
new_lines, line_count = _get_new_lines(filehandle, line_count)
except IOError:
# the log file might not exist yet. Wait a little, then try again ...
pass
else:
if max_lines == 0:
# don't show any lines from old file
new_lines = []
elif max_lines:
# show some lines from first startup
new_lines = new_lines[-max_lines:]
# print to stdout without line break (log has its own line feeds)
sys.stdout.write("".join(new_lines))
sys.stdout.flush()
# set up the next poll
reactor.callLater(rate, _tail_file, filename, file_size, line_count, max_lines=100)
reactor.callLater(0, _tail_file, filename1, 0, 0, max_lines=start_lines1)
reactor.callLater(0, _tail_file, filename2, 0, 0, max_lines=start_lines2)
REACTOR_RUN = True
# ------------------------------------------------------------
#
# Environment setup
#
# ------------------------------------------------------------
[docs]def evennia_version():
"""
Get the Evennia version info from the main package.
"""
version = "Unknown"
try:
version = evennia.__version__
except (ImportError, AttributeError):
# even if evennia is not found, we should not crash here.
pass
else:
return version
try:
rev = (
check_output("git rev-parse --short HEAD", shell=True, cwd=EVENNIA_ROOT, stderr=STDOUT)
.strip()
.decode()
)
version = "%s (rev %s)" % (version, rev)
except (IOError, CalledProcessError, OSError):
# move on if git is not answering
pass
return version
EVENNIA_VERSION = evennia_version()
[docs]def check_main_evennia_dependencies():
"""
Checks and imports the Evennia dependencies. This must be done
already before the paths are set up.
Returns:
not_error (bool): True if no dependency error was found.
"""
def _test_python_version():
"""Test Python version"""
python_version = ".".join(str(num) for num in sys.version_info if isinstance(num, int))
python_curr = LooseVersion(python_version)
python_min = LooseVersion(PYTHON_MIN)
python_max = LooseVersion(PYTHON_MAX_TESTED)
if python_curr < python_min:
print(ERROR_PYTHON_VERSION.format(python_version=python_version, python_min=PYTHON_MIN))
return False
elif python_curr > python_max:
print(
WARNING_PYTHON_MAX_TESTED_VERSION.format(
python_version=python_version,
python_min=PYTHON_MIN,
python_max_tested=PYTHON_MAX_TESTED,
)
)
return True
def _test_twisted_version():
"""Test Twisted version"""
try:
import twisted
except ImportError:
print(ERROR_NOTWISTED)
return False
else:
twisted_version = twisted.version.short()
twisted_curr = LooseVersion(twisted_version)
twisted_min = LooseVersion(TWISTED_MIN)
if twisted_curr < twisted_min:
print(
ERROR_TWISTED_VERSION.format(
twisted_version=twisted_version, twisted_min=TWISTED_MIN
)
)
return False
else:
return True
def _test_django_version():
"""Test Django version"""
try:
import django
except ImportError:
print(ERROR_NODJANGO)
return False
else:
django_version = ".".join(str(num) for num in django.VERSION if isinstance(num, int))
# only the main version (1.5, not 1.5.4.0)
django_version = ".".join(django_version.split(".")[:2])
django_curr = LooseVersion(django_version)
django_min = LooseVersion(DJANGO_MIN)
django_max = LooseVersion(DJANGO_MAX_TESTED)
if django_curr < django_min:
print(
ERROR_DJANGO_MIN.format(
django_version=django_version,
django_min=DJANGO_MIN,
django_max_tested=DJANGO_MAX_TESTED,
)
)
return False
elif django_curr > django_max:
print(
NOTE_DJANGO_NEW.format(
django_version=django_version, django_max_tested=DJANGO_MAX_TESTED
)
)
return True
# return True/False if error was reported or not
return all((_test_python_version(), _test_twisted_version(), _test_django_version()))
[docs]def set_gamedir(path):
"""
Set GAMEDIR based on path, by figuring out where the setting file
is inside the directory tree. This allows for running the launcher
from elsewhere than the top of the gamedir folder.
"""
global GAMEDIR
Ndepth = 10
settings_path = SETTINGS_DOTPATH.replace(".", os.sep) + ".py"
os.chdir(path)
for i in range(Ndepth):
gpath = os.getcwd()
if "server" in os.listdir(gpath):
if os.path.isfile(settings_path):
GAMEDIR = gpath
return
os.chdir(os.pardir)
print(ERROR_NO_GAMEDIR)
sys.exit()
[docs]def create_secret_key():
"""
Randomly create the secret key for the settings file
"""
import random
import string
secret_key = list(
(string.ascii_letters + string.digits + string.punctuation)
.replace("\\", "")
.replace("'", '"')
.replace("{", "_")
.replace("}", "-")
)
random.shuffle(secret_key)
secret_key = "".join(secret_key[:40])
return secret_key
[docs]def create_settings_file(init=True, secret_settings=False):
"""
Uses the template settings file to build a working settings file.
Args:
init (bool): This is part of the normal evennia --init
operation. If false, this function will copy a fresh
template file in (asking if it already exists).
secret_settings (bool, optional): If False, create settings.py, otherwise
create the secret_settings.py file.
"""
if secret_settings:
settings_path = os.path.join(GAMEDIR, "server", "conf", "secret_settings.py")
setting_dict = {"secret_key": "'%s'" % create_secret_key()}
else:
settings_path = os.path.join(GAMEDIR, "server", "conf", "settings.py")
setting_dict = {
"settings_default": os.path.join(EVENNIA_LIB, "settings_default.py"),
"servername": '"%s"' % GAMEDIR.rsplit(os.path.sep, 1)[1],
"secret_key": "'%s'" % create_secret_key(),
}
if not init:
# if not --init mode, settings file may already exist from before
if os.path.exists(settings_path):
inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path)
if not inp.lower() == "y":
print("Aborted.")
sys.exit()
else:
print("Reset the settings file.")
if secret_settings:
default_settings_path = os.path.join(
EVENNIA_TEMPLATE, "server", "conf", "secret_settings.py"
)
else:
default_settings_path = os.path.join(EVENNIA_TEMPLATE, "server", "conf", "settings.py")
shutil.copy(default_settings_path, settings_path)
with open(settings_path, "r") as f:
settings_string = f.read()
settings_string = settings_string.format_map(setting_dict)
with open(settings_path, "w") as f:
f.write(settings_string)
[docs]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.
Args:
dirname (str): The directory name to create.
"""
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)
# rename gitignore to .gitignore
os.rename(os.path.join(GAMEDIR, "gitignore"), os.path.join(GAMEDIR, ".gitignore"))
# pre-build settings file in the new GAMEDIR
create_settings_file()
create_settings_file(secret_settings=True)
[docs]def create_superuser():
"""
Create the superuser account
"""
print(
"\nCreate a superuser below. The superuser is Account #1, the 'owner' "
"account of the server. Email is optional and can be empty.\n"
)
from os import environ
username = environ.get("EVENNIA_SUPERUSER_USERNAME")
email = environ.get("EVENNIA_SUPERUSER_EMAIL")
password = environ.get("EVENNIA_SUPERUSER_PASSWORD")
if (username is not None) and (password is not None) and len(password) > 0:
from evennia.accounts.models import AccountDB
superuser = AccountDB.objects.create_superuser(username, email, password)
superuser.save()
else:
django.core.management.call_command("createsuperuser", interactive=True)
[docs]def check_database(always_return=False):
"""
Check so the database exists.
Args:
always_return (bool, optional): If set, will always return True/False
also on critical errors. No output will be printed.
Returns:
exists (bool): `True` if the database exists, otherwise `False`.
"""
# Check so a database exists and is accessible
from django.db import connection
tables = connection.introspection.get_table_list(connection.cursor())
if not tables or not isinstance(tables[0], str): # django 1.8+
tables = [tableinfo.name for tableinfo in tables]
if tables and "accounts_accountdb" in tables:
# database exists and seems set up. Initialize evennia.
evennia._init()
# Try to get Account#1
from evennia.accounts.models import AccountDB
try:
AccountDB.objects.get(id=1)
except (django.db.utils.OperationalError, ProgrammingError) as e:
if always_return:
return False
print(ERROR_DATABASE.format(traceback=e))
sys.exit()
except AccountDB.DoesNotExist:
# no superuser yet. We need to create it.
other_superuser = AccountDB.objects.filter(is_superuser=True)
if other_superuser:
# Another superuser was found, but not with id=1. This may
# happen if using flush (the auto-id starts at a higher
# value). Wwe copy this superuser into id=1. To do
# this we must deepcopy it, delete it then save the copy
# with the new id. This allows us to avoid the UNIQUE
# constraint on usernames.
other = other_superuser[0]
other_id = other.id
other_key = other.username
print(WARNING_MOVING_SUPERUSER.format(other_key=other_key, other_id=other_id))
res = ""
while res.upper() != "Y":
# ask for permission
res = eval(input("Continue [Y]/N: "))
if res.upper() == "N":
sys.exit()
elif not res:
break
# continue with the
from copy import deepcopy
new = deepcopy(other)
other.delete()
new.id = 1
new.save()
else:
create_superuser()
check_database(always_return=always_return)
return True
[docs]def getenv():
"""
Get current environment and add PYTHONPATH.
Returns:
env (dict): Environment global dict.
"""
sep = ";" if _is_windows() else ":"
env = os.environ.copy()
env["PYTHONPATH"] = sep.join(sys.path)
return env
[docs]def get_pid(pidfile, default=None):
"""
Get the PID (Process ID) by trying to access an PID file.
Args:
pidfile (str): The path of the pid file.
default (int, optional): What to return if file does not exist.
Returns:
pid (str): The process id or `default`.
"""
if os.path.exists(pidfile):
with open(pidfile, "r") as f:
pid = f.read()
return pid
return default
[docs]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.
Args:
pidfile (str): The path of the pid file.
"""
if os.path.exists(pidfile):
os.remove(pidfile)
[docs]def kill(pidfile, component="Server", callback=None, errback=None, killsignal=SIG):
"""
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. On Windows, no arguments
are useful since Windows has no ability to direct signals except to all
children of a console.
Args:
pidfile (str): The path of the pidfile to get the PID from. This is ignored
on Windows.
component (str, optional): Usually one of 'Server' or 'Portal'. This is
ignored on Windows.
errback (callable, optional): Called if signal failed to send. This
is ignored on Windows.
callback (callable, optional): Called if kill signal was sent successfully.
This is ignored on Windows.
killsignal (int, optional): Signal identifier for signal to send. This is
ignored on Windows.
"""
if _is_windows():
# Windows signal sending is very limited.
from win32api import GenerateConsoleCtrlEvent, SetConsoleCtrlHandler
try:
# Windows can only send a SIGINT-like signal to
# *every* process spawned off the same console, so we must
# avoid killing ourselves here.
SetConsoleCtrlHandler(None, True)
GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)
except KeyboardInterrupt:
# We must catch and ignore the interrupt sent.
pass
print("Sent kill signal to all spawned processes")
else:
# Linux/Unix/Mac can send kill signal directly to specific PIDs.
pid = get_pid(pidfile)
if pid:
if _is_windows():
os.remove(pidfile)
try:
os.kill(int(pid), killsignal)
except OSError:
print(
"{component} ({pid}) cannot be stopped. "
"The PID file '{pidfile}' seems stale. "
"Try removing it manually.".format(
component=component, pid=pid, pidfile=pidfile
)
)
return
if callback:
callback()
else:
print("Sent kill signal to {component}.".format(component=component))
return
if errback:
errback()
else:
print(
"Could not send kill signal - {component} does not appear to be running.".format(
component=component
)
)
[docs]def show_version_info(about=False):
"""
Display version info.
Args:
about (bool): Include ABOUT info as well as version numbers.
Returns:
version_info (str): A complete version info string.
"""
import sys
import twisted
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(),
)
[docs]def error_check_python_modules(show_warnings=False):
"""
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.
Keyword Args:
show_warnings (bool): If non-fatal warning messages should be shown.
"""
from django.conf import settings
def _imp(path, split=True):
"helper method"
mod, fromlist = path, "None"
if split:
mod, fromlist = path.rsplit(".", 1)
__import__(mod, fromlist=[fromlist])
# check the historical deprecations
from evennia.server import deprecations
try:
deprecations.check_errors(settings)
except DeprecationWarning as err:
print(err)
sys.exit()
if show_warnings:
deprecations.check_warnings(settings)
# core modules
_imp(settings.COMMAND_PARSER)
_imp(settings.SEARCH_AT_RESULT)
_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)
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_ACCOUNT, None):
print("Warning: CMDSET_ACCOUNT failed to load")
# typeclasses
_imp(settings.BASE_ACCOUNT_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)
# ------------------------------------------------------------
#
# Options
#
# ------------------------------------------------------------
[docs]def init_game_directory(path, check_db=True, need_gamedir=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.
Args:
path (str): Path to new game directory, including its name.
check_db (bool, optional): Check if the databae exists.
need_gamedir (bool, optional): set to False if Evennia doesn't require to
be run in a valid game directory.
"""
# set the GAMEDIR path
if need_gamedir:
set_gamedir(path)
# Add gamedir to python path
sys.path.insert(0, GAMEDIR)
if TEST_MODE or not need_gamedir:
if ENFORCED_SETTING:
print(NOTE_TEST_CUSTOM.format(settings_dotpath=SETTINGS_DOTPATH))
os.environ["DJANGO_SETTINGS_MODULE"] = SETTINGS_DOTPATH
else:
print(NOTE_TEST_DEFAULT)
os.environ["DJANGO_SETTINGS_MODULE"] = "evennia.settings_default"
else:
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 as 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()
# if we don't have to check the game directory, return right away
if not need_gamedir:
return
# set up the Evennia executables and log file locations
global AMP_PORT, AMP_HOST, AMP_INTERFACE
global SERVER_PY_FILE, PORTAL_PY_FILE
global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE
global SERVER_PIDFILE, PORTAL_PIDFILE
global SPROFILER_LOGFILE, PPROFILER_LOGFILE
global EVENNIA_VERSION
AMP_PORT = settings.AMP_PORT
AMP_HOST = settings.AMP_HOST
AMP_INTERFACE = settings.AMP_INTERFACE
SERVER_PY_FILE = os.path.join(EVENNIA_LIB, "server", "server.py")
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "server", "portal", "portal.py")
SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid")
PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid")
SPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "server.prof")
PPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "portal.prof")
SERVER_LOGFILE = settings.SERVER_LOG_FILE
PORTAL_LOGFILE = settings.PORTAL_LOG_FILE
HTTP_LOGFILE = settings.HTTP_LOG_FILE
# verify existence of log file dir (this can be missing e.g.
# if the game dir itself was cloned since log files are in .gitignore)
logdirs = [
logfile.rsplit(os.path.sep, 1) for logfile in (SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE)
]
if not all(os.path.isdir(pathtup[0]) for pathtup in logdirs):
errstr = "\n ".join(
"%s (log file %s)" % (pathtup[0], pathtup[1])
for pathtup in logdirs
if not os.path.isdir(pathtup[0])
)
print(ERROR_LOGDIR_MISSING.format(logfiles=errstr))
sys.exit()
if _is_windows():
# We need to handle Windows twisted separately. We create a
# batchfile in game/server, linking to the actual binary
global TWISTED_BINARY
# Windows requires us to use the absolute path for the bat file.
server_path = os.path.dirname(os.path.abspath(__file__))
TWISTED_BINARY = os.path.join(server_path, "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()
batpath = os.path.join(EVENNIA_SERVER, TWISTED_BINARY)
if not os.path.exists(batpath):
# 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.
# Update: In 2016, it seems Twisted 16 has changed the name of
# of its executable from 'twistd.py' to 'twistd.exe'.
twistd_path = os.path.abspath(
os.path.join(
twistd_dir, os.pardir, os.pardir, os.pardir, os.pardir, "scripts", "twistd.exe"
)
)
with open(batpath, "w") as bat_file:
# build a custom bat file for windows
bat_file.write('@"%s" %%*' % twistd_path)
print(INFO_WINDOWS_BATFILE.format(twistd_path=twistd_path))
[docs]def run_dummyrunner(number_of_dummies):
"""
Start an instance of the dummyrunner
Args:
number_of_dummies (int): The number of dummy accounts to start.
Notes:
The dummy accounts' 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:
# this signals the dummyrunner to stop cleanly and should
# not lead to a traceback here.
pass
[docs]def run_connect_wizard():
"""
Run the linking wizard, for adding new external connections.
"""
from .connection_wizard import ConnectionWizard, node_start
wizard = ConnectionWizard()
node_start(wizard)
[docs]def list_settings(keys):
"""
Display the server settings. We only display the Evennia specific
settings here. The result will be printed to the terminal.
Args:
keys (str or list): Setting key or keys to inspect.
"""
from importlib import import_module
from evennia.utils import evtable
evsettings = import_module(SETTINGS_DOTPATH)
if len(keys) == 1 and keys[0].upper() == "ALL":
# show a list of all keys
# a specific key
table = evtable.EvTable()
confs = [key for key in sorted(evsettings.__dict__) if key.isupper()]
for i in range(0, len(confs), 4):
table.add_row(*confs[i : i + 4])
else:
# a specific key
table = evtable.EvTable(width=131)
keys = [key.upper() for key in keys]
confs = dict((key, var) for key, var in evsettings.__dict__.items() if key in keys)
for key, val in confs.items():
table.add_row(key, str(val))
print(table)
[docs]def run_custom_commands(option, *args):
"""
Inject a custom option into the evennia launcher command chain.
Args:
option (str): Incoming option - the first argument after `evennia` on
the command line.
*args: All args will passed to a found callable.__dict__
Returns:
bool: If a custom command was found and handled the option.
Notes:
Provide new commands in settings with
CUSTOM_EVENNIA_LAUNCHER_COMMANDS = {"mycmd": "path.to.callable", ...}
The callable will be passed any `*args` given on the command line and is expected to
handle/validate the input correctly. Use like any other evennia command option on
in the terminal/console, for example:
evennia mycmd foo bar
"""
import importlib
from django.conf import settings
try:
# a dict of {option: callable(*args), ...}
custom_commands = settings.EXTRA_LAUNCHER_COMMANDS
except AttributeError:
return False
cmdpath = custom_commands.get(option)
if cmdpath:
modpath, *cmdname = cmdpath.rsplit(".", 1)
if cmdname:
cmdname = cmdname[0]
mod = importlib.import_module(modpath)
command = mod.__dict__.get(cmdname)
if command:
command(*args)
return True
return False
[docs]def main():
"""
Run the evennia launcher main program.
"""
# set up argument parser
parser = ArgumentParser(description=CMDLINE_HELP, formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(
"--gamedir",
nargs=1,
action="store",
dest="altgamedir",
metavar="<path>",
help="location of gamedir (default: current location)",
)
parser.add_argument(
"--init",
action="store",
dest="init",
metavar="<gamename>",
help="creates a new gamedir 'name' at current location",
)
parser.add_argument(
"--log",
"-l",
action="store_true",
dest="tail_log",
default=False,
help="tail the portal and server logfiles and print to stdout",
)
parser.add_argument(
"--list",
nargs="+",
action="store",
dest="listsetting",
metavar="all|<key>",
help="list settings, use 'all' to list all available keys",
)
parser.add_argument(
"--settings",
nargs=1,
action="store",
dest="altsettings",
default=None,
metavar="<path>",
help=(
"start evennia with alternative settings file from\n"
" gamedir/server/conf/. (default is settings.py)"
),
)
parser.add_argument(
"--initsettings",
action="store_true",
dest="initsettings",
default=False,
help="create a new, empty settings file as\n gamedir/server/conf/settings.py",
)
parser.add_argument(
"--initmissing",
action="store_true",
dest="initmissing",
default=False,
help=(
"checks for missing secret_settings or server logs\n directory, and adds them if needed"
),
)
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="test a server by connecting <N> dummy accounts to it",
)
parser.add_argument(
"-v",
"--version",
action="store_true",
dest="show_version",
default=False,
help="show version info",
)
parser.add_argument("operation", nargs="?", default="noop", help=ARG_OPTIONS)
parser.epilog = (
"Common Django-admin commands are shell, dbshell, test and migrate.\n"
"See the Django documentation for more management commands."
)
args, unknown_args = parser.parse_known_args()
# handle arguments
option = args.operation
# make sure we have everything
check_main_evennia_dependencies()
if not args:
# show help pane
print(CMDLINE_HELP)
sys.exit()
if args.altgamedir:
# use alternative gamedir path
global GAMEDIR
altgamedir = args.altgamedir[0]
if not os.path.isdir(altgamedir) and not args.init:
print(ERROR_NO_ALT_GAMEDIR.format(gamedir=altgamedir))
sys.exit()
GAMEDIR = altgamedir
if args.init:
# initialization of game directory
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:
# show the version info
print(show_version_info(option == "help"))
sys.exit()
if args.altsettings:
# use alternative settings file
global SETTINGSFILE, SETTINGS_DOTPATH, ENFORCED_SETTING
sfile = args.altsettings[0]
SETTINGSFILE = sfile
ENFORCED_SETTING = True
SETTINGS_DOTPATH = "server.conf.%s" % sfile.rstrip(".py")
print("Using settings file '%s' (%s)." % (SETTINGSFILE, SETTINGS_DOTPATH))
if args.initsettings:
# create new settings file
try:
create_settings_file(init=False)
print(RECREATED_SETTINGS)
except IOError:
print(ERROR_INITSETTINGS)
sys.exit()
if args.initmissing:
created = False
try:
log_path = os.path.join(SERVERDIR, "logs")
if not os.path.exists(log_path):
os.makedirs(log_path)
print(f" ... Created missing log dir {log_path}.")
created = True
settings_path = os.path.join(CONFDIR, "secret_settings.py")
if not os.path.exists(settings_path):
create_settings_file(init=False, secret_settings=True)
print(f" ... Created missing secret_settings.py file as {settings_path}.")
created = True
if created:
print(RECREATED_MISSING)
else:
print(" ... No missing resources to create/init. You are good to go.")
except IOError:
print(ERROR_INITMISSING)
sys.exit()
if args.tail_log:
# set up for tailing the log files
global NO_REACTOR_STOP
NO_REACTOR_STOP = True
if not SERVER_LOGFILE:
init_game_directory(CURRENT_DIR, check_db=False)
# adjust how many lines we show from existing logs
start_lines1, start_lines2 = 20, 20
if option not in ("reload", "reset", "noop"):
start_lines1, start_lines2 = 0, 0
tail_log_files(PORTAL_LOGFILE, SERVER_LOGFILE, start_lines1, start_lines2)
print(
" Tailing logfiles {} (Ctrl-C to exit) ...".format(
_file_names_compact(SERVER_LOGFILE, PORTAL_LOGFILE)
)
)
if args.dummyrunner:
# launch the dummy runner
init_game_directory(CURRENT_DIR, check_db=True)
run_dummyrunner(args.dummyrunner[0])
elif args.listsetting:
# display all current server settings
init_game_directory(CURRENT_DIR, check_db=False)
list_settings(args.listsetting)
elif option == "menu":
# launch menu for operation
init_game_directory(CURRENT_DIR, check_db=True)
run_menu()
elif option in (
"status",
"info",
"start",
"istart",
"ipstart",
"reload",
"restart",
"reboot",
"reset",
"stop",
"sstop",
"kill",
"skill",
"sstart",
"connections",
):
# operate the server directly
if not SERVER_LOGFILE:
init_game_directory(CURRENT_DIR, check_db=True)
if option == "status":
query_status()
elif option == "info":
query_info()
elif option == "start":
init_game_directory(CURRENT_DIR, check_db=True)
error_check_python_modules(show_warnings=args.tail_log)
start_evennia(args.profiler, args.profiler)
elif option == "istart":
init_game_directory(CURRENT_DIR, check_db=True)
error_check_python_modules(show_warnings=args.tail_log)
start_server_interactive()
elif option == "ipstart":
start_portal_interactive()
elif option in ("reload", "restart"):
reload_evennia(args.profiler)
elif option == "reboot":
reboot_evennia(args.profiler, args.profiler)
elif option == "reset":
reload_evennia(args.profiler, reset=True)
elif option == "stop":
stop_evennia()
elif option == "sstop":
stop_server_only()
elif option == "sstart":
start_only_server()
elif option == "kill":
if _is_windows():
print("This option is not supported on Windows.")
else:
kill(SERVER_PIDFILE, "Server")
kill(PORTAL_PIDFILE, "Portal")
elif option == "skill":
if _is_windows():
print("This option is not supported on Windows.")
else:
kill(SERVER_PIDFILE, "Server")
elif option == "connections":
run_connect_wizard()
elif option != "noop":
# pass-through to django manager, but set things up first
check_db = False
need_gamedir = True
# handle special django commands
if option in ("runserver", "testserver"):
# we don't want the django test-webserver
print(WARNING_RUNSERVER)
if option in ("makemessages", "compilemessages"):
# some commands don't require the presence of a game directory to work
need_gamedir = False
if CURRENT_DIR != EVENNIA_LIB:
print(
"You must stand in the evennia/evennia/ folder (where the 'locale/' "
"folder is located) to run this command."
)
sys.exit()
if option in ("shell", "check", "makemigrations", "createsuperuser", "shell_plus"):
# some django commands requires the database to exist,
# or evennia._init to have run before they work right.
check_db = True
if option == "test":
global TEST_MODE
TEST_MODE = True
# init the db/game dir, if needed
init_game_directory(CURRENT_DIR, check_db=check_db, need_gamedir=need_gamedir)
if option == "migrate":
# we need to bypass some checks here for the first db creation
if not check_database(always_return=True):
django.core.management.call_command(*([option] + unknown_args))
sys.exit(0)
if option in ("createsuperuser",):
print(
"Note: Don't create an additional superuser this way. It will not be set up "
"correctly.\n Instead, use the web admin or the in-game `py` command to "
"set `is_superuser=True` on a existing Account."
)
sys.exit()
if run_custom_commands(option, *unknown_args):
# run any custom commands
sys.exit()
else:
# pass on to the core django manager - re-parse the entire input line
# but keep 'evennia' as the name instead of django-admin. This is
# an exit condition.
sys.argv[0] = re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0])
sys.exit(execute_from_command_line(sys.argv))
elif not args.tail_log:
# no input; print evennia info (don't pring if we're tailing log)
print(ABOUT_INFO)
if REACTOR_RUN:
reactor.run()
if __name__ == "__main__":
# start Evennia from the command line
main()