[Zope3-checkins] CVS: Packages3/zdaemon - Daemon.py:1.21 sample.conf:1.1 schema.xml:1.1 zdoptions.py:1.1 zdrun.py:1.1 __init__.py:1.7 zdctl.py:1.10 zdaemon.py:NONE zdctl.sh:NONE

Jeremy Hylton jeremy@zope.com
Thu, 19 Jun 2003 11:44:35 -0400


Update of /cvs-repository/Packages3/zdaemon
In directory cvs.zope.org:/tmp/cvs-serv28024

Modified Files:
	__init__.py zdctl.py 
Added Files:
	Daemon.py sample.conf schema.xml zdoptions.py zdrun.py 
Removed Files:
	zdaemon.py zdctl.sh 
Log Message:
Merge the zdaemon code from Zope2.


=== Packages3/zdaemon/Daemon.py 1.20 => 1.21 ===
--- /dev/null	Thu Jun 19 11:44:35 2003
+++ Packages3/zdaemon/Daemon.py	Thu Jun 19 11:44:34 2003
@@ -0,0 +1,149 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+import os, sys, signal
+import logging
+
+pyth = sys.executable
+
+log = logging.getLogger("zdaemon")
+
+class DieNow(Exception):
+    pass
+
+class SignalPasser:
+    """ A class used for passing signal that the daemon receives along to
+    its child """
+    def __init__(self, pid):
+        self.pid = pid
+
+    def __call__(self, signum, frame):
+        # send the signal to our child
+        os.kill(self.pid, signum)
+        # we want to die ourselves if we're signaled with SIGTERM or SIGINT
+        if signum in [signal.SIGTERM, signal.SIGINT]:
+            raise DieNow
+
+def run(argv, pidfile=''):
+    if os.environ.has_key('ZDAEMON_MANAGED'):
+        # We're being run by the child.
+        return
+
+    os.environ['ZDAEMON_MANAGED']='TRUE'
+
+    if not os.environ.has_key('Z_DEBUG_MODE'):
+        detach() # detach from the controlling terminal
+
+    while 1:
+        try:
+            pid = os.fork()
+            if pid:
+                # We're the parent (the daemon process)
+                # pass all "normal" signals along to our child, but don't
+                # respond to them ourselves unless they say "die"!
+                interesting = [1, 2, 3, 10, 12, 15]
+                # ie. HUP, INT, QUIT, USR1, USR2, TERM
+                for sig in interesting:
+                    signal.signal(sig, SignalPasser(pid))
+                log.info("Started subprocess: pid %s", pid)
+                write_pidfile(pidfile)
+                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)
+                    continue
+                else:
+                    log.info("zdaemon exiting")
+                    # no need to restart, our child wanted to die.
+                    raise DieNow
+
+            else:
+                # we're the child (Zope/ZEO)
+                args = [pyth]
+                if not __debug__:
+                    # we're running in optimized mode
+                    args.append('-O')
+                os.execv(pyth, tuple(args) + tuple(argv))
+
+        except DieNow:
+            sys.exit()
+
+def detach():
+    # do the funky chicken dance to detach from the terminal
+    pid = os.fork()
+    if pid: sys.exit(0)
+    os.close(0); sys.stdin  = open('/dev/null')
+    os.close(1); sys.stdout = open('/dev/null','w')
+    os.close(2); sys.stderr = open('/dev/null','w')
+    os.setsid()
+
+def write_pidfile(pidfile):
+    if pidfile:
+        pf = open(pidfile, 'w+')
+        pf.write(("%s\n" % os.getpid()))
+        pf.close()
+
+def wait(pid):
+    while 1:
+        try:
+            p,s = os.waitpid(pid, 0)
+        except OSError:
+            # catch EINTR, it's raised as a result of
+            # interrupting waitpid with a signal
+            # and we don't care about it.
+            continue
+        else:
+            return p, s
+
+def log_pid(p, s):
+    if os.WIFEXITED(s):
+        es = os.WEXITSTATUS(s)
+        msg = "terminated normally, exit status: %s" % es
+    elif os.WIFSIGNALED(s):
+        signum = os.WTERMSIG(s)
+        signame = get_signal_name(signum)
+        msg = "terminated by signal %s(%s)" % (signame, signum)
+        if hasattr(os, 'WCOREDUMP'):
+            iscore = os.WCOREDUMP(s)
+        else:
+            iscore = s & 0x80
+        if iscore:
+            msg += " (core dumped)"
+    else:
+        # XXX what should we do here?
+        signum = os.WSTOPSIG(s)
+        signame = get_signal_name(signum)
+        msg = "stopped by signal %s(%s)" % (signame, signum)
+    log.error("Process %s %s", p, msg)
+
+_signals = None
+
+def get_signal_name(n):
+    """Return the symbolic name for signal n.
+
+    Returns 'unknown' if there is no SIG name bound to n in the signal
+    module.
+    """
+    global _signals
+    if _signals is None:
+        _signals = {}
+        for k, v in signal.__dict__.items():
+            startswith = getattr(k, 'startswith', None)
+            if startswith is None:
+                continue
+            if startswith('SIG') and not startswith('SIG_'):
+                _signals[v] = k
+    return _signals.get(n, 'unknown')


=== Added File Packages3/zdaemon/sample.conf ===
# Sample config file for zdctl.py and zdrun.py (which share a schema).

<runner>
  # Harmless example
  program       sleep 100
  # Repeat the defaults
  backoff-limit 10
  daemon	True
  forever	True
  socket-name	zdsock
  exit-codes	0,2
  # user has no default
  directory	.
  default-to-interactive True
  hang-around   False
</runner>

<eventlog>
  level info
  <logfile>
    path /tmp/zdrun.log
  </logfile>
</eventlog>


