evennia/src/server/server.py

408 lines
17 KiB
Python
Raw Normal View History

"""
This module implements the main Evennia server process, the core of
2012-03-30 23:57:04 +02:00
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
2012-03-30 23:57:04 +02:00
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')
2012-03-30 23:57:04 +02:00
# module containing hook methods
SERVER_HOOK_MODULE = mod_import(settings.AT_SERVER_STARTSTOP_MODULE)
#------------------------------------------------------------
2012-03-30 23:57:04 +02:00
# Evennia Server settings
#------------------------------------------------------------
SERVERNAME = settings.SERVERNAME
VERSION = get_evennia_version()
2012-03-30 23:57:04 +02:00
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
#------------------------------------------------------------
2012-03-30 23:57:04 +02:00
# 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.
2012-03-30 23:57:04 +02:00
"""
def __init__(self, application):
"""
2012-03-30 23:57:04 +02:00
Setup the server.
application - an instantiated Twisted application
2012-03-30 23:57:04 +02:00
"""
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
2012-03-30 23:57:04 +02:00
print '\n' + '-'*50
# Database-specific startup optimizations.
self.sqlite3_prep()
2012-03-30 23:57:04 +02:00
# Run the initial setup if needed
self.run_initial_setup()
self.start_time = time.time()
# initialize channelhandler
channelhandler.CHANNELHANDLER.update()
2012-03-30 23:57:04 +02:00
# Make info output to the terminal.
self.terminal_output()
2012-03-30 23:57:04 +02:00
print '-'*50
2012-03-30 23:57:04 +02:00
# 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()
2012-03-30 23:57:04 +02:00
# Server startup methods
def sqlite3_prep(self):
"""
Optimize some SQLite stuff at startup since we
can't save it to the database.
2012-03-30 23:57:04 +02:00
"""
if ((".".join(str(i) for i in django.VERSION) < "1.2" and settings.DATABASE_ENGINE == "sqlite3")
2012-03-30 23:57:04 +02:00
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:
2012-03-30 23:57:04 +02:00
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
2012-03-30 23:57:04 +02:00
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.
2012-03-30 23:57:04 +02:00
Either way, the active restart setting (Restart=True/False) is
returned so the server knows which more it's in.
2012-03-30 23:57:04 +02:00
"""
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):
"""
2012-03-30 23:57:04 +02:00
Shuts down the server from inside it.
2012-03-30 23:57:04 +02:00
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.
2012-03-30 23:57:04 +02:00
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()]
2012-03-30 23:57:04 +02:00
ServerConfig.objects.conf("server_restart_mode", "reset")
2012-03-30 23:57:04 +02:00
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):
2012-03-30 23:57:04 +02:00
# 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)
2012-03-30 23:57:04 +02:00
#------------------------------------------------------------
#
# Start the Evennia game server and add all active services
#
#------------------------------------------------------------
# Tell the system the server is starting up; some things are not available yet
2012-03-30 23:57:04 +02:00
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')
2012-03-30 23:57:04 +02:00
# 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
2012-03-30 23:57:04 +02:00
# it would be during testing and debugging.
2012-03-30 23:57:04 +02:00
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
2012-03-30 23:57:04 +02:00
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:
2012-03-30 23:57:04 +02:00
# RSS feed channel connections
from src.comms import rss
2012-03-30 23:57:04 +02:00
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()