#!/usr/bin/env python """ This runner is controlled by evennia.py and should normally not be launched directly. It manages the two main Evennia processes (Server and Portal) and most importanly runs a passive, threaded loop that makes sure to restart Server whenever it shuts down. Since twistd does not allow for returning an optional exit code we need to handle the current reload state for server and portal with flag-files instead. The files, one each for server and portal either contains True or False indicating if the process should be restarted upon returning, or not. A process returning != 0 will always stop, no matter the value of this file. """ import os import sys from optparse import OptionParser from subprocess import Popen, call import Queue, thread, subprocess # # System Configuration # SERVER_PIDFILE = "server.pid" PORTAL_PIDFILE = "portal.pid" SERVER_RESTART = "server.restart" PORTAL_RESTART = "portal.restart" # Set the Python path up so we can get to settings.py from here. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' # i18n from django.utils.translation import ugettext as _ if not os.path.exists('settings.py'): print _("No settings.py file found. Run evennia.py to create it.") sys.exit() # Get the settings from django.conf import settings # Setup access of the evennia server itself SERVER_PY_FILE = os.path.join(settings.SRC_DIR, 'server/server.py') PORTAL_PY_FILE = os.path.join(settings.SRC_DIR, 'server/portal.py') # Get logfile names SERVER_LOGFILE = settings.SERVER_LOG_FILE PORTAL_LOGFILE = settings.PORTAL_LOG_FILE # Add this to the environmental variable for the 'twistd' command. currpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if 'PYTHONPATH' in os.environ: os.environ['PYTHONPATH'] += (":%s" % currpath) else: os.environ['PYTHONPATH'] = currpath TWISTED_BINARY = 'twistd' if os.name == 'nt': TWISTED_BINARY = 'twistd.bat' err = False try: import win32api # Test for for win32api except ImportError: err = True if not os.path.exists(TWISTED_BINARY): err = True if err: print _("Twisted binary for Windows is not ready to use. Please run evennia.py.") sys.exit() # Functions def set_restart_mode(restart_file, flag=True): """ This sets a flag file for the restart mode. """ f = open(restart_file, 'w') f.write(str(flag)) f.close() def get_restart_mode(restart_file): """ Parse the server/portal restart status """ if os.path.exists(restart_file): flag = open(restart_file, 'r').read() return flag == "True" return False def get_pid(pidfile): """ Get the PID (Process ID) by trying to access an PID file. """ pid = None if os.path.exists(pidfile): f = open(pidfile, 'r') pid = f.read() return pid def cycle_logfile(logfile): """ Move the old log files to .old """ logfile_old = logfile + '.old' if os.path.exists(logfile): # Cycle the old logfiles to *.old if os.path.exists(logfile_old): # E.g. Windows don't support rename-replace os.remove(logfile_old) os.rename(logfile, logfile_old) logfile = settings.HTTP_LOG_FILE.strip() logfile_old = logfile + '.old' if os.path.exists(logfile): # Cycle the old logfiles to *.old if os.path.exists(logfile_old): # E.g. Windows don't support rename-replace os.remove(logfile_old) os.rename(logfile, logfile_old) # Start program management SERVER = None PORTAL = None def start_services(server_argv, portal_argv): """ This calls a threaded loop that launces the Portal and Server and then restarts them when they finish. """ global SERVER, PORTAL processes = Queue.Queue() def server_waiter(queue): try: rc = Popen(server_argv).wait() except Exception, e: print _("Server process error: %(e)s") % {'e': e} queue.put(("server_stopped", rc)) # this signals the controller that the program finished def portal_waiter(queue): try: rc = Popen(portal_argv).wait() except Exception, e: print _("Portal process error: %(e)s") % {'e': e} queue.put(("portal_stopped", rc)) # this signals the controller that the program finished if server_argv: # start server as a reloadable thread SERVER = thread.start_new_thread(server_waiter, (processes, )) if portal_argv: if get_restart_mode(PORTAL_RESTART): # start portal as interactive, reloadable thread PORTAL = thread.start_new_thread(portal_waiter, (processes, )) else: # normal operation: start portal as a daemon; we don't care to monitor it for restart PORTAL = Popen(portal_argv) if not SERVER: # if portal is daemon and no server is running, we have no reason to continue to the loop. return # Reload loop while True: # this blocks until something is actually returned. message, rc = processes.get() # restart only if process stopped cleanly if message == "server_stopped" and int(rc) == 0 and get_restart_mode(SERVER_RESTART): print _("Evennia Server stopped. Restarting ...") SERVER = thread.start_new_thread(server_waiter, (processes, )) continue # normally the portal is not reloaded since it's run as a daemon. if message == "portal_stopped" and int(rc) == 0 and get_restart_mode(PORTAL_RESTART): print _("Evennia Portal stopped in interactive mode. Restarting ...") PORTAL = thread.start_new_thread(portal_waiter, (processes, )) continue break # Setup signal handling def main(): """ This handles the command line input of the runner (it's most often called by evennia.py) """ parser = OptionParser(usage="%prog [options] start", description=_("This runner should normally *not* be called directly - it is called automatically from the evennia.py main program. It manages the Evennia game server and portal processes an hosts a threaded loop to restart the Server whenever it is stopped (this constitues Evennia's reload mechanism).")) parser.add_option('-s', '--noserver', action='store_true', dest='noserver', default=False, help=_('Do not start Server process')) parser.add_option('-p', '--noportal', action='store_true', dest='noportal', default=False, help=_('Do not start Portal process')) parser.add_option('-i', '--iserver', action='store_true', dest='iserver', default=False, help=_('output server log to stdout instead of logfile')) parser.add_option('-d', '--iportal', action='store_true', dest='iportal', default=False, help=_('output portal log to stdout. Does not make portal a daemon.')) parser.add_option('-S', '--profile-server', action='store_true', dest='sprof', default=False, help='run server under cProfile') parser.add_option('-P', '--profile-portal', action='store_true', dest='pprof', default=False, help='run portal under cProfile') options, args = parser.parse_args() if not args or args[0] != 'start': # this is so as to not be accidentally launched. parser.print_help() sys.exit() # set up default project calls server_argv = [TWISTED_BINARY, '--nodaemon', '--logfile=%s' % SERVER_LOGFILE, '--pidfile=%s' % SERVER_PIDFILE, '--python=%s' % SERVER_PY_FILE] portal_argv = [TWISTED_BINARY, '--logfile=%s' % PORTAL_LOGFILE, '--pidfile=%s' % PORTAL_PIDFILE, '--python=%s' % PORTAL_PY_FILE] # Profiling settings (read file from python shell e.g with # p = pstats.Stats('server.prof') sprof_argv = ['--savestats', '--profiler=cprofile', '--profile=server.prof'] pprof_argv = ['--savestats', '--profiler=cprofile', '--profile=portal.prof'] # Server pid = get_pid(SERVER_PIDFILE) if pid and not options.noserver: print _("\nEvennia Server is already running as process %(pid)s. Not restarted.") % {'pid': pid} options.noserver = True if options.noserver: server_argv = None else: set_restart_mode(SERVER_RESTART, True) if options.iserver: # don't log to server logfile del server_argv[2] print _("\nStarting Evennia Server (output to stdout).") else: print _("\nStarting Evennia Server (output to server logfile).") if options.sprof: server_argv.extend(sprof_argv) print "\nRunning Evennia Server under cProfile." cycle_logfile(SERVER_LOGFILE) # Portal pid = get_pid(PORTAL_PIDFILE) if pid and not options.noportal: print _("\nEvennia Portal is already running as process %(pid)s. Not restarted.") % {'pid': pid} options.noportal = True if options.noportal: portal_argv = None else: if options.iportal: # make portal interactive portal_argv[1] = '--nodaemon' PORTAL_INTERACTIVE = True set_restart_mode(PORTAL_RESTART, True) print _("\nStarting Evennia Portal in non-Daemon mode (output to stdout).") else: set_restart_mode(PORTAL_RESTART, False) print _("\nStarting Evennia Portal in Daemon mode (output to portal logfile).") if options.pprof: server_argv.extend(pprof_argv) print "\nRunning Evennia Portal under cProfile." cycle_logfile(PORTAL_LOGFILE) # Windows fixes (Windows don't support pidfiles natively) if os.name == 'nt': if server_argv: del server_argv[-2] if portal_argv: del portal_argv[-2] # Start processes start_services(server_argv, portal_argv) if __name__ == '__main__': from src.utils.utils import check_evennia_dependencies if check_evennia_dependencies(): main()