diff --git a/evennia/__init__.py b/evennia/__init__.py index 9b5107023f..ec69a2326c 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -16,6 +16,7 @@ to launch such a shell (using python or ipython depending on your install). See www.evennia.com for full documentation. """ +import evennia # docstring header @@ -47,6 +48,7 @@ AccountDB = None ScriptDB = None ChannelDB = None Msg = None +ServerConfig = None # Properties AttributeProperty = None @@ -98,6 +100,8 @@ FuncParser = None # Handlers SESSION_HANDLER = None +PORTAL_SESSION_HANDLER = None +SERVER_SESSION_HANDLER = None TASK_HANDLER = None TICKER_HANDLER = None MONITOR_HANDLER = None @@ -106,8 +110,10 @@ MONITOR_HANDLER = None GLOBAL_SCRIPTS = None OPTION_CLASSES = None -# variables -PORTAL_MODE = False +PROCESS_ID = None + +TWISTED_APPLICATION = None +EVENNIA_SERVICE = None def _create_version(): @@ -140,6 +146,9 @@ def _create_version(): __version__ = _create_version() del _create_version +_LOADED = False + +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 in a safe environment where all models are available already. """ + global _LOADED + if _LOADED: + return + _LOADED = True global DefaultAccount, DefaultObject, DefaultGuest, DefaultCharacter global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg @@ -157,18 +170,19 @@ def _init(portal_mode=False): global create_message, create_help_entry global signals global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers - global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER - global TASK_HANDLER - global GLOBAL_SCRIPTS, OPTION_CLASSES + global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, PROCESS_ID + global TASK_HANDLER, PORTAL_SESSION_HANDLER, SERVER_SESSION_HANDLER + global GLOBAL_SCRIPTS, OPTION_CLASSES, EVENNIA_SERVICE, TWISTED_APPLICATION global EvMenu, EvTable, EvForm, EvMore, EvEditor global ANSIString, FuncParser - global AttributeProperty, TagProperty, TagCategoryProperty + global AttributeProperty, TagProperty, TagCategoryProperty, ServerConfig global PORTAL_MODE PORTAL_MODE = portal_mode # Parent typeclasses # utilities from django.conf import settings + import os from . import contrib from .accounts.accounts import DefaultAccount, DefaultGuest @@ -192,9 +206,10 @@ def _init(portal_mode=False): from .scripts.taskhandler import TASK_HANDLER from .scripts.tickerhandler import TICKER_HANDLER from .server import signals + from .server.models import ServerConfig from .typeclasses.attributes import AttributeProperty 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 if not PORTAL_MODE: @@ -229,12 +244,29 @@ def _init(portal_mode=False): ) 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 from evennia.server.portal import portalsessionhandler - portal_sess_handler_class = class_from_module(settings.PORTAL_SESSION_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: # Create the ServerSesssionHandler from evennia.server import sessionhandler @@ -243,6 +275,11 @@ def _init(portal_mode=False): sessionhandler.SESSIONS = sess_handler_class() sessionhandler.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 diff --git a/evennia/server/amp_client.py b/evennia/server/amp_client.py index 7e4d7b2bd6..83135c9c47 100644 --- a/evennia/server/amp_client.py +++ b/evennia/server/amp_client.py @@ -9,6 +9,7 @@ import os from django.conf import settings from twisted.internet import protocol +import evennia from evennia.server.portal import amp from evennia.utils import logger from evennia.utils.utils import class_from_module @@ -188,9 +189,9 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): """ 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: - self.factory.server.sessions.data_in(session, **kwargs) + evennia.SERVER_SESSION_HANDLER.data_in(session, **kwargs) return {} @amp.AdminPortal2Server.responder @@ -207,46 +208,45 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): """ sessid, kwargs = self.data_in(packed_data) operation = kwargs.pop("operation", "") - server_sessionhandler = self.factory.server.sessions if operation == amp.PCONN: # portal_session_connect # 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 - 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 # session closed from portal sid - session = server_sessionhandler.get(sessid) + session = evennia.SERVER_SESSION_HANDLER.get(sessid) if session: - server_sessionhandler.portal_disconnect(session) + evennia.SERVER_SESSION_HANDLER.portal_disconnect(session) elif operation == amp.PDISCONNALL: # portal_disconnect_all # 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 # force a resync of sessions from the portal side. This happens on # first server-connect. server_restart_mode = kwargs.get("server_restart_mode", "shutdown") - self.factory.server.run_init_hooks(server_restart_mode) - server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata")) - server_sessionhandler.portal_start_time = kwargs.get("portal_start_time") + evennia.EVENNIA_SERVICE.run_init_hooks(server_restart_mode) + evennia.SERVER_SESSION_HANDLER.portal_sessions_sync(kwargs.get("sessiondata")) + evennia.SERVER_SESSION_HANDLER.portal_start_time = kwargs.get("portal_start_time") elif operation == amp.SRELOAD: # server reload # shut down in reload mode - server_sessionhandler.all_sessions_portal_sync() - server_sessionhandler.server.shutdown(mode="reload") + evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync() + evennia.EVENNIA_SERVICE.shutdown(mode="reload") elif operation == amp.SRESET: # shut down in reset mode - server_sessionhandler.all_sessions_portal_sync() - server_sessionhandler.server.shutdown(mode="reset") + evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync() + evennia.EVENNIA_SERVICE.shutdown(mode="reset") elif operation == amp.SSHUTD: # server shutdown # shutdown in stop mode - server_sessionhandler.server.shutdown(mode="shutdown") + evennia.EVENNIA_SERVICE.shutdown(mode="shutdown") else: raise Exception("operation %(op)s not recognized." % {"op": operation}) diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index 681c611edf..80318c6a1b 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -11,6 +11,7 @@ from subprocess import STDOUT, Popen from django.conf import settings from twisted.internet import protocol +import evennia from evennia.server.portal import amp from evennia.utils import logger from evennia.utils.utils import class_from_module @@ -379,9 +380,9 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): """ try: 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: - self.factory.portal.sessions.data_out(session, **kwargs) + evennia.PORTAL_SESSION_HANDLER.data_out(session, **kwargs) except Exception: logger.log_trace("packed_data len {}".format(len(packed_data))) return {} @@ -405,7 +406,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): # logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs)) operation = kwargs.pop("operation") - portal_sessionhandler = self.factory.portal.sessions + portal_sessionhandler = evennia.PORTAL_SESSION_HANDLER if operation == amp.SLOGIN: # server_session_login # 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 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( amp.DUMMYSESSION, amp.PSYNC, @@ -457,7 +458,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): sessiondata=sessdata, 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: # this is an indication the server has successfully connected, so diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index cfff662c4c..d3e825bd6e 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -9,243 +9,23 @@ by game/evennia.py). """ import os 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 +import django django.setup() -from django.conf import settings -from django.db import connection import evennia - 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.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 # 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: @@ -257,186 +37,3 @@ if "--nodaemon" not in sys.argv and "test" not in sys.argv: max_size=settings.PORTAL_LOG_MAX_SIZE, ) 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) diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 386263cbc1..a9a1f5d23a 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -11,6 +11,7 @@ from django.conf import settings from django.utils.translation import gettext as _ from twisted.internet import reactor +import evennia from evennia.server.portal.amp import PCONN, PCONNSYNC, PDISCONN, PDISCONNALL from evennia.server.sessionhandler import SessionHandler from evennia.utils.logger import log_trace @@ -62,7 +63,6 @@ class PortalSessionHandler(SessionHandler): """ super().__init__(*args, **kwargs) - self.portal = None self.latest_sessid = 0 self.uptime = time.time() self.connection_time = 0 @@ -132,7 +132,7 @@ class PortalSessionHandler(SessionHandler): now = time.time() if ( 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: self.connection_task = reactor.callLater( _MIN_TIME_BETWEEN_CONNECTS, self.connect, None @@ -156,7 +156,7 @@ class PortalSessionHandler(SessionHandler): self[session.sessid] = session session.server_connected = True - self.portal.amp_protocol.send_AdminPortal2Server( + evennia.EVENNIA_SERVICE.amp_protocol.send_AdminPortal2Server( 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 # we skip this step. 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 # at the server level at this point sessdata = dict( @@ -192,7 +192,7 @@ class PortalSessionHandler(SessionHandler): "server_data", ) ) - self.portal.amp_protocol.send_AdminPortal2Server( + evennia.EVENNIA_SERVICE.amp_protocol.send_AdminPortal2Server( session, operation=PCONNSYNC, sessiondata=sessdata ) @@ -222,7 +222,7 @@ class PortalSessionHandler(SessionHandler): del self[session.sessid] # 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): """ @@ -240,7 +240,7 @@ class PortalSessionHandler(SessionHandler): # inform Server; wait until finished sending before we continue # removing all the sessions. - self.portal.amp_protocol.send_AdminPortal2Server( + evennia.EVENNIA_SERVICE.amp_protocol.send_AdminPortal2Server( DUMMYSESSION, operation=PDISCONNALL ).addCallback(_callback, self) @@ -434,7 +434,7 @@ class PortalSessionHandler(SessionHandler): self.data_out(session, text=[[_ERROR_COMMAND_OVERFLOW], {}]) return - if not self.portal.amp_protocol: + if not evennia.EVENNIA_SERVICE.amp_protocol: # this can happen if someone connects before AMP connection # was established (usually on first start) reactor.callLater(1.0, self.data_in, session, **kwargs) @@ -445,7 +445,7 @@ class PortalSessionHandler(SessionHandler): # relay data to Server 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) if "text" in kwargs and session.protocol_flags.get("LOCALECHO", False): diff --git a/evennia/server/portal/service.py b/evennia/server/portal/service.py new file mode 100644 index 0000000000..1970831787 --- /dev/null +++ b/evennia/server/portal/service.py @@ -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) diff --git a/evennia/server/portal/tests.py b/evennia/server/portal/tests.py index 5388ca5351..571ee3b415 100644 --- a/evennia/server/portal/tests.py +++ b/evennia/server/portal/tests.py @@ -21,6 +21,7 @@ from twisted.internet.base import DelayedCall from twisted.test import proto_helpers from twisted.trial.unittest import TestCase as TwistedTestCase +import evennia from evennia.server.portal import irc from evennia.utils.test_resources import BaseEvenniaTest @@ -35,7 +36,6 @@ from .mccp import MCCP from .mssp import MSSP from .mxp import MXP from .naws import DEFAULT_HEIGHT, DEFAULT_WIDTH -from .portal import PORTAL_SESSIONS from .suppress_ga import SUPPRESS_GA from .telnet import TelnetProtocol, TelnetServerFactory from .telnet_oob import MSDP, MSDP_VAL, MSDP_VAR @@ -223,7 +223,7 @@ class TestTelnet(TwistedTestCase): super().setUp() factory = TelnetServerFactory() factory.protocol = TelnetProtocol - factory.sessionhandler = PORTAL_SESSIONS + factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER factory.sessionhandler.portal = Mock() self.proto = factory.buildProtocol(("localhost", 0)) self.transport = proto_helpers.StringTransport() @@ -289,8 +289,8 @@ class TestWebSocket(BaseEvenniaTest): super().setUp() self.proto = WebSocketClient() self.proto.factory = WebSocketServerFactory() - self.proto.factory.sessionhandler = PORTAL_SESSIONS - self.proto.sessionhandler = PORTAL_SESSIONS + self.proto.factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER + self.proto.sessionhandler = evennia.PORTAL_SESSION_HANDLER self.proto.sessionhandler.portal = Mock() self.proto.transport = proto_helpers.StringTransport() # self.proto.transport = proto_helpers.FakeDatagramTransport() diff --git a/evennia/server/server.py b/evennia/server/server.py index bce588f733..fa7aa124e5 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -9,684 +9,23 @@ evennia/server/server_runner.py). """ import os import sys -import time -import traceback + +from twisted.logger import globalLogPublisher 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() -import importlib - import evennia - evennia._init() 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.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 # 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: # 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)) -# 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.") diff --git a/evennia/server/service.py b/evennia/server/service.py new file mode 100644 index 0000000000..c3d645c339 --- /dev/null +++ b/evennia/server/service.py @@ -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") diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 0dd7956b48..9f1a342bd3 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -18,6 +18,7 @@ from codecs import decode as codecs_decode from django.conf import settings from django.utils.translation import gettext as _ +import evennia from evennia.commands.cmdhandler import CMD_LOGINSTART from evennia.server.portal import amp from evennia.server.signals import ( @@ -306,8 +307,7 @@ class ServerSessionHandler(SessionHandler): """ super().__init__(*args, **kwargs) - self.server = None # set at server initialization - self.server_data = {"servername": _SERVERNAME} + evennia.EVENNIA_SERVICE_data = {"servername": _SERVERNAME} # will be set on psync self.portal_start_time = 0.0 @@ -411,7 +411,7 @@ class ServerSessionHandler(SessionHandler): mode = "reload" # tell the server hook we synced - self.server.at_post_portal_sync(mode) + evennia.EVENNIA_SERVICE.at_post_portal_sync(mode) # announce the reconnection if _BROADCAST_SERVER_RESTART_MESSAGES: self.announce_all(_(" ... Server restarted.")) @@ -467,7 +467,7 @@ class ServerSessionHandler(SessionHandler): 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 ) @@ -476,14 +476,14 @@ class ServerSessionHandler(SessionHandler): 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): """ 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): """ @@ -491,7 +491,7 @@ class ServerSessionHandler(SessionHandler): 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): """ @@ -537,7 +537,7 @@ class ServerSessionHandler(SessionHandler): session.logged_in = True # sync the portal to the session 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} ) account.at_post_login(session=session) @@ -582,7 +582,7 @@ class ServerSessionHandler(SessionHandler): del self[sessid] if sync_portal: # 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 ) @@ -593,7 +593,7 @@ class ServerSessionHandler(SessionHandler): """ 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 ) @@ -604,7 +604,7 @@ class ServerSessionHandler(SessionHandler): """ 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 ) @@ -617,7 +617,7 @@ class ServerSessionHandler(SessionHandler): 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 ) @@ -633,7 +633,7 @@ class ServerSessionHandler(SessionHandler): for session in self: del session # 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 ) @@ -817,7 +817,7 @@ class ServerSessionHandler(SessionHandler): kwargs = self.clean_senddata(session, kwargs) # send across AMP - self.server.amp_protocol.send_MsgServer2Portal(session, **kwargs) + evennia.EVENNIA_SERVICE.amp_protocol.send_MsgServer2Portal(session, **kwargs) def get_inputfuncs(self): """ diff --git a/evennia/server/tests/test_amp_connection.py b/evennia/server/tests/test_amp_connection.py index 05e9e82017..14e0c4a790 100644 --- a/evennia/server/tests/test_amp_connection.py +++ b/evennia/server/tests/test_amp_connection.py @@ -11,9 +11,14 @@ from model_mommy import mommy from twisted.internet.base import DelayedCall from twisted.trial.unittest import TestCase as TwistedTestCase +import evennia from evennia.server import amp_client, server, serversession, session from evennia.server.portal import amp, amp_server, portal 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 @@ -24,22 +29,23 @@ class _TestAMP(TwistedTestCase): def setUp(self): super().setUp() self.account = mommy.make("accounts.AccountDB", id=1) - self.server = server.Evennia(MagicMock()) - self.server.sessions.data_in = MagicMock() - self.server.sessions.data_out = MagicMock() + self.server = EvenniaServerService() + evennia.SERVER_SESSION_HANDLER = ServerSessionHandler() + 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 = self.amp_client_factory.buildProtocol("127.0.0.1") self.session = MagicMock() # serversession.ServerSession() self.session.sessid = 1 - self.server.sessions[1] = self.session + evennia.SERVER_SESSION_HANDLER[1] = self.session - self.portal = portal.Portal(MagicMock()) - self.portal.maintenance_task.stop() + self.portal = EvenniaPortalService() self.portalsession = session.Session() self.portalsession.sessid = 1 - self.portal.sessions[1] = self.portalsession - self.portal.sessions.data_in = MagicMock() - self.portal.sessions.data_out = MagicMock() + evennia.PORTAL_SESSION_HANDLER = PortalSessionHandler() + evennia.PORTAL_SESSION_HANDLER[1] = self.portalsession + 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 = self.amp_server_factory.buildProtocol("127.0.0.1") diff --git a/evennia/server/tests/testrunner.py b/evennia/server/tests/testrunner.py index 9d9f8b9265..9f4359838b 100644 --- a/evennia/server/tests/testrunner.py +++ b/evennia/server/tests/testrunner.py @@ -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" """ -from unittest import mock - from django.test.runner import DiscoverRunner @@ -19,16 +17,7 @@ class EvenniaTestSuiteRunner(DiscoverRunner): """ 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 - evennia._init() from django.conf import settings diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 3ec5f55776..9711af3167 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -127,6 +127,7 @@ EVENNIA_ADMIN = True # 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 # whatever reason. +AMP_ENABLED = True AMP_HOST = "localhost" AMP_PORT = 4006 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 # 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 # Telnet and SSH.) Changing this could be really dangerous. It will cascade # to tons of classes. You generally shouldn't need to touch protocols.