=== Added File Packages3/zdaemon/schema.xml ===
<schema>

  <description>
    This schema describes various options that control zdctl.py and
    zdrun.py.  zdrun.py is the "daemon process manager"; it runs a
    subprocess in the background and restarts it when it crashes.
    zdctl.py is the user interface to zdrun.py; it can tell zdrun.py
    to start, stop or restart the subprocess, send it a signal, etc.

    There are two sections: &lt;runner&gt; defines options unique
    zdctl.py and zdrun.py, and &lt;eventlog&gt; defines a standard
    event logging section used by zdrun.py.

    More information about zdctl.py and zdrun.py can be found in the
    file Doc/zdctl.txt.  This all is specific to Unix/Linux.
  </description>

  <sectiontype name="runner">

    <description>
      This section describes the options for zdctl.py and zdrun.py.
      The only required option is "program".  Many other options have
      no default value specified in the schema; in some cases, the
      program calculates a dynamic default, in others, the feature
      associated with the option is disabled.

      For those options that also have corresponding command-line
      options, the command line option (short and long form) are given
      here too.
    </description>

    <key name="program" datatype="string-list"
	 required="yes">
      <description>
        Command-line option: -p or --program (zdctl.py only).

        This option gives the command used to start the subprocess
        managed by zdrun.py.  This is currently a simple list of
        whitespace-delimited words. The first word is the program
        file, subsequent words are its command line arguments.  If the
        program file contains no slashes, it is searched using $PATH.
        (XXX There is no way to to include whitespace in the program
        file or an argument, and under certain circumstances other
        shell metacharacters are also a problem, e.g. the "foreground"
        command of zdctl.py.)

        NOTE: zdrun.py doesn't use this option; it uses its positional
        arguments.  Rather, zdctl.py uses this option to determine the
        positional argument with which to invoke zdrun.py.  (XXX This
        could be better.)
      </description>
    </key>

    <key name="python" datatype="existing-path"
	 required="no">
      <description>
        Path to the Python interpreter.  Used by zdctl.py to start the
        zdrun.py process.  Defaults to sys.executable.
      </description>
    </key>

    <key name="zdrun" datatype="existing-path"
	 required="no">
      <description>
        Path to the zdrun.py script.  Used by zdctl.py to start the
        zdrun.py process.  Defaults to a file named "zdrun.py" in the
        same directory as zdctl.py.
      </description>
    </key>

    <key name="socket-name" datatype="existing-dirpath"
	 required="no"
	 default="zdsock">
      <description>
        Command-line option: -s or --socket-name.

        The pathname of the Unix domain socket used for communication
        between zdctl.py and zdrun.py.  The default is relative to the
        current directory in which zdctl.py and zdrun.py are started.
        You want to specify an absolute pathname here.
      </description>
    </key>

    <key name="daemon" datatype="boolean"
	 required="no"
	 default="false">
      <description>
        Command-line option: -d or --daemon.

        If this option is true, zdrun.py runs in the background as a
        true daemon.  It forks an child process which becomes the
        subprocess manager, while the parent exits (making the shell
        that started it believe it is done).  The child process also
        does the following:

        - if the directory option is set, change into that directory

        - redirect stdin, stdout and stderr to /dev/null

        - call setsid() so it becomes a session leader

        - call umask(022)
      </description>
    </key>

    <key name="directory" datatype="existing-directory"
	 required="no">
      <description>
        Command-line option: -z or --directory.

        If the daemon option is true, this option can specify a
        directory into which zdrun.py changes as part of the
        "daemonizing".  If the daemon option is false, this option is
        ignored.
      </description>
    </key>

    <key name="backoff-limit" datatype="integer"
	 required="no"
	 default="10">
      <description>
        Command-line option: -b or --backoff-limit.

        When the subprocess crashes, zdrun.py inserts a one-second
        delay before it restarts it.  When the subprocess crashes
        again right away, the delay is incremented by one second, and
        so on.  What happens when the delay has reached the value of
        backoff-limit (in seconds), depends on the value of the
        forever option.  If forever is false, zdrun.py gives up at
        this point, and exits.  An always-crashing subprocess will
        have been restarted exactly backoff-limit times in this case.
        If forever is true, zdrun.py continues to attempt to restart
        the process, keeping the delay at backoff-limit seconds.

        If the subprocess stays up for more than backoff-limit
        seconds, the delay is reset to 1 second.
      </description>
    </key>

    <key name="forever" datatype="boolean"
	 required="no"
	 default="false">
      <description>
        Command-line option: -f or --forever.

        If this option is true, zdrun.py will keep restarting a
        crashing subprocess forever.  If it is false, it will give up
        after backoff-limit crashes in a row.  See the description of
        backoff-limit for details.
      </description>
    </key>

    <key name="exit-codes" datatype="zdaemon.zdoptions.list_of_ints"
	 required="no"
	 default="0,2">
      <description>
        Command-line option: -x or --exit-codes.

        If the subprocess exits with an exit status that is equal to
        one of the integers in this list, zdrun.py will not restart
        it.  The default list requires some explanation.  Exit status
        0 is considered a willful successful exit; the ZEO and Zope
        server processes use this exit status when they want to stop
        without being restarted.  (Including in response to a
        SIGTERM.)  Exit status 2 is typically issued for command line
        syntax errors; in this case, restarting the program will not
        help!

        NOTE: this mechanism overrides the backoff-limit and forever
        options; i.e. even if forever is true, a subprocess exit
        status code in this list makes zdrun.py give up.  To disable
        this, change the value to an empty list.
      </description>
    </key>

    <key name="user" datatype="string"
	 required="no">
      <description>
        Command-line option: -u or --user.

        When zdrun.py is started by root, this option specifies the
        user as who the the zdrun.py process (and hence the daemon
        subprocess) will run.  This can be a user name or a numeric
        user id.  Both the user and the group are set from the
        corresponding password entry, using setuid() and setgid().
        This is done before zdrun.py does anything else besides
        parsing its command line arguments.

        NOTE: when zdrun.py is not started by root, specifying this
        option is an error.  (XXX This may be a mistake.)

        XXX The zdrun.py event log file may be opened *before*
        setuid() is called.  Is this good or bad?
      </description>
    </key>

    <key name="hang-around" datatype="boolean"
	 required="no"
	 default="false">
      <description>
        If this option is true, the zdrun.py process will remain even
        when the daemon subprocess is stopped.  In this case, zdctl.py
        will restart zdrun.py as necessary.  If this option is false,
        zdrun.py will exit when the daemon subprocess is stopped
        (unless zdrun.py intends to restart it).
      </description>
    </key>

    <key name="default-to-interactive" datatype="boolean"
	 required="no"
	 default="true">
      <description>
        If this option is true, zdctl.py enters interactive mode
        when it is invoked without a positional command argument.  If
        it is false, you must use the -i or --interactive command line
        option to zdctl.py to enter interactive mode.
      </description>
    </key>

    <key name="logfile" datatype="existing-dirpath"
	 required="no">
      <description>
        This option specifies a log file that is the default target of
        the "logtail" zdctl.py command.

        NOTE: This is NOT the log file to which zdrun.py writes its
        logging messages!  That log file is specified by the
        &lt;eventlog&gt; section.
      </description>
    </key>

    <key name="prompt" datatype="string"
         required="no" default="zdctl>">
       <description>
         The prompt shown by the controller program.
       </description>
    </key>

  </sectiontype>

  <section name="*" type="runner" attribute="runner" required="yes" />

  <section name="*" type="eventlog" attribute="eventlog" required="no" />

</schema>


=== Added File Packages3/zdaemon/zdoptions.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################

"""Option processing for zdaemon and related code."""

import os
import sys
import getopt

import ZConfig

