From 2baf419bbc450cea45029f0c02d329c6a7041f34 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Wed, 2 Mar 2011 10:33:57 +0100 Subject: [PATCH] python: Added some actual documentation(!). --- python/ashd/__init__.py | 5 +++ python/ashd/proto.py | 76 ++++++++++++++++++++++++++++++++ python/ashd/util.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+) diff --git a/python/ashd/__init__.py b/python/ashd/__init__.py index e69de29..c918ad6 100644 --- a/python/ashd/__init__.py +++ b/python/ashd/__init__.py @@ -0,0 +1,5 @@ +"""Base module for ashd(7)-related fucntions. + +This module implements nothing. Please see the ashd.util or ashd.proto +modules. +""" diff --git a/python/ashd/proto.py b/python/ashd/proto.py index 90ba012..92a21ca 100644 --- a/python/ashd/proto.py +++ b/python/ashd/proto.py @@ -1,3 +1,12 @@ +"""Low-level protocol module for ashd(7) + +This module provides primitive functions that speak the raw ashd(7) +protocol. Primarily, it implements the `req' class that is used to +represent ashd requests. The functions it provides can also be used to +create ashd handlers, but unless you require very precise control, the +ashd.util module provides an easier-to-use interface. +""" + import os, socket import htlib @@ -5,6 +14,30 @@ class protoerr(Exception): pass class req(object): + """Represents a single ashd request. Normally, you would not + create instances of this class manually, but receive them from the + recvreq function. + + For the abstract structure of ashd requests, please see the + ashd(7) manual page. This class provides access to the HTTP + method, raw URL, HTTP version and rest string via the `method', + `url', `ver' and `rest' variables respectively. It also implements + a dict-like interface for case-independent access to the HTTP + headers. The raw headers are available as a list of (name, value) + tuples in the `headers' variable. + + For responding, the response socket is available as a standard + Python stream object in the `sk' variable. Again, see the ashd(7) + manpage for what to receive and transmit on the response socket. + + Note that instances of this class contain a reference to the live + socket used for responding to requests, which should be closed + when you are done with the request. The socket can be closed + manually by calling the close() method on this + object. Alternatively, this class implements the resource-manager + interface, so that it can be used in `with' statements. + """ + def __init__(self, method, url, ver, rest, headers, fd): self.method = method self.url = url @@ -15,9 +48,15 @@ class req(object): os.close(fd) def close(self): + "Close this request's response socket." self.sk.close() def __getitem__(self, header): + """Find a HTTP header case-insensitively. For example, + req["Content-Type"] returns the value of the content-type + header regardlessly of whether the client specified it as + "Content-Type", "content-type" or "Content-type". + """ header = header.lower() for key, val in self.headers: if key.lower() == header: @@ -25,6 +64,9 @@ class req(object): raise KeyError(header) def __contains__(self, header): + """Works analogously to the __getitem__ method for checking + header presence case-insensitively. + """ header = header.lower() for key, val in self.headers: if key.lower() == header: @@ -32,9 +74,25 @@ class req(object): return False def dup(self): + """Creates a duplicate of this request, referring to a + duplicate of the response socket. + """ return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.sk.fileno())) def match(self, match): + """If the `match' argument matches exactly the leading part of + the rest string, this method strips that part of the rest + string off and returns True. Otherwise, it returns False + without doing anything. + + This can be used for simple dispatching. For example: + if req.match("foo/"): + handle(req) + elif req.match("bar/"): + handle_otherwise(req) + else: + util.respond(req, "Not found", status = "404 Not Found", ctype = "text/plain") + """ if self.rest[:len(match)] == match: self.rest = self.rest[len(match):] return True @@ -51,6 +109,17 @@ class req(object): return False def recvreq(sock = 0): + """Receive a single ashd request on the specified socket file + descriptor (or standard input if unspecified). + + The returned value is an instance of the `req' class. As per its + description, care should be taken to close() the request when + done, to avoid leaking response sockets. + + This function may either raise on OSError if an error occurs on + the socket, or a ashd.proto.protoerr if the incoming request is + invalidly encoded. + """ data, fd = htlib.recvfd(sock) if fd is None: return None @@ -72,6 +141,13 @@ def recvreq(sock = 0): os.close(fd) def sendreq(sock, req): + """Encode and send a single request to the specified socket file + descriptor using the ashd protocol. The request should be an + instance of the `req' class. + + This function may raise an OSError if an error occurs on the + socket. + """ data = "" data += req.method + '\0' data += req.url + '\0' diff --git a/python/ashd/util.py b/python/ashd/util.py index 15d61b0..2ac7dc0 100644 --- a/python/ashd/util.py +++ b/python/ashd/util.py @@ -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: -- 2.11.0