mirror of
https://github.com/evennia/evennia.git
synced 2026-03-22 07:46:30 +01:00
This allows e.g. utils.utils.run_async to offload long-running functions to a completely different subprocess entirely, offering real parallelism. Implementation is still experimental, notably not all objects can be transferred safely across the wire; also there is no concept of updating caches yet - so adding an object from the subprocess side will not be known in the main thread yet (since caches cannot yet tell the underlying database has changed).
407 lines
17 KiB
Python
407 lines
17 KiB
Python
"""
|
|
This module implements the main Evennia server process, the core of
|
|
the game engine.
|
|
|
|
This module should be started with the 'twistd' executable since it
|
|
sets up all the networking features. (this is done automatically
|
|
by game/evennia.py).
|
|
|
|
"""
|
|
import time
|
|
import sys
|
|
import os
|
|
if os.name == 'nt':
|
|
# For Windows batchfile we need an extra path insertion here.
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
|
os.path.dirname(os.path.abspath(__file__)))))
|
|
|
|
from twisted.application import internet, service
|
|
from twisted.internet import reactor, defer
|
|
import django
|
|
from django.db import connection
|
|
from django.conf import settings
|
|
|
|
from src.scripts.models import ScriptDB
|
|
from src.server.models import ServerConfig
|
|
from src.server import initial_setup
|
|
|
|
from src.utils.utils import get_evennia_version, mod_import
|
|
from src.comms import channelhandler
|
|
from src.server.sessionhandler import SESSIONS
|
|
|
|
if os.name == 'nt':
|
|
# For Windows we need to handle pid files manually.
|
|
SERVER_PIDFILE = os.path.join(settings.GAME_DIR, 'server.pid')
|
|
|
|
# a file with a flag telling the server to restart after shutdown or not.
|
|
SERVER_RESTART = os.path.join(settings.GAME_DIR, 'server.restart')
|
|
|
|
# module containing hook methods
|
|
SERVER_HOOK_MODULE = mod_import(settings.AT_SERVER_STARTSTOP_MODULE)
|
|
|
|
#------------------------------------------------------------
|
|
# Evennia Server settings
|
|
#------------------------------------------------------------
|
|
|
|
SERVERNAME = settings.SERVERNAME
|
|
VERSION = get_evennia_version()
|
|
|
|
AMP_ENABLED = True
|
|
AMP_HOST = settings.AMP_HOST
|
|
AMP_PORT = settings.AMP_PORT
|
|
|
|
PROCPOOL_ENABLED = settings.PROCPOOL_ENABLED
|
|
PROCPOOL_DEBUG = settings.PROCPOOL_DEBUG
|
|
PROCPOOL_MIN_NPROC = settings.PROCPOOL_MIN_NPROC
|
|
PROCPOOL_MAX_NPROC = settings.PROCPOOL_MAX_NPROC
|
|
PROCPOOL_TIMEOUT = settings.PROCPOOL_TIMEOUT
|
|
PROCPOOL_IDLETIME = settings.PROCPOOL_IDLETIME
|
|
PROCPOOL_HOST = settings.PROCPOOL_HOST
|
|
PROCPOOL_PORT = settings.PROCPOOL_PORT
|
|
PROCPOOL_INTERFACE = settings.PROCPOOL_INTERFACE
|
|
PROCPOOL_UID = settings.PROCPOOL_UID
|
|
PROCPOOL_GID = settings.PROCPOOL_GID
|
|
PROCPOOL_DIRECTORY = settings.PROCPOOL_DIRECTORY
|
|
|
|
# server-channel mappings
|
|
IMC2_ENABLED = settings.IMC2_ENABLED
|
|
IRC_ENABLED = settings.IRC_ENABLED
|
|
RSS_ENABLED = settings.RSS_ENABLED
|
|
|
|
|
|
#------------------------------------------------------------
|
|
# Evennia Main Server object
|
|
#------------------------------------------------------------
|
|
class Evennia(object):
|
|
|
|
"""
|
|
The main Evennia server handler. This object sets up the database and
|
|
tracks and interlinks all the twisted network services that make up
|
|
evennia.
|
|
"""
|
|
|
|
def __init__(self, application):
|
|
"""
|
|
Setup the server.
|
|
|
|
application - an instantiated Twisted application
|
|
|
|
"""
|
|
sys.path.append('.')
|
|
|
|
# create a store of services
|
|
self.services = service.IServiceCollection(application)
|
|
self.amp_protocol = None # set by amp factory
|
|
self.sessions = SESSIONS
|
|
self.sessions.server = self
|
|
|
|
print '\n' + '-'*50
|
|
|
|
# Database-specific startup optimizations.
|
|
self.sqlite3_prep()
|
|
|
|
# Run the initial setup if needed
|
|
self.run_initial_setup()
|
|
|
|
self.start_time = time.time()
|
|
|
|
# initialize channelhandler
|
|
channelhandler.CHANNELHANDLER.update()
|
|
|
|
# Make info output to the terminal.
|
|
self.terminal_output()
|
|
|
|
print '-'*50
|
|
|
|
# set a callback if the server is killed abruptly,
|
|
# by Ctrl-C, reboot etc.
|
|
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True)
|
|
|
|
self.game_running = True
|
|
|
|
self.run_init_hooks()
|
|
|
|
# Server startup methods
|
|
|
|
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.DATABASE_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_DEFAULT", "CMDSET_OOC", "BASE_PLAYER_TYPECLASS", "BASE_OBJECT_TYPECLASS",
|
|
"BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS", "BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS")
|
|
# get previous and current settings so they can be compared
|
|
settings_compare = zip([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 src.objects.models import ObjectDB
|
|
from src.players.models import PlayerDB
|
|
for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches):
|
|
# update the database
|
|
print " one or more default cmdset/typeclass settings changed. Updating defaults stored in database ..."
|
|
if i == 0: [obj.__setattr__("cmdset_storage", curr) for obj in ObjectDB.objects.filter(db_cmdset_storage__exact=prev)]
|
|
if i == 1: [ply.__setattr__("cmdset_storage", curr) for ply in PlayerDB.objects.filter(db_cmdset_storage__exact=prev)]
|
|
if i == 2: [ply.__setattr__("typeclass_path", curr) for ply in PlayerDB.objects.filter(db_typeclass_path__exact=prev)]
|
|
if i in (3,4,5,6): [obj.__setattr__("typeclass_path",curr)
|
|
for obj in ObjectDB.objects.filter(db_typeclass_path__exact=prev)]
|
|
if i == 7: [scr.__setattr__("typeclass_path", curr) for scr in ScriptDB.objects.filter(db_typeclass_path__exact=prev)]
|
|
# store the new default and clean caches
|
|
ServerConfig.objects.conf(settings_names[i], curr)
|
|
ObjectDB.flush_instance_cache()
|
|
PlayerDB.flush_instance_cache()
|
|
ScriptDB.flush_instance_cache()
|
|
# if this is the first start we might not have a "previous" setup saved. Store it now.
|
|
[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 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.
|
|
"""
|
|
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.
|
|
print ' Server started for the first time. Setting defaults.'
|
|
initial_setup.handle_setup(0)
|
|
print '-'*50
|
|
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.
|
|
print ' Resuming initial setup from step %(last)s.' % \
|
|
{'last': last_initial_setup_step}
|
|
initial_setup.handle_setup(int(last_initial_setup_step))
|
|
print '-'*50
|
|
|
|
def run_init_hooks(self):
|
|
"""
|
|
Called every server start
|
|
"""
|
|
from src.objects.models import ObjectDB
|
|
from src.players.models import PlayerDB
|
|
|
|
#update eventual changed defaults
|
|
self.update_defaults()
|
|
|
|
#print "run_init_hooks:", ObjectDB.get_all_cached_instances()
|
|
[(o.typeclass, o.at_init()) for o in ObjectDB.get_all_cached_instances()]
|
|
[(p.typeclass, p.at_init()) for p in PlayerDB.get_all_cached_instances()]
|
|
|
|
# call server hook.
|
|
if SERVER_HOOK_MODULE:
|
|
SERVER_HOOK_MODULE.at_server_start()
|
|
|
|
def terminal_output(self):
|
|
"""
|
|
Outputs server startup info to the terminal.
|
|
"""
|
|
print ' %(servername)s Server (%(version)s) started.' % {'servername': SERVERNAME, 'version': VERSION}
|
|
print ' amp (to Portal): %s' % AMP_PORT
|
|
if PROCPOOL_ENABLED:
|
|
print ' amp (Process Pool): %s' % PROCPOOL_PORT
|
|
|
|
|
|
def set_restart_mode(self, mode=None):
|
|
"""
|
|
This manages the flag file that tells the runner if the server is
|
|
reloading, resetting or shutting down. Valid modes are
|
|
'reload', 'reset', 'shutdown' and None.
|
|
If mode is None, no change will be done to the flag file.
|
|
|
|
Either way, the active restart setting (Restart=True/False) is
|
|
returned so the server knows which more it's in.
|
|
"""
|
|
if mode == None:
|
|
f = open(SERVER_RESTART, 'r')
|
|
if os.path.exists(SERVER_RESTART) and 'True' == f.read():
|
|
mode = 'reload'
|
|
else:
|
|
mode = 'shutdown'
|
|
f.close()
|
|
else:
|
|
restart = mode in ('reload', 'reset')
|
|
f = open(SERVER_RESTART, 'w')
|
|
f.write(str(restart))
|
|
f.close()
|
|
return mode
|
|
|
|
@defer.inlineCallbacks
|
|
def shutdown(self, mode=None, _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.
|
|
'shutdown' - like reset, but server will not auto-restart.
|
|
None - keep currently set flag from flag file.
|
|
_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)
|
|
|
|
mode = self.set_restart_mode(mode)
|
|
# call shutdown hooks on all cached objects
|
|
|
|
from src.objects.models import ObjectDB
|
|
from src.players.models import PlayerDB
|
|
from src.server.models import ServerConfig
|
|
|
|
if mode == 'reload':
|
|
# call restart hooks
|
|
yield [(o.typeclass, o.at_server_reload()) for o in ObjectDB.get_all_cached_instances()]
|
|
yield [(p.typeclass, p.at_server_reload()) for p in PlayerDB.get_all_cached_instances()]
|
|
yield [(s.typeclass, s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()]
|
|
yield self.sessions.all_sessions_portal_sync()
|
|
ServerConfig.objects.conf("server_restart_mode", "reload")
|
|
else:
|
|
if mode == 'reset':
|
|
# don't call disconnect hooks on reset
|
|
yield [(o.typeclass, o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
|
|
else: # shutdown
|
|
yield [(o.typeclass, o.at_disconnect(), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
|
|
|
|
yield [(p.typeclass, p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()]
|
|
yield [(s.typeclass, s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()]
|
|
|
|
ServerConfig.objects.conf("server_restart_mode", "reset")
|
|
|
|
if SERVER_HOOK_MODULE:
|
|
SERVER_HOOK_MODULE.at_server_stop()
|
|
# if _reactor_stopping is true, reactor does not need to be stopped again.
|
|
if os.name == 'nt' and os.path.exists(SERVER_PIDFILE):
|
|
# for Windows we need to remove pid files manually
|
|
os.remove(SERVER_PIDFILE)
|
|
if not _reactor_stopping:
|
|
# this will also send a reactor.stop signal, so we set a flag to avoid loops.
|
|
self.shutdown_complete = True
|
|
reactor.callLater(0, reactor.stop)
|
|
|
|
#------------------------------------------------------------
|
|
#
|
|
# Start the Evennia game server and add all active services
|
|
#
|
|
#------------------------------------------------------------
|
|
|
|
# Tell the system the server is starting up; some things are not available yet
|
|
ServerConfig.objects.conf("server_starting_mode", True)
|
|
|
|
# twistd requires us to define the variable 'application' so it knows
|
|
# what to execute from.
|
|
application = service.Application('Evennia')
|
|
|
|
# The main evennia server program. This sets up the database
|
|
# and is where we store all the other services.
|
|
EVENNIA = Evennia(application)
|
|
|
|
# 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.
|
|
|
|
if AMP_ENABLED:
|
|
|
|
from src.server import amp
|
|
|
|
factory = amp.AmpServerFactory(EVENNIA)
|
|
amp_service = internet.TCPServer(AMP_PORT, factory)
|
|
amp_service.setName("EvenniaPortal")
|
|
EVENNIA.services.addService(amp_service)
|
|
|
|
# The ampoule twisted extension manages asynchronous process pools
|
|
# via an AMP port. It can be used to offload expensive operations
|
|
# to another process asynchronously.
|
|
|
|
if PROCPOOL_ENABLED:
|
|
|
|
from src.utils.ampoule import main as ampoule_main
|
|
from src.utils.ampoule import service as ampoule_service
|
|
from src.utils.ampoule import pool as ampoule_pool
|
|
from src.utils.ampoule.main import BOOTSTRAP as _BOOTSTRAP
|
|
from src.server.procpool import ProcPoolChild
|
|
|
|
# for some reason absolute paths don't work here, only relative ones.
|
|
apackages = ("twisted",
|
|
os.path.join(os.pardir, "src", "utils", "ampoule"),
|
|
os.path.join(os.pardir, "ev"),
|
|
os.path.join(os.pardir))
|
|
aenv = {"DJANGO_SETTINGS_MODULE":"settings",
|
|
"DATABASE_NAME":settings.DATABASES.get("default", {}).get("NAME") or settings.DATABASE_NAME}
|
|
if PROCPOOL_DEBUG:
|
|
_BOOTSTRAP = _BOOTSTRAP % "log.startLogging(sys.stderr)"
|
|
else:
|
|
_BOOTSTRAP = _BOOTSTRAP % ""
|
|
|
|
procpool_starter = ampoule_main.ProcessStarter(packages=apackages,
|
|
env=aenv,
|
|
path=PROCPOOL_DIRECTORY,
|
|
uid=PROCPOOL_UID,
|
|
gid=PROCPOOL_GID,
|
|
bootstrap=_BOOTSTRAP,
|
|
childReactor=os.name == 'nt' and "select" or "epoll")
|
|
procpool = ampoule_pool.ProcessPool(name="ProcPool",
|
|
min=PROCPOOL_MIN_NPROC,
|
|
max=PROCPOOL_MAX_NPROC,
|
|
recycleAfter=500,
|
|
ampChild=ProcPoolChild,
|
|
starter=procpool_starter)
|
|
procpool_service = ampoule_service.AMPouleService(procpool,
|
|
ProcPoolChild,
|
|
PROCPOOL_PORT,
|
|
PROCPOOL_INTERFACE)
|
|
procpool_service.setName("ProcPool")
|
|
EVENNIA.services.addService(procpool_service)
|
|
|
|
if IRC_ENABLED:
|
|
|
|
# IRC channel connections
|
|
|
|
from src.comms import irc
|
|
irc.connect_all()
|
|
|
|
if IMC2_ENABLED:
|
|
|
|
# IMC2 channel connections
|
|
|
|
from src.comms import imc2
|
|
imc2.connect_all()
|
|
|
|
if RSS_ENABLED:
|
|
|
|
# RSS feed channel connections
|
|
from src.comms import rss
|
|
rss.connect_all()
|
|
|
|
# clear server startup mode
|
|
ServerConfig.objects.conf("server_starting_mode", delete=True)
|
|
|
|
if os.name == 'nt':
|
|
# Windows only: Set PID file manually
|
|
f = open(os.path.join(settings.GAME_DIR, 'server.pid'), 'w')
|
|
f.write(str(os.getpid()))
|
|
f.close()
|