Fix/refactor initial_setup function. Resolve #2438.

This commit is contained in:
Griatch 2021-08-06 00:16:12 +02:00
parent 2ba0f42628
commit 792c3db230
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
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

View file

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

View file

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

View file

@ -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