[Zodb-checkins] CVS: ZODB3/zdaemon - zdaemon.py:1.1

Guido van Rossum guido@python.org
Thu, 7 Nov 2002 17:44:00 -0500


Update of /cvs-repository/ZODB3/zdaemon
In directory cvs.zope.org:/tmp/cvs-serv5531

Added Files:
	zdaemon.py 
Log Message:
A daemon manager.  Not quite done, but this is a checkpoint checkin so
I can continue working from home.


=== Added File ZODB3/zdaemon/zdaemon.py ===
#! /usr/bin/env python

"""
zdaemon -- run a program as a daemon.

Usage: python zdaemon.py [zdaemon-options] program [program-options]

Options:

-d -- run as a proper daemon; fork a background process, close files etc.
-h -- print usage message and exit

Arguments:

program [program-options] -- an arbitrary command line to be run
"""

import os
assert os.name == "posix" # This code makes Unix-specific assumptions
import sys
import getopt
import signal
from stat import ST_MODE

import zLOG

class Daemonizer:

    def __init__(self):
        self.filename = None
        self.args = []
        self.daemon = 0

    def main(self, args=None):
        self.prepare(args)
        self.run()

    def prepare(self, args=None):
        if args is None:
            args = sys.argv[1:]
        self.blather("args=%s" % repr(args))
        try:
            opts, args = getopt.getopt(args, "dh")
        except getopt.error, msg:
            self.usage(str(msg))
        self.parseoptions(opts)
        self.setprogram(args)

    def parseoptions(self, opts):
        self.info("opts=%s" % repr(opts))
        for o, a in opts:
            if o == "-h":
                self.usage(exit=0)
            if o == "-d":
                self.daemon += 1

    def setprogram(self, args):
        if not args:
            self.usage("missing 'program' argument")
        self.filename = self.checkcommand(args[0])
        self.args = args # A list of strings like for execvp()
        self.info("filename=%s; args=%s" %
                  (repr(self.filename), repr(self.args)))

    def checkcommand(self, command):
        if "/" in command:
            filename = command
            try:
                st = os.stat(filename)
            except os.error:
                self.usage("can't stat command: %s\n" % repr(command))
        else:
            path = self.getpath()
            for dir in path:
                filename = os.path.join(dir, command)
                try:
                    st = os.stat(filename)
                except os.error:
                    continue
                mode = st[ST_MODE]
                if mode & 0111:
                    break
            else:
                self.usage("can't find command: %s on PATH %s\n" %
                           (repr(command), path))
        if not os.access(filename, os.X_OK):
            self.usage("no permission to run command %s\n" % repr(filename))
        return filename

    def getpath(self):
        path = ["/bin", "/usr/bin", "/usr/local/bin"]
        if os.environ.has_key("PATH"):
            p = os.environ["PATH"]
            if p:
                path = p.split(os.pathsep)
        return path

    def run(self):
        self.setsignals()
        if self.daemon:
            self.daemonize()
        self.runforever()

    def setsignals(self):
        signal.signal(signal.SIGTERM, self.sigexit)
        signal.signal(signal.SIGHUP, self.sigexit)
        signal.signal(signal.SIGINT, self.sigexit)

    def sigexit(self, sig, frame):
        self.info("daemon manager killed by signal %s(%d)" %
                  (self.signalname(sig), sig))
        self.exit(1)

    def daemonize(self):
        pid = os.fork()
        if pid != 0:
            # Parent
            self.blather("daemon manager forked; parent exiting")
            self.exit()
        # Child
        self.info("daemonizing the process")
        os.close(0)
        sys.stdin = sys.__stdin__ = open("/dev/null")
        os.close(1)
        sys.stdout = sys.__stdout__ = open("/dev/null", "w")
        os.close(2)
        sys.stderr = sys.__stderr__ = open("/dev/null", "w")
        os.setsid()

    def runforever(self):
        self.info("daemon manager started")
        while 1:
            self.forkandexec()

    def forkandexec(self):
        pid = os.fork()
        if pid != 0:
            # Parent
            self.info("forked child pid %d" % pid)
            wpid, wsts = os.waitpid(pid, 0)
            self.reportstatus(wpid, wsts)
        else:
            # Child
            self.startprogram()

    def startprogram(self):
        try:
            self.blather("about to exec %s" % self.filename)
            try:
                os.execv(self.filename, self.args)
            except os.error, err:
                self.panic("can't exec %s: %s" %
                           (repr(self.filename), str(err)))
        finally:
            os._exit(127)

    def reportstatus(self, pid, sts):
        if os.WIFEXITED(sts):
            es = os.WEXITSTATUS(sts)
            msg = "pid %d: exit status %s" % (pid, es)
            if es == 0:
                self.info(msg)
            else:
                self.warning(msg)
                if es == 2:
                    self.exit(es)
        elif os.WIFSIGNALED(sts):
            signum = os.WTERMSIG(sts)
            signame = self.signalname(signum)
            msg = ("pid %d: terminated by signal %s(%s)" %
                   (pid, signame, signum))
            if hasattr(os, "WCOREDUMP"):
                iscore = os.WCOREDUMP(sts)
            else:
                iscore = s & 0x80
            if iscore:
                msg += " (core dumped)"
            self.warning(msg)
        else:
            # XXX what should we do here?
            signum = os.WSTOPSIG(sts)
            signame = self.signalname(signum)
            msg = "pid %d: stopped by signal %s(%s)" % (pid, signame, signum)
            self.warning(msg)

    signames = None

    def signalname(self, sig):
        """Return the symbolic name for signal sig.

        Returns 'unknown' if there is no SIG name bound to sig in the
        signal module.
        """

        if self.signames is None:
            self.setupsignames()
        return self.signames.get(sig, "unknown")

    def setupsignames(self):
            self.signames = {}
            for k, v in signal.__dict__.items():
                startswith = getattr(k, "startswith", None)
                if startswith is None:
                    continue
                if startswith("SIG") and not startswith("SIG_"):
                    self.signames[v] = k

    # Error handling

    def usage(self, msg=None, exit=2):
        if msg:
            self.errwrite("Error: %s\n" % str(msg))
            if exit == 2:
                self.error(str(msg))
        self.errwrite(__doc__)
        self.exit(exit)

    def errwrite(self, msg):
        sys.stderr.write(msg)

    def exit(self, sts=0):
        sys.exit(sts)

    # Log messages with various severities

    def trace(self, msg):
        self.log(msg, zLOG.TRACE)

    def debug(self, msg):
        self.log(msg, zLOG.DEBUG)

    def blather(self, msg):
        self.log(msg, zLOG.BLATHER)

    def info(self, msg):
        self.log(msg, zLOG.INFO)

    def warning(self, msg):
        self.log(msg, zLOG.WARNING)

    def error(self, msg):
        self.log(msg, zLOG.ERROR)

    def panic(self, msg):
        self.log(msg, zLOG.PANIC)

    def getsubsystem(self):
        return "ZD:%d" % os.getpid()

    def log(self, msg, severity=zLOG.INFO):
        zLOG.LOG(self.getsubsystem(), severity, msg)

def main(args=None):
    d = Daemonizer()
    d.main(args)

if __name__ == "__main__":
    main()