"""Run a Quixote application with the server of your choice.
"""
import errno, grp, os, pwd, signal, sys
from optparse import OptionParser
from quixote.publish import Publisher

#### PUBLIC FUNCTIONS ####
def main_part1():
    # The config module sets up database connections, so we can't import it
    # till after we daemonize.
    parser = make_parser()
    opts, args = parser.parse_args()
    errmsg = check_usage(opts, args)
    if errmsg:
        print "ERROR:", errmsg
        parser.print_help()
        sys.exit(1)
    change_user(opts.user, opts.group)
    if opts.stop:
        stop(opts.pidfile)    # Exits program, does not return.
    if opts.daemon:
        daemon(opts.pidfile)
    return opts.scgi

def main_part2(root_directory, config, is_scgi):
    C = config
    if is_scgi:
        access_log = C.scgi.access_log
        error_log = C.scgi.error_log
    else:
        access_log = None   # No access log.
        error_log = None    # Send errors to standard error.

    def create_publisher():  # Inner function.
        return Publisher(root_directory,
            session_manager=C.session.manager,
            display_exceptions=C.display_exceptions,
            access_log=access_log,
            error_log=error_log)

    if is_scgi:
        mp_safe = C.session.store.is_multiprocess_safe
        if C.scgi.max_children > 1 and not mp_safe:
            m = "configured session store is unsafe for multiple SCGI children"
            raise RuntimeError(m)
        scgi(create_publisher, C.scgi.port, C.scgi.script_name, 
            C.scgi.max_children)
    else:
        simple(create_publisher, C.simple.host, C.simple.port, C.htTitle)

#### OPTIONS-PARSING FUNCTIONS ####
def make_parser():
    parser = OptionParser()
    pao = parser.add_option
    pao('--simple', action="store_true", dest="simple", 
        help="Run under the simple server")
    pao('--scgi', action="store_true", dest="scgi", 
        help="Run under the SCGI server")
    pao('--daemon', action="store_true", dest="daemon",
        help="Run as system service (daemon) (requires --pidfile)")
    pao('--stop', action="store_true", dest="stop",
        help="Stop a running daemon (requires --pidfile)")
    pao('--pidfile', action="store", dest="pidfile",
        help="Record daemon PID in this file")
    pao('--user', action="store", dest="user",
        help="Run as this user (name or UID)")
    pao('--group', action="store", dest="group",
        help="Run as this group (name or GID)")
    return parser

def check_usage(opts, args):
    if (not opts.stop) and count_true(opts.scgi, opts.simple) != 1:
        return "must specify --simple or --scgi flag (but not both)"
    if opts.daemon and opts.stop:
        return "can't specify both --daemon and --stop"
    if opts.daemon and not opts.pidfile:
        return "--daemon requires --pidfile"
    if opts.stop and not opts.pidfile:
        return "--stop requires --pidfile"
    if opts.stop and opts.pidfile and not os.path.exists(opts.pidfile):
        return "--pidfile refers to non-existent file"
    return None

def count_true(*values):
    """Return the number of true values."""
    return len(filter(None, values))

#### SERVER FUNCTIONS ####
def scgi(create_publisher, port, script_name, max_children):
    from quixote.server import scgi_server
    scgi_server.run(create_publisher, port=port, 
        script_name=script_name, max_children=max_children)

def simple(create_publisher, host, port, title):
    from quixote.server import simple_server
    tup = title, host or "[ALL INTERFACES]", port
    print "%s listening on http://%s:%s/" % tup
    try:
        simple_server.run(create_publisher, host=host, port=port)
    except KeyboardInterrupt:
        print "Exiting.  (Ctrl-C pressed.)"

#### USER-CHANGING FUNCTIONS ####
def change_user(user=None, group=None):
    # JJ Behrens says we must set the group first, so we do.
    if group is not None:
        if group.isdigit():
            gid = int(group)
        else:
            gid = grp.getgrnam(group)[2]
        os.setegid(gid)
    if user is not None:
        if user.isdigit():
            uid = int(user)
        else:
            uid = pwd.getpwnam(user)[2]
        os.seteuid(uid)

#### DAEMON FUNCTIONS ####
def daemon(pidfile):
    if os.path.exists(pidfile):
        pid = read_pidfile(pidfile)
        if process_exists(pid):
            msg = "daemon already running (PID %s, file %s)" % (pid, pidfile)
            sys.exit(msg)
    daemonize()
    write_pidfile(pidfile)

def stop(pidfile):
    pid = read_pidfile(pidfile)
    try:
        os.kill(pid, signal.SIGTERM)
    except OSError, e:
        if e.errno == errno.ESRCH: # No such process.
            sys.exit(1)   
        else:
            raise
    # Signal delivered, delete pidfile and exit program.
    os.remove(pidfile)
    sys.exit(0)     

#### DAEMON UTILITY FUNCTIONS ####
def read_pidfile(pidfile):
    f = open(pidfile, 'r')
    pid = f.read()
    f.close()
    try:
        pid = int(pid)
    except ValueError:
        msg = "corrupt PID file %s (content not numeric)" % pidfile
        raise RuntimeError(message)
    return pid
        
def write_pidfile(pidfile):
    pid = os.getpid()
    f = open(pidfile, 'w')
    print >>f, pid
    f.close()

def process_exists(pid):
    try:
        os.kill(pid, 0)
    except OSError, e:
        if e.errno == errno.ESRCH:
            return False
        else:
            raise
    return True

def daemonize():
    """Detach a process from the controlling terminal and run it in the
       background as a daemon.  A Python Cookbook recipe by Chad J Schroeder.
       http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731
       Original documentation follows.

       Disk And Execution MONitor (Daemon)
   
       Default daemon behaviors (they can be modified):
          1.) Ignore SIGHUP signals.
          2.) Default current working directory to the "/" directory.
       #  3.) Set the current file creation mode mask to 0.
          4.) Close all open files (0 to [SC_OPEN_MAX or 256]).
          5.) Redirect standard I/O streams to "/dev/null".
   
       [# indicates behaviors disabled in this version.]

       Failed fork() calls will return a tuple: (errno, strerror).  This
       behavior can be modified to meet your program's needs.
   
       Resources:
          Advanced Programming in the Unix Environment: W. Richard Stevens
          Unix Network Programming (Volume 1): W. Richard Stevens
          http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
    """
    try:
       # Fork a child process so the parent can exit.  This will return control
       # to the command line or shell.  This is required so that the new process
       # is guaranteed not to be a process group leader.  We have this guarantee
       # because the process GID of the parent is inherited by the child, but
       # the child gets a new PID, making it impossible for its PID to equal its
       # PGID.
       pid = os.fork()
    except OSError, e:
       return((e.errno, e.strerror))     # ERROR (return a tuple)
 
    if (pid == 0):       # The first child.
 
       # Next we call os.setsid() to become the session leader of this new
       # session.  The process also becomes the process group leader of the
       # new process group.  Since a controlling terminal is associated with a
       # session, and this new session has not yet acquired a controlling
       # terminal our process now has no controlling terminal.  This shouldn't
       # fail, since we're guaranteed that the child is not a process group
       # leader.
       os.setsid()
 
       # When the first child terminates, all processes in the second child
       # are sent a SIGHUP, so it's ignored.
       signal.signal(signal.SIGHUP, signal.SIG_IGN)
 
       try:
          # Fork a second child to prevent zombies.  Since the first child is
          # a session leader without a controlling terminal, it's possible for
          # it to acquire one by opening a terminal in the future.  This second
          # fork guarantees that the child is no longer a session leader, thus
          # preventing the daemon from ever acquiring a controlling terminal.
          pid = os.fork()        # Fork a second child.
       except OSError, e:
          return((e.errno, e.strerror))  # ERROR (return a tuple)
 
       if (pid == 0):      # The second child.
          # Ensure that the daemon doesn't keep any directory in use.  Failure
          # to do this could make a filesystem unmountable.
          os.chdir("/")
          # Give the child complete control over permissions.
          os.umask(0)
       else:
          os._exit(0)      # Exit parent (the first child) of the second child.
    else:
       os._exit(0)         # Exit parent of the first child.
 
    # Close all open files.  Try the system configuration variable, SC_OPEN_MAX,
    # for the maximum number of open files to close.  If it doesn't exist, use
    # the default value (configurable).
    try:
       maxfd = os.sysconf("SC_OPEN_MAX")
    except (AttributeError, ValueError):
       maxfd = 256       # default maximum
 
    for fd in range(0, maxfd):
       try:
          os.close(fd)
       except OSError:   # ERROR (ignore)
          pass
 
    # Redirect the standard file descriptors to /dev/null.
    os.open("/dev/null", os.O_RDONLY)    # standard input (0)
    os.open("/dev/null", os.O_RDWR)       # standard output (1)
    os.open("/dev/null", os.O_RDWR)       # standard error (2)
 
    return(0)
