Refactoring Server Startup to be more idiomatic for Twisted.

This commit is contained in:
Andrew Bastien 2023-10-31 13:31:33 -04:00
parent 40a4bd0592
commit 15653ef1f1
13 changed files with 1155 additions and 1250 deletions

View file

@ -16,6 +16,7 @@ to launch such a shell (using python or ipython depending on your install).
See www.evennia.com for full documentation. See www.evennia.com for full documentation.
""" """
import evennia
# docstring header # docstring header
@ -47,6 +48,7 @@ AccountDB = None
ScriptDB = None ScriptDB = None
ChannelDB = None ChannelDB = None
Msg = None Msg = None
ServerConfig = None
# Properties # Properties
AttributeProperty = None AttributeProperty = None
@ -98,6 +100,8 @@ FuncParser = None
# Handlers # Handlers
SESSION_HANDLER = None SESSION_HANDLER = None
PORTAL_SESSION_HANDLER = None
SERVER_SESSION_HANDLER = None
TASK_HANDLER = None TASK_HANDLER = None
TICKER_HANDLER = None TICKER_HANDLER = None
MONITOR_HANDLER = None MONITOR_HANDLER = None
@ -106,8 +110,10 @@ MONITOR_HANDLER = None
GLOBAL_SCRIPTS = None GLOBAL_SCRIPTS = None
OPTION_CLASSES = None OPTION_CLASSES = None
# variables PROCESS_ID = None
PORTAL_MODE = False
TWISTED_APPLICATION = None
EVENNIA_SERVICE = None
def _create_version(): def _create_version():
@ -140,6 +146,9 @@ def _create_version():
__version__ = _create_version() __version__ = _create_version()
del _create_version del _create_version
_LOADED = False
PORTAL_MODE = False
def _init(portal_mode=False): def _init(portal_mode=False):
""" """
@ -147,6 +156,10 @@ def _init(portal_mode=False):
Evennia has fully initialized all its models. It sets up the API Evennia has fully initialized all its models. It sets up the API
in a safe environment where all models are available already. in a safe environment where all models are available already.
""" """
global _LOADED
if _LOADED:
return
_LOADED = True
global DefaultAccount, DefaultObject, DefaultGuest, DefaultCharacter global DefaultAccount, DefaultObject, DefaultGuest, DefaultCharacter
global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript
global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg
@ -157,18 +170,19 @@ def _init(portal_mode=False):
global create_message, create_help_entry global create_message, create_help_entry
global signals global signals
global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers
global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, PROCESS_ID
global TASK_HANDLER global TASK_HANDLER, PORTAL_SESSION_HANDLER, SERVER_SESSION_HANDLER
global GLOBAL_SCRIPTS, OPTION_CLASSES global GLOBAL_SCRIPTS, OPTION_CLASSES, EVENNIA_SERVICE, TWISTED_APPLICATION
global EvMenu, EvTable, EvForm, EvMore, EvEditor global EvMenu, EvTable, EvForm, EvMore, EvEditor
global ANSIString, FuncParser global ANSIString, FuncParser
global AttributeProperty, TagProperty, TagCategoryProperty global AttributeProperty, TagProperty, TagCategoryProperty, ServerConfig
global PORTAL_MODE global PORTAL_MODE
PORTAL_MODE = portal_mode PORTAL_MODE = portal_mode
# Parent typeclasses # Parent typeclasses
# utilities # utilities
from django.conf import settings from django.conf import settings
import os
from . import contrib from . import contrib
from .accounts.accounts import DefaultAccount, DefaultGuest from .accounts.accounts import DefaultAccount, DefaultGuest
@ -192,9 +206,10 @@ def _init(portal_mode=False):
from .scripts.taskhandler import TASK_HANDLER from .scripts.taskhandler import TASK_HANDLER
from .scripts.tickerhandler import TICKER_HANDLER from .scripts.tickerhandler import TICKER_HANDLER
from .server import signals from .server import signals
from .server.models import ServerConfig
from .typeclasses.attributes import AttributeProperty from .typeclasses.attributes import AttributeProperty
from .typeclasses.tags import TagCategoryProperty, TagProperty from .typeclasses.tags import TagCategoryProperty, TagProperty
from .utils import ansi, gametime, logger from .utils import ansi, gametime, logger, class_from_module
from .utils.ansi import ANSIString from .utils.ansi import ANSIString
if not PORTAL_MODE: if not PORTAL_MODE:
@ -229,12 +244,29 @@ def _init(portal_mode=False):
) )
from .utils.utils import class_from_module from .utils.utils import class_from_module
if PORTAL_MODE: PROCESS_ID = os.getpid()
from twisted.application.service import Application
TWISTED_APPLICATION = Application("Evennia")
_evennia_service_class = None
if portal_mode:
# Set up the PortalSessionHandler # Set up the PortalSessionHandler
from evennia.server.portal import portalsessionhandler from evennia.server.portal import portalsessionhandler
portal_sess_handler_class = class_from_module(settings.PORTAL_SESSION_HANDLER_CLASS) portal_sess_handler_class = class_from_module(settings.PORTAL_SESSION_HANDLER_CLASS)
portalsessionhandler.PORTAL_SESSIONS = portal_sess_handler_class() portalsessionhandler.PORTAL_SESSIONS = portal_sess_handler_class()
SESSION_HANDLER = portalsessionhandler.PORTAL_SESSIONS
evennia.PORTAL_SESSION_HANDLER = evennia.SESSION_HANDLER
_evennia_service_class = class_from_module(settings.EVENNIA_PORTAL_SERVICE_CLASS)
from django.db import connection
# we don't need a connection to the database so close it right away
try:
connection.close()
except Exception:
pass
else: else:
# Create the ServerSesssionHandler # Create the ServerSesssionHandler
from evennia.server import sessionhandler from evennia.server import sessionhandler
@ -243,6 +275,11 @@ def _init(portal_mode=False):
sessionhandler.SESSIONS = sess_handler_class() sessionhandler.SESSIONS = sess_handler_class()
sessionhandler.SESSION_HANDLER = sessionhandler.SESSIONS sessionhandler.SESSION_HANDLER = sessionhandler.SESSIONS
SESSION_HANDLER = sessionhandler.SESSIONS SESSION_HANDLER = sessionhandler.SESSIONS
SERVER_SESSION_HANDLER = SESSION_HANDLER
_evennia_service_class = class_from_module(settings.EVENNIA_SERVER_SERVICE_CLASS)
EVENNIA_SERVICE = _evennia_service_class()
EVENNIA_SERVICE.setServiceParent(TWISTED_APPLICATION)
# API containers # API containers

View file

@ -9,6 +9,7 @@ import os
from django.conf import settings from django.conf import settings
from twisted.internet import protocol from twisted.internet import protocol
import evennia
from evennia.server.portal import amp from evennia.server.portal import amp
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.utils import class_from_module from evennia.utils.utils import class_from_module
@ -188,9 +189,9 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
""" """
sessid, kwargs = self.data_in(packed_data) sessid, kwargs = self.data_in(packed_data)
session = self.factory.server.sessions.get(sessid, None) session = evennia.SERVER_SESSION_HANDLER.get(sessid, None)
if session: if session:
self.factory.server.sessions.data_in(session, **kwargs) evennia.SERVER_SESSION_HANDLER.data_in(session, **kwargs)
return {} return {}
@amp.AdminPortal2Server.responder @amp.AdminPortal2Server.responder
@ -207,46 +208,45 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
""" """
sessid, kwargs = self.data_in(packed_data) sessid, kwargs = self.data_in(packed_data)
operation = kwargs.pop("operation", "") operation = kwargs.pop("operation", "")
server_sessionhandler = self.factory.server.sessions
if operation == amp.PCONN: # portal_session_connect if operation == amp.PCONN: # portal_session_connect
# create a new session and sync it # create a new session and sync it
server_sessionhandler.portal_connect(kwargs.get("sessiondata")) evennia.SERVER_SESSION_HANDLER.portal_connect(kwargs.get("sessiondata"))
elif operation == amp.PCONNSYNC: # portal_session_sync elif operation == amp.PCONNSYNC: # portal_session_sync
server_sessionhandler.portal_session_sync(kwargs.get("sessiondata")) evennia.SERVER_SESSION_HANDLER.portal_session_sync(kwargs.get("sessiondata"))
elif operation == amp.PDISCONN: # portal_session_disconnect elif operation == amp.PDISCONN: # portal_session_disconnect
# session closed from portal sid # session closed from portal sid
session = server_sessionhandler.get(sessid) session = evennia.SERVER_SESSION_HANDLER.get(sessid)
if session: if session:
server_sessionhandler.portal_disconnect(session) evennia.SERVER_SESSION_HANDLER.portal_disconnect(session)
elif operation == amp.PDISCONNALL: # portal_disconnect_all elif operation == amp.PDISCONNALL: # portal_disconnect_all
# portal orders all sessions to close # portal orders all sessions to close
server_sessionhandler.portal_disconnect_all() evennia.SERVER_SESSION_HANDLER.portal_disconnect_all()
elif operation == amp.PSYNC: # portal_session_sync elif operation == amp.PSYNC: # portal_session_sync
# force a resync of sessions from the portal side. This happens on # force a resync of sessions from the portal side. This happens on
# first server-connect. # first server-connect.
server_restart_mode = kwargs.get("server_restart_mode", "shutdown") server_restart_mode = kwargs.get("server_restart_mode", "shutdown")
self.factory.server.run_init_hooks(server_restart_mode) evennia.EVENNIA_SERVICE.run_init_hooks(server_restart_mode)
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata")) evennia.SERVER_SESSION_HANDLER.portal_sessions_sync(kwargs.get("sessiondata"))
server_sessionhandler.portal_start_time = kwargs.get("portal_start_time") evennia.SERVER_SESSION_HANDLER.portal_start_time = kwargs.get("portal_start_time")
elif operation == amp.SRELOAD: # server reload elif operation == amp.SRELOAD: # server reload
# shut down in reload mode # shut down in reload mode
server_sessionhandler.all_sessions_portal_sync() evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync()
server_sessionhandler.server.shutdown(mode="reload") evennia.EVENNIA_SERVICE.shutdown(mode="reload")
elif operation == amp.SRESET: elif operation == amp.SRESET:
# shut down in reset mode # shut down in reset mode
server_sessionhandler.all_sessions_portal_sync() evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync()
server_sessionhandler.server.shutdown(mode="reset") evennia.EVENNIA_SERVICE.shutdown(mode="reset")
elif operation == amp.SSHUTD: # server shutdown elif operation == amp.SSHUTD: # server shutdown
# shutdown in stop mode # shutdown in stop mode
server_sessionhandler.server.shutdown(mode="shutdown") evennia.EVENNIA_SERVICE.shutdown(mode="shutdown")
else: else:
raise Exception("operation %(op)s not recognized." % {"op": operation}) raise Exception("operation %(op)s not recognized." % {"op": operation})

View file

@ -11,6 +11,7 @@ from subprocess import STDOUT, Popen
from django.conf import settings from django.conf import settings
from twisted.internet import protocol from twisted.internet import protocol
import evennia
from evennia.server.portal import amp from evennia.server.portal import amp
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.utils import class_from_module from evennia.utils.utils import class_from_module
@ -379,9 +380,9 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
""" """
try: try:
sessid, kwargs = self.data_in(packed_data) sessid, kwargs = self.data_in(packed_data)
session = self.factory.portal.sessions.get(sessid, None) session = evennia.PORTAL_SESSION_HANDLER.get(sessid, None)
if session: if session:
self.factory.portal.sessions.data_out(session, **kwargs) evennia.PORTAL_SESSION_HANDLER.data_out(session, **kwargs)
except Exception: except Exception:
logger.log_trace("packed_data len {}".format(len(packed_data))) logger.log_trace("packed_data len {}".format(len(packed_data)))
return {} return {}
@ -405,7 +406,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
# logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs)) # logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs))
operation = kwargs.pop("operation") operation = kwargs.pop("operation")
portal_sessionhandler = self.factory.portal.sessions portal_sessionhandler = evennia.PORTAL_SESSION_HANDLER
if operation == amp.SLOGIN: # server_session_login if operation == amp.SLOGIN: # server_session_login
# a session has authenticated; sync it. # a session has authenticated; sync it.
@ -449,7 +450,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
# this defaults to 'shutdown' or whatever value set in server_stop # this defaults to 'shutdown' or whatever value set in server_stop
server_restart_mode = self.factory.portal.server_restart_mode server_restart_mode = self.factory.portal.server_restart_mode
sessdata = self.factory.portal.sessions.get_all_sync_data() sessdata = evennia.PORTAL_SESSION_HANDLER.get_all_sync_data()
self.send_AdminPortal2Server( self.send_AdminPortal2Server(
amp.DUMMYSESSION, amp.DUMMYSESSION,
amp.PSYNC, amp.PSYNC,
@ -457,7 +458,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
sessiondata=sessdata, sessiondata=sessdata,
portal_start_time=self.factory.portal.start_time, portal_start_time=self.factory.portal.start_time,
) )
self.factory.portal.sessions.at_server_connection() evennia.PORTAL_SESSION_HANDLER.at_server_connection()
if self.factory.server_connection: if self.factory.server_connection:
# this is an indication the server has successfully connected, so # this is an indication the server has successfully connected, so