class ZDOptions:

    doc = None
    progname = None
    configfile = None
    schemadir = None
    schemafile = "schema.xml"
    schema = None
    configroot = None

    # Class variable to control automatic processing of an <eventlog>
    # section.  This should be the (possibly dotted) name of something
    # accessible from configroot, typically "eventlog".
    logsectionname = None
    config_logger = None # The configured event logger, if any

    # Class variable deciding whether positional arguments are allowed.
    # If you want positional arguments, set this to 1 in your subclass.
    positional_args_allowed = 0

    def __init__(self):
        self.names_list = []
        self.short_options = []
        self.long_options = []
        self.options_map = {}
        self.default_map = {}
        self.required_map = {}
        self.environ_map = {}
        self.zconfig_options = []
        self.add(None, None, "h", "help", self.help)
        self.add("configfile", None, "C:", "configure=")
        self.add(None, None, "X:", handler=self.zconfig_options.append)

    def help(self, dummy):
        """Print a long help message (self.doc) to stdout and exit(0).

        Occurrences of "%s" in self.doc are replaced by self.progname.
        """
        doc = self.doc
        if doc.find("%s") > 0:
            doc = doc.replace("%s", self.progname)
        print doc,
        sys.exit(0)

    def usage(self, msg):
        """Print a brief error message to stderr and exit(2)."""
        sys.stderr.write("Error: %s\n" % str(msg))
        sys.stderr.write("For help, use %s -h\n" % self.progname)
        sys.exit(2)

    def remove(self,
               name=None,               # attribute name on self
               confname=None,           # name in ZConfig (may be dotted)
               short=None,              # short option name
               long=None,               # long option name
               ):
        """Remove all traces of name, confname, short and/or long."""
        if name:
            for n, cn in self.names_list[:]:
                if n == name:
                    self.names_list.remove((n, cn))
            if self.default_map.has_key(name):
                del self.default_map[name]
            if self.required_map.has_key(name):
                del self.required_map[name]
        if confname:
            for n, cn in self.names_list[:]:
                if cn == confname:
                    self.names_list.remove((n, cn))
        if short:
            key = "-" + short[0]
            if self.options_map.has_key(key):
                del self.options_map[key]
        if long:
            key = "--" + long
            if key[-1] == "=":
                key = key[:-1]
            if self.options_map.has_key(key):
                del self.options_map[key]

    def add(self,
            name=None,                  # attribute name on self
            confname=None,              # name in ZConfig (may be dotted)
            short=None,                 # short option name
            long=None,                  # long option name
            handler=None,               # handler (defaults to string)
            default=None,               # default value
            required=None,              # message if not provided
            flag=None,                  # if not None, flag value
            env=None,                   # if not None, environment variable
            ):
        """Add information about a configuration option.

        This can take several forms:

        add(name, confname)
            Configuration option 'confname' maps to attribute 'name'
        add(name, None, short, long)
            Command line option '-short' or '--long' maps to 'name' 
        add(None, None, short, long, handler)
            Command line option calls handler
        add(name, None, short, long, handler)
            Assign handler return value to attribute 'name'

        In addition, one of the following keyword arguments may be given:

        default=...  -- if not None, the default value
        required=... -- if nonempty, an error message if no value provided
        flag=...     -- if not None, flag value for command line option
        env=...      -- if not None, name of environment variable that
                        overrides the configuration file or default
        """

        if flag is not None:
            if handler is not None:
                raise ValueError, "use at most one of flag= and handler="
            if not long and not short:
                raise ValueError, "flag= requires a command line flag"
            if short and short.endswith(":"):
                raise ValueError, "flag= requires a command line flag"
            if long and long.endswith("="):
                raise ValueError, "flag= requires a command line flag"
            handler = lambda arg, flag=flag: flag

        if short and long:
            if short.endswith(":") != long.endswith("="):
                raise ValueError, "inconsistent short/long options: %r %r" % (
                    short, long)

        if short:
            if short[0] == "-":
                raise ValueError, "short option should not start with '-'"
            key, rest = short[:1], short[1:]
            if rest not in ("", ":"):
                raise ValueError, "short option should be 'x' or 'x:'"
            key = "-" + key
            if self.options_map.has_key(key):
                raise ValueError, "duplicate short option key '%s'" % key
            self.options_map[key] = (name, handler)
            self.short_options.append(short)

        if long:
            if long[0] == "-":
                raise ValueError, "long option should not start with '-'"
            key = long
            if key[-1] == "=":
                key = key[:-1]
            key = "--" + key
            if self.options_map.has_key(key):
                raise ValueError, "duplicate long option key '%s'" % key
            self.options_map[key] = (name, handler)
            self.long_options.append(long)

        if env:
            self.environ_map[env] = (name, handler)

        if name:
            if not hasattr(self, name):
                setattr(self, name, None)
            self.names_list.append((name, confname))
            if default is not None:
                self.default_map[name] = default
            if required:
                self.required_map[name] = required

    def realize(self, args=None, progname=None, doc=None):
        """Realize a configuration.

        Optional arguments:

        args     -- the command line arguments, less the program name
                    (default is sys.argv[1:])

        progname -- the program name (default is sys.argv[0])

        doc      -- usage message (default is __main__.__doc__)
        """

         # Provide dynamic default method arguments
        if args is None:
            args = sys.argv[1:]
        if progname is None:
            progname = sys.argv[0]
        if doc is None:
            import __main__
            doc = __main__.__doc__
        self.progname = progname
        self.doc = doc

        # Call getopt
        try:
            self.options, self.args = getopt.getopt(
                args, "".join(self.short_options), self.long_options)
        except getopt.error, msg:
            self.usage(msg)

        # Check for positional args
        if self.args and not self.positional_args_allowed:
            self.usage("positional arguments are not supported")

        # Process options returned by getopt
        for opt, arg in self.options:
            name, handler = self.options_map[opt]
            if handler is not None:
                try:
                    arg = handler(arg)
                except ValueError, msg:
                    self.usage("invalid value for %s %r: %s" % (opt, arg, msg))
            if name and arg is not None:
                if getattr(self, name) is not None:
                    self.usage("conflicting command line option %r" % opt)
                setattr(self, name, arg)

        # Process environment variables
        for envvar in self.environ_map.keys():
            name, handler = self.environ_map[envvar]
            if name and getattr(self, name, None) is not None:
                continue
            if os.environ.has_key(envvar):
                value = os.environ[envvar]
                if handler is not None:
                    try:
                        value = handler(value)
                    except ValueError, msg:
                        self.usage("invalid environment value for %s %r: %s"
                                   % (envvar, value, msg))
                if name and value is not None:
                    setattr(self, name, value)

        if self.zconfig_options and self.configfile is None:
            self.usage("configuration overrides (-X) cannot be used"
                       " without a configuration file")
        if self.configfile is not None:
            # Process config file
            self.load_schema()
            try:
                self.load_configfile()
            except ZConfig.ConfigurationError, msg:
                self.usage(str(msg))

        # Copy config options to attributes of self.  This only fills
        # in options that aren't already set from the command line.
        for name, confname in self.names_list:
            if confname and getattr(self, name) is None:
                parts = confname.split(".")
                obj = self.configroot
                for part in parts:
                    if obj is None:
                        break
                    # Here AttributeError is not a user error!
                    obj = getattr(obj, part)
                setattr(self, name, obj)

        # Process defaults
        for name, value in self.default_map.items():
            if getattr(self, name) is None:
                setattr(self, name, value)

        # Process required options
        for name, message in self.required_map.items():
            if getattr(self, name) is None:
                self.usage(message)

        if self.logsectionname:
            # Let the environment override the config file
            if (os.getenv("EVENT_LOG_FILE") is None and
                os.getenv("STUPID_LOG_FILE") is None):
                self.load_logconf(self.logsectionname)

    def load_schema(self):
        if self.schema is None:
            # Load schema
            if self.schemadir is None:
                self.schemadir = os.path.dirname(__file__)
            self.schemafile = os.path.join(self.schemadir, self.schemafile)
            self.schema = ZConfig.loadSchema(self.schemafile)

    def load_configfile(self):
        self.configroot, self.confighandlers = \
            ZConfig.loadConfig(self.schema, self.configfile,
                               self.zconfig_options)

    def load_logconf(self, sectname="eventlog"):
        parts = sectname.split(".")
        obj = self.configroot
        for p in parts:
            if obj == None:
                break
            obj = getattr(obj, p)
        self.config_logger = obj
        if obj is not None:
            import zLOG
            zLOG.set_initializer(self.log_initializer)
            zLOG.initialize()

    def log_initializer(self):
        from zLOG import EventLogger
        logger = self.config_logger()
        for handler in logger.handlers:
            if hasattr(handler, "reopen"):
                handler.reopen()
        EventLogger.event_logger.logger = logger


