Fix/refactor initial_setup function. Resolve #2438.

This commit is contained in:
Griatch 2021-08-06 00:16:12 +02:00
parent d3cc7cf630
commit b9771e701f
4 changed files with 135 additions and 126 deletions

View file

@ -1,7 +1,7 @@
""" """
This module handles initial database propagation, which is only run the first This module handles initial database propagation, which is only run the first time the game starts.
time the game starts. It will create some default channels, objects, and It will create some default objects (notably give #1 its evennia-specific properties, and create the
other things. Limbo room). It will also hooks, and then perform an initial restart.
Everything starts at handle_setup() 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: try:
god_account = AccountDB.objects.get(id=1) superuser = AccountDB.objects.get(id=1)
except AccountDB.DoesNotExist: except AccountDB.DoesNotExist:
raise AccountDB.DoesNotExist(ERROR_NO_SUPERUSER) raise AccountDB.DoesNotExist(ERROR_NO_SUPERUSER)
return god_account return superuser
def create_objects(): def create_objects():
@ -63,84 +69,68 @@ def create_objects():
# Set the initial User's account object's username on the #1 object. # 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. # 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 # Create an Account 'user profile' object to hold eventual
# mud-specific settings for the AccountDB object. # mud-specific settings for the AccountDB object.
account_typeclass = settings.BASE_ACCOUNT_TYPECLASS 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) # since the manage.py command does not)
god_account.swap_typeclass(account_typeclass, clean_attributes=True) superuser.swap_typeclass(account_typeclass, clean_attributes=True)
god_account.basetype_setup() superuser.basetype_setup()
god_account.at_account_creation() superuser.at_account_creation()
god_account.locks.add( superuser.locks.add(
"examine:perm(Developer);edit:false();delete:false();boot:false();msg:all()" "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all()"
) )
# this is necessary for quelling to work correctly. # this is necessary for quelling to work correctly.
god_account.permissions.add("Developer") superuser.permissions.add("Developer")
# Limbo is the default "nowhere" starting room # Limbo is the default "nowhere" starting room
# Create the in-game god-character for account #1 and set # Create the in-game god-character for account #1 and set
# it to exist in Limbo. # it to exist in Limbo.
character_typeclass = settings.BASE_CHARACTER_TYPECLASS 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 superuser_character.db_typeclass_path = character_typeclass
god_character.save() superuser_character.db.desc = _("This is User #1.")
god_character.db.desc = _("This is User #1.") superuser_character.locks.add(
god_character.locks.add(
"examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()" "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()"
) )
# we set this low so that quelling is more useful # 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) superuser.attributes.add("_first_login", True)
god_account.attributes.add("_last_puppet", god_character) superuser.attributes.add("_last_puppet", superuser_character)
try: try:
god_account.db._playable_characters.append(god_character) superuser.db._playable_characters.append(superuser_character)
except AttributeError: except AttributeError:
god_account.db_playable_characters = [god_character] superuser.db_playable_characters = [superuser_character]
room_typeclass = settings.BASE_ROOM_TYPECLASS room_typeclass = settings.BASE_ROOM_TYPECLASS
limbo_obj = create.create_object(room_typeclass, _("Limbo"), nohome=True) try:
limbo_obj.id = 2 limbo_obj = ObjectDB.objects.get(id=2)
limbo_obj.save() 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.db.desc = LIMBO_DESC.strip()
limbo_obj.save() limbo_obj.save()
# Now that Limbo exists, try to set the user up in Limbo (unless # Now that Limbo exists, try to set the user up in Limbo (unless
# the creation hooks already fixed this). # the creation hooks already fixed this).
if not god_character.location: if not superuser_character.location:
god_character.location = limbo_obj superuser_character.location = limbo_obj
if not god_character.home: if not superuser_character.home:
god_character.home = limbo_obj superuser_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)
def at_initial_setup(): def at_initial_setup():
""" """
@ -188,49 +178,46 @@ def reset_server():
SESSIONS.portal_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 Main logic for the module. It allows for restarting the
initialization at any point if one of the modules should crash. initialization at any point if one of the modules should crash.
Args: Args:
last_step (int): The last stored successful step, for starting last_step (str, None): The last stored successful step, for starting
over on errors. If `< 0`, initialization has finished and no over on errors. None if starting from scratch. If this is 'done',
steps need to be redone. the function will exit immediately.
""" """
if last_step in('done', -1):
if last_step < 0:
# this means we don't need to handle setup since # 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 return
# if None, set it to 0
last_step = last_step or 0
# setting up the list of functions to run # setup sequence
setup_queue = [create_objects, create_channels, at_initial_setup, collectstatic, reset_server] setup_sequence = {
'create_objects': create_objects,
'at_initial_setup': at_initial_setup,
'collectstatic': collectstatic,
'done': reset_server,
}
# step through queue, from last completed function # determine the sequence so we can skip ahead
for num, setup_func in enumerate(setup_queue[last_step:]): steps = list(setup_sequence)
# run the setup function. Note that if there is a steps = steps[steps.index(last_step) + 1 if last_step is not None else 0:]
# traceback we let it stop the system so the config
# step is not saved.
# step through queue from last completed function. Once completed,
# the 'done' key should be set.
for stepname in steps:
try: try:
setup_func() setup_sequence[stepname]()
except Exception: except Exception:
if last_step + num == 1: # we re-raise to make sure to stop startup
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()
raise raise
# save this step else:
ServerConfig.objects.conf("last_initial_setup_step", last_step + num + 1) # save the step
# We got through the entire list. Set last_step to -1 so we don't ServerConfig.objects.conf("last_initial_setup_step", stepname)
# have to run this again. if stepname == 'done':
ServerConfig.objects.conf("last_initial_setup_step", -1) # always exit on 'done'
break