View file

@ -9,243 +9,23 @@ by game/evennia.py).
""" """
import os import os
import sys import sys
import time
from os.path import abspath, dirname
import django
from twisted.application import internet, service
from twisted.internet import protocol, reactor
from twisted.internet.task import LoopingCall
from twisted.logger import globalLogPublisher from twisted.logger import globalLogPublisher
import django
django.setup() django.setup()
from django.conf import settings
from django.db import connection
import evennia import evennia
evennia._init(portal_mode=True) evennia._init(portal_mode=True)
from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS
from evennia.server.webserver import EvenniaReverseProxyResource from django.conf import settings
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.utils import (
class_from_module,
get_evennia_version,
make_iter,
mod_import,
)
# we don't need a connection to the database so close it right away
try:
connection.close()
except Exception:
pass
PORTAL_SERVICES_PLUGIN_MODULES = [
mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)
]
LOCKDOWN_MODE = settings.LOCKDOWN_MODE
# -------------------------------------------------------------
# Evennia Portal settings
# -------------------------------------------------------------
VERSION = get_evennia_version()
SERVERNAME = settings.SERVERNAME
PORTAL_RESTART = os.path.join(settings.GAME_DIR, "server", "portal.restart")
TELNET_PORTS = settings.TELNET_PORTS
SSL_PORTS = settings.SSL_PORTS
SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
WEBSOCKET_CLIENT_PORT = settings.WEBSOCKET_CLIENT_PORT
TELNET_INTERFACES = ["127.0.0.1"] if LOCKDOWN_MODE else settings.TELNET_INTERFACES
SSL_INTERFACES = ["127.0.0.1"] if LOCKDOWN_MODE else settings.SSL_INTERFACES
SSH_INTERFACES = ["127.0.0.1"] if LOCKDOWN_MODE else settings.SSH_INTERFACES
WEBSERVER_INTERFACES = ["127.0.0.1"] if LOCKDOWN_MODE else settings.WEBSERVER_INTERFACES
WEBSOCKET_CLIENT_INTERFACE = "127.0.0.1" if LOCKDOWN_MODE else settings.WEBSOCKET_CLIENT_INTERFACE
WEBSOCKET_CLIENT_URL = settings.WEBSOCKET_CLIENT_URL
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
WEBSOCKET_CLIENT_ENABLED = (
settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE
)
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
AMP_INTERFACE = settings.AMP_INTERFACE
AMP_ENABLED = AMP_HOST and AMP_PORT and AMP_INTERFACE
INFO_DICT = {
"servername": SERVERNAME,
"version": VERSION,
"errors": "",
"info": "",
"lockdown_mode": "",
"amp": "",
"telnet": [],
"telnet_ssl": [],
"ssh": [],
"webclient": [],
"webserver_proxy": [],
"webserver_internal": [],
}
try:
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
except ImportError:
WEB_PLUGINS_MODULE = None
INFO_DICT["errors"] = (
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to "
"mygame/server/conf."
)
_MAINTENANCE_COUNT = 0
def _portal_maintenance():
"""
Repeated maintenance tasks for the portal.
"""
global _MAINTENANCE_COUNT
_MAINTENANCE_COUNT += 1
if _MAINTENANCE_COUNT % (60 * 7) == 0:
# drop database connection every 7 hrs to avoid default timeouts on MySQL
# (see https://github.com/evennia/evennia/issues/1376)
connection.close()
# -------------------------------------------------------------
# Portal Service object
# -------------------------------------------------------------
class Portal(object):
"""
The main Portal server handler. This object sets up the database
and tracks and interlinks all the twisted network services that
make up Portal.
"""
def __init__(self, application):
"""
Setup the server.
Args:
application (Application): An instantiated Twisted application
"""
sys.path.append(".")
# create a store of services
self.services = service.MultiService()
self.services.setServiceParent(application)
self.amp_protocol = None # set by amp factory
self.sessions = PORTAL_SESSIONS
self.sessions.portal = self
self.process_id = os.getpid()
self.server_process_id = None
self.server_restart_mode = "shutdown"
self.server_info_dict = {}
self.start_time = time.time()
self.maintenance_task = LoopingCall(_portal_maintenance)
self.maintenance_task.start(60, now=True) # call every minute
# in non-interactive portal mode, this gets overwritten by
# cmdline sent by the evennia launcher
self.server_twistd_cmd = self._get_backup_server_twistd_cmd()
# set a callback if the server is killed abruptly,
# by Ctrl-C, reboot etc.
reactor.addSystemEventTrigger(
"before", "shutdown", self.shutdown, _reactor_stopping=True, _stop_server=True
)
def _get_backup_server_twistd_cmd(self):
"""
For interactive Portal mode there is no way to get the server cmdline from the launcher, so
we need to guess it here (it's very likely to not change)
Returns:
server_twistd_cmd (list): An instruction for starting the server, to pass to Popen.
"""
server_twistd_cmd = [
"twistd",
"--python={}".format(os.path.join(dirname(dirname(abspath(__file__))), "server.py")),
]
if os.name != "nt":
gamedir = os.getcwd()
server_twistd_cmd.append(
"--pidfile={}".format(os.path.join(gamedir, "server", "server.pid"))
)
return server_twistd_cmd
def get_info_dict(self):
"""
Return the Portal info, for display.
"""
return INFO_DICT
def shutdown(self, _reactor_stopping=False, _stop_server=False):
"""
Shuts down the server from inside it.
Args:
_reactor_stopping (bool, optional): This is set if server
is already in the process of shutting down; in this case
we don't need to stop it again.
_stop_server (bool, optional): Only used in portal-interactive mode;
makes sure to stop the Server cleanly.
Note that restarting (regardless of the setting) will not work
if the Portal is currently running in daemon mode. In that
case it always needs to be restarted manually.
"""
if _reactor_stopping and hasattr(self, "shutdown_complete"):
# we get here due to us calling reactor.stop below. No need
# to do the shutdown procedure again.
return
self.sessions.disconnect_all()
if _stop_server:
self.amp_protocol.stop_server(mode="shutdown")
if not _reactor_stopping:
# shutting down the reactor will trigger another signal. We set
# a flag to avoid loops.
self.shutdown_complete = True
reactor.callLater(0, reactor.stop)
# -------------------------------------------------------------
#
# Start the Portal proxy server and add all active services
#
# -------------------------------------------------------------
# twistd requires us to define the variable 'application' so it knows # twistd requires us to define the variable 'application' so it knows
# what to execute from. # what to execute from.
application = service.Application("Portal") # The guts of the application are in the service.py file,
# which is instantiated and attached to application in evennia._init()
application = evennia.TWISTED_APPLICATION
if "--nodaemon" not in sys.argv and "test" not in sys.argv: if "--nodaemon" not in sys.argv and "test" not in sys.argv:
@ -257,186 +37,3 @@ if "--nodaemon" not in sys.argv and "test" not in sys.argv:
max_size=settings.PORTAL_LOG_MAX_SIZE, max_size=settings.PORTAL_LOG_MAX_SIZE,
) )
globalLogPublisher.addObserver(logger.GetPortalLogObserver()(logfile)) globalLogPublisher.addObserver(logger.GetPortalLogObserver()(logfile))
# The main Portal server program. This sets up the database
# and is where we store all the other services.
PORTAL = Portal(application)
if LOCKDOWN_MODE:
INFO_DICT["lockdown_mode"] = " LOCKDOWN_MODE active: Only local connections."
if AMP_ENABLED:
# The AMP protocol handles the communication between
# the portal and the mud server. Only reason to ever deactivate
# it would be during testing and debugging.
from evennia.server.portal import amp_server
INFO_DICT["amp"] = "amp: %s" % AMP_PORT
factory = amp_server.AMPServerFactory(PORTAL)
amp_service = internet.TCPServer(AMP_PORT, factory, interface=AMP_INTERFACE)
amp_service.setName("PortalAMPServer")
PORTAL.services.addService(amp_service)
# We group all the various services under the same twisted app.
# These will gradually be started as they are initialized below.
if TELNET_ENABLED:
# Start telnet game connections
from evennia.server.portal import telnet
_telnet_protocol = class_from_module(settings.TELNET_PROTOCOL_CLASS)
for interface in TELNET_INTERFACES:
ifacestr = ""
if interface not in ("0.0.0.0", "::") or len(TELNET_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in TELNET_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = telnet.TelnetServerFactory()
factory.noisy = False
factory.protocol = _telnet_protocol
factory.sessionhandler = PORTAL_SESSIONS
telnet_service = internet.TCPServer(port, factory, interface=interface)
telnet_service.setName("EvenniaTelnet%s" % pstring)
PORTAL.services.addService(telnet_service)
INFO_DICT["telnet"].append("telnet%s: %s" % (ifacestr, port))
if SSL_ENABLED:
# Start Telnet+SSL game connection (requires PyOpenSSL).
from evennia.server.portal import telnet_ssl
_ssl_protocol = class_from_module(settings.SSL_PROTOCOL_CLASS)
for interface in SSL_INTERFACES:
ifacestr = ""
if interface not in ("0.0.0.0", "::") or len(SSL_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSL_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.noisy = False
factory.sessionhandler = PORTAL_SESSIONS
factory.protocol = _ssl_protocol
ssl_context = telnet_ssl.getSSLContext()
if ssl_context:
ssl_service = internet.SSLServer(
port, factory, telnet_ssl.getSSLContext(), interface=interface
)
ssl_service.setName("EvenniaSSL%s" % pstring)
PORTAL.services.addService(ssl_service)
INFO_DICT["telnet_ssl"].append("telnet+ssl%s: %s" % (ifacestr, port))
else:
INFO_DICT["telnet_ssl"].append(
"telnet+ssl%s: %s (deactivated - keys/cert unset)" % (ifacestr, port)
)
if SSH_ENABLED:
# Start SSH game connections. Will create a keypair in
# evennia/game if necessary.
from evennia.server.portal import ssh
_ssh_protocol = class_from_module(settings.SSH_PROTOCOL_CLASS)
for interface in SSH_INTERFACES:
ifacestr = ""
if interface not in ("0.0.0.0", "::") or len(SSH_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory(
{"protocolFactory": _ssh_protocol, "protocolArgs": (), "sessions": PORTAL_SESSIONS}
)
factory.noisy = False
ssh_service = internet.TCPServer(port, factory, interface=interface)
ssh_service.setName("EvenniaSSH%s" % pstring)
PORTAL.services.addService(ssh_service)
INFO_DICT["ssh"].append("ssh%s: %s" % (ifacestr, port))
if WEBSERVER_ENABLED:
from evennia.server.webserver import Website
# Start a reverse proxy to relay data to the Server-side webserver
websocket_started = False
_websocket_protocol = class_from_module(settings.WEBSOCKET_PROTOCOL_CLASS)
for interface in WEBSERVER_INTERFACES:
ifacestr = ""
if interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1:
ifacestr = "-%s" % interface
for proxyport, serverport in WEBSERVER_PORTS:
web_root = EvenniaReverseProxyResource("127.0.0.1", serverport, "")
webclientstr = ""
if WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata
from evennia.server.portal import webclient_ajax
ajax_webclient = webclient_ajax.AjaxWebClient()
ajax_webclient.sessionhandler = PORTAL_SESSIONS
web_root.putChild(b"webclientdata", ajax_webclient)
webclientstr = "webclient (ajax only)"
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
# start websocket client port for the webclient
# we only support one websocket client
from autobahn.twisted.websocket import WebSocketServerFactory
from evennia.server.portal import webclient # noqa
w_interface = WEBSOCKET_CLIENT_INTERFACE
w_ifacestr = ""
if w_interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1:
w_ifacestr = "-%s" % w_interface
port = WEBSOCKET_CLIENT_PORT
class Websocket(WebSocketServerFactory):
"Only here for better naming in logs"
pass
factory = Websocket()
factory.noisy = False
factory.protocol = _websocket_protocol
factory.sessionhandler = PORTAL_SESSIONS
websocket_service = internet.TCPServer(port, factory, interface=w_interface)
websocket_service.setName("EvenniaWebSocket%s:%s" % (w_ifacestr, port))
PORTAL.services.addService(websocket_service)
websocket_started = True
webclientstr = "webclient-websocket%s: %s" % (w_ifacestr, port)
INFO_DICT["webclient"].append(webclientstr)
if WEB_PLUGINS_MODULE:
try:
web_root = WEB_PLUGINS_MODULE.at_webproxy_root_creation(web_root)
except Exception:
# Legacy user has not added an at_webproxy_root_creation function in existing
# web plugins file
INFO_DICT["errors"] = (
"WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() "
"not found copy 'evennia/game_template/server/conf/web_plugins.py to "
"mygame/server/conf."
)
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
web_root.is_portal = True
proxy_service = internet.TCPServer(proxyport, web_root, interface=interface)
proxy_service.setName("EvenniaWebProxy%s:%s" % (ifacestr, proxyport))
PORTAL.services.addService(proxy_service)
INFO_DICT["webserver_proxy"].append("webserver-proxy%s: %s" % (ifacestr, proxyport))
INFO_DICT["webserver_internal"].append("webserver: %s" % serverport)
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
# external plugin services to start
if plugin_module:
plugin_module.start_plugin_services(PORTAL)

View file

@ -11,6 +11,7 @@ from django.conf import settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from twisted.internet import reactor from twisted.internet import reactor
import evennia
from evennia.server.portal.amp import PCONN, PCONNSYNC, PDISCONN, PDISCONNALL from evennia.server.portal.amp import PCONN, PCONNSYNC, PDISCONN, PDISCONNALL
from evennia.server.sessionhandler import SessionHandler from evennia.server.sessionhandler import SessionHandler
from evennia.utils.logger import log_trace from evennia.utils.logger import log_trace
@ -62,7 +63,6 @@ class PortalSessionHandler(SessionHandler):
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.portal = None
self.latest_sessid = 0 self.latest_sessid = 0
self.uptime = time.time() self.uptime = time.time()
self.connection_time = 0 self.connection_time = 0
@ -132,7 +132,7 @@ class PortalSessionHandler(SessionHandler):
now = time.time() now = time.time()
if ( if (
now - self.connection_last < _MIN_TIME_BETWEEN_CONNECTS now - self.connection_last < _MIN_TIME_BETWEEN_CONNECTS
) or not self.portal.amp_protocol: ) or not evennia.EVENNIA_SERVICE.amp_protocol:
if not session or not self.connection_task: if not session or not self.connection_task:
self.connection_task = reactor.callLater( self.connection_task = reactor.callLater(
_MIN_TIME_BETWEEN_CONNECTS, self.connect, None _MIN_TIME_BETWEEN_CONNECTS, self.connect, None
@ -156,7 +156,7 @@ class PortalSessionHandler(SessionHandler):
self[session.sessid] = session self[session.sessid] = session
session.server_connected = True session.server_connected = True
self.portal.amp_protocol.send_AdminPortal2Server( evennia.EVENNIA_SERVICE.amp_protocol.send_AdminPortal2Server(
session, operation=PCONN, sessiondata=sessdata session, operation=PCONN, sessiondata=sessdata
) )
@ -175,7 +175,7 @@ class PortalSessionHandler(SessionHandler):
# once to the server - if so we must re-sync woth the server, otherwise # once to the server - if so we must re-sync woth the server, otherwise
# we skip this step. # we skip this step.
sessdata = session.get_sync_data() sessdata = session.get_sync_data()
if self.portal.amp_protocol: if evennia.EVENNIA_SERVICE.amp_protocol:
# we only send sessdata that should not have changed # we only send sessdata that should not have changed
# at the server level at this point # at the server level at this point
sessdata = dict( sessdata = dict(
@ -192,7 +192,7 @@ class PortalSessionHandler(SessionHandler):
"server_data", "server_data",
) )
) )
self.portal.amp_protocol.send_AdminPortal2Server( evennia.EVENNIA_SERVICE.amp_protocol.send_AdminPortal2Server(
session, operation=PCONNSYNC, sessiondata=sessdata session, operation=PCONNSYNC, sessiondata=sessdata
) )
@ -222,7 +222,7 @@ class PortalSessionHandler(SessionHandler):
del self[session.sessid] del self[session.sessid]
# Tell the Server to disconnect its version of the Session as well. # Tell the Server to disconnect its version of the Session as well.
self.portal.amp_protocol.send_AdminPortal2Server(session, operation=PDISCONN) evennia.EVENNIA_SERVICE.amp_protocol.send_AdminPortal2Server(session, operation=PDISCONN)
def disconnect_all(self): def disconnect_all(self):
""" """
@ -240,7 +240,7 @@ class PortalSessionHandler(SessionHandler):
# inform Server; wait until finished sending before we continue # inform Server; wait until finished sending before we continue
# removing all the sessions. # removing all the sessions.
self.portal.amp_protocol.send_AdminPortal2Server( evennia.EVENNIA_SERVICE.amp_protocol.send_AdminPortal2Server(
DUMMYSESSION, operation=PDISCONNALL DUMMYSESSION, operation=PDISCONNALL
).addCallback(_callback, self) ).addCallback(_callback, self)
@ -434,7 +434,7 @@ class PortalSessionHandler(SessionHandler):
self.data_out(session, text=[[_ERROR_COMMAND_OVERFLOW], {}]) self.data_out(session, text=[[_ERROR_COMMAND_OVERFLOW], {}])
return return
if not self.portal.amp_protocol: if not evennia.EVENNIA_SERVICE.amp_protocol:
# this can happen if someone connects before AMP connection # this can happen if someone connects before AMP connection
# was established (usually on first start) # was established (usually on first start)
reactor.callLater(1.0, self.data_in, session, **kwargs) reactor.callLater(1.0, self.data_in, session, **kwargs)
@ -445,7 +445,7 @@ class PortalSessionHandler(SessionHandler):
# relay data to Server # relay data to Server
session.cmd_last = now session.cmd_last = now
self.portal.amp_protocol.send_MsgPortal2Server(session, **kwargs) evennia.EVENNIA_SERVICE.amp_protocol.send_MsgPortal2Server(session, **kwargs)
# eventual local echo (text input only) # eventual local echo (text input only)
if "text" in kwargs and session.protocol_flags.get("LOCALECHO", False): if "text" in kwargs and session.protocol_flags.get("LOCALECHO", False):

View file

@ -0,0 +1,346 @@
import os
import sys
import time
from os.path import abspath, dirname
from twisted.application.service import MultiService
from django.conf import settings
from django.db import connection
from twisted.application import internet, service
from twisted.internet import protocol, reactor
from twisted.internet.task import LoopingCall
import evennia
from evennia.utils.utils import (
class_from_module,
get_evennia_version,
make_iter,
mod_import,
)
class EvenniaPortalService(MultiService):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.amp_protocol = None
self.server_process_id = None
self.server_restart_mode = "shutdown"
self.server_info_dict = dict()
self.start_time = 0
self._maintenance_count = 0
self.maintenance_task = None
self.info_dict = {
"servername": settings.SERVERNAME,
"version": get_evennia_version(),
"errors": "",
"info": "",
"lockdown_mode": "",
"amp": "",
"telnet": [],
"telnet_ssl": [],
"ssh": [],
"webclient": [],
"webserver_proxy": [],
"webserver_internal": [],
}
# in non-interactive portal mode, this gets overwritten by
# cmdline sent by the evennia launcher
self.server_twistd_cmd = self._get_backup_server_twistd_cmd()
def portal_maintenance(self):
"""
Repeated maintenance tasks for the portal.
"""
self._maintenance_count += 1
if self._maintenance_count % (60 * 7) == 0:
# drop database connection every 7 hrs to avoid default timeouts on MySQL
# (see https://github.com/evennia/evennia/issues/1376)
connection.close()
def privilegedStartService(self):
self.start_time = time.time()
self.maintenance_task = LoopingCall(self.portal_maintenance)
self.maintenance_task.start(60, now=True) # call every minute
# set a callback if the server is killed abruptly,
# by Ctrl-C, reboot etc.
reactor.addSystemEventTrigger(
"before", "shutdown", self.shutdown, _reactor_stopping=True, _stop_server=True
)
if settings.AMP_HOST and settings.AMP_PORT and settings.AMP_INTERFACE:
self.register_amp()
if settings.TELNET_ENABLED and settings.TELNET_PORTS and settings.TELNET_INTERFACES:
self.register_telnet()
if settings.SSL_ENABLED and settings.SSL_PORTS and settings.SSL_INTERFACES:
self.register_ssl()
if settings.SSH_ENABLED and settings.SSH_PORTS and settings.SSH_INTERFACES:
self.register_ssh()
if settings.WEBSERVER_ENABLED:
self.register_webserver()
if settings.LOCKDOWN_MODE:
self.info_dict["lockdown_mode"] = " LOCKDOWN_MODE active: Only local connections."
super().privilegedStartService()
def register_plugins(self):
PORTAL_SERVICES_PLUGIN_MODULES = [
mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)
]
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
# external plugin services to start
if plugin_module:
plugin_module.start_plugin_services(self)
def check_lockdown(self, interfaces: list[str]):
if settings.LOCKDOWN_MODE:
return ["127.0.0.1"]
return interfaces
def register_ssl(self):
# Start Telnet+SSL game connection (requires PyOpenSSL).
from evennia.server.portal import telnet_ssl
_ssl_protocol = class_from_module(settings.SSL_PROTOCOL_CLASS)
interfaces = self.check_lockdown(settings.SSL_INTERFACES)
for interface in interfaces:
ifacestr = ""
if interface not in ("0.0.0.0", "::") or len(interfaces) > 1:
ifacestr = "-%s" % interface
for port in settings.SSL_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.noisy = False
factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
factory.protocol = _ssl_protocol
ssl_context = telnet_ssl.getSSLContext()
if ssl_context:
ssl_service = internet.SSLServer(
port, factory, telnet_ssl.getSSLContext(), interface=interface
)
ssl_service.setName("EvenniaSSL%s" % pstring)
ssl_service.setServiceParent(self)
self.info_dict["telnet_ssl"].append("telnet+ssl%s: %s" % (ifacestr, port))
else:
self.info_dict["telnet_ssl"].append(
"telnet+ssl%s: %s (deactivated - keys/cert unset)" % (ifacestr, port)
)
def register_ssh(self):
# Start SSH game connections. Will create a keypair in
# evennia/game if necessary.
from evennia.server.portal import ssh
_ssh_protocol = class_from_module(settings.SSH_PROTOCOL_CLASS)
interfaces = self.check_lockdown(settings.SSH_INTERFACES)
for interface in interfaces:
ifacestr = ""
if interface not in ("0.0.0.0", "::") or len(interfaces) > 1:
ifacestr = "-%s" % interface
for port in settings.SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory(
{"protocolFactory": _ssh_protocol, "protocolArgs": (), "sessions": evennia.PORTAL_SESSION_HANDLER}
)
factory.noisy = False
ssh_service = internet.TCPServer(port, factory, interface=interface)
ssh_service.setName("EvenniaSSH%s" % pstring)
ssh_service.setServiceParent(self)
self.info_dict["ssh"].append("ssh%s: %s" % (ifacestr, port))
def register_webserver(self):
from evennia.server.webserver import Website, EvenniaReverseProxyResource
# Start a reverse proxy to relay data to the Server-side webserver
interfaces = self.check_lockdown(settings.WEBSERVER_INTERFACES)
websocket_started = False
_websocket_protocol = class_from_module(settings.WEBSOCKET_PROTOCOL_CLASS)
for interface in interfaces:
ifacestr = ""
if interface not in ("0.0.0.0", "::") or len(interfaces) > 1:
ifacestr = "-%s" % interface
for proxyport, serverport in settings.WEBSERVER_PORTS:
web_root = EvenniaReverseProxyResource("127.0.0.1", serverport, "")
webclientstr = ""
if settings.WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata
from evennia.server.portal import webclient_ajax
ajax_webclient = webclient_ajax.AjaxWebClient()
ajax_webclient.sessionhandler = evennia.PORTAL_SESSION_HANDLER
web_root.putChild(b"webclientdata", ajax_webclient)
webclientstr = "webclient (ajax only)"
if (settings.WEBSOCKET_CLIENT_ENABLED and settings.WEBSOCKET_CLIENT_PORT and
settings.WEBSOCKET_CLIENT_INTERFACE) and not websocket_started:
# start websocket client port for the webclient
# we only support one websocket client
from autobahn.twisted.websocket import WebSocketServerFactory
from evennia.server.portal import webclient # noqa
w_interface = "127.0.0.1" if settings.LOCKDOWN_MODE else settings.WEBSOCKET_CLIENT_INTERFACE
w_ifacestr = ""
if w_interface not in ("0.0.0.0", "::") or len(settings.WEBSERVER_INTERFACES) > 1:
w_ifacestr = "-%s" % w_interface
port = settings.WEBSOCKET_CLIENT_PORT
class Websocket(WebSocketServerFactory):
"Only here for better naming in logs"
pass
factory = Websocket()
factory.noisy = False
factory.protocol = _websocket_protocol
factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
websocket_service = internet.TCPServer(port, factory, interface=w_interface)
websocket_service.setName("EvenniaWebSocket%s:%s" % (w_ifacestr, port))
websocket_service.setServiceParent(self)
websocket_started = True
webclientstr = "webclient-websocket%s: %s" % (w_ifacestr, port)
self.info_dict["webclient"].append(webclientstr)
try:
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
except ImportError:
WEB_PLUGINS_MODULE = None
self.info_dict["errors"] = (
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to "
"mygame/server/conf."
)
if WEB_PLUGINS_MODULE:
try:
web_root = WEB_PLUGINS_MODULE.at_webproxy_root_creation(web_root)
except Exception:
# Legacy user has not added an at_webproxy_root_creation function in existing
# web plugins file
self.info_dict["errors"] = (
"WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() "
"not found copy 'evennia/game_template/server/conf/web_plugins.py to "
"mygame/server/conf."
)
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
web_root.is_portal = True
proxy_service = internet.TCPServer(proxyport, web_root, interface=interface)
proxy_service.setName("EvenniaWebProxy%s:%s" % (ifacestr, proxyport))
proxy_service.setServiceParent(self)
self.info_dict["webserver_proxy"].append("webserver-proxy%s: %s" % (ifacestr, proxyport))
self.info_dict["webserver_internal"].append("webserver: %s" % serverport)
def register_telnet(self):
# Start telnet game connections
from evennia.server.portal import telnet
_telnet_protocol = class_from_module(settings.TELNET_PROTOCOL_CLASS)
interfaces = self.check_lockdown(settings.TELNET_INTERFACES)
for interface in interfaces:
ifacestr = ""
if interface not in ("0.0.0.0", "::") or len(interfaces) > 1:
ifacestr = "-%s" % interface
for port in settings.TELNET_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = telnet.TelnetServerFactory()
factory.noisy = False
factory.protocol = _telnet_protocol
factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
telnet_service = internet.TCPServer(port, factory, interface=interface)
telnet_service.setName("EvenniaTelnet%s" % pstring)
telnet_service.setServiceParent(self)
self.info_dict["telnet"].append("telnet%s: %s" % (ifacestr, port))
def register_amp(self):
# The AMP protocol handles the communication between
# the portal and the mud server. Only reason to ever deactivate
# it would be during testing and debugging.
from evennia.server.portal import amp_server
self.info_dict["amp"] = "amp: %s" % settings.AMP_PORT
factory = amp_server.AMPServerFactory(self)
amp_service = internet.TCPServer(settings.AMP_PORT, factory, interface=settings.AMP_INTERFACE)
amp_service.setName("PortalAMPServer")
amp_service.setServiceParent(self)
def _get_backup_server_twistd_cmd(self):
"""
For interactive Portal mode there is no way to get the server cmdline from the launcher, so
we need to guess it here (it's very likely to not change)
Returns:
server_twistd_cmd (list): An instruction for starting the server, to pass to Popen.
"""
server_twistd_cmd = [
"twistd",
"--python={}".format(os.path.join(dirname(dirname(abspath(__file__))), "server.py")),
]
if os.name != "nt":
gamedir = os.getcwd()
server_twistd_cmd.append(
"--pidfile={}".format(os.path.join(gamedir, "server", "server.pid"))
)
return server_twistd_cmd
def get_info_dict(self):
"""
Return the Portal info, for display.
"""
return self.info_dict
def shutdown(self, _reactor_stopping=False, _stop_server=False):
"""
Shuts down the server from inside it.
Args:
_reactor_stopping (bool, optional): This is set if server
is already in the process of shutting down; in this case
we don't need to stop it again.
_stop_server (bool, optional): Only used in portal-interactive mode;
makes sure to stop the Server cleanly.
Note that restarting (regardless of the setting) will not work
if the Portal is currently running in daemon mode. In that
case it always needs to be restarted manually.
"""
if _reactor_stopping and hasattr(self, "shutdown_complete"):
# we get here due to us calling reactor.stop below. No need
# to do the shutdown procedure again.
return
evennia.PORTAL_SESSION_HANDLER.disconnect_all()
if _stop_server:
self.amp_protocol.stop_server(mode="shutdown")
if not _reactor_stopping:
# shutting down the reactor will trigger another signal. We set
# a flag to avoid loops.
self.shutdown_complete = True
reactor.callLater(0, reactor.stop)

View file

@ -21,6 +21,7 @@ from twisted.internet.base import DelayedCall
from twisted.test import proto_helpers from twisted.test import proto_helpers
from twisted.trial.unittest import TestCase as TwistedTestCase from twisted.trial.unittest import TestCase as TwistedTestCase
import evennia
from evennia.server.portal import irc from evennia.server.portal import irc
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
@ -35,7 +36,6 @@ from .mccp import MCCP
from .mssp import MSSP from .mssp import MSSP
from .mxp import MXP from .mxp import MXP
from .naws import DEFAULT_HEIGHT, DEFAULT_WIDTH from .naws import DEFAULT_HEIGHT, DEFAULT_WIDTH
from .portal import PORTAL_SESSIONS
from .suppress_ga import SUPPRESS_GA from .suppress_ga import SUPPRESS_GA
from .telnet import TelnetProtocol, TelnetServerFactory from .telnet import TelnetProtocol, TelnetServerFactory
from .telnet_oob import MSDP, MSDP_VAL, MSDP_VAR from .telnet_oob import MSDP, MSDP_VAL, MSDP_VAR
@ -223,7 +223,7 @@ class TestTelnet(TwistedTestCase):
super().setUp() super().setUp()
factory = TelnetServerFactory() factory = TelnetServerFactory()
factory.protocol = TelnetProtocol factory.protocol = TelnetProtocol
factory.sessionhandler = PORTAL_SESSIONS factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
factory.sessionhandler.portal = Mock() factory.sessionhandler.portal = Mock()
self.proto = factory.buildProtocol(("localhost", 0)) self.proto = factory.buildProtocol(("localhost", 0))
self.transport = proto_helpers.StringTransport() self.transport = proto_helpers.StringTransport()
@ -289,8 +289,8 @@ class TestWebSocket(BaseEvenniaTest):
super().setUp() super().setUp()
self.proto = WebSocketClient() self.proto = WebSocketClient()
self.proto.factory = WebSocketServerFactory() self.proto.factory = WebSocketServerFactory()
self.proto.factory.sessionhandler = PORTAL_SESSIONS self.proto.factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
self.proto.sessionhandler = PORTAL_SESSIONS self.proto.sessionhandler = evennia.PORTAL_SESSION_HANDLER
self.proto.sessionhandler.portal = Mock() self.proto.sessionhandler.portal = Mock()
self.proto.transport = proto_helpers.StringTransport() self.proto.transport = proto_helpers.StringTransport()
# self.proto.transport = proto_helpers.FakeDatagramTransport() # self.proto.transport = proto_helpers.FakeDatagramTransport()

View file

@ -9,684 +9,23 @@ evennia/server/server_runner.py).
""" """
import os import os
import sys import sys
import time
import traceback from twisted.logger import globalLogPublisher
import django import django
from twisted.application import internet, service
from twisted.internet import defer, reactor
from twisted.internet.task import LoopingCall
from twisted.logger import globalLogPublisher
from twisted.web import static
django.setup() django.setup()
import importlib
import evennia import evennia
evennia._init() evennia._init()
from django.conf import settings from django.conf import settings
from django.db import connection
from django.db.utils import OperationalError
from django.utils.translation import gettext as _
from evennia.accounts.models import AccountDB
from evennia.scripts.models import ScriptDB
from evennia.server.models import ServerConfig
from evennia.server.sessionhandler import SESSIONS
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.utils import get_evennia_version, make_iter, mod_import
_SA = object.__setattr__
# a file with a flag telling the server to restart after shutdown or not.
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server", "server.restart")
# modules containing hook methods called during start_stop
SERVER_STARTSTOP_MODULES = [
mod_import(mod)
for mod in make_iter(settings.AT_SERVER_STARTSTOP_MODULE)
if isinstance(mod, str)
]
# modules containing plugin services
SERVER_SERVICES_PLUGIN_MODULES = make_iter(settings.SERVER_SERVICES_PLUGIN_MODULES)
# ------------------------------------------------------------
# Evennia Server settings
# ------------------------------------------------------------
SERVERNAME = settings.SERVERNAME
VERSION = get_evennia_version()
AMP_ENABLED = True
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
AMP_INTERFACE = settings.AMP_INTERFACE
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
GUEST_ENABLED = settings.GUEST_ENABLED
# server-channel mappings
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
IRC_ENABLED = settings.IRC_ENABLED
RSS_ENABLED = settings.RSS_ENABLED
GRAPEVINE_ENABLED = settings.GRAPEVINE_ENABLED
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
GAME_INDEX_ENABLED = settings.GAME_INDEX_ENABLED
INFO_DICT = {
"servername": SERVERNAME,
"version": VERSION,
"amp": "",
"errors": "",
"info": "",
"webserver": "",
"irc_rss": "",
}
try:
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
except ImportError:
WEB_PLUGINS_MODULE = None
INFO_DICT["errors"] = (
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
)
# Maintenance function - this is called repeatedly by the server
_IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
_LAST_SERVER_TIME_SNAPSHOT = 0
_MAINTENANCE_COUNT = 0
_FLUSH_CACHE = None
_GAMETIME_MODULE = None
_OBJECTDB = None
def _server_maintenance():
"""
This maintenance function handles repeated checks and updates that
the server needs to do. It is called every minute.
"""
global EVENNIA, _MAINTENANCE_COUNT, _FLUSH_CACHE, _GAMETIME_MODULE
global _LAST_SERVER_TIME_SNAPSHOT
global _OBJECTDB
if not _OBJECTDB:
from evennia.objects.models import ObjectDB as _OBJECTDB
if not _GAMETIME_MODULE:
from evennia.utils import gametime as _GAMETIME_MODULE
if not _FLUSH_CACHE:
from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE
_MAINTENANCE_COUNT += 1
now = time.time()
if _MAINTENANCE_COUNT == 1:
# first call after a reload
_GAMETIME_MODULE.SERVER_START_TIME = now
_GAMETIME_MODULE.SERVER_RUNTIME = ServerConfig.objects.conf("runtime", default=0.0)
_LAST_SERVER_TIME_SNAPSHOT = now
else:
# adjust the runtime not with 60s but with the actual elapsed time
# in case this may varies slightly from 60s.
_GAMETIME_MODULE.SERVER_RUNTIME += now - _LAST_SERVER_TIME_SNAPSHOT
_LAST_SERVER_TIME_SNAPSHOT = now
# update game time and save it across reloads
_GAMETIME_MODULE.SERVER_RUNTIME_LAST_UPDATED = now
ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.SERVER_RUNTIME)
if _MAINTENANCE_COUNT % 5 == 0:
# check cache size every 5 minutes
_FLUSH_CACHE(_IDMAPPER_CACHE_MAXSIZE)
if _MAINTENANCE_COUNT % (60 * 7) == 0:
# drop database connection every 7 hrs to avoid default timeouts on MySQL
# (see https://github.com/evennia/evennia/issues/1376)
connection.close()
# handle idle timeouts
if _IDLE_TIMEOUT > 0:
reason = _("idle timeout exceeded")
to_disconnect = []
for session in (
sess for sess in SESSIONS.values() if (now - sess.cmd_last) > _IDLE_TIMEOUT
):
if not session.account or not session.account.access(
session.account, "noidletimeout", default=False
):
to_disconnect.append(session)
for session in to_disconnect:
SESSIONS.disconnect(session, reason=reason)
# run unpuppet hooks for objects that are marked as being puppeted,
# but which lacks an account (indicates a broken unpuppet operation
# such as a server crash)
if _MAINTENANCE_COUNT > 1:
unpuppet_count = 0
for obj in _OBJECTDB.objects.get_by_tag(key="puppeted", category="account"):
if not obj.has_account:
obj.at_pre_unpuppet()
obj.at_post_unpuppet(None, reason=_(" (connection lost)"))
obj.tags.remove("puppeted", category="account")
unpuppet_count += 1
if unpuppet_count:
logger.log_msg(f"Ran unpuppet-hooks for {unpuppet_count} link-dead puppets.")
# ------------------------------------------------------------
# Evennia Main Server object
# ------------------------------------------------------------
class Evennia:
"""
The main Evennia server handler. This object sets up the database and
tracks and interlinks all the twisted network services that make up
evennia.
"""
def __init__(self, application):
"""
Setup the server.
application - an instantiated Twisted application
"""
sys.path.insert(1, ".")
# create a store of services
self.services = service.MultiService()
self.services.setServiceParent(application)
self.amp_protocol = None # set by amp factory
self.sessions = SESSIONS
self.sessions.server = self
self.process_id = os.getpid()
# Database-specific startup optimizations.
self.sqlite3_prep()
self.start_time = time.time()
# wrap the SIGINT handler to make sure we empty the threadpool
# even when we reload and we have long-running requests in queue.
# this is necessary over using Twisted's signal handler.
# (see https://github.com/evennia/evennia/issues/1128)
def _wrap_sigint_handler(*args):
from twisted.internet.defer import Deferred
if hasattr(self, "web_root"):
d = self.web_root.empty_threadpool()
d.addCallback(lambda _: self.shutdown("reload", _reactor_stopping=True))
else:
d = Deferred(lambda _: self.shutdown("reload", _reactor_stopping=True))
d.addCallback(lambda _: reactor.stop())
reactor.callLater(1, d.callback, None)
reactor.sigInt = _wrap_sigint_handler
# Server startup methods
def sqlite3_prep(self):
"""
Optimize some SQLite stuff at startup since we
can't save it to the database.
"""
if (
".".join(str(i) for i in django.VERSION) < "1.2"
and settings.DATABASES.get("default", {}).get("ENGINE") == "sqlite3"
) or (
hasattr(settings, "DATABASES")
and settings.DATABASES.get("default", {}).get("ENGINE", None)
== "django.db.backends.sqlite3"
):
cursor = connection.cursor()
cursor.execute("PRAGMA cache_size=10000")
cursor.execute("PRAGMA synchronous=OFF")
cursor.execute("PRAGMA count_changes=OFF")
cursor.execute("PRAGMA temp_store=2")
def update_defaults(self):
"""
We make sure to store the most important object defaults here, so
we can catch if they change and update them on-objects automatically.
This allows for changing default cmdset locations and default
typeclasses in the settings file and have them auto-update all
already existing objects.
"""
global INFO_DICT
# setting names
settings_names = (
"CMDSET_CHARACTER",
"CMDSET_ACCOUNT",
"BASE_ACCOUNT_TYPECLASS",
"BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS",
"BASE_ROOM_TYPECLASS",
"BASE_EXIT_TYPECLASS",
"BASE_SCRIPT_TYPECLASS",
"BASE_CHANNEL_TYPECLASS",
)
# get previous and current settings so they can be compared
settings_compare = list(
zip(
[ServerConfig.objects.conf(name) for name in settings_names],
[settings.__getattr__(name) for name in settings_names],
)
)
mismatches = [
i for i, tup in enumerate(settings_compare) if tup[0] and tup[1] and tup[0] != tup[1]
]
if len(
mismatches
): # can't use any() since mismatches may be [0] which reads as False for any()
# we have a changed default. Import relevant objects and
# run the update
from evennia.comms.models import ChannelDB
from evennia.objects.models import ObjectDB
# from evennia.accounts.models import AccountDB
for i, prev, curr in (
(i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches
):
# update the database
INFO_DICT[
"info"
] = " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (
settings_names[i],
prev,
curr,
)
if i == 0:
ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(
db_cmdset_storage=curr
)
if i == 1:
AccountDB.objects.filter(db_cmdset_storage__exact=prev).update(
db_cmdset_storage=curr
)
if i == 2:
AccountDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i in (3, 4, 5, 6):
ObjectDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i == 7:
ScriptDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i == 8:
ChannelDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
# store the new default and clean caches
ServerConfig.objects.conf(settings_names[i], curr)
ObjectDB.flush_instance_cache()
AccountDB.flush_instance_cache()
ScriptDB.flush_instance_cache()
ChannelDB.flush_instance_cache()
# if this is the first start we might not have a "previous"
# setup saved. Store it now.
[
ServerConfig.objects.conf(settings_names[i], tup[1])
for i, tup in enumerate(settings_compare)
if not tup[0]
]
def run_initial_setup(self):
"""
This is triggered by the amp protocol when the connection
to the portal has been established.
This attempts to run the initial_setup script of the server.
It returns if this is not the first time the server starts.
Once finished the last_initial_setup_step is set to 'done'
"""
global INFO_DICT
initial_setup = importlib.import_module(settings.INITIAL_SETUP_MODULE)
last_initial_setup_step = ServerConfig.objects.conf("last_initial_setup_step")
try:
if not last_initial_setup_step:
# None is only returned if the config does not exist,
# i.e. this is an empty DB that needs populating.
INFO_DICT["info"] = " Server started for the first time. Setting defaults."
initial_setup.handle_setup()
elif last_initial_setup_step not in ("done", -1):
# last step crashed, so we weill resume from this step.
# modules and setup will resume from this step, retrying
# the last failed module. When all are finished, the step
# is set to 'done' to show it does not need to be run again.
INFO_DICT["info"] = " Resuming initial setup from step '{last}'.".format(
last=last_initial_setup_step
)
initial_setup.handle_setup(last_initial_setup_step)
except Exception:
# stop server if this happens.
print(traceback.format_exc())
print("Error in initial setup. Stopping Server + Portal.")
self.sessions.portal_shutdown()
def create_default_channels(self):
"""
check so default channels exist on every restart, create if not.
"""
from evennia.accounts.models import AccountDB
from evennia.comms.models import ChannelDB
from evennia.utils.create import create_channel
superuser = AccountDB.objects.get(id=1)
# mudinfo
mudinfo_chan = settings.CHANNEL_MUDINFO
if mudinfo_chan and not ChannelDB.objects.filter(db_key__iexact=mudinfo_chan["key"]):
channel = create_channel(**mudinfo_chan)
channel.connect(superuser)
# connectinfo
connectinfo_chan = settings.CHANNEL_CONNECTINFO
if connectinfo_chan and not ChannelDB.objects.filter(
db_key__iexact=connectinfo_chan["key"]
):
channel = create_channel(**connectinfo_chan)
# default channels
for chan_info in settings.DEFAULT_CHANNELS:
if not ChannelDB.objects.filter(db_key__iexact=chan_info["key"]):
channel = create_channel(**chan_info)
channel.connect(superuser)
def run_init_hooks(self, mode):
"""
Called by the amp client once receiving sync back from Portal
Args:
mode (str): One of shutdown, reload or reset
"""
from evennia.typeclasses.models import TypedObject
# start server time and maintenance task
self.maintenance_task = LoopingCall(_server_maintenance)
self.maintenance_task.start(60, now=True) # call every minute
# update eventual changed defaults
self.update_defaults()
# run at_init() on all cached entities on reconnect
[
[entity.at_init() for entity in typeclass_db.get_all_cached_instances()]
for typeclass_db in TypedObject.__subclasses__()
]
self.at_server_init()
# call correct server hook based on start file value
if mode == "reload":
logger.log_msg("Server successfully reloaded.")
self.at_server_reload_start()
elif mode == "reset":
# only run hook, don't purge sessions
self.at_server_cold_start()
logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
elif mode == "shutdown":
from evennia.objects.models import ObjectDB
self.at_server_cold_start()
# clear eventual lingering session storages
ObjectDB.objects.clear_all_sessids()
logger.log_msg("Evennia Server successfully started.")
# always call this regardless of start type
self.at_server_start()
# Moved here from evennia._init() to ensure it only runs after
# setup is complete and only in server mode.
evennia.GLOBAL_SCRIPTS.start()
@defer.inlineCallbacks
def shutdown(self, mode="reload", _reactor_stopping=False):
"""
Shuts down the server from inside it.
mode - sets the server restart mode.
- 'reload' - server restarts, no "persistent" scripts
are stopped, at_reload hooks called.
- 'reset' - server restarts, non-persistent scripts stopped,
at_shutdown hooks called but sessions will not
be disconnected.
- 'shutdown' - like reset, but server will not auto-restart.
_reactor_stopping - this is set if server is stopped by a kill
command OR this method was already called
once - in both cases the reactor is
dead/stopping already.
"""
if _reactor_stopping and hasattr(self, "shutdown_complete"):
# this means we have already passed through this method
# once; we don't need to run the shutdown procedure again.
defer.returnValue(None)
from evennia.objects.models import ObjectDB
from evennia.server.models import ServerConfig
from evennia.utils import gametime as _GAMETIME_MODULE
if mode == "reload":
# call restart hooks
ServerConfig.objects.conf("server_restart_mode", "reload")
yield [o.at_server_reload() for o in ObjectDB.get_all_cached_instances()]
yield [p.at_server_reload() for p in AccountDB.get_all_cached_instances()]
yield [
(s._pause_task(auto_pause=True) if s.is_active else None, s.at_server_reload())
for s in ScriptDB.get_all_cached_instances()
if s.id
]
yield self.sessions.all_sessions_portal_sync()
self.at_server_reload_stop()
# only save monitor state on reload, not on shutdown/reset
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.save()
else:
if mode == "reset":
# like shutdown but don't unset the is_connected flag and don't disconnect sessions
yield [o.at_server_shutdown() for o in ObjectDB.get_all_cached_instances()]
yield [p.at_server_shutdown() for p in AccountDB.get_all_cached_instances()]
if self.amp_protocol:
yield self.sessions.all_sessions_portal_sync()
else: # shutdown
yield [_SA(p, "is_connected", False) for p in AccountDB.get_all_cached_instances()]
yield [o.at_server_shutdown() for o in ObjectDB.get_all_cached_instances()]
yield [
(p.unpuppet_all(), p.at_server_shutdown())
for p in AccountDB.get_all_cached_instances()
]
yield ObjectDB.objects.clear_all_sessids()
yield [
(s._pause_task(auto_pause=True), s.at_server_shutdown())
for s in ScriptDB.get_all_cached_instances()
if s.id and s.is_active
]
ServerConfig.objects.conf("server_restart_mode", "reset")
self.at_server_cold_stop()
# tickerhandler state should always be saved.
from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.save()
# always called, also for a reload
self.at_server_stop()
if hasattr(self, "web_root"): # not set very first start
yield self.web_root.empty_threadpool()
if not _reactor_stopping:
# kill the server
self.shutdown_complete = True
reactor.callLater(1, reactor.stop)
# we make sure the proper gametime is saved as late as possible
ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.runtime())
def get_info_dict(self):
"""
Return the server info, for display.
"""
return INFO_DICT
# server start/stop hooks
def at_server_init(self):
"""
This is called first when the server is starting, before any other hooks, regardless of how it's starting.
"""
for mod in SERVER_STARTSTOP_MODULES:
if hasattr(mod, "at_server_init"):
mod.at_server_init()
def at_server_start(self):
"""
This is called every time the server starts up, regardless of
how it was shut down.
"""
for mod in SERVER_STARTSTOP_MODULES:
if hasattr(mod, "at_server_start"):
mod.at_server_start()
def at_server_stop(self):
"""
This is called just before a server is shut down, regardless
of it is fore a reload, reset or shutdown.
"""
for mod in SERVER_STARTSTOP_MODULES:
if hasattr(mod, "at_server_stop"):
mod.at_server_stop()
def at_server_reload_start(self):
"""
This is called only when server starts back up after a reload.
"""
for mod in SERVER_STARTSTOP_MODULES:
if hasattr(mod, "at_server_reload_start"):
mod.at_server_reload_start()
def at_post_portal_sync(self, mode):
"""
This is called just after the portal has finished syncing back data to the server
after reconnecting.
Args:
mode (str): One of 'reload', 'reset' or 'shutdown'.
"""
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.restore(mode == "reload")
from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.restore(mode == "reload")
# Un-pause all scripts, stop non-persistent timers
ScriptDB.objects.update_scripts_after_server_start()
# start the task handler
from evennia.scripts.taskhandler import TASK_HANDLER
TASK_HANDLER.load()
TASK_HANDLER.create_delays()
# create/update channels
self.create_default_channels()
# delete the temporary setting
ServerConfig.objects.conf("server_restart_mode", delete=True)
def at_server_reload_stop(self):
"""
This is called only time the server stops before a reload.
"""
for mod in SERVER_STARTSTOP_MODULES:
if hasattr(mod, "at_server_reload_stop"):
mod.at_server_reload_stop()
def at_server_cold_start(self):
"""
This is called only when the server starts "cold", i.e. after a
shutdown or a reset.
"""
# We need to do this just in case the server was killed in a way where
# the normal cleanup operations did not have time to run.
from evennia.objects.models import ObjectDB
ObjectDB.objects.clear_all_sessids()
# Remove non-persistent scripts
from evennia.scripts.models import ScriptDB
for script in ScriptDB.objects.filter(db_persistent=False):
script._stop_task()
if GUEST_ENABLED:
for guest in AccountDB.objects.all().filter(
db_typeclass_path=settings.BASE_GUEST_TYPECLASS
):
for character in guest.characters:
character.delete()
guest.delete()
for mod in SERVER_STARTSTOP_MODULES:
if hasattr(mod, "at_server_cold_start"):
mod.at_server_cold_start()
def at_server_cold_stop(self):
"""
This is called only when the server goes down due to a shutdown or reset.
"""
for mod in SERVER_STARTSTOP_MODULES:
if hasattr(mod, "at_server_cold_stop"):
mod.at_server_cold_stop()
# ------------------------------------------------------------
#
# Start the Evennia game server and add all active services
#
# ------------------------------------------------------------
# Tell the system the server is starting up; some things are not available yet
try:
ServerConfig.objects.conf("server_starting_mode", True)
except OperationalError:
print("Server server_starting_mode couldn't be set - database not set up.")
# twistd requires us to define the variable 'application' so it knows # twistd requires us to define the variable 'application' so it knows
# what to execute from. # what to execute from.
application = service.Application("Evennia") # The guts of the application are in the service.py file,
# which is instantiated and attached to application in evennia._init()
application = evennia.TWISTED_APPLICATION
if "--nodaemon" not in sys.argv and "test" not in sys.argv: if "--nodaemon" not in sys.argv and "test" not in sys.argv:
# activate logging for interactive/testing mode # activate logging for interactive/testing mode
@ -699,101 +38,3 @@ if "--nodaemon" not in sys.argv and "test" not in sys.argv:
globalLogPublisher.addObserver(logger.GetServerLogObserver()(logfile)) globalLogPublisher.addObserver(logger.GetServerLogObserver()(logfile))
# The main evennia server program. This sets up the database
# and is where we store all the other services.
EVENNIA = Evennia(application)
if AMP_ENABLED:
# The AMP protocol handles the communication between
# the portal and the mud server. Only reason to ever deactivate
# it would be during testing and debugging.
ifacestr = ""
if AMP_INTERFACE != "127.0.0.1":
ifacestr = "-%s" % AMP_INTERFACE
INFO_DICT["amp"] = "amp %s: %s" % (ifacestr, AMP_PORT)
from evennia.server import amp_client
factory = amp_client.AMPClientFactory(EVENNIA)
amp_service = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
amp_service.setName("ServerAMPClient")
EVENNIA.services.addService(amp_service)
if WEBSERVER_ENABLED:
# Start a django-compatible webserver.
from evennia.server.webserver import (
DjangoWebRoot,
LockableThreadPool,
PrivateStaticRoot,
Website,
WSGIWebServer,
)
# start a thread pool and define the root url (/) as a wsgi resource
# recognized by Django
threads = LockableThreadPool(
minthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[0]),
maxthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[1]),
)
web_root = DjangoWebRoot(threads)
# point our media resources to url /media
web_root.putChild(b"media", PrivateStaticRoot(settings.MEDIA_ROOT))
# point our static resources to url /static
web_root.putChild(b"static", PrivateStaticRoot(settings.STATIC_ROOT))
EVENNIA.web_root = web_root
if WEB_PLUGINS_MODULE:
# custom overloads
web_root = WEB_PLUGINS_MODULE.at_webserver_root_creation(web_root)
web_site = Website(web_root, logPath=settings.HTTP_LOG_FILE)
web_site.is_portal = False
INFO_DICT["webserver"] = ""
for proxyport, serverport in WEBSERVER_PORTS:
# create the webserver (we only need the port for this)
webserver = WSGIWebServer(threads, serverport, web_site, interface="127.0.0.1")
webserver.setName("EvenniaWebServer%s" % serverport)
EVENNIA.services.addService(webserver)
INFO_DICT["webserver"] += "webserver: %s" % serverport
ENABLED = []
if IRC_ENABLED:
# IRC channel connections
ENABLED.append("irc")
if RSS_ENABLED:
# RSS feed channel connections
ENABLED.append("rss")
if GRAPEVINE_ENABLED:
# Grapevine channel connections
ENABLED.append("grapevine")
if GAME_INDEX_ENABLED:
from evennia.server.game_index_client.service import EvenniaGameIndexService
egi_service = EvenniaGameIndexService()
EVENNIA.services.addService(egi_service)
if ENABLED:
INFO_DICT["irc_rss"] = ", ".join(ENABLED) + " enabled."
for plugin_module in SERVER_SERVICES_PLUGIN_MODULES:
# external plugin protocols - load here
plugin_module = mod_import(plugin_module)
if plugin_module:
plugin_module.start_plugin_services(EVENNIA)
else:
print(f"Could not load plugin module {plugin_module}")
# clear server startup mode
try:
ServerConfig.objects.conf("server_starting_mode", delete=True)
except OperationalError:
print("Server server_starting_mode couldn't unset - db not set up.")

