Note

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

.

# Source code for evennia.server.service

    """
    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 importlib
    import time
    import traceback

    import django
    import evennia
    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.utils import logger
    from evennia.utils.utils import get_evennia_version, make_iter, mod_import
    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

    _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.amp_service = None
            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)
            ]

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

            self.process_idle_timeouts()

            # 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 process_idle_timeouts(self):
            # handle idle timeouts
            if settings.IDLE_TIMEOUT > 0:
                now = time.time()
                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)

        # Server startup methods
        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)
            self.amp_service = internet.TCPClient(settings.AMP_HOST, settings.AMP_PORT, factory)
            self.amp_service.setName("ServerAMPClient")
            self.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())
                if not settings.TEST_ENVIRONMENT or not evennia.SESSION_HANDLER:
                    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, 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")

Note

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

.