class RunnerOptions(ZDOptions):

    uid = gid = None

    def __init__(self):
        ZDOptions.__init__(self)
        self.add("backofflimit", "runner.backoff_limit",
                 "b:", "backoff-limit=", int, default=10)
        self.add("daemon", "runner.daemon", "d", "daemon", flag=1, default=0)
        self.add("forever", "runner.forever", "f", "forever",
                 flag=1, default=0)
        self.add("sockname", "runner.socket_name", "s:", "socket-name=",
                 ZConfig.datatypes.existing_dirpath, default="zdsock")
        self.add("exitcodes", "runner.exit_codes", "x:", "exit-codes=",
                 list_of_ints, default=[0, 2])
        self.add("user", "runner.user", "u:", "user=")
        self.add("directory", "runner.directory", "z:", "directory=",
                 ZConfig.datatypes.existing_directory)
        self.add("hang_around", "runner.hang_around", default=0)

    def realize(self, *args, **kwds):
        ZDOptions.realize(self, *args, **kwds)

        # Additional checking of user option; set uid and gid
        if self.user is not None:
            import pwd
            try:
                uid = int(self.user)
            except ValueError:
                try:
                    pwrec = pwd.getpwnam(self.user)
                except KeyError:
                    self.usage("username %r not found" % self.user)
                uid = pwrec[2]
            else:
                try:
                    pwrec = pwd.getpwuid(uid)
                except KeyError:
                    self.usage("uid %r not found" % self.user)
            gid = pwrec[3]
            self.uid = uid
            self.gid = gid


# ZConfig datatype

def list_of_ints(arg):
    if not arg:
        return []
    else:
        return map(int, arg.split(","))


def _test():
    # Stupid test program
    z = ZDOptions()
    z.add("program", "zdctl.program", "p:", "program=")
    print z.names_list
    z.realize()
    names = z.names_list[:]
    names.sort()
    for name, confname in names:
        print "%-20s = %.56r" % (name, getattr(z, name))

if __name__ == "__main__":
    __file__ = sys.argv[0]
    _test()


=== Added File Packages3/zdaemon/zdrun.py ===
#!python
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""zrdun -- run an application as a daemon.

Usage: python zrdun.py [zrdun-options] program [program-arguments]

Options:
-C/--configuration URL -- configuration file or URL
-b/--backoff-limit SECONDS -- set backoff limit to SECONDS (default 10)
-d/--daemon -- run as a proper daemon; fork a subprocess, setsid(), etc.
-f/--forever -- run forever (by default, exit when backoff limit is exceeded)
-h/--help -- print this usage message and exit
-s/--socket-name SOCKET -- Unix socket name for client (default "zdsock")
-u/--user USER -- run as this user (or numeric uid)
-x/--exit-codes LIST -- list of fatal exit codes (default "0,2")
-z/--directory DIRECTORY -- directory to chdir to when using -d (default off)
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).

Exit codes: if at any point the application exits with an exit status
listed by the -x option, it is not restarted.  Any other form of
termination (either being killed by a signal or exiting with an exit
status not listed in the -x option) 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

- Finish OO design -- use multiple classes rather than folding
  everything into one class.

- Add unit tests.

- Add doc strings.

