diff --git a/evennia/server/initial_setup.py b/evennia/server/initial_setup.py index a6b3100ada..0d0b18a70e 100644 --- a/evennia/server/initial_setup.py +++ b/evennia/server/initial_setup.py @@ -1,7 +1,7 @@ """ -This module handles initial database propagation, which is only run the first -time the game starts. It will create some default channels, objects, and -other things. +This module handles initial database propagation, which is only run the first time the game starts. +It will create some default objects (notably give #1 its evennia-specific properties, and create the +Limbo room). It will also hooks, and then perform an initial restart. Everything starts at handle_setup() """ @@ -41,16 +41,22 @@ WARNING_POSTGRESQL_FIX = """ """ -def get_god_account(): +def _get_superuser_account(): """ - Creates the god user and don't take no for an answer. + Get the superuser (created at the command line) and don't take no for an answer. + + Returns: + Account: The first superuser (User #1). + + Raises: + AccountDB.DoesNotExist: If the superuser couldn't be found. """ try: - god_account = AccountDB.objects.get(id=1) + superuser = AccountDB.objects.get(id=1) except AccountDB.DoesNotExist: raise AccountDB.DoesNotExist(ERROR_NO_SUPERUSER) - return god_account + return superuser def create_objects(): @@ -63,84 +69,68 @@ def create_objects(): # Set the initial User's account object's username on the #1 object. # This object is pure django and only holds name, email and password. - god_account = get_god_account() + superuser = _get_superuser_account() + from evennia.objects.models import ObjectDB # Create an Account 'user profile' object to hold eventual # mud-specific settings for the AccountDB object. account_typeclass = settings.BASE_ACCOUNT_TYPECLASS - # run all creation hooks on god_account (we must do so manually + # run all creation hooks on superuser (we must do so manually # since the manage.py command does not) - god_account.swap_typeclass(account_typeclass, clean_attributes=True) - god_account.basetype_setup() - god_account.at_account_creation() - god_account.locks.add( + superuser.swap_typeclass(account_typeclass, clean_attributes=True) + superuser.basetype_setup() + superuser.at_account_creation() + superuser.locks.add( "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all()" ) # this is necessary for quelling to work correctly. - god_account.permissions.add("Developer") + superuser.permissions.add("Developer") # Limbo is the default "nowhere" starting room # Create the in-game god-character for account #1 and set # it to exist in Limbo. character_typeclass = settings.BASE_CHARACTER_TYPECLASS - god_character = create.create_object(character_typeclass, key=god_account.username, nohome=True) + try: + superuser_character = ObjectDB.objects.get(id=1) + except ObjectDB.DoesNotExist: + superuser_character = create.create_object( + character_typeclass, key=superuser.username, nohome=True) - god_character.id = 1 - god_character.save() - god_character.db.desc = _("This is User #1.") - god_character.locks.add( + superuser_character.db_typeclass_path = character_typeclass + superuser_character.db.desc = _("This is User #1.") + superuser_character.locks.add( "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()" ) # we set this low so that quelling is more useful - god_character.permissions.add("Player") + superuser_character.permissions.add("Player") + superuser_character.save() - god_account.attributes.add("_first_login", True) - god_account.attributes.add("_last_puppet", god_character) + superuser.attributes.add("_first_login", True) + superuser.attributes.add("_last_puppet", superuser_character) try: - god_account.db._playable_characters.append(god_character) + superuser.db._playable_characters.append(superuser_character) except AttributeError: - god_account.db_playable_characters = [god_character] + superuser.db_playable_characters = [superuser_character] room_typeclass = settings.BASE_ROOM_TYPECLASS - limbo_obj = create.create_object(room_typeclass, _("Limbo"), nohome=True) - limbo_obj.id = 2 - limbo_obj.save() + try: + limbo_obj = ObjectDB.objects.get(id=2) + except ObjectDB.DoesNotExist: + limbo_obj = create.create_object(room_typeclass, _("Limbo"), nohome=True) + + limbo_obj.db_typeclass_path = room_typeclass limbo_obj.db.desc = LIMBO_DESC.strip() limbo_obj.save() # Now that Limbo exists, try to set the user up in Limbo (unless # the creation hooks already fixed this). - if not god_character.location: - god_character.location = limbo_obj - if not god_character.home: - god_character.home = limbo_obj - - -def create_channels(): - """ - Creates some sensible default channels. - - """ - logger.log_info("Initial setup: Creating default channels ...") - - goduser = get_god_account() - - channel_mudinfo = settings.CHANNEL_MUDINFO - if channel_mudinfo: - channel = create.create_channel(**channel_mudinfo) - channel.connect(goduser) - - channel_connectinfo = settings.CHANNEL_CONNECTINFO - if channel_connectinfo: - channel = create.create_channel(**channel_connectinfo) - - for channeldict in settings.DEFAULT_CHANNELS: - channel = create.create_channel(**channeldict) - channel.connect(goduser) - + if not superuser_character.location: + superuser_character.location = limbo_obj + if not superuser_character.home: + superuser_character.home = limbo_obj def at_initial_setup(): """ @@ -188,49 +178,46 @@ def reset_server(): SESSIONS.portal_reset_server() -def handle_setup(last_step): +def handle_setup(last_step=None): """ Main logic for the module. It allows for restarting the initialization at any point if one of the modules should crash. Args: - last_step (int): The last stored successful step, for starting - over on errors. If `< 0`, initialization has finished and no - steps need to be redone. + last_step (str, None): The last stored successful step, for starting + over on errors. None if starting from scratch. If this is 'done', + the function will exit immediately. """ - - if last_step < 0: + if last_step in('done', -1): # this means we don't need to handle setup since - # it already ran sucessfully once. + # it already ran sucessfully once. -1 is the legacy + # value for existing databases. return - # if None, set it to 0 - last_step = last_step or 0 - # setting up the list of functions to run - setup_queue = [create_objects, create_channels, at_initial_setup, collectstatic, reset_server] + # setup sequence + setup_sequence = { + 'create_objects': create_objects, + 'at_initial_setup': at_initial_setup, + 'collectstatic': collectstatic, + 'done': reset_server, + } - # step through queue, from last completed function - for num, setup_func in enumerate(setup_queue[last_step:]): - # run the setup function. Note that if there is a - # traceback we let it stop the system so the config - # step is not saved. + # determine the sequence so we can skip ahead + steps = list(setup_sequence) + steps = steps[steps.index(last_step) + 1 if last_step is not None else 0:] + # step through queue from last completed function. Once completed, + # the 'done' key should be set. + for stepname in steps: try: - setup_func() + setup_sequence[stepname]() except Exception: - if last_step + num == 1: - from evennia.objects.models import ObjectDB - - for obj in ObjectDB.objects.all(): - obj.delete() - elif last_step + num == 2: - from evennia.comms.models import ChannelDB - - ChannelDB.objects.all().delete() + # we re-raise to make sure to stop startup raise - # save this step - ServerConfig.objects.conf("last_initial_setup_step", last_step + num + 1) - # We got through the entire list. Set last_step to -1 so we don't - # have to run this again. - ServerConfig.objects.conf("last_initial_setup_step", -1) + else: + # save the step + ServerConfig.objects.conf("last_initial_setup_step", stepname) + if stepname == 'done': + # always exit on 'done' + break diff --git a/evennia/server/server.py b/evennia/server/server.py index 1c49aa70bf..9b749f2f63 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -10,6 +10,7 @@ evennia/server/server_runner.py). import time import sys import os +import traceback from twisted.web import static from twisted.application import internet, service @@ -331,25 +332,60 @@ class Evennia: 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 -1. + 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") - 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(0) - elif int(last_initial_setup_step) >= 0: - # a positive value means the setup crashed on one of its - # modules and setup will resume from this step, retrying - # the last failed module. When all are finished, the step - # is set to -1 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(int(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.comms.models import ChannelDB + from evennia.accounts.models import AccountDB + from evennia.utils.create import create_channel + + superuser = AccountDB.objects.get(id=1) + # mudinfo + mudinfo_chan = settings.CHANNEL_MUDINFO + if mudinfo_chan: + if not ChannelDB.objects.filter(db_key=mudinfo_chan["key"]): + channel = create_channel(**mudinfo_chan) + channel.connect(superuser) + # connectinfo + connectinfo_chan = settings.CHANNEL_MUDINFO + if connectinfo_chan: + if not ChannelDB.objects.filter(db_key=mudinfo_chan["key"]): + channel = create_channel(**connectinfo_chan) + # default channels + for chan_info in settings.DEFAULT_CHANNELS: + if not ChannelDB.objects.filter(db_key=chan_info["key"]): + channel = create_channel(**chan_info) + channel.connect(superuser) def run_init_hooks(self, mode): """ @@ -534,28 +570,8 @@ class Evennia: TASK_HANDLER.load() TASK_HANDLER.create_delays() - # check so default channels exist - from evennia.comms.models import ChannelDB - from evennia.accounts.models import AccountDB - from evennia.utils.create import create_channel - - god_account = AccountDB.objects.get(id=1) - # mudinfo - mudinfo_chan = settings.CHANNEL_MUDINFO - if mudinfo_chan: - if not ChannelDB.objects.filter(db_key=mudinfo_chan["key"]): - channel = create_channel(**mudinfo_chan) - channel.connect(god_account) - # connectinfo - connectinfo_chan = settings.CHANNEL_MUDINFO - if connectinfo_chan: - if not ChannelDB.objects.filter(db_key=mudinfo_chan["key"]): - channel = create_channel(**connectinfo_chan) - # default channels - for chan_info in settings.DEFAULT_CHANNELS: - if not ChannelDB.objects.filter(db_key=chan_info["key"]): - channel = create_channel(**chan_info) - channel.connect(god_account) + # create/update channels + self.create_default_channels() # delete the temporary setting ServerConfig.objects.conf("server_restart_mode", delete=True) diff --git a/evennia/server/tests/test_initial_setup.py b/evennia/server/tests/test_initial_setup.py index a0f2cee28b..b79ebd2f9a 100644 --- a/evennia/server/tests/test_initial_setup.py +++ b/evennia/server/tests/test_initial_setup.py @@ -14,5 +14,5 @@ class TestInitialSetup(TestCase): @patch("evennia.server.initial_setup.AccountDB") def test_get_god_account(self, mocked_accountdb): mocked_accountdb.objects.get = MagicMock(return_value=1) - self.assertEqual(initial_setup.get_god_account(), 1) + self.assertEqual(initial_setup._get_superuser_account(), 1) mocked_accountdb.objects.get.assert_called_with(id=1) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 2f4d0ad640..e39dac5972 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -358,8 +358,14 @@ CONNECTION_SCREEN_MODULE = "server.conf.connection_screens" # started when the autoconnects starts sending menu commands. DELAY_CMD_LOGINSTART = 0.3 # A module that must exist - this holds the instructions Evennia will use to -# first prepare the database for use. Generally should not be changed. If this -# cannot be imported, bad things will happen. +# first prepare the database for use (create user #1 and Limbo etc). Only override if +# you really know what # you are doing. If replacing, it must contain a function +# handle_setup(stepname=None). The function will start being called with no argument +# and is expected to maintain a named sequence of steps. Once each step is completed, it +# should be saved with ServerConfig.objects.conf('last_initial_setup_step', stepname) +# on a crash, the system will continue by calling handle_setup with the last completed +# step. The last step in the sequence must be named 'done'. Once this key is saved, +# initialization will not run again. INITIAL_SETUP_MODULE = "evennia.server.initial_setup" # An optional module that, if existing, must hold a function # named at_initial_setup(). This hook method can be used to customize