[Zodb-checkins] CVS: Zope/lib/python/zdaemon - zdaemon.py:1.3.2.1 Daemon.py:1.14.6.1
Andreas Jung
andreas@andreas-jung.com
Sat, 9 Nov 2002 03:43:02 -0500
Update of /cvs-repository/Zope/lib/python/zdaemon
In directory cvs.zope.org:/tmp/cvs-serv26094/lib/python/zdaemon
Modified Files:
Tag: ajung-restructuredtext-integration-branch
Daemon.py
Added Files:
Tag: ajung-restructuredtext-integration-branch
zdaemon.py
Log Message:
merge from trunk
=== Added File Zope/lib/python/zdaemon/zdaemon.py ===
#! /usr/bin/env python
"""
zdaemon -- run an application as a daemon.
Usage: python zdaemon.py [zdaemon-options] program [program-arguments]
Options:
-b SECONDS -- set backoff limit to SECONDS (default 10; see below)
-d -- run as a proper daemon; fork a background process, close files etc.
-f -- run forever (by default, exit when the backoff limit is exceeded)
-h -- print usage message and exit
program [program-arguments] -- an arbitrary application to run
This daemon manager has two purposes: it restarts the application when
it dies, and (when requested to do so with the -d option) it runs the
application in the background, detached from the foreground tty
session that started it (if any).
Important: if at any point the application exits with exit status 2,
it is not restarted. Any other form of termination (either being
killed by a signal or exiting with an exit status other than 2) causes
it to be restarted.
Backoff limit: when the application exits (nearly) immediately after a
restart, the daemon manager starts slowing down by delaying between
restarts. The delay starts at 1 second and is increased by one on
each restart up to the backoff limit given by the -b option; it is
reset when the application runs for more than the backoff limit
seconds. By default, when the delay reaches the backoff limit, the
daemon manager exits (under the assumption that the application has a
persistent fault). The -f (forever) option prevents this exit; use it
when you expect that a temporary external problem (such as a network
outage or an overfull disk) may prevent the application from starting
but you want the daemon manager to keep trying.
"""
"""
XXX TO DO
- A way to stop both the daemon manager and the application.
- A way to restart the application.
- More control over logging (zdaemon logging should be controllable
separate from application logging).
"""
import os
assert os.name == "posix" # This code has many Unix-specific assumptions
import sys
import time
import getopt
import signal
from stat import ST_MODE
import zLOG
class Daemonizer:
# Settable options
daemon = 0
forever = 0
backofflimit = 10
def __init__(self):
self.filename = None
self.args = []
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, "b:dfh")
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 == "-b":
try:
self.backofflimit = float(a)
except:
self.usage("invalid number: %s" % repr(a))
if o == "-d":
self.daemon += 1
if o == "-f":
self.forever += 1
if o == "-h":
print __doc__,
self.exit()
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 program %s" % 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 program %s on PATH %s" %
(repr(command), path))
if not os.access(filename, os.X_OK):
self.usage("no permission to run program %s" % 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.signame(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.governor()
self.forkandexec()
backoff = 0
lasttime = None
def governor(self):
# Back off if respawning too often
if not self.lasttime:
pass
elif time.time() - self.lasttime < self.backofflimit:
# Exited rather quickly; slow down the restarts
self.backoff += 1
if self.backoff >= self.backofflimit:
if self.forever:
self.backoff = self.backofflimit
else:
self.problem("restarting too often; quit")
self.exit(1)
self.info("sleep %s to avoid rapid restarts" % self.backoff)
time.sleep(self.backoff)
else:
# Reset the backoff timer
self.backoff = 0
self.lasttime = time.time()
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)
elif es == 2:
self.problem(msg)
self.exit(es)
else:
self.warning(msg)
elif os.WIFSIGNALED(sts):
signum = os.WTERMSIG(sts)
signame = self.signame(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:
msg = "pid %d: unknown termination cause 0x%04x" % (pid, sts)
self.warning(msg)
signames = None
def signame(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):
self.problem(str(msg))
self.errwrite("Error: %s\n" % str(msg))
self.errwrite("For help, use zdaemon.py -h\n")
self.exit(2)
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 problem(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()
=== Zope/lib/python/zdaemon/Daemon.py 1.14 => 1.14.6.1 ===
--- Zope/lib/python/zdaemon/Daemon.py:1.14 Fri Oct 11 09:29:55 2002
+++ Zope/lib/python/zdaemon/Daemon.py Sat Nov 9 03:43:01 2002
@@ -57,14 +57,15 @@
signal.signal(sig, SignalPasser(pid))
pstamp('Started subprocess: pid %s' % pid, zLOG.INFO)
write_pidfile(pidfile)
- p,s = wait(pid) # waitpid will block until child exit
+ p, s = wait(pid) # waitpid will block until child exit
+ log_pid(p, s)
if s:
# continue and restart because our child died
# with a nonzero exit code, meaning he bit it in
# an unsavory way (likely a segfault or something)
- log_pid(p, s)
continue
else:
+ pstamp("zdaemon exiting", zLOG.INFO)
# no need to restart, our child wanted to die.
raise DieNow
@@ -142,6 +143,6 @@
startswith = getattr(k, 'startswith', None)
if startswith is None:
continue
- if startswith('SIG'):
+ if startswith('SIG') and not startswith('SIG_'):
_signals[v] = k
return _signals.get(n, 'unknown')