#!/usr/bin/env python
"""

This runner is controlled by the evennia launcher 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 argparse import ArgumentParser
from subprocess import Popen
import Queue, thread

try:
    # check if launched with pypy
    import __pypy__ as is_pypy
except ImportError:
    is_pypy = False

SERVER = None
PORTAL = None

EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
EVENNIA_BIN = os.path.join(EVENNIA_ROOT, "bin")
EVENNIA_LIB = os.path.join(EVENNIA_ROOT, "evennia")

SERVER_PY_FILE = os.path.join(EVENNIA_LIB,'server', 'server.py')
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, 'server', 'portal', 'portal.py')

GAMEDIR = None
SERVERDIR = "server"
SERVER_PIDFILE = None
PORTAL_PIDFILE = None
SERVER_RESTART = None
PORTAL_RESTART = None
SERVER_LOGFILE = None
PORTAL_LOGFILE = None
HTTP_LOGFILE = None

# messages

CMDLINE_HELP = \
    """
    This program manages the running Evennia processes. It is called
    by evennia and should not be started manually. Its main task is to
    sit and watch the Server and restart it whenever the user reloads.
    The runner depends on four files for its operation, two PID files
    and two RESTART files for Server and Portal respectively; these
    are stored in the game's server/ directory.
    """

PROCESS_ERROR = \
    """
    {component} process error: {traceback}.
    """

PROCESS_IOERROR = \
    """
    {component} IOError: {traceback}
    One possible explanation is that 'twistd' was not found.
    """

PROCESS_RESTART = "{component} restarting ..."


# Functions

def set_restart_mode(restart_file, flag="reload"):
    """
    This sets a flag file for the restart mode.
    """
    with open(restart_file, 'w') as f:
        f.write(str(flag))


def getenv():
    """
    Get current environment and add PYTHONPATH
    """
    sep = ";" if os.name == "nt" else ":"
    env = os.environ.copy()
    env['PYTHONPATH'] = sep.join(sys.path)
    return env


def get_restart_mode(restart_file):
    """
    Parse the server/portal restart status
    """
    if os.path.exists(restart_file):
        with open(restart_file, 'r') as f:
            return f.read()
    return "shutdown"


def get_pid(pidfile):
    """
    Get the PID (Process ID) by trying to access
    an PID file.
    """
    pid = None
    if os.path.exists(pidfile):
        with open(pidfile, 'r') as f:
            pid = f.read()
    return pid


def cycle_logfile(logfile):
    """
    Rotate the old log files to <filename>.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)

# Start program management


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, env=getenv()).wait()
        except Exception, e:
            print PROCESS_ERROR.format(component="Server", traceback=e)
            return
        # this signals the controller that the program finished
        queue.put(("server_stopped", rc))

    def portal_waiter(queue):
        try:
            rc = Popen(portal_argv, env=getenv()).wait()
        except Exception, e:
            print PROCESS_ERROR.format(component="Portal", traceback=e)
            return
        # this signals the controller that the program finished
        queue.put(("portal_stopped", rc))

    if portal_argv:
        try:
            if get_restart_mode(PORTAL_RESTART) == "True":
                # 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, env=getenv())
        except IOError, e:
            print PROCESS_IOERROR.format(component="Portal", traceback=e)
            return

    try:
        if server_argv:
            # start server as a reloadable thread
            SERVER = thread.start_new_thread(server_waiter, (processes, ))
    except IOError, e:
        print PROCESS_IOERROR.format(component="Server", traceback=e)
        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) in ("True", "reload", "reset")):
            print PROCESS_RESTART.format(component="Server")
            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) == "True"):
            print PROCESS_RESTART.format(component="Portal")
            PORTAL = thread.start_new_thread(portal_waiter, (processes, ))
            continue
        break


