Refactoring Server Startup to be more idiomatic for Twisted.

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

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

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

View file

@ -21,6 +21,7 @@ from twisted.internet.base import DelayedCall
from twisted.test import proto_helpers
from twisted.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()