683
evennia/server/service.py Normal file
View file

@ -0,0 +1,683 @@
"""
This module contains the main EvenniaService class, which is the very core of the
Evennia server. It is instantiated by the evennia/server/server.py module.
"""
import time
import traceback
import importlib
from twisted.application import internet
from twisted.application.service import MultiService
from twisted.internet import defer, reactor
from twisted.internet.defer import Deferred
from twisted.internet.task import LoopingCall
import django
from django.db import connection
from django.db.utils import OperationalError
from django.conf import settings
from django.utils.translation import gettext as _
import evennia
from evennia.utils.utils import get_evennia_version, make_iter, mod_import
from evennia.utils import logger
_SA = object.__setattr__
class EvenniaServerService(MultiService):
def _wrap_sigint_handler(self, *args):
if hasattr(self, "web_root"):
d = self.web_root.empty_threadpool()
d.addCallback(lambda _: self.shutdown("reload", _reactor_stopping=True))
else:
d = Deferred(lambda _: self.shutdown("reload", _reactor_stopping=True))
d.addCallback(lambda _: reactor.stop())
reactor.callLater(1, d.callback, None)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.maintenance_count = 0
self.amp_protocol = None # set by amp factory
self.info_dict = {
"servername": settings.SERVERNAME,
"version": get_evennia_version(),
"amp": "",
"errors": "",
"info": "",
"webserver": "",
"irc_rss": "",
}
self._flush_cache = None
self._last_server_time_snapshot = 0
self.maintenance_task = None
# Database-specific startup optimizations.
self.sqlite3_prep()
self.start_time = 0
# wrap the SIGINT handler to make sure we empty the threadpool
# even when we reload and we have long-running requests in queue.
# this is necessary over using Twisted's signal handler.
# (see https://github.com/evennia/evennia/issues/1128)
reactor.sigInt = self._wrap_sigint_handler
self.start_stop_modules = [
mod_import(mod)
for mod in make_iter(settings.AT_SERVER_STARTSTOP_MODULE)
if isinstance(mod, str)
]
# Server startup methods
def server_maintenance(self):
"""
This maintenance function handles repeated checks and updates that
the server needs to do. It is called every minute.
"""
if not self._flush_cache:
from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE
self._flush_cache = _FLUSH_CACHE
self.maintenance_count += 1
now = time.time()
if self.maintenance_count == 1:
# first call after a reload
evennia.gametime.SERVER_START_TIME = now
evennia.gametime.SERVER_RUNTIME = evennia.ServerConfig.objects.conf("runtime", default=0.0)
_LAST_SERVER_TIME_SNAPSHOT = now
else:
# adjust the runtime not with 60s but with the actual elapsed time
# in case this may varies slightly from 60s.
evennia.gametime.SERVER_RUNTIME += now - self._last_server_time_snapshot
self._last_server_time_snapshot = now
# update game time and save it across reloads
evennia.gametime.SERVER_RUNTIME_LAST_UPDATED = now
evennia.ServerConfig.objects.conf("runtime", evennia.gametime.SERVER_RUNTIME)
if self.maintenance_count % 5 == 0:
# check cache size every 5 minutes
self._flush_cache(settings.IDMAPPER_CACHE_MAXSIZE)
if self.maintenance_count % (60 * 7) == 0:
# drop database connection every 7 hrs to avoid default timeouts on MySQL
# (see https://github.com/evennia/evennia/issues/1376)
connection.close()
# handle idle timeouts
if settings.IDLE_TIMEOUT > 0:
reason = _("idle timeout exceeded")
to_disconnect = []
for session in (
sess for sess in evennia.SESSION_HANDLER.values() if (now - sess.cmd_last) > settings.IDLE_TIMEOUT
):
if not session.account or not session.account.access(
session.account, "noidletimeout", default=False
):
to_disconnect.append(session)
for session in to_disconnect:
evennia.SESSION_HANDLER.disconnect(session, reason=reason)
# run unpuppet hooks for objects that are marked as being puppeted,
# but which lacks an account (indicates a broken unpuppet operation
# such as a server crash)
if self.maintenance_count > 1:
unpuppet_count = 0
for obj in evennia.ObjectDB.objects.get_by_tag(key="puppeted", category="account"):
if not obj.has_account:
obj.at_pre_unpuppet()
obj.at_post_unpuppet(None, reason=_(" (connection lost)"))
obj.tags.remove("puppeted", category="account")
unpuppet_count += 1
if unpuppet_count:
logger.log_msg(f"Ran unpuppet-hooks for {unpuppet_count} link-dead puppets.")
def privilegedStartService(self):
self.start_time = time.time()
# Tell the system the server is starting up; some things are not available yet
try:
evennia.ServerConfig.objects.conf("server_starting_mode", True)
except OperationalError:
print("Server server_starting_mode couldn't be set - database not set up.")
if settings.AMP_ENABLED:
self.register_amp()
if settings.WEBSERVER_ENABLED:
self.register_webserver()
ENABLED = []
if settings.IRC_ENABLED:
# IRC channel connections
ENABLED.append("irc")
if settings.RSS_ENABLED:
# RSS feed channel connections
ENABLED.append("rss")
if settings.GRAPEVINE_ENABLED:
# Grapevine channel connections
ENABLED.append("grapevine")
if settings.GAME_INDEX_ENABLED:
from evennia.server.game_index_client.service import EvenniaGameIndexService
egi_service = EvenniaGameIndexService()
egi_service.setServiceParent(self)
if ENABLED:
self.info_dict["irc_rss"] = ", ".join(ENABLED) + " enabled."
self.register_plugins()
super().privilegedStartService()
# clear server startup mode
try:
evennia.ServerConfig.objects.conf("server_starting_mode", delete=True)
except OperationalError:
print("Server server_starting_mode couldn't unset - db not set up.")
def register_plugins(self):
SERVER_SERVICES_PLUGIN_MODULES = make_iter(settings.SERVER_SERVICES_PLUGIN_MODULES)
for plugin_module in SERVER_SERVICES_PLUGIN_MODULES:
# external plugin protocols - load here
plugin_module = mod_import(plugin_module)
if plugin_module:
plugin_module.start_plugin_services(self)
else:
print(f"Could not load plugin module {plugin_module}")
def register_amp(self):
# The AMP protocol handles the communication between
# the portal and the mud server. Only reason to ever deactivate
# it would be during testing and debugging.
ifacestr = ""
if settings.AMP_INTERFACE != "127.0.0.1":
ifacestr = "-%s" % settings.AMP_INTERFACE
self.info_dict["amp"] = "amp %s: %s" % (ifacestr, settings.AMP_PORT)
from evennia.server import amp_client
factory = amp_client.AMPClientFactory(self)
amp_service = internet.TCPClient(settings.AMP_HOST, settings.AMP_PORT, factory)
amp_service.setName("ServerAMPClient")
amp_service.setServiceParent(self)
def register_webserver(self):
# Start a django-compatible webserver.
from evennia.server.webserver import (
DjangoWebRoot,
LockableThreadPool,
PrivateStaticRoot,
Website,
WSGIWebServer,
)
# start a thread pool and define the root url (/) as a wsgi resource
# recognized by Django
threads = LockableThreadPool(
minthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[0]),
maxthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[1]),
)
web_root = DjangoWebRoot(threads)
# point our media resources to url /media
web_root.putChild(b"media", PrivateStaticRoot(settings.MEDIA_ROOT))
# point our static resources to url /static
web_root.putChild(b"static", PrivateStaticRoot(settings.STATIC_ROOT))
self.web_root = web_root
try:
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
except ImportError:
WEB_PLUGINS_MODULE = None
self.info_dict["errors"] = (
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
)
if WEB_PLUGINS_MODULE:
# custom overloads
web_root = WEB_PLUGINS_MODULE.at_webserver_root_creation(web_root)
web_site = Website(web_root, logPath=settings.HTTP_LOG_FILE)
web_site.is_portal = False
self.info_dict["webserver"] = ""
for proxyport, serverport in settings.WEBSERVER_PORTS:
# create the webserver (we only need the port for this)
webserver = WSGIWebServer(threads, serverport, web_site, interface="127.0.0.1")
webserver.setName("EvenniaWebServer%s" % serverport)
webserver.setServiceParent(self)
self.info_dict["webserver"] += "webserver: %s" % serverport
def sqlite3_prep(self):
"""
Optimize some SQLite stuff at startup since we
can't save it to the database.
"""
if (
".".join(str(i) for i in django.VERSION) < "1.2"
and settings.DATABASES.get("default", {}).get("ENGINE") == "sqlite3"
) or (
hasattr(settings, "DATABASES")
and settings.DATABASES.get("default", {}).get("ENGINE", None)
== "django.db.backends.sqlite3"
):
cursor = connection.cursor()
cursor.execute("PRAGMA cache_size=10000")
cursor.execute("PRAGMA synchronous=OFF")
cursor.execute("PRAGMA count_changes=OFF")
cursor.execute("PRAGMA temp_store=2")
def update_defaults(self):
"""
We make sure to store the most important object defaults here, so
we can catch if they change and update them on-objects automatically.
This allows for changing default cmdset locations and default
typeclasses in the settings file and have them auto-update all
already existing objects.
"""
# setting names
settings_names = (
"CMDSET_CHARACTER",
"CMDSET_ACCOUNT",
"BASE_ACCOUNT_TYPECLASS",
"BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS",
"BASE_ROOM_TYPECLASS",
"BASE_EXIT_TYPECLASS",
"BASE_SCRIPT_TYPECLASS",
"BASE_CHANNEL_TYPECLASS",
)
# get previous and current settings so they can be compared
settings_compare = list(
zip(
[evennia.ServerConfig.objects.conf(name) for name in settings_names],
[settings.__getattr__(name) for name in settings_names],
)
)
mismatches = [
i for i, tup in enumerate(settings_compare) if tup[0] and tup[1] and tup[0] != tup[1]
]
if len(
mismatches
): # can't use any() since mismatches may be [0] which reads as False for any()
# we have a changed default. Import relevant objects and
# run the update
# from evennia.accounts.models import AccountDB
for i, prev, curr in (
(i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches
):
# update the database
self.info_dict[
"info"
] = " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (
settings_names[i],
prev,
curr,
)
if i == 0:
evennia.ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(
db_cmdset_storage=curr
)
if i == 1:
evennia.AccountDB.objects.filter(db_cmdset_storage__exact=prev).update(
db_cmdset_storage=curr
)
if i == 2:
evennia.AccountDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i in (3, 4, 5, 6):
evennia.ObjectDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i == 7:
evennia.ScriptDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i == 8:
evennia.ChannelDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
# store the new default and clean caches
evennia.ServerConfig.objects.conf(settings_names[i], curr)
evennia.ObjectDB.flush_instance_cache()
evennia.AccountDB.flush_instance_cache()
evennia.ScriptDB.flush_instance_cache()
evennia.ChannelDB.flush_instance_cache()
# if this is the first start we might not have a "previous"
# setup saved. Store it now.
[
evennia.ServerConfig.objects.conf(settings_names[i], tup[1])
for i, tup in enumerate(settings_compare)
if not tup[0]
]
def run_initial_setup(self):
"""
This is triggered by the amp protocol when the connection
to the portal has been established.
This attempts to run the initial_setup script of the server.
It returns if this is not the first time the server starts.
Once finished the last_initial_setup_step is set to 'done'
"""
initial_setup = importlib.import_module(settings.INITIAL_SETUP_MODULE)
last_initial_setup_step = evennia.ServerConfig.objects.conf("last_initial_setup_step")
try:
if not last_initial_setup_step:
# None is only returned if the config does not exist,
# i.e. this is an empty DB that needs populating.
self.info_dict["info"] = " Server started for the first time. Setting defaults."
initial_setup.handle_setup()
elif last_initial_setup_step not in ("done", -1):
# last step crashed, so we weill resume from this step.
# modules and setup will resume from this step, retrying
# the last failed module. When all are finished, the step
# is set to 'done' to show it does not need to be run again.
self.info_dict["info"] = " Resuming initial setup from step '{last}'.".format(
last=last_initial_setup_step
)
initial_setup.handle_setup(last_initial_setup_step)
except Exception:
# stop server if this happens.
print(traceback.format_exc())
print("Error in initial setup. Stopping Server + Portal.")
evennia.SESSION_HANDLER.portal_shutdown()
def create_default_channels(self):
"""
check so default channels exist on every restart, create if not.
"""
from evennia import AccountDB
from evennia import ChannelDB
from evennia.utils.create import create_channel
superuser = AccountDB.objects.get(id=1)
# mudinfo
mudinfo_chan = settings.CHANNEL_MUDINFO
if mudinfo_chan and not ChannelDB.objects.filter(db_key__iexact=mudinfo_chan["key"]):
channel = create_channel(**mudinfo_chan)
channel.connect(superuser)
# connectinfo
connectinfo_chan = settings.CHANNEL_CONNECTINFO
if connectinfo_chan and not ChannelDB.objects.filter(
db_key__iexact=connectinfo_chan["key"]
):
channel = create_channel(**connectinfo_chan)
# default channels
for chan_info in settings.DEFAULT_CHANNELS:
if not ChannelDB.objects.filter(db_key__iexact=chan_info["key"]):
channel = create_channel(**chan_info)
channel.connect(superuser)
def run_init_hooks(self, mode):
"""
Called by the amp client once receiving sync back from Portal
Args:
mode (str): One of shutdown, reload or reset
"""
from evennia.typeclasses.models import TypedObject
# start server time and maintenance task
self.maintenance_task = LoopingCall(self.server_maintenance)
self.maintenance_task.start(60, now=True) # call every minute
# update eventual changed defaults
self.update_defaults()
# run at_init() on all cached entities on reconnect
[
[entity.at_init() for entity in typeclass_db.get_all_cached_instances()]
for typeclass_db in TypedObject.__subclasses__()
]
self.at_server_init()
# call correct server hook based on start file value
if mode == "reload":
logger.log_msg("Server successfully reloaded.")
self.at_server_reload_start()
elif mode == "reset":
# only run hook, don't purge sessions
self.at_server_cold_start()
logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
elif mode == "shutdown":
from evennia.objects.models import ObjectDB
self.at_server_cold_start()
# clear eventual lingering session storages
ObjectDB.objects.clear_all_sessids()
logger.log_msg("Evennia Server successfully started.")
# always call this regardless of start type
self.at_server_start()
@defer.inlineCallbacks
def shutdown(self, mode="reload", _reactor_stopping=False):
"""
Shuts down the server from inside it.
mode - sets the server restart mode.
- 'reload' - server restarts, no "persistent" scripts
are stopped, at_reload hooks called.
- 'reset' - server restarts, non-persistent scripts stopped,
at_shutdown hooks called but sessions will not
be disconnected.
- 'shutdown' - like reset, but server will not auto-restart.
_reactor_stopping - this is set if server is stopped by a kill
command OR this method was already called
once - in both cases the reactor is
dead/stopping already.
"""
if _reactor_stopping and hasattr(self, "shutdown_complete"):
# this means we have already passed through this method
# once; we don't need to run the shutdown procedure again.
defer.returnValue(None)
if mode == "reload":
# call restart hooks
evennia.ServerConfig.objects.conf("server_restart_mode", "reload")
yield [o.at_server_reload() for o in evennia.ObjectDB.get_all_cached_instances()]
yield [p.at_server_reload() for p in evennia.AccountDB.get_all_cached_instances()]
yield [
(s._pause_task(auto_pause=True) if s.is_active else None, s.at_server_reload())
for s in evennia.ScriptDB.get_all_cached_instances()
if s.id
]
yield evennia.SESSION_HANDLER.all_sessions_portal_sync()
self.at_server_reload_stop()
# only save monitor state on reload, not on shutdown/reset
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.save()
else:
if mode == "reset":
# like shutdown but don't unset the is_connected flag and don't disconnect sessions
yield [o.at_server_shutdown() for o in evennia.ObjectDB.get_all_cached_instances()]
yield [p.at_server_shutdown() for p in evennia.AccountDB.get_all_cached_instances()]
if self.amp_protocol:
yield evennia.SESSION_HANDLER.all_sessions_portal_sync()
else: # shutdown
yield [_SA(p, "is_connected", False) for p in evennia.AccountDB.get_all_cached_instances()]
yield [o.at_server_shutdown() for o in evennia.ObjectDB.get_all_cached_instances()]
yield [
(p.unpuppet_all(), p.at_server_shutdown())
for p in evennia.AccountDB.get_all_cached_instances()
]
yield evennia.ObjectDB.objects.clear_all_sessids()
yield [
(s._pause_task(auto_pause=True), s.at_server_shutdown())
for s in evennia.ScriptDB.get_all_cached_instances()
if s.id and s.is_active
]
evennia.ServerConfig.objects.conf("server_restart_mode", "reset")
self.at_server_cold_stop()
# tickerhandler state should always be saved.
from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.save()
# always called, also for a reload
self.at_server_stop()
if hasattr(self, "web_root"): # not set very first start
yield self.web_root.empty_threadpool()
if not _reactor_stopping:
# kill the server
self.shutdown_complete = True
reactor.callLater(1, reactor.stop)
# we make sure the proper gametime is saved as late as possible
evennia.ServerConfig.objects.conf("runtime", evennia.gametime.runtime())
def get_info_dict(self):
"""
Return the server info, for display.
"""
return self.info_dict
# server start/stop hooks
def _call_start_stop(self, hookname):
"""
Helper method for calling hooks on all modules.
Args:
hookname (str): Name of hook to call.
"""
for mod in self.start_stop_modules:
if (hook := getattr(mod, hookname, None)):
hook()
def at_server_init(self):
"""
This is called first when the server is starting, before any other hooks, regardless of how it's starting.
"""
self._call_start_stop("at_server_init")
def at_server_start(self):
"""
This is called every time the server starts up, regardless of
how it was shut down.
"""
self._call_start_stop("at_server_start")
def at_server_stop(self):
"""
This is called just before a server is shut down, regardless
of it is fore a reload, reset or shutdown.
"""
self._call_start_stop("at_server_stop")
def at_server_reload_start(self):
"""
This is called only when server starts back up after a reload.
"""
self._call_start_stop("at_server_reload_start")
def at_post_portal_sync(self, mode):
"""
This is called just after the portal has finished syncing back data to the server
after reconnecting.
Args:
mode (str): One of 'reload', 'reset' or 'shutdown'.
"""
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.restore(mode == "reload")
from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.restore(mode == "reload")
# Un-pause all scripts, stop non-persistent timers
evennia.ScriptDB.objects.update_scripts_after_server_start()
# start the task handler
from evennia.scripts.taskhandler import TASK_HANDLER
TASK_HANDLER.load()
TASK_HANDLER.create_delays()
# create/update channels
self.create_default_channels()
# delete the temporary setting
evennia.ServerConfig.objects.conf("server_restart_mode", delete=True)
def at_server_reload_stop(self):
"""
This is called only time the server stops before a reload.
"""
self._call_start_stop("at_server_reload_stop")
def at_server_cold_start(self):
"""
This is called only when the server starts "cold", i.e. after a
shutdown or a reset.
"""
# We need to do this just in case the server was killed in a way where
# the normal cleanup operations did not have time to run.
from evennia.objects.models import ObjectDB
ObjectDB.objects.clear_all_sessids()
# Remove non-persistent scripts
from evennia.scripts.models import ScriptDB
for script in ScriptDB.objects.filter(db_persistent=False):
script._stop_task()
if settings.GUEST_ENABLED:
for guest in evennia.AccountDB.objects.all().filter(
db_typeclass_path=settings.BASE_GUEST_TYPECLASS
):
for character in guest.db._playable_characters:
if character:
character.delete()
guest.delete()
self._call_start_stop("at_server_cold_start")
def at_server_cold_stop(self):
"""
This is called only when the server goes down due to a shutdown or reset.
"""
self._call_start_stop("at_server_cold_stop")

