python: Added some actual documentation(!).
[ashd.git] / python / ashd / util.py
index 15d61b0..2ac7dc0 100644 (file)
@@ -1,7 +1,25 @@
+"""High-level utility module for ashd(7)
+
+This module implements a rather convenient interface for writing ashd
+handlers, wrapping the low-level ashd.proto module.
+"""
+
 import os, socket
 import proto
 
 def stdfork(argv, chinit = None):
+    """Fork a persistent handler process using the `argv' argument
+    list, as per the standard ashd(7) calling convention. For an
+    easier-to-use interface, see the `pchild' class.
+
+    If a callable object of no arguments is provided in the `chinit'
+    argument, it will be called in the child process before exec()'ing
+    the handler program, and can be used to set parameters for the new
+    process, such as working directory, nice level or ulimits.
+
+    Returns the file descriptor of the socket for sending requests to
+    the new child.
+    """
     csk, psk = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
     pid = os.fork()
     if pid == 0:
@@ -22,7 +40,95 @@ def stdfork(argv, chinit = None):
     psk.close()
     return fd
 
+class pchild(object):
+    """class pchild(argv, autorespawn=False, chinit=None)
+
+    Represents a persistent child handler process, started as per the
+    standard ashd(7) calling convention. It will be called with the
+    `argv' argument lest, which should be a list (or other iterable)
+    of strings.
+
+    If `autorespawn' is specified as True, the child process will be
+    automatically restarted if a request cannot be successfully sent
+    to it.
+
+    For a description of the `chinit' argument, see `stdfork'.
+
+    When this child handler should be disposed of, care should be
+    taken to call the close() method to release its socket and let it
+    exit. This class also implements the resource-manager interface,
+    so that it can be used in `with' statements.
+    """
+    
+    def __init__(self, argv, autorespawn = False, chinit = None):
+        self.argv = argv
+        self.chinit = chinit
+        self.fd = -1
+        self.respawn = autorespawn
+        self.spawn()
+
+    def spawn(self):
+        """Start the child handler, or restart it if it is already
+        running. You should not have to call this method manually
+        unless you explicitly want to manage the process' lifecycle.
+        """
+        self.close()
+        self.fd = stdfork(self.argv, self.chinit)
+
+    def close(self):
+        """Close this child handler's socket. For normal child
+        handlers, this will make the program terminate normally.
+        """
+        if self.fd >= 0:
+            os.close(self.fd)
+            self.fd = -1
+
+    def __del__(self):
+        self.close()
+
+    def passreq(self, req):
+        """Pass the specified request (which should be an instance of
+        the ashd.proto.req class) to this child handler. If the child
+        handler fails for some reason, and `autorespawn' was specified
+        as True when creating this handler, one attempt will be made
+        to restart it.
+
+        Note: You still need to close the request normally.
+
+        This method may raise an OSError if the request fails and
+        autorespawning was either not requested, or if the
+        autorespawning fails.
+        """
+        try:
+            proto.sendreq(self.fd, req)
+        except OSError:
+            if self.respawn:
+                self.spawn()
+                proto.sendreq(self.fd, req)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *excinfo):
+        self.close()
+        return False
+
 def respond(req, body, status = ("200 OK"), ctype = "text/html"):
+    """Simple function for conveniently responding to a request.
+
+    Sends the specified body text to the request's response socket,
+    prepending an HTTP header with the appropriate Content-Type and
+    Content-Length headers, and then closes the response socket.
+
+    The `status' argument can be used to specify a non-200 response,
+    and the `ctype' argument can be used to specify a non-HTML
+    MIME-type.
+
+    If `body' is a Unicode object, it will be encoded as UTF-8.
+
+    For example:
+        respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
+    """
     if type(body) == unicode:
         body = body.decode("utf-8")
         if ctype[:5] == "text/" and ctype.find(';') < 0:
@@ -39,6 +145,15 @@ def respond(req, body, status = ("200 OK"), ctype = "text/html"):
         req.close()
 
 def serveloop(handler, sock = 0):
+    """Implements a simple loop for serving requests sequentially, by
+    receiving requests from standard input (or the specified socket),
+    passing them to the specified handler function, and finally making
+    sure to close them. Returns when end-of-file is received on the
+    incoming socket.
+
+    The handler function should be a callable object of one argument,
+    and is called once for each received request.
+    """
     while True:
         req = proto.recvreq(sock)
         if req is None: