#!/usr/bin/python2.3 -ut

# Copyright 2003, 2004 Iustin Pop
#
# This file is part of cfvers.
#
# cfvers is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# cfvers is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cfvers; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

"""cfvers network server

This programs runs as a Pyro network server, exporting an object which
allows clients to remotely access the repository.

"""

import os, os.path
import sys
import signal
import ConfigParser
import logging, logging.config

from optparse import OptionParser

import Pyro.core

import cfvers.gateway

def daemonize():
    """Daemonize the running program.

    The function detaches the process from its controlling terminal
    and runs it in the background as a daemon.

    """
    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                   # ERROR

    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)

        # 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.
        try:
            pid = os.fork()        # Fork a second child.
        except OSError, e:
            return e               # ERROR

        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

def writepidfile(filename):
    """Writes the PID of the program to a file"""
    
    if os.path.exists(filename):
        logging.getLogger("cfversd").critical("PID file %s already exists. Please remove it if no other instance is running" % filename)
        return False
    try:
        f = file(filename, "w")
        f.write("%d\n" % os.getpid())
        f.close()
    except (OSError, IOError):
        pass
    return True

def parseconfig(options, filename):
    """Merges the options from the config file

    The items which were not found in the command line are read from
    the configuration file.

    """
    fp = ConfigParser.ConfigParser()
    fp.read(filename)

    if options.pidfile is None:
        if fp.has_option("server", "pidfile"):
            options.pidfile = fp.get("server", "pidfile")
        
    if options.port is None:
        if fp.has_option("server", "port"):
            options.port = fp.getint("server", "port")
            
    repo_meth = fp.get("repository", "method")
    repo_data = fp.get("repository", "connect")

    userlist = fp.get("auth", "users").split(",")
    udict = {}
    for username in userlist:
        section = "user_%s" % username
        c_passphrase = fp.get(section, "client_password")
        s_passphrase = fp.get(section, "server_password")
        valid_addrs = fp.get(section, "valid_from").split(",")
        valid_areas = fp.get(section, "areas").split(",")
        admin = fp.getboolean(section, "admin")
        udict[username] = cfvers.gateway.User(username, c_passphrase, s_passphrase, valid_addrs, valid_areas, admin)
    return repo_meth, repo_data, udict

def validateconfig(options):
    """Validates the configuration

    It is used after the reading the configuration file and verifies
    that the parameters from the command line together with the
    contents of the configuration file create a valid configuration.

    """
    for i in ('pidfile', 'port', 'logging'):
        if not hasattr(options, i):
            return False, "missing option '%s'" % i
    for i in ('cfgfile', 'logging', 'pidfile'):
        setattr(options, i, os.path.abspath(getattr(options, i)))
    try:
        options.port = int(options.port)
    except ValueError:
        return False, "invalid port value '%s'" % options.port

    if not os.path.exists(options.logging):
        return False, "invalid logging configuration file '%s' (not existing)" % options.logging
    return True, ""

def parseargs():
    """Parse the command line arguments"""
    
    parser = OptionParser()
    parser.add_option("-c", "--config", dest="cfgfile",
                      help="configuration file name", metavar="FILE",
                      default="/etc/cfvers/cfversd.conf")
    parser.add_option("-p", "--pidfile", dest="pidfile",
                      help="write PID to FILE", metavar="FILE",
                      default=None)
    parser.add_option("-P", "--port", dest="port",
                      help="port to listen on", metavar="PORT",
                      type="int", default=None)
    parser.add_option("-f", "--foreground", dest="foreground",
                      help="don't fork to background",
                      action="store_true", default=False)
    parser.add_option("-l", "--logging", dest="logging",
                      help="logging configuration file name", metavar="FILE",
                      default="/etc/cfvers/logging.conf")
    (options, args) = parser.parse_args()
    return options, args

def sighandler(signal, frame):
    """Handles a signal

    This function raises the KeyboardInterrupt exception so that we
    treat a SIGTERM and a CTRL-C the same.

    """
    raise KeyboardInterrupt

def main():
    """Main function"""
    
    options, args = parseargs()
    if not os.path.exists(options.cfgfile):
        print >> sys.stderr, "Configuration file '%s' cannot be found." % options.cfgfile
        sys.exit(1)
    try:
        rm, rd, ud = parseconfig(options, options.cfgfile)
    except ConfigParser.Error, e:
        print >> sys.stderr, "Invalid configuration file (%s).\nError details: %s" % (options.cfgfile, str(e))
        sys.exit(1)
        
    res, var = validateconfig(options)
    if not res:
        print >> sys.stderr, "Invalid configuration: %s" % var
        sys.exit(1)
    
    if not options.foreground:
        err = daemonize()
        if err != 0:
            # Let's try writing to stdout
            logging.getLogger("cfversd").error("Can't fork: errno=%s, %s" % (err.errno, err.strerr))
            os._exit(1)
    logging.config.fileConfig(options.logging)
    Pyro.config.PYRO_STDLOGGING = 1
    Pyro.config.PYRO_STDLOGGING_CFGFILE = os.path.abspath(options.logging)
    Pyro.config.PYRO_TRACELEVEL = 0
    logging.getLogger("cfversd").info("Starting up")

    signal.signal(signal.SIGTERM, sighandler)
    if not writepidfile(options.pidfile):
        return
    try:
        Pyro.core.initServer(banner=0)

        connval = cfvers.gateway.PortalValidator()
        connval.setUsers(ud)
        daemon = Pyro.core.Daemon(port=options.port, norange=1)
        daemon.setTimeout(120)
        daemon.setTransientsCleanupAge(600)
        daemon.setNewConnectionValidator(connval)

        pf = cfvers.gateway.PortalFactory(users=ud, repo_meth=rm, repo_data=rd)
        daemon.connect(pf, "PortalFactory")

        try:
            daemon.requestLoop()
        except KeyboardInterrupt:
            pass
    finally:
        try:
            os.unlink(options.pidfile)
        except OSError:
            pass
    logging.getLogger("cfversd").info("Shutting down")
    return

if __name__ == "__main__":
    main()