View file

@ -18,6 +18,7 @@ from codecs import decode as codecs_decode
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
import evennia
from evennia.commands.cmdhandler import CMD_LOGINSTART from evennia.commands.cmdhandler import CMD_LOGINSTART
from evennia.server.portal import amp from evennia.server.portal import amp
from evennia.server.signals import ( from evennia.server.signals import (
@ -306,8 +307,7 @@ class ServerSessionHandler(SessionHandler):
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.server = None # set at server initialization evennia.EVENNIA_SERVICE_data = {"servername": _SERVERNAME}
self.server_data = {"servername": _SERVERNAME}
# will be set on psync # will be set on psync
self.portal_start_time = 0.0 self.portal_start_time = 0.0
@ -411,7 +411,7 @@ class ServerSessionHandler(SessionHandler):
mode = "reload" mode = "reload"
# tell the server hook we synced # tell the server hook we synced
self.server.at_post_portal_sync(mode) evennia.EVENNIA_SERVICE.at_post_portal_sync(mode)
# announce the reconnection # announce the reconnection
if _BROADCAST_SERVER_RESTART_MESSAGES: if _BROADCAST_SERVER_RESTART_MESSAGES:
self.announce_all(_(" ... Server restarted.")) self.announce_all(_(" ... Server restarted."))
@ -467,7 +467,7 @@ class ServerSessionHandler(SessionHandler):
the Server. the Server.
""" """
self.server.amp_protocol.send_AdminServer2Portal( evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=amp.SCONN, protocol_path=protocol_path, config=configdict DUMMYSESSION, operation=amp.SCONN, protocol_path=protocol_path, config=configdict
) )
@ -476,14 +476,14 @@ class ServerSessionHandler(SessionHandler):
Called by server when reloading. We tell the portal to start a new server instance. Called by server when reloading. We tell the portal to start a new server instance.
""" """
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=amp.SRELOAD) evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=amp.SRELOAD)
def portal_reset_server(self): def portal_reset_server(self):
""" """
Called by server when reloading. We tell the portal to start a new server instance. Called by server when reloading. We tell the portal to start a new server instance.
""" """
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=amp.SRESET) evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=amp.SRESET)
def portal_shutdown(self): def portal_shutdown(self):
""" """
@ -491,7 +491,7 @@ class ServerSessionHandler(SessionHandler):
itself down) itself down)
""" """
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=amp.PSHUTD) evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=amp.PSHUTD)
def login(self, session, account, force=False, testmode=False): def login(self, session, account, force=False, testmode=False):
""" """
@ -537,7 +537,7 @@ class ServerSessionHandler(SessionHandler):
session.logged_in = True session.logged_in = True
# sync the portal to the session # sync the portal to the session
if not testmode: if not testmode:
self.server.amp_protocol.send_AdminServer2Portal( evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(
session, operation=amp.SLOGIN, sessiondata={"logged_in": True, "uid": session.uid} session, operation=amp.SLOGIN, sessiondata={"logged_in": True, "uid": session.uid}
) )
account.at_post_login(session=session) account.at_post_login(session=session)
@ -582,7 +582,7 @@ class ServerSessionHandler(SessionHandler):
del self[sessid] del self[sessid]
if sync_portal: if sync_portal:
# inform portal that session should be closed. # inform portal that session should be closed.
self.server.amp_protocol.send_AdminServer2Portal( evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(
session, operation=amp.SDISCONN, reason=reason session, operation=amp.SDISCONN, reason=reason
) )
@ -593,7 +593,7 @@ class ServerSessionHandler(SessionHandler):
""" """
sessdata = self.get_all_sync_data() sessdata = self.get_all_sync_data()
return self.server.amp_protocol.send_AdminServer2Portal( return evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=amp.SSYNC, sessiondata=sessdata DUMMYSESSION, operation=amp.SSYNC, sessiondata=sessdata
) )
@ -604,7 +604,7 @@ class ServerSessionHandler(SessionHandler):
""" """
sessdata = {session.sessid: session.get_sync_data()} sessdata = {session.sessid: session.get_sync_data()}
return self.server.amp_protocol.send_AdminServer2Portal( return evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=amp.SSYNC, sessiondata=sessdata, clean=False DUMMYSESSION, operation=amp.SSYNC, sessiondata=sessdata, clean=False
) )
@ -617,7 +617,7 @@ class ServerSessionHandler(SessionHandler):
more sessions in detail. more sessions in detail.
""" """
return self.server.amp_protocol.send_AdminServer2Portal( return evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=amp.SSYNC, sessiondata=session_data, clean=False DUMMYSESSION, operation=amp.SSYNC, sessiondata=session_data, clean=False
) )
@ -633,7 +633,7 @@ class ServerSessionHandler(SessionHandler):
for session in self: for session in self:
del session del session
# tell portal to disconnect all sessions # tell portal to disconnect all sessions
self.server.amp_protocol.send_AdminServer2Portal( evennia.EVENNIA_SERVICE.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=amp.SDISCONNALL, reason=reason DUMMYSESSION, operation=amp.SDISCONNALL, reason=reason
) )
@ -817,7 +817,7 @@ class ServerSessionHandler(SessionHandler):
kwargs = self.clean_senddata(session, kwargs) kwargs = self.clean_senddata(session, kwargs)
# send across AMP # send across AMP
self.server.amp_protocol.send_MsgServer2Portal(session, **kwargs) evennia.EVENNIA_SERVICE.amp_protocol.send_MsgServer2Portal(session, **kwargs)
def get_inputfuncs(self): def get_inputfuncs(self):
""" """

