"""
The Evennia Server service acts as an AMP-client when talking to the
Portal. This module sets up the Client-side communication.
"""
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
[docs]class AMPClientFactory(protocol.ReconnectingClientFactory):
"""
This factory creates an instance of an AMP client connection. This handles communication from
the be the Evennia 'Server' service to the 'Portal'. The client will try to auto-reconnect on a
connection error.
"""
# Initial reconnect delay in seconds.
initialDelay = 1
factor = 1.5
maxDelay = 1
noisy = False
[docs] def __init__(self, server):
"""
Initializes the client factory.
Args:
server (server): server instance.
"""
self.server = server
self.protocol = class_from_module(settings.AMP_CLIENT_PROTOCOL_CLASS)
self.maxDelay = 10
# not really used unless connecting to multiple servers, but
# avoids having to check for its existence on the protocol
self.broadcasts = []
[docs] def startedConnecting(self, connector):
"""
Called when starting to try to connect to the Portal AMP server.
Args:
connector (Connector): Twisted Connector instance representing
this connection.
"""
pass
[docs] def buildProtocol(self, addr):
"""
Creates an AMPProtocol instance when connecting to the AMP server.
Args:
addr (str): Connection address. Not used.
"""
self.resetDelay()
self.server.amp_protocol = AMPServerClientProtocol()
self.server.amp_protocol.factory = self
return self.server.amp_protocol
[docs] def clientConnectionLost(self, connector, reason):
"""
Called when the AMP connection to the MUD server is lost.
Args:
connector (Connector): Twisted Connector instance representing
this connection.
reason (str): Eventual text describing why connection was lost.
"""
logger.log_info("Server disconnected from the portal.")
protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
[docs] def clientConnectionFailed(self, connector, reason):
"""
Called when an AMP connection attempt to the MUD server fails.
Args:
connector (Connector): Twisted Connector instance representing
this connection.
reason (str): Eventual text describing why connection failed.
"""
logger.log_msg("Attempting to reconnect to Portal ...")
protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
[docs]class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
"""
This protocol describes the Server service (acting as an AMP-client)'s communication with the
Portal (which acts as the AMP-server)
"""
# sending AMP data
[docs] def connectionMade(self):
"""
Called when a new connection is established.
"""
# print("AMPClient new connection {}".format(self))
info_dict = self.factory.server.get_info_dict()
super().connectionMade()
# first thing we do is to request the Portal to sync all sessions
# back with the Server side. We also need the startup mode (reload, reset, shutdown)
self.send_AdminServer2Portal(
amp.DUMMYSESSION, operation=amp.PSYNC, spid=os.getpid(), info_dict=info_dict
)
# run the intial setup if needed
self.factory.server.run_initial_setup()
[docs] def data_to_portal(self, command, sessid, **kwargs):
"""
Send data across the wire to the Portal
Args:
command (AMP Command): A protocol send command.
sessid (int): A unique Session id.
kwargs (any): Any data to pickle into the command.
Returns:
deferred (deferred or None): A deferred with an errback.
Notes:
Data will be sent across the wire pickled as a tuple
(sessid, kwargs).
"""
# print("server data_to_portal: {}, {}, {}".format(command, sessid, kwargs))
return self.callRemote(command, packed_data=amp.dumps((sessid, kwargs))).addErrback(
self.errback, command.key
)
[docs] def send_MsgServer2Portal(self, session, **kwargs):
"""
Access method - executed on the Server for sending data
to Portal.
Args:
session (Session): Unique Session.
kwargs (any, optiona): Extra data.
"""
return self.data_to_portal(amp.MsgServer2Portal, session.sessid, **kwargs)
[docs] def send_AdminServer2Portal(self, session, operation="", **kwargs):
"""
Administrative access method called by the Server to send an
instruction to the Portal.
Args:
session (Session): Session.
operation (char, optional): Identifier for the server
operation, as defined by the global variables in
`evennia/server/amp.py`.
kwargs (dict, optional): Data going into the adminstrative.
"""
return self.data_to_portal(
amp.AdminServer2Portal, session.sessid, operation=operation, **kwargs
)
# receiving AMP data
[docs] @amp.MsgStatus.responder
def server_receive_status(self, question):
return {"status": "OK"}
@amp.MsgPortal2Server.responder
@amp.catch_traceback
def server_receive_msgportal2server(self, packed_data):
"""
Receives message arriving to server. This method is executed
on the Server.
Args:
packed_data (str): Data to receive (a pickled tuple (sessid,kwargs))
"""
sessid, kwargs = self.data_in(packed_data)
session = evennia.SERVER_SESSION_HANDLER.get(sessid, None)
if session:
evennia.SERVER_SESSION_HANDLER.data_in(session, **kwargs)
return {}
@amp.AdminPortal2Server.responder
@amp.catch_traceback
def server_receive_adminportal2server(self, packed_data):
"""
Receives admin data from the Portal (allows the portal to
perform admin operations on the server). This is executed on
the Server.
Args:
packed_data (str): Incoming, pickled data.
"""
sessid, kwargs = self.data_in(packed_data)
operation = kwargs.pop("operation", "")
if operation == amp.PCONN: # portal_session_connect
# create a new session and sync it
evennia.SERVER_SESSION_HANDLER.portal_connect(kwargs.get("sessiondata"))
elif operation == amp.PCONNSYNC: # portal_session_sync
evennia.SERVER_SESSION_HANDLER.portal_session_sync(kwargs.get("sessiondata"))
elif operation == amp.PDISCONN: # portal_session_disconnect
# session closed from portal sid
session = evennia.SERVER_SESSION_HANDLER.get(sessid)
if session:
evennia.SERVER_SESSION_HANDLER.portal_disconnect(session)
elif operation == amp.PDISCONNALL: # portal_disconnect_all
# portal orders all sessions to close
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")
evennia.EVENNIA_SERVER_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
evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync()
evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="reload")
elif operation == amp.SRESET:
# shut down in reset mode
evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync()
evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="reset")
elif operation == amp.SSHUTD: # server shutdown
# shutdown in stop mode
evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="shutdown")
else:
raise Exception("operation %(op)s not recognized." % {"op": operation})
return {}