"""

import os
import sys
import time
import errno
import socket
import select
import signal
import logging
from stat import ST_MODE

if __name__ == "__main__":
    # Add the parent of the script directory to the module search path
    # (but only when the script is run from inside the zdaemon package)
    from os.path import dirname, basename, abspath, normpath
    scriptdir = dirname(normpath(abspath(sys.argv[0])))
    if basename(scriptdir).lower() == "zdaemon":
        sys.path.append(dirname(scriptdir))

import ZConfig.datatypes
from zdaemon.zdoptions import RunnerOptions

log = logging.getLogger("ZD:%s" % os.getpid())

class ZDRunOptions(RunnerOptions):

    positional_args_allowed = 1
    logsectionname = "eventlog"
    program = None

    def realize(self, *args, **kwds):
        RunnerOptions.realize(self, *args, **kwds)
        if self.args:
            self.program = self.args
        if not self.program:
            self.usage("no program specified (use -C or positional args)")
        if self.sockname:
            # Convert socket name to absolute path
            self.sockname = os.path.abspath(self.sockname)


class Subprocess:

    """A class to manage a subprocess."""

    # Initial state; overridden by instance variables
    pid = 0 # Subprocess pid; 0 when not running
    lasttime = 0 # Last time the subprocess was started; 0 if never

    def __init__(self, options, args=None):
        """Constructor.

        Arguments are a ZDRunOptions instance and a list of program
        arguments; the latter's first item must be the program name.
        """
        if args is None:
            args = options.args
        if not args:
            options.usage("missing 'program' argument")
        self.options = options
        self.args = args
        self._set_filename(args[0])

    def _set_filename(self, program):
        """Internal: turn a program name into a file name, using $PATH."""
        if "/" in program:
            filename = program
            try:
                st = os.stat(filename)
            except os.error:
                self.options.usage("can't stat program %r" % program)
        else:
            path = get_path()
            for dir in path:
                filename = os.path.join(dir, program)
                try:
                    st = os.stat(filename)
                except os.error:
                    continue
                mode = st[ST_MODE]
                if mode & 0111:
                    break
            else:
                self.options.usage("can't find program %r on PATH %s" %
                                   (program, path))
        if not os.access(filename, os.X_OK):
            self.options.usage("no permission to run program %r" % filename)
        self.filename = filename

    def spawn(self):
        """Start the subprocess.  It must not be running already.

        Return the process id.  If the fork() call fails, return 0.
        """
        assert not self.pid
        self.lasttime = time.time()
        try:
            pid = os.fork()
        except os.error:
            return 0
        if pid != 0:
            # Parent
            self.pid = pid
            log.info("spawned process pid=%d", pid)
            return pid
        else:
            # Child
            try:
                # Close file descriptors except std{in,out,err}.
                # XXX We don't know how many to close; hope 100 is plenty.
                for i in range(3, 100):
                    try:
                        os.close(i)
                    except os.error:
                        pass
                try:
                    os.execv(self.filename, self.args)
                except os.error, err:
                    sys.stderr.write("can't exec %r: %s\n" %
                                     (self.filename, err))
            finally:
                os._exit(127)
            # Does not return

    def kill(self, sig):
        """Send a signal to the subprocess.  This may or may not kill it.

        Return None if the signal was sent, or an error message string
        if an error occurred or if the subprocess is not running.
        """
        if not self.pid:
            return "no subprocess running"
        try:
            os.kill(self.pid, sig)
        except os.error, msg:
            return str(msg)
        return None

    def setstatus(self, sts):
        """Set process status returned by wait() or waitpid().

        This simply notes the fact that the subprocess is no longer
        running by setting self.pid to 0.
        """
        self.pid = 0


class Daemonizer:

    def main(self, args=None):
        self.options = ZDRunOptions()
        self.options.realize(args)
        self.set_uid()
        self.run()

    def set_uid(self):
        if self.options.uid is None:
            return
        uid = os.geteuid()
        if uid != 0 and uid != self.options.uid:
            self.options.usage("only root can use -u USER to change users")
        os.setuid(self.options.uid)
        os.setgid(self.options.gid)

    def run(self):
        self.proc = Subprocess(self.options)
        self.opensocket()
        try:
            self.setsignals()
            if self.options.daemon:
                self.daemonize()
            self.runforever()
        finally:
            try:
                os.unlink(self.options.sockname)
            except os.error:
                pass

    mastersocket = None
    commandsocket = None

    def opensocket(self):
        sockname = self.options.sockname
        tempname = "%s.%d" % (sockname, os.getpid())
        self.unlink_quietly(tempname)
        while 1:
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            try:
                sock.bind(tempname)
                os.chmod(tempname, 0700)
                try:
                    os.link(tempname, sockname)
                    break
                except os.error:
                    # Lock contention, or stale socket.
                    self.checkopen()
                    # Stale socket -- delete, sleep, and try again.
                    msg = "Unlinking stale socket %s; sleep 1" % sockname
                    sys.stderr.write(msg + "\n")
                    log.warn(msg)
                    self.unlink_quietly(sockname)
                    sock.close()
                    time.sleep(1)
                    continue
            finally:
                self.unlink_quietly(tempname)
        sock.listen(1)
        sock.setblocking(0)
        self.mastersocket = sock

    def unlink_quietly(self, filename):
        try:
            os.unlink(filename)
        except os.error:
            pass

    def checkopen(self):
        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        try:
            s.connect(self.options.sockname)
            s.send("status\n")
            data = s.recv(1000)
            s.close()
        except socket.error:
            pass
        else:
            while data.endswith("\n"):
                data = data[:-1]
            msg = ("Another zrdun is already up using socket %r:\n%s" %
                   (self.options.sockname, data))
            sys.stderr.write(msg + "\n")
            log.critical(msg)
            sys.exit(1)

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

    def sigexit(self, sig, frame):
        log.critical("daemon manager killed by %s", signame(sig))
        sys.exit(1)

    waitstatus = None

    def sigchild(self, sig, frame):
        try:
            pid, sts = os.waitpid(-1, os.WNOHANG)
        except os.error:
            return
        if pid:
            self.waitstatus = pid, sts

    def daemonize(self):
        pid = os.fork()
        if pid != 0:
            # Parent
            log.debug("daemon manager forked; parent exiting")
            os._exit(0)
        # Child
        log.info("daemonizing the process")
        if self.options.directory:
            try:
                os.chdir(self.options.directory)
            except os.error, err:
                log.warn("can't chdir into %r: %s",
                         self.options.directory, err)
            else:
                log.info("set current directory: %r", self.options.directory)
        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()
        os.umask(022) # Create no group/other writable files/directories
        # XXX Stevens, in his Advanced Unix book, section 13.3 (page
        # 417) recommends calling umask(0) and closing unused
        # file descriptors.  In his Network Programming book, he
        # additionally recommends ignoring SIGHUP and forking again
        # after the setsid() call, for obscure SVR4 reasons.

    mood = 1 # 1: up, 0: down, -1: suicidal
    delay = 0 # If nonzero, delay starting or killing until this time
    killing = 0 # If true, send SIGKILL when delay expires
    proc = None # Subprocess instance

    def runforever(self):
        log.info("daemon manager started")
        min_mood = not self.options.hang_around
        while self.mood >= min_mood or self.proc.pid:
            if self.mood > 0 and not self.proc.pid and not self.delay:
                pid = self.proc.spawn()
                if not pid:
                    # Can't fork.  Try again later...
                    self.delay = time.time() + self.backofflimit
            if self.waitstatus:
                self.reportstatus()
            r, w, x = [self.mastersocket], [], []
            if self.commandsocket:
                r.append(self.commandsocket)
            timeout = self.options.backofflimit
            if self.delay:
                timeout = max(0, min(timeout, self.delay - time.time()))
                if timeout <= 0:
                    self.delay = 0
                    if self.killing and self.proc.pid:
                        self.proc.kill(signal.SIGKILL)
                        self.delay = time.time() + self.options.backofflimit
            try:
                r, w, x = select.select(r, w, x, timeout)
            except select.error, err:
                if err[0] != errno.EINTR:
                    raise
                r = w = x = []
            if self.waitstatus:
                self.reportstatus()
            if self.commandsocket and self.commandsocket in r:
                try:
                    self.dorecv()
                except socket.error, msg:
                    log.exception("socket.error in dorecv(): %s", str(msg))
                    self.commandsocket = None
            if self.mastersocket in r:
                try:
                    self.doaccept()
                except socket.error, msg:
                    log.exception("socket.error in doaccept(): %s", str(msg))
                    self.commandsocket = None
        log.info("Exiting")
        sys.exit(0)

    def reportstatus(self):
        pid, sts = self.waitstatus
        self.waitstatus = None
        es, msg = decode_wait_status(sts)
        msg = "pid %d: " % pid + msg
        if pid != self.proc.pid:
            msg = "unknown " + msg
            log.warn(msg)
        else:
            killing = self.killing
            if killing:
                self.killing = 0
                self.delay = 0
            else:
                self.governor()
            self.proc.setstatus(sts)
            if es in self.options.exitcodes and not killing:
                msg = msg + "; exiting now"
                log.info(msg)
                sys.exit(es)
            log.info(msg)

    backoff = 0

    def governor(self):
        # Back off if respawning too frequently
        now = time.time()
        if not self.proc.lasttime:
            pass
        elif now - self.proc.lasttime < self.options.backofflimit:
            # Exited rather quickly; slow down the restarts
            self.backoff += 1
            if self.backoff >= self.options.backofflimit:
                if self.options.forever:
                    self.backoff = self.options.backofflimit
                else:
                    log.critical("restarting too frequently; quit")
                    sys.exit(1)
            log.info("sleep %s to avoid rapid restarts", self.backoff)
            self.delay = now + self.backoff
        else:
            # Reset the backoff timer
            self.backoff = 0
            self.delay = 0

    def doaccept(self):
        if self.commandsocket:
            # Give up on previous command socket!
            self.sendreply("Command superseded by new command")
            self.commandsocket.close()
            self.commandsocket = None
        self.commandsocket, addr = self.mastersocket.accept()
        self.commandbuffer = ""

    def dorecv(self):
        data = self.commandsocket.recv(1000)
        if not data:
            self.sendreply("Command not terminated by newline")
            self.commandsocket.close()
            self.commandsocket = None
        self.commandbuffer += data
        if "\n" in self.commandbuffer:
            self.docommand()
            self.commandsocket.close()
            self.commandsocket = None
        elif len(self.commandbuffer) > 10000:
            self.sendreply("Command exceeds 10 KB")
            self.commandsocket.close()
            self.commandsocket = None

    def docommand(self):
        lines = self.commandbuffer.split("\n")
        args = lines[0].split()
        if not args:
            self.sendreply("Empty command")
            return
        command = args[0]
        methodname = "cmd_" + command
        method = getattr(self, methodname, None)
        if method:
            method(args)
        else:
            self.sendreply("Unknown command %r; 'help' for a list" % args[0])

    def cmd_start(self, args):
        self.mood = 1 # Up
        self.backoff = 0
        self.delay = 0
        self.killing = 0
        if not self.proc.pid:
            self.proc.spawn()
            self.sendreply("Application started")
        else:
            self.sendreply("Application already started")

    def cmd_stop(self, args):
        self.mood = 0 # Down
        self.backoff = 0
        self.delay = 0
        self.killing = 0
        if self.proc.pid:
            self.proc.kill(signal.SIGTERM)
            self.sendreply("Sent SIGTERM")
            self.killing = 1
            self.delay = time.time() + self.options.backofflimit
        else:
            self.sendreply("Application already stopped")

    def cmd_restart(self, args):
        self.mood = 1 # Up
        self.backoff = 0
        self.delay = 0
        self.killing = 0
        if self.proc.pid:
            self.proc.kill(signal.SIGTERM)
            self.sendreply("Sent SIGTERM; will restart later")
            self.killing = 1
            self.delay = time.time() + self.options.backofflimit
        else:
            self.proc.spawn()
            self.sendreply("Application started")

    def cmd_exit(self, args):
        self.mood = -1 # Suicidal
        self.backoff = 0
        self.delay = 0
        self.killing = 0
        if self.proc.pid:
            self.proc.kill(signal.SIGTERM)
            self.sendreply("Sent SIGTERM; will exit later")
            self.killing = 1
            self.delay = time.time() + self.options.backofflimit
        else:
            self.sendreply("Exiting now")
            log.info("Exiting")
            sys.exit(0)

    def cmd_kill(self, args):
        if args[1:]:
            try:
                sig = int(args[1])
            except:
                self.sendreply("Bad signal %r" % args[1])
                return
        else:
            sig = signal.SIGTERM
        if not self.proc.pid:
            self.sendreply("Application not running")
        else:
            msg = self.proc.kill(sig)
            if msg:
                self.sendreply("Kill %d failed: %s" % (sig, msg))
            else:
                self.sendreply("Signal %d sent" % sig)

    def cmd_status(self, args):
        if not self.proc.pid:
            status = "stopped"
        else:
            status = "running"
        self.sendreply("status=%s\n" % status +
                       "now=%r\n" % time.time() +
                       "mood=%d\n" % self.mood +
                       "delay=%r\n" % self.delay +
                       "backoff=%r\n" % self.backoff +
                       "lasttime=%r\n" % self.proc.lasttime +
                       "application=%r\n" % self.proc.pid +
                       "manager=%r\n" % os.getpid() + 
                       "backofflimit=%r\n" % self.options.backofflimit +
                       "filename=%r\n" % self.proc.filename +
                       "args=%r\n" % self.proc.args)

    def cmd_help(self, args):
        self.sendreply(
            "Available commands:\n"
            "  help -- return command help\n"
            "  status -- report application status (default command)\n"
            "  kill [signal] -- send a signal to the application\n"
            "                   (default signal is SIGTERM)\n"
            "  start -- start the application if not already running\n"
            "  stop -- stop the application if running\n"
            "          (the daemon manager keeps running)\n"
            "  restart -- stop followed by start\n"
            "  exit -- stop the application and exit\n"
            )

    def sendreply(self, msg):
        try:
            if not msg.endswith("\n"):
                msg = msg + "\n"
            if hasattr(self.commandsocket, "sendall"):
                self.commandsocket.sendall(msg)
            else:
                # This is quadratic, but msg is rarely more than 100 bytes :-)
                while msg:
                    sent = self.commandsocket.send(msg)
                    msg = msg[sent:]
        except socket.error, msg:
            log.warn("Error sending reply: %s", str(msg))

# Helpers for dealing with signals and exit status

def decode_wait_status(sts):
    """Decode the status returned by wait() or waitpid().
    
    Return a tuple (exitstatus, message) where exitstatus is the exit
    status, or -1 if the process was killed by a signal; and message
    is a message telling what happened.  It is the caller's
    responsibility to display the message.
    """
    if os.WIFEXITED(sts):
        es = os.WEXITSTATUS(sts) & 0xffff
        msg = "exit status %s" % es
        return es, msg
    elif os.WIFSIGNALED(sts):
        sig = os.WTERMSIG(sts)
        msg = "terminated by %s" % signame(sig)
        if hasattr(os, "WCOREDUMP"):
            iscore = os.WCOREDUMP(sts)
        else:
            iscore = sts & 0x80
        if iscore:
            msg += " (core dumped)"
        return -1, msg
    else:
        msg = "unknown termination cause 0x%04x" % sts
        return -1, msg

_signames = None

def signame(sig):
    """Return a symbolic name for a signal.

    Return "signal NNN" if there is no corresponding SIG name in the
    signal module.
    """

    if _signames is None:
        _init_signames()
    return _signames.get(sig) or "signal %d" % sig

def _init_signames():
    global _signames
    d = {}
    for k, v in signal.__dict__.items():
        k_startswith = getattr(k, "startswith", None)
        if k_startswith is None:
            continue
        if k_startswith("SIG") and not k_startswith("SIG_"):
            d[v] = k
    _signames = d

def get_path():
    """Return a list corresponding to $PATH, or a default."""
    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

# Main program

def main(args=None):
    assert os.name == "posix", "This code makes many Unix-specific assumptions"
    d = Daemonizer()
    d.main(args)

if __name__ == "__main__":
    main()


=== Packages3/zdaemon/__init__.py 1.6 => 1.7 ===
--- Packages3/zdaemon/__init__.py:1.6	Fri Dec 20 13:07:37 2002
+++ Packages3/zdaemon/__init__.py	Thu Jun 19 11:44:34 2003
@@ -13,3 +13,7 @@
 #
 ##############################################################################
 """zdaemon -- a package to manage a daemon application."""
+
+def run(*args):
+    import zdaemon.Daemon
+    zdaemon.Daemon.run(*args)


=== Packages3/zdaemon/zdctl.py 1.9 => 1.10 ===
--- Packages3/zdaemon/zdctl.py:1.9	Fri Dec 13 12:10:02 2002
+++ Packages3/zdaemon/zdctl.py	Thu Jun 19 11:44:34 2003
@@ -1,4 +1,4 @@
-#! /usr/bin/env python
+#!python
 ##############################################################################
 #
 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
@@ -9,30 +9,37 @@
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
 """zdctl -- control an application run by zdaemon.
 