View file

@ -11,9 +11,14 @@ from model_mommy import mommy
from twisted.internet.base import DelayedCall from twisted.internet.base import DelayedCall
from twisted.trial.unittest import TestCase as TwistedTestCase from twisted.trial.unittest import TestCase as TwistedTestCase
import evennia
from evennia.server import amp_client, server, serversession, session from evennia.server import amp_client, server, serversession, session
from evennia.server.portal import amp, amp_server, portal from evennia.server.portal import amp, amp_server, portal
from evennia.utils import create from evennia.utils import create
from evennia.server.service import EvenniaServerService
from evennia.server.portal.service import EvenniaPortalService
from evennia.server.sessionhandler import ServerSessionHandler
from evennia.server.portal.portalsessionhandler import PortalSessionHandler
DelayedCall.debug = True DelayedCall.debug = True
@ -24,22 +29,23 @@ class _TestAMP(TwistedTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.account = mommy.make("accounts.AccountDB", id=1) self.account = mommy.make("accounts.AccountDB", id=1)
self.server = server.Evennia(MagicMock()) self.server = EvenniaServerService()
self.server.sessions.data_in = MagicMock() evennia.SERVER_SESSION_HANDLER = ServerSessionHandler()
self.server.sessions.data_out = MagicMock() evennia.SERVER_SESSION_HANDLER.data_in = MagicMock()
evennia.SERVER_SESSION_HANDLER.data_out = MagicMock()
self.amp_client_factory = amp_client.AMPClientFactory(self.server) self.amp_client_factory = amp_client.AMPClientFactory(self.server)
self.amp_client = self.amp_client_factory.buildProtocol("127.0.0.1") self.amp_client = self.amp_client_factory.buildProtocol("127.0.0.1")
self.session = MagicMock() # serversession.ServerSession() self.session = MagicMock() # serversession.ServerSession()
self.session.sessid = 1 self.session.sessid = 1
self.server.sessions[1] = self.session evennia.SERVER_SESSION_HANDLER[1] = self.session
self.portal = portal.Portal(MagicMock()) self.portal = EvenniaPortalService()
self.portal.maintenance_task.stop()
self.portalsession = session.Session() self.portalsession = session.Session()
self.portalsession.sessid = 1 self.portalsession.sessid = 1
self.portal.sessions[1] = self.portalsession evennia.PORTAL_SESSION_HANDLER = PortalSessionHandler()
self.portal.sessions.data_in = MagicMock() evennia.PORTAL_SESSION_HANDLER[1] = self.portalsession
self.portal.sessions.data_out = MagicMock() evennia.PORTAL_SESSION_HANDLER.data_in = MagicMock()
evennia.PORTAL_SESSION_HANDLER.data_out = MagicMock()
self.amp_server_factory = amp_server.AMPServerFactory(self.portal) self.amp_server_factory = amp_server.AMPServerFactory(self.portal)
self.amp_server = self.amp_server_factory.buildProtocol("127.0.0.1") self.amp_server = self.amp_server_factory.buildProtocol("127.0.0.1")

View file

@ -5,8 +5,6 @@ all over the code base and runs them.
Runs as part of the Evennia's test suite with 'evennia test evennia" Runs as part of the Evennia's test suite with 'evennia test evennia"
""" """
from unittest import mock
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner
@ -19,16 +17,7 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
""" """
def setup_test_environment(self, **kwargs): def setup_test_environment(self, **kwargs):
# the portal looping call starts before the unit-test suite so we
# can't mock it - instead we stop it before starting the test - otherwise
# we'd get unclean reactor errors across test boundaries.
from evennia.server.portal.portal import PORTAL
PORTAL.maintenance_task.stop()
# initialize evennia itself
import evennia import evennia
evennia._init() evennia._init()
from django.conf import settings from django.conf import settings

View file

@ -127,6 +127,7 @@ EVENNIA_ADMIN = True
# operating between two processes on the same machine. You usually don't need to # operating between two processes on the same machine. You usually don't need to
# change this unless you cannot use the default AMP port/host for # change this unless you cannot use the default AMP port/host for
# whatever reason. # whatever reason.
AMP_ENABLED = True
AMP_HOST = "localhost" AMP_HOST = "localhost"
AMP_PORT = 4006 AMP_PORT = 4006
AMP_INTERFACE = "127.0.0.1" AMP_INTERFACE = "127.0.0.1"
@ -1155,6 +1156,10 @@ REST_API_ENABLED = False
# together with your own variations. You should usually never have to touch # together with your own variations. You should usually never have to touch
# this, and if so, you really need to know what you are doing. # this, and if so, you really need to know what you are doing.
# The primary Twisted Services used to start up Evennia.
EVENNIA_SERVER_SERVICE_CLASS = "evennia.server.service.EvenniaServerService"
EVENNIA_PORTAL_SERVICE_CLASS = "evennia.server.portal.service.EvenniaPortalService"
# The Base Session Class is used as a parent class for all Protocols such as # The Base Session Class is used as a parent class for all Protocols such as
# Telnet and SSH.) Changing this could be really dangerous. It will cascade # Telnet and SSH.) Changing this could be really dangerous. It will cascade
# to tons of classes. You generally shouldn't need to touch protocols. # to tons of classes. You generally shouldn't need to touch protocols.