def main():
    """
    This handles the command line input of the runner, usually created by
    the evennia launcher
    """

    parser = ArgumentParser(description=CMDLINE_HELP)
    parser.add_argument('--noserver', action='store_true', dest='noserver',
                        default=False, help='Do not start Server process')
    parser.add_argument('--noportal', action='store_true', dest='noportal',
                        default=False, help='Do not start Portal process')
    parser.add_argument('--iserver', action='store_true', dest='iserver',
                        default=False, help='Server in interactive mode')
    parser.add_argument('--iportal', action='store_true', dest='iportal',
                        default=False, help='Portal in interactive mode')
    parser.add_argument('--pserver', action='store_true', dest='pserver',
                        default=False, help='Profile Server')
    parser.add_argument('--pportal', action='store_true', dest='pportal',
                        default=False, help='Profile Portal')
    parser.add_argument('--nologcycle', action='store_false', dest='nologcycle',
                        default=True, help='Do not cycle log files')
    parser.add_argument('gamedir', help="path to game dir")
    parser.add_argument('twistdbinary', help="path to twistd binary")
    parser.add_argument('slogfile', help="path to server log file")
    parser.add_argument('plogfile', help="path to portal log file")
    parser.add_argument('hlogfile', help="path to http log file")

    args = parser.parse_args()

    global GAMEDIR
    global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE
    global SERVER_PIDFILE, PORTAL_PIDFILE
    global SERVER_RESTART, PORTAL_RESTART

    GAMEDIR = args.gamedir
    sys.path.insert(1, os.path.join(GAMEDIR, SERVERDIR))

    SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid")
    PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid")
    SERVER_RESTART = os.path.join(GAMEDIR, SERVERDIR, "server.restart")
    PORTAL_RESTART = os.path.join(GAMEDIR, SERVERDIR, "portal.restart")
    SERVER_LOGFILE = args.slogfile
    PORTAL_LOGFILE = args.plogfile
    HTTP_LOGFILE = args.hlogfile
    TWISTED_BINARY = args.twistdbinary

    # 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')
    pserver_argv = ['--savestats',
                    '--profiler=cprofile',
                    '--profile=server.prof']
    pportal_argv = ['--savestats',
                    '--profiler=cprofile',
                    '--profile=portal.prof']

    # Server

    pid = get_pid(SERVER_PIDFILE)
    if pid and not args.noserver:
            print "\nEvennia Server is already running as process %(pid)s. Not restarted." % {'pid': pid}
            args.noserver = True
    if args.noserver:
        server_argv = None
    else:
        set_restart_mode(SERVER_RESTART, "shutdown")
        if args.iserver:
            # don't log to server logfile
            del server_argv[2]
            print "\nStarting Evennia Server (output to stdout)."
        else:
            if not args.nologcycle:
                cycle_logfile(SERVER_LOGFILE)
            print "\nStarting Evennia Server (output to server logfile)."
        if args.pserver:
            server_argv.extend(pserver_argv)
            print "\nRunning Evennia Server under cProfile."

    # Portal

    pid = get_pid(PORTAL_PIDFILE)
    if pid and not args.noportal:
        print "\nEvennia Portal is already running as process %(pid)s. Not restarted." % {'pid': pid}
        args.noportal = True
    if args.noportal:
        portal_argv = None
    else:
        if args.iportal:
            # make portal interactive
            portal_argv[1] = '--nodaemon'
            set_restart_mode(PORTAL_RESTART, True)
            print "\nStarting Evennia Portal in non-Daemon mode (output to stdout)."
        else:
            if not args.nologcycle:
                cycle_logfile(PORTAL_LOGFILE)
                cycle_logfile(HTTP_LOGFILE)
            set_restart_mode(PORTAL_RESTART, False)
            print "\nStarting Evennia Portal in Daemon mode (output to portal logfile)."
        if args.pportal:
            portal_argv.extend(pportal_argv)
            print "\nRunning Evennia Portal under cProfile."

    # 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__':
    main()
