Note

You are reading an old version of the Evennia documentation. The latest version is here

.

# Source code for evennia.server.portal.amp_server

    """
    The Evennia Portal service acts as an AMP-server, handling AMP
    communication to the AMP clients connecting to it (by default
    these are the Evennia Server and the evennia launcher).

    """
    import os
    import sys
    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

    def _is_windows():
        return os.name == "nt"

    def getenv():
        """
        Get current environment and add PYTHONPATH.

        Returns:
            env (dict): Environment global dict.

        """
        sep = ";" if _is_windows() else ":"
        env = os.environ.copy()
        env["PYTHONPATH"] = sep.join(sys.path)
        return env

    class AMPServerFactory(protocol.ServerFactory):

        """
        This factory creates AMP Server connection. This acts as the 'Portal'-side communication to the
        'Server' process.

        """

        noisy = False

        def logPrefix(self):
            """
            How this is named in logs

            """
            return "AMP"

        def __init__(self, portal):
            """
            Initialize the factory. This is called as the Portal service starts.

            Args:
                portal (Portal): The Evennia Portal service instance.
                protocol (Protocol): The protocol the factory creates
                    instances of.

            """
            self.portal = portal
            self.protocol = class_from_module(settings.AMP_SERVER_PROTOCOL_CLASS)
            self.broadcasts = []
            self.server_connection = None
            self.launcher_connection = None
            self.disconnect_callbacks = {}
            self.server_connect_callbacks = []

        def buildProtocol(self, addr):
            """
            Start a new connection, and store it on the service object.

            Args:
                addr (str): Connection address. Not used.

            Returns:
                protocol (Protocol): The created protocol.

            """
            self.portal.amp_protocol = self.protocol()
            self.portal.amp_protocol.factory = self
            return self.portal.amp_protocol

    class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
        """
        Protocol subclass for the AMP-server run by the Portal.

        """

        def connectionLost(self, reason):
            """
            Set up a simple callback mechanism to let the amp-server wait for a connection to close.

            """
            # wipe broadcast and data memory
            super().connectionLost(reason)
            if self.factory.server_connection == self:
                self.factory.server_connection = None
                self.factory.portal.server_info_dict = {}
            if self.factory.launcher_connection == self:
                self.factory.launcher_connection = None

            callback, args, kwargs = self.factory.disconnect_callbacks.pop(self, (None, None, None))
            if callback:
                try:
                    callback(*args, **kwargs)
                except Exception:
                    logger.log_trace()

        def get_status(self):
            """
            Return status for the Evennia infrastructure.

            Returns:
                status (tuple): The portal/server status and pids
                    (portal_live, server_live, portal_PID, server_PID).

            """
            server_connected = bool(
                self.factory.server_connection and self.factory.server_connection.transport.connected
            )
            portal_info_dict = self.factory.portal.get_info_dict()
            server_info_dict = self.factory.portal.server_info_dict
            server_pid = self.factory.portal.server_process_id
            portal_pid = os.getpid()
            return (True, server_connected, portal_pid, server_pid, portal_info_dict, server_info_dict)

        def data_to_server(self, command, sessid, **kwargs):
            """
            Send data across the wire to the Server.

            Args:
                command (AMP Command): A protocol send command.
                sessid (int): A unique Session id.
                kwargs (any): Data to send. This will be pickled.

            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("portal data_to_server: {}, {}, {}".format(command, sessid, kwargs))
            if self.factory.server_connection:
                return self.factory.server_connection.callRemote(
                    command, packed_data=amp.dumps((sessid, kwargs))
                ).addErrback(self.errback, command.key)
            else:
                # if no server connection is available, broadcast
                return self.broadcast(command, sessid, packed_data=amp.dumps((sessid, kwargs)))

        def start_server(self, server_twistd_cmd):
            """
            (Re-)Launch the Evennia server.

            Args:
                server_twisted_cmd (list): The server start instruction
                    to pass to POpen to start the server.

            """
            # start the Server
            print("Portal starting server ... ")
            process = None
            with open(settings.SERVER_LOG_FILE, "a") as logfile:
                # we link stdout to a file in order to catch
                # eventual errors happening before the Server has
                # opened its logger.
                try:
                    if _is_windows():
                        # Windows requires special care
                        create_no_window = 0x08000000
                        process = Popen(
                            server_twistd_cmd,
                            env=getenv(),
                            bufsize=-1,
                            stdout=logfile,
                            stderr=STDOUT,
                            creationflags=create_no_window,
                        )

                    else:
                        process = Popen(
                            server_twistd_cmd, env=getenv(), bufsize=-1, stdout=logfile, stderr=STDOUT
                        )
                except Exception:
                    logger.log_trace()

                self.factory.portal.server_twistd_cmd = server_twistd_cmd
                logfile.flush()
            if process and not _is_windows():
                # avoid zombie-process on Unix/BSD
                process.wait()
            # unset the reset-mode flag on the portal
            self.factory.portal.server_restart_mode = None
            return

        def wait_for_disconnect(self, callback, *args, **kwargs):
            """
            Add a callback for when this connection is lost.

            Args:
                callback (callable): Will be called with *args, **kwargs
                    once this protocol is disconnected.

            """
            self.factory.disconnect_callbacks[self] = (callback, args, kwargs)

        def wait_for_server_connect(self, callback, *args, **kwargs):
            """
            Add a callback for when the Server is sure to have connected.

            Args:
                callback (callable): Will be called with *args, **kwargs
                    once the Server handshake with Portal is complete.

            """
            self.factory.server_connect_callbacks.append((callback, args, kwargs))

        def stop_server(self, mode="shutdown"):
            """
            Shut down server in one or more modes.

            Args:
                mode (str): One of 'shutdown', 'reload' or 'reset'.

            """
            if mode == "reload":
                self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD)
            elif mode == "reset":
                self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET)
            elif mode == "shutdown":
                self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD)
            self.factory.portal.server_restart_mode = mode

        # sending amp data

        def send_Status2Launcher(self):
            """
            Send a status stanza to the launcher.

            """
            # print("send status to launcher")
            # print("self.get_status(): {}".format(self.get_status()))
            if self.factory.launcher_connection:
                self.factory.launcher_connection.callRemote(
                    amp.MsgStatus, status=amp.dumps(self.get_status())
                ).addErrback(self.errback, amp.MsgStatus.key)

        def send_MsgPortal2Server(self, session, **kwargs):
            """
            Access method called by the Portal and executed on the Portal.

            Args:
                session (session): Session
                kwargs (any, optional): Optional data.

            Returns:
                deferred (Deferred): Asynchronous return.

            """
            return self.data_to_server(amp.MsgPortal2Server, session.sessid, **kwargs)

        def send_AdminPortal2Server(self, session, operation="", **kwargs):
            """
            Send Admin instructions from the Portal to the Server.
            Executed on 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`.
                data (str or dict, optional): Data used in the administrative operation.

            """
            return self.data_to_server(
                amp.AdminPortal2Server, session.sessid, operation=operation, **kwargs
            )

        # receive amp data

        @amp.MsgStatus.responder
        @amp.catch_traceback
        def portal_receive_status(self, status):
            """
            Returns run-status for the server/portal.

            Args:
                status (str): Not used.
            Returns:
                status (dict): The status is a tuple
                    (portal_running, server_running, portal_pid, server_pid).

            """
            # print('Received PSTATUS request')
            return {"status": amp.dumps(self.get_status())}

        @amp.MsgLauncher2Portal.responder
        @amp.catch_traceback
        def portal_receive_launcher2portal(self, operation, arguments):
            """
            Receives message arriving from evennia_launcher.
            This method is executed on the Portal.

            Args:
                operation (str): The action to perform.
                arguments (str): Possible argument to the instruction, or the empty string.

            Returns:
                result (dict): The result back to the launcher.

            Notes:
                This is the entrypoint for controlling the entire Evennia system from the evennia
                launcher. It can obviously only accessed when the Portal is already up and running.

            """
            # Since the launcher command uses amp.String() we need to convert from byte here.
            operation = str(operation, "utf-8")
            self.factory.launcher_connection = self
            _, server_connected, _, _, _, _ = self.get_status()

            # logger.log_msg("Evennia Launcher->Portal operation %s:%s received" % (ord(operation), arguments))

            # logger.log_msg("operation == amp.SSTART: {}: {}".format(operation == amp.SSTART, amp.loads(arguments)))

            if operation == amp.SSTART:  # portal start  #15
                # first, check if server is already running
                if not server_connected:
                    self.wait_for_server_connect(self.send_Status2Launcher)
                    self.start_server(amp.loads(arguments))

            elif operation == amp.SRELOAD:  # reload server #14
                if server_connected:
                    # We let the launcher restart us once they get the signal
                    self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
                    self.stop_server(mode="reload")
                else:
                    self.wait_for_server_connect(self.send_Status2Launcher)
                    self.start_server(amp.loads(arguments))

            elif operation == amp.SRESET:  # reload server #19
                if server_connected:
                    self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
                    self.stop_server(mode="reset")
                else:
                    self.wait_for_server_connect(self.send_Status2Launcher)
                    self.start_server(amp.loads(arguments))

            elif operation == amp.SSHUTD:  # server-only shutdown #17
                if server_connected:
                    self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
                    self.stop_server(mode="shutdown")

            elif operation == amp.PSHUTD:  # portal + server shutdown  #16
                if server_connected:
                    self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)
                else:
                    self.factory.portal.shutdown()

            else:
                logger.log_err("Operation {} not recognized".format(operation))
                raise Exception("operation %(op)s not recognized." % {"op": operation})

            return {}

        @amp.MsgServer2Portal.responder
        @amp.catch_traceback
        def portal_receive_server2portal(self, packed_data):
            """
            Receives message arriving to Portal from Server.
            This method is executed on the Portal.

            Args:
                packed_data (str): Pickled data (sessid, kwargs) coming over the wire.

            """
            try:
                sessid, kwargs = self.data_in(packed_data)
                session = evennia.PORTAL_SESSION_HANDLER.get(sessid, None)
                if session:
                    evennia.PORTAL_SESSION_HANDLER.data_out(session, **kwargs)
            except Exception:
                logger.log_trace("packed_data len {}".format(len(packed_data)))
            return {}

        @amp.AdminServer2Portal.responder
        @amp.catch_traceback
        def portal_receive_adminserver2portal(self, packed_data):
            """

            Receives and handles admin operations sent to the Portal
            This is executed on the Portal.

            Args:
                packed_data (str): Data received, a pickled tuple (sessid, kwargs).

            """
            self.factory.server_connection = self

            sessid, kwargs = self.data_in(packed_data)

            # logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs))

            operation = kwargs.pop("operation")
            portal_sessionhandler = evennia.PORTAL_SESSION_HANDLER

            if operation == amp.SLOGIN:  # server_session_login
                # a session has authenticated; sync it.
                session = portal_sessionhandler.get(sessid)
                if session:
                    portal_sessionhandler.server_logged_in(session, kwargs.get("sessiondata"))

            elif operation == amp.SDISCONN:  # server_session_disconnect
                # the server is ordering to disconnect the session
                session = portal_sessionhandler.get(sessid)
                if session:
                    portal_sessionhandler.server_disconnect(session, reason=kwargs.get("reason"))

            elif operation == amp.SDISCONNALL:  # server_session_disconnect_all
                # server orders all sessions to disconnect
                portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))

            elif operation == amp.SRELOAD:  # server reload
                self.factory.server_connection.wait_for_disconnect(
                    self.start_server, self.factory.portal.server_twistd_cmd
                )
                self.stop_server(mode="reload")

            elif operation == amp.SRESET:  # server reset
                self.factory.server_connection.wait_for_disconnect(
                    self.start_server, self.factory.portal.server_twistd_cmd
                )
                self.stop_server(mode="reset")

            elif operation == amp.SSHUTD:  # server-only shutdown
                self.stop_server(mode="shutdown")

            elif operation == amp.PSHUTD:  # full server+server shutdown
                self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)
                self.stop_server(mode="shutdown")

            elif operation == amp.PSYNC:  # portal sync
                # Server has (re-)connected and wants the session data from portal
                self.factory.portal.server_info_dict = kwargs.get("info_dict", {})
                self.factory.portal.server_process_id = kwargs.get("spid", None)
                # this defaults to 'shutdown' or whatever value set in server_stop
                server_restart_mode = self.factory.portal.server_restart_mode

                sessdata = evennia.PORTAL_SESSION_HANDLER.get_all_sync_data()
                self.send_AdminPortal2Server(
                    amp.DUMMYSESSION,
                    amp.PSYNC,
                    server_restart_mode=server_restart_mode,
                    sessiondata=sessdata,
                    portal_start_time=self.factory.portal.start_time,
                )
                evennia.PORTAL_SESSION_HANDLER.at_server_connection()

                if self.factory.server_connection:
                    # this is an indication the server has successfully connected, so
                    # we trigger any callbacks (usually to tell the launcher server is up)
                    for callback, args, kwargs in self.factory.server_connect_callbacks:
                        try:
                            callback(*args, **kwargs)
                        except Exception:
                            logger.log_trace()
                    self.factory.server_connect_callbacks = []

            elif operation == amp.SSYNC:  # server_session_sync
                # server wants to save session data to the portal,
                # maybe because it's about to shut down.
                portal_sessionhandler.server_session_sync(
                    kwargs.get("sessiondata"), kwargs.get("clean", True)
                )

                # set a flag in case we are about to shut down soon
                self.factory.server_restart_mode = True

            elif operation == amp.SCONN:  # server_force_connection (for irc/etc)
                portal_sessionhandler.server_connect(**kwargs)

            else:
                raise Exception("operation %(op)s not recognized." % {"op": operation})
            return {}

Note

You are reading an old version of the Evennia documentation. The latest version is here

.