#!/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()