-Usage: python zdctl.py -C config-file [action [arguments]]
+Usage: python zdctl.py [-C URL] [-h] [-p PROGRAM]
+       [zdrun-options] [action [arguments]]
 
 Options:
 -C/--configuration URL -- configuration file or URL
+-h/--help -- print usage message and exit
+-b/--backoff-limit SECONDS -- set backoff limit to SECONDS (default 10)
+-d/--daemon -- run as a proper daemon; fork a subprocess, close files etc.
+-f/--forever -- run forever (by default, exit when backoff limit is exceeded)
+-h/--help -- print this usage message and exit
+-i/--interactive -- start an interactive shell after executing commands
+-l/--logfile -- log file to be read by logtail command
+-p/--program PROGRAM -- the program to run
+-s/--socket-name SOCKET -- Unix socket name for client (default "zdsock")
+-u/--user USER -- run as this user (or numeric uid)
+-x/--exit-codes LIST -- list of fatal exit codes (default "0,2")
+-z/--directory DIRECTORY -- directory to chdir to when using -d (default off)
 action [arguments] -- see below
 
-If no action is specified on the command line, a "shell" interpreting
-actions typed interactively is started.
-
-Use the action "help" to find out about available actions.
+Actions are commands like "start", "stop" and "status".  If -i is
+specified or no action is specified on the command line, a "shell"
+interpreting actions typed interactively is started (unless the
+configuration option default_to_interactive is set to false).  Use the
+action "help" to find out about available actions.
 """
 
-from __future__ import nested_scopes
-
-# XXX Related code lives in lib/python/Controller/ZctlLib.py on the
-# 'chrism-install-branch' branch.
-# The code there knows more about Zope and about Windows, but doesn't
-# use zdaemon.py or ZConfig.
-
 import os
 import re
 import cmd
@@ -40,57 +47,70 @@
 import time
 import signal
 import socket
+import stat
 
 if __name__ == "__main__":
     # Add the parent of the script directory to the module search path
-    from os.path import dirname, abspath, normpath
-    sys.path.append(dirname(dirname(normpath(abspath(sys.argv[0])))))
-
-from ZEO.runsvr import Options
-
-
-class ZDOptions(Options):
-
-    # Where's python?
-    python = sys.executable
-
-    # Where's zdaemon?
-    if __name__ == "__main__":
-        _file = sys.argv[0]
-    else:
-        _file = __file__
-    _file = os.path.normpath(os.path.abspath(_file))
-    _dir = os.path.dirname(_file)
-    zdaemon = os.path.join(_dir, "zdaemon.py")
-
-    # Options for zdaemon
-    backofflimit = 10                   # -b SECONDS
-    forever = 0                         # -f
-    sockname = os.path.abspath("zdsock") # -s SOCKET
-    exitcodes = [0, 2]                  # -x LIST
-    user = None                         # -u USER
-    zdirectory = "/"                    # -z DIRECTORY
-
-    # Program (and arguments) for zdaemon
-    program = None
-
-    def load_configuration(self):
-        Options.load_configuration(self) # Sets self.rootconf
-        if not self.rootconf:
-            self.usage("a configuration file is required; use -C")
-        # XXX Should allow overriding more zdaemon options here
-        if self.program is None:
-            self.program = self.rootconf.getlist("program")
-        if self.program is None:
-            self.usage("no program specified in configuration")
+    # (but only when the script is run from inside the zdaemon package)
+    from os.path import dirname, basename, abspath, normpath
+    scriptdir = dirname(normpath(abspath(sys.argv[0])))
+    if basename(scriptdir).lower() == "zdaemon":
+        sys.path.append(dirname(scriptdir))
+
+import ZConfig
+from zdaemon.zdoptions import RunnerOptions
+
+
+def string_list(arg):
+    return arg.split()
+
+
+class ZDCtlOptions(RunnerOptions):
+
+    positional_args_allowed = 1
+
+    def __init__(self):
+        RunnerOptions.__init__(self)
+        self.add("interactive", None, "i", "interactive", flag=1)
+        self.add("default_to_interactive", "runner.default_to_interactive",
+                 default=1)
+        self.add("program", "runner.program", "p:", "program=",
+                 handler=string_list,
+                 required="no program specified; use -p or -C")
+        self.add("logfile", "runner.logfile", "l:", "logfile=")
+        self.add("python", "runner.python")
+        self.add("zdrun", "runner.zdrun")
+        self.add("prompt", "runner.prompt")
+
+    def realize(self, *args, **kwds):
+        RunnerOptions.realize(self, *args, **kwds)
+
+        # Maybe the config file requires -i or positional args
+        if not self.args and not self.interactive:
+            if not self.default_to_interactive:
+                self.usage("either -i or an action argument is required")
+            self.interactive = 1
+
+        # Where's python?
+        if not self.python:
+            self.python = sys.executable
+
+        # Where's zdrun?
+        if not self.zdrun:
+            if __name__ == "__main__":
+                file = sys.argv[0]
+            else:
+                file = __file__
+            file = os.path.normpath(os.path.abspath(file))
+            dir = os.path.dirname(file)
+            self.zdrun = os.path.join(dir, "zdrun.py")
 
 
 class ZDCmd(cmd.Cmd):
 
-    prompt = "(zdctl) "
-
     def __init__(self, options):
         self.options = options
+        self.prompt = self.options.prompt + ' '
         cmd.Cmd.__init__(self)
         self.get_status()
         if self.zd_status:
@@ -99,12 +119,17 @@
                 s = m.group(1)
                 args = eval(s, {"__builtins__": {}})
                 if args != self.options.program:
-                    print "WARNING! zdaemon is managing a different program!"
+                    print "WARNING! zdrun is managing a different program!"
                     print "our program   =", self.options.program
                     print "daemon's args =", args
 
+    def emptyline(self):
+        # We don't want a blank line to repeat the last command.
+        # Showing status is a nice alternative.
+        self.do_status()
+
     def send_action(self, action):
-        """Send an action to the zdaemon server and return the response.
+        """Send an action to the zdrun server and return the response.
 
         Return None if the server is not up or any other error happened.
         """