View file

@ -10,6 +10,7 @@ evennia/server/server_runner.py).
import time import time
import sys import sys
import os import os
import traceback
from twisted.web import static from twisted.web import static
from twisted.application import internet, service from twisted.application import internet, service
@ -331,25 +332,60 @@ class Evennia:
to the portal has been established. to the portal has been established.
This attempts to run the initial_setup script of the server. This attempts to run the initial_setup script of the server.
It returns if this is not the first time the server starts. 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 global INFO_DICT
initial_setup = importlib.import_module(settings.INITIAL_SETUP_MODULE) initial_setup = importlib.import_module(settings.INITIAL_SETUP_MODULE)
last_initial_setup_step = ServerConfig.objects.conf("last_initial_setup_step") last_initial_setup_step = ServerConfig.objects.conf("last_initial_setup_step")
if not last_initial_setup_step: try:
# None is only returned if the config does not exist, if not last_initial_setup_step:
# i.e. this is an empty DB that needs populating. # None is only returned if the config does not exist,
INFO_DICT["info"] = " Server started for the first time. Setting defaults." # i.e. this is an empty DB that needs populating.
initial_setup.handle_setup(0) INFO_DICT["info"] = " Server started for the first time. Setting defaults."
elif int(last_initial_setup_step) >= 0: initial_setup.handle_setup()
# a positive value means the setup crashed on one of its elif last_initial_setup_step not in ('done', -1):
# modules and setup will resume from this step, retrying # last step crashed, so we weill resume from this step.
# the last failed module. When all are finished, the step # modules and setup will resume from this step, retrying
# is set to -1 to show it does not need to be run again. # the last failed module. When all are finished, the step
INFO_DICT["info"] = " Resuming initial setup from step {last}.".format( # is set to 'done' to show it does not need to be run again.
last=last_initial_setup_step INFO_DICT["info"] = " Resuming initial setup from step '{last}'.".format(
) last=last_initial_setup_step
initial_setup.handle_setup(int(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): def run_init_hooks(self, mode):
""" """
@ -534,28 +570,8 @@ class Evennia:
TASK_HANDLER.load() TASK_HANDLER.load()
TASK_HANDLER.create_delays() TASK_HANDLER.create_delays()
# check so default channels exist # create/update channels
from evennia.comms.models import ChannelDB self.create_default_channels()
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)
# delete the temporary setting # delete the temporary setting
ServerConfig.objects.conf("server_restart_mode", delete=True) ServerConfig.objects.conf("server_restart_mode", delete=True)

View file

@ -14,5 +14,5 @@ class TestInitialSetup(TestCase):
@patch("evennia.server.initial_setup.AccountDB") @patch("evennia.server.initial_setup.AccountDB")
def test_get_god_account(self, mocked_accountdb): def test_get_god_account(self, mocked_accountdb):
mocked_accountdb.objects.get = MagicMock(return_value=1) 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) mocked_accountdb.objects.get.assert_called_with(id=1)

View file

@ -358,8 +358,14 @@ CONNECTION_SCREEN_MODULE = "server.conf.connection_screens"
# started when the autoconnects starts sending menu commands. # started when the autoconnects starts sending menu commands.
DELAY_CMD_LOGINSTART = 0.3 DELAY_CMD_LOGINSTART = 0.3
# A module that must exist - this holds the instructions Evennia will use to # 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 # first prepare the database for use (create user #1 and Limbo etc). Only override if
# cannot be imported, bad things will happen. # 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" INITIAL_SETUP_MODULE = "evennia.server.initial_setup"
# An optional module that, if existing, must hold a function # An optional module that, if existing, must hold a function
# named at_initial_setup(). This hook method can be used to customize # named at_initial_setup(). This hook method can be used to customize