@@ -155,24 +180,35 @@
         print "help          -- Print a list of available actions."
         print "help <action> -- Print help for <action>."
 
+    def do_EOF(self, arg):
+        print
+        return 1
+
+    def help_EOF(self):
+        print "To quit, type ^D or use the quit command."
+
     def do_start(self, arg):
         self.get_status()
         if not self.zd_up:
             args = [
                 self.options.python,
-                self.options.zdaemon,
-                "-b", str(self.options.backofflimit),
-                "-d",
-                "-s", self.options.sockname,
-                "-x", ",".join(map(str, self.options.exitcodes)),
-                "-z", self.options.zdirectory,
+                self.options.zdrun,
                 ]
-            if self.options.forever:
-                args.append("-f")
-            if self.options.user:
-                argss.extend(["-u", str(self.options.user)])
+            args += self._get_override("-C", "configfile")
+            args += self._get_override("-b", "backofflimit")
+            args += self._get_override("-d", "daemon", flag=1)
+            args += self._get_override("-f", "forever", flag=1)
+            args += self._get_override("-s", "sockname")
+            args += self._get_override("-u", "user")
+            args += self._get_override(
+                "-x", "exitcodes", ",".join(map(str, self.options.exitcodes)))
+            args += self._get_override("-z", "directory")
             args.extend(self.options.program)
-            os.spawnvp(os.P_WAIT, args[0], args)
+            if self.options.daemon:
+                flag = os.P_WAIT
+            else:
+                flag = os.P_NOWAIT
+            os.spawnvp(flag, args[0], args)
         elif not self.zd_pid:
             self.send_action("start")
         else:
@@ -181,6 +217,33 @@
         self.awhile(lambda: self.zd_pid,
                     "daemon process started, pid=%(zd_pid)d")
 
+    def _get_override(self, opt, name, svalue=None, flag=0):
+        value = getattr(self.options, name)
+        if value is None:
+            return []
+        configroot = self.options.configroot
+        if configroot is not None:
+            for n, cn in self.options.names_list:
+                if n == name and cn:
+                    v = configroot
+                    for p in cn.split("."):
+                        v = getattr(v, p, None)
+                        if v is None:
+                            break
+                    if v == value: # We didn't override anything
+                        return []
+                    break
+        if flag:
+            if value:
+                args = [opt]
+            else:
+                args = []
+        else:
+            if svalue is None:
+                svalue = str(value)
+            args = [opt, svalue]
+        return args
+
     def help_start(self):
         print "start -- Start the daemon process."
         print "         If it is already running, do nothing."
@@ -245,6 +308,9 @@
         print "wait -- Wait for the daemon process to exit."
 
     def do_status(self, arg=""):
+        if arg not in ["", "-l"]:
+            print "status argument must be absent or -l"
+            return
         self.get_status()
         if not self.zd_up:
             print "daemon manager not running"
@@ -259,6 +325,63 @@
         print "status [-l] -- Print status for the daemon process."
         print "               With -l, show raw status output as well."
 
+    def do_show(self, arg):
+        if not arg:
+            arg = "options"
+        try:
+            method = getattr(self, "show_" + arg)
+        except AttributeError, err:
+            print err
+            self.help_show()
+            return
+        method()
+
+    def show_options(self):
+        print "zdctl/zdrun options:"
+        print "schemafile:  ", repr(self.options.schemafile)
+        print "configfile:  ", repr(self.options.configfile)
+        print "interactive: ", repr(self.options.interactive)
+        print "default_to_interactive:",
+        print                  repr(self.options.default_to_interactive)
+        print "zdrun:       ", repr(self.options.zdrun)
+        print "python:      ", repr(self.options.python)
+        print "program:     ", repr(self.options.program)
+        print "backofflimit:", repr(self.options.backofflimit)
+        print "daemon:      ", repr(self.options.daemon)
+        print "forever:     ", repr(self.options.forever)
+        print "sockname:    ", repr(self.options.sockname)
+        print "exitcodes:   ", repr(self.options.exitcodes)
+        print "user:        ", repr(self.options.user)
+        print "directory:   ", repr(self.options.directory)
+        print "logfile:     ", repr(self.options.logfile)
+        print "hang_around: ", repr(self.options.hang_around)
+
+    def show_python(self):
+        print "Python info:"
+        version = sys.version.replace("\n", "\n              ")
+        print "Version:     ", version
+        print "Platform:    ", sys.platform
+        print "Executable:  ", repr(sys.executable)
+        print "Arguments:   ", repr(sys.argv)
+        print "Directory:   ", repr(os.getcwd())
+        print "Path:"
+        for dir in sys.path:
+            print "    " + repr(dir)
+
+    def show_all(self):
+        self.show_options()
+        print
+        self.show_python()
+
+    def help_show(self):
+        print "show options -- show zdctl options"
+        print "show python -- show Python version and details"
+        print "show all -- show all of the above"
+
+    def complete_show(self, text, *ignored):
+        options = ["options", "python", "all"]
+        return [x for x in options if x.startswith(text)]
+
     def do_logreopen(self, arg):
         self.do_kill(str(signal.SIGUSR2))
 
@@ -266,6 +389,91 @@
         print "logreopen -- Send a SIGUSR2 signal to the daemon process."
         print "             This is designed to reopen the log file."
 
+    def do_logtail(self, arg):
+        if not arg:
+            arg = self.options.logfile
+            if not arg:
+                print "No default log file specified; use logtail <logfile>"
+                return
+        try:
+            helper = TailHelper(arg)
+            helper.tailf()
+        except KeyboardInterrupt:
+            print
+        except IOError, msg:
+            print msg
+        except OSError, msg:
+            print msg
+
+    def help_logtail(self):
+        print "logtail [logfile] -- Run tail -f on the given logfile."
+        print "                     A default file may exist."
+        print "                     Hit ^C to exit this mode."
+
+    def do_shell(self, arg):
+        if not arg:
+            arg = os.getenv("SHELL") or "/bin/sh"
+        try:
+            os.system(arg)
+        except KeyboardInterrupt:
+            print
+
+    def help_shell(self):
+        print "shell [command] -- Execute a shell command."
+        print "                   Without a command, start an interactive sh."
+        print "An alias for this command is ! [command]"
+
+    def do_reload(self, arg):
+        if arg:
+            args = arg.split()
+            if self.options.configfile:
+                args = ["-C", self.options.configfile] + args
+        else:
+            args = None
+        options = ZDCtlOptions()
+        options.positional_args_allowed = 0
+        try:
+            options.realize(args)
+        except SystemExit:
+            print "Configuration not reloaded"
+        else:
+            self.options = options
+            if self.options.configfile:
+                print "Configuration reloaded from", self.options.configfile
+            else:
+                print "Configuration reloaded without a config file"
+
+    def help_reload(self):
+        print "reload [options] -- Reload the configuration."
+        print "    Without options, this reparses the command line."
+        print "    With options, this substitutes 'options' for the"
+        print "    command line, except that if no -C option is given,"
+        print "    the last configuration file is used."
+
+    def do_foreground(self, arg):
+        self.get_status()
+        pid = self.zd_pid
+        if pid:
+            print "To run the program in the foreground, please stop it first."
+            return
+        program = " ".join(self.options.program)
+        program = "\n".join (["export EVENT_LOG_FILE",
+                              "EVENT_LOG_FILE=",
+                              program])
+        print program
+        try:
+            os.system(program)
+        except KeyboardInterrupt:
+            print
+
+    do_fg = do_foreground
+
+    def help_foreground(self):
+        print "foreground -- Run the program in the forground."
+        print "fg -- an alias for foreground."
+
+    help_fg = help_foreground
+
     def do_quit(self, arg):
         self.get_status()
         if not self.zd_up:
@@ -280,15 +488,79 @@
 
     def help_quit(self):
         print "quit -- Exit the zdctl shell."
-        print ("        If the daemon process is not running, "
-               "stop the daemon manager.")
+        print "        If the daemon process is not running,"
+        print "        stop the daemon manager."
+
+
+class TailHelper:
+
+    MAX_BUFFSIZE = 1024
+
+    def __init__(self, fname):
+        self.f = open(fname, 'r')
+
+    def tailf(self):
+        sz, lines = self.tail(10)
+        for line in lines:
+            sys.stdout.write(line)
+            sys.stdout.flush()
+        while 1:
+            newsz = self.fsize()
+            bytes_added = newsz - sz
+            if bytes_added < 0:
+                sz = 0
+                print "==> File truncated <=="
+                bytes_added = newsz
+            if bytes_added > 0:
+                self.f.seek(-bytes_added, 2)
+                bytes = self.f.read(bytes_added)
+                sys.stdout.write(bytes)
+                sys.stdout.flush()
+                sz = newsz
+            time.sleep(1)
+
+    def tail(self, max=10):
+        self.f.seek(0, 2)
+        pos = sz = self.f.tell()
+
+        lines = []
+        bytes = []
+        num_bytes = 0
+
+        while 1:
+            if pos == 0:
+                break
+            self.f.seek(pos)
+            byte = self.f.read(1)
+            if byte == '\n':
+                if len(lines) == max:
+                    break
+                bytes.reverse()
+                line = ''.join(bytes)
+                line and lines.append(line)
+                bytes = []
+            bytes.append(byte)
+            num_bytes = num_bytes + 1
+            if num_bytes > self.MAX_BUFFSIZE:
+                break
+            pos = pos - 1
+        lines.reverse()
+        return sz, lines
+
+    def fsize(self):
+        return os.fstat(self.f.fileno())[stat.ST_SIZE]
 
 def main(args=None):
-    options = ZDOptions(args)
+    options = ZDCtlOptions()
+    options.realize(args)
     c = ZDCmd(options)
     if options.args:
         c.onecmd(" ".join(options.args))
-    else:
+    if options.interactive:
+        try:
+            import readline
+        except ImportError:
+            pass
         print "program:", " ".join(options.program)
         c.do_status()
         c.cmdloop()

=== Removed File Packages3/zdaemon/zdaemon.py ===

=== Removed File Packages3/zdaemon/zdctl.sh ===