Merge branch 'master' into python3
authorFredrik Tolf <fredrik@dolda2000.com>
Fri, 23 Dec 2011 00:53:15 +0000 (01:53 +0100)
committerFredrik Tolf <fredrik@dolda2000.com>
Fri, 23 Dec 2011 00:53:15 +0000 (01:53 +0100)
Conflicts:
pdm/cli.py
pdm/srv.py

pdm/__init__.py
pdm/cli.py
pdm/srv.py

index e69de29..383b2e1 100644 (file)
@@ -0,0 +1,31 @@
+"""Python Daemon Management
+
+This package aids in inspecting and managing daemon processes. A
+program intended for running as a daemon may create PDM listeners, to
+which PDM clients may connect in order to interact with the
+process.
+
+This package contains the following modules:
+
+ * srv -- Server module
+ * cli -- Client module
+ * perf -- Library for implementing object for the PERF protocol
+
+The protocol allows multiple management subprotocols for different
+modes of operation. Currently, the following two management protocols
+are supported.
+
+ * The REPL protocol implements a simple read-eval-print loop which
+   accepts arbitrary Python code, executes it in the daemon process,
+   and returns its replies, all in text form. The protocol is simple,
+   generic, and has few failure modes, but is hardly suitable for
+   programmatic interaction. See the documentation for pdm.srv.repl
+   and pdm.cli.replclient for further details.
+
+ * The PERF protocol is intended for programmatic interaction with the
+   daemon process. Various Python modules may expose objects that
+   implement one or several of a few pre-defined interfaces that allow
+   for various forms of inspection and management of the program
+   state. See the documentation for pdm.srv.perf and
+   pdm.cli.perfclient for further details.
+"""
index 6af06f1..9924d40 100644 (file)
@@ -6,9 +6,10 @@ provided in the pdm.srv module.
 
 import socket, pickle, struct, select, threading
 
-__all__ = ["client", "replclient"]
+__all__ = ["client", "replclient", "perfclient"]
 
 class protoerr(Exception):
+    """Raised on protocol errors"""
     pass
 
 def resolve(spec):
@@ -35,7 +36,26 @@ def resolve(spec):
     return rv
 
 class client(object):
+    """PDM client
+
+    This class provides general facilities to speak to PDM servers,
+    and is mainly intended to be subclassed to provide for the
+    specific protocols, such as replclient and perfclient do.
+
+    `client' instances can be passed as arguments to select.select(),
+    and can be used in `with' statements.
+    """
     def __init__(self, sk, proto = None):
+        """Create a client object connected to the specified
+        server. `sk' can either be a socket object, which is used as
+        it is, or a string specification very similar to the
+        specification for pdm.srv.listen, so see its documentation for
+        details. The differences are only that this function does not
+        take arguments specific to socket creation, like the mode and
+        group arguments for Unix sockets. If `proto' is given, that
+        subprotocol will negotiated with the server (by calling the
+        select() method).
+        """
         self.sk = resolve(sk)
         self.buf = b""
         line = self.readline()
@@ -45,12 +65,15 @@ class client(object):
             self.select(proto)
 
     def close(self):
+        """Close this connection"""
         self.sk.close()
 
     def fileno(self):
+        """Return the file descriptor of the underlying socket."""
         return self.sk.fileno()
 
     def readline(self):
+        """Read a single NL-terminated line and return it."""
         while True:
             p = self.buf.find(b"\n")
             if p >= 0:
@@ -63,6 +86,7 @@ class client(object):
             self.buf += ret
 
     def select(self, proto):
+        """Negotiate the given subprotocol with the server"""
         if isinstance(proto, str):
             proto = proto.encode("ascii")
         if b"\n" in proto:
@@ -80,10 +104,20 @@ class client(object):
         return False
 
 class replclient(client):
+    """REPL protocol client
+    
+    Implements the client side of the REPL protocol; see pdm.srv.repl
+    for details on the protocol and its functionality.
+    """
     def __init__(self, sk):
+        """Create a connected client as documented in the `client' class."""
         super().__init__(sk, "repl")
 
     def run(self, code):
+        """Run a single block of Python code on the server. Returns
+        the output of the command (as documented in pdm.srv.repl) as a
+        string.
+        """
         while True:
             ncode = code.replace("\n\n", "\n")
             if ncode == code: break
@@ -155,8 +189,13 @@ class perfproxy(object):
             except: pass
 
     def close(self):
-        self.cl.run("unbind", self.id)
-        del self.cl.proxies[self.id]
+        if self.id is not None:
+            self.cl.run("unbind", self.id)
+            del self.cl.proxies[self.id]
+            self.id = None
+
+    def __del__(self):
+        self.close()
 
     def __enter__(self):
         return self
@@ -166,7 +205,24 @@ class perfproxy(object):
         return False
 
 class perfclient(client):
+    """PERF protocol client
+    
+    Implements the client side of the PERF protocol; see pdm.srv.perf
+    for details on the protocol and its functionality.
+
+    This client class implements functions for finding PERF objects on
+    the server, and returns, for each server-side object looked up, a
+    proxy object that mimics exactly the PERF interfaces that the
+    object implements. As the proxy objects reference live objects on
+    the server, they should be released when they are no longer used;
+    they implement a close() method for that purpose, and can also be
+    used in `with' statements.
+
+    See pdm.srv.perf for details on the various PERF interfaces that
+    the proxy objects might implement.
+    """
     def __init__(self, sk):
+        """Create a connected client as documented in the `client' class."""
         super().__init__(sk, "perf")
         self.nextid = 0
         self.lock = threading.Lock()
@@ -196,6 +252,12 @@ class perfclient(client):
         proxy.notify(ev)
 
     def dispatch(self, timeout = None):
+        """Wait for an incoming notification from the server, and
+        dispatch it to the callback functions that have been
+        registered for it. If `timeout' is specified, wait no longer
+        than so many seconds; otherwise, wait forever. This client
+        object may also be used as argument to select.select().
+        """
         rfd, wfd, efd = select.select([self.sk], [], [], timeout)
         if self.sk in rfd:
             msg = self.recv()
@@ -227,6 +289,10 @@ class perfclient(client):
             self.lock.release()
 
     def lookup(self, module, obnm):
+        """Look up a single server-side object by the given name in
+        the given module. Will return a new proxy object for each
+        call when called multiple times for the same name.
+        """
         self.lock.acquire()
         try:
             id = self.nextid
@@ -239,6 +305,17 @@ class perfclient(client):
         return proxy
 
     def find(self, name):
+        """Convenience function for looking up server-side objects
+        through PERF directories and for multiple uses. The object
+        name can be given as "MODULE.OBJECT", which will look up the
+        named OBJECT in the named MODULE, and can be followed by any
+        number of slash-separated names, which will assume that the
+        object to the left of the slash is a PERF directory, and will
+        return the object in that directory by the name to the right
+        of the slash. For instance, find("pdm.perf.sysres/cputime")
+        will return the built-in attribute for reading the CPU time
+        used by the server process.
+        """
         ret = self.names.get(name)
         if ret is None:
             if "/" in name:
index e2aef4e..3ddc682 100644 (file)
@@ -1,18 +1,40 @@
-"""Management for daemon processes
+"""Python Daemon Management -- Server functions
 
-This module contains a utility to listen for management commands on a
-socket, lending itself to managing daemon processes.
+This module implements the server part of the PDM protocols. The
+primary object of interest herein is the listen() function, which is
+the most generic way to create PDM listeners based on user
+configuration, and the documentation for the repl and perf classes,
+which describes the functioning of the REPL and PERF protocols.
 """
 
 import os, sys, socket, threading, grp, select
 import types, pprint, traceback
 import pickle, struct
 
-__all__ = ["listener", "unixlistener", "tcplistener", "listen"]
+__all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"]
 
 protocols = {}
 
 class repl(object):
+    """REPL protocol handler
+    
+    Provides a read-eval-print loop. The primary client-side interface
+    is the pdm.cli.replclient class. Clients can send arbitrary code,
+    which is compiled and run on its own thread in the server process,
+    and output responses that are echoed back to the client.
+
+    Each client is provided with its own module, in which the code
+    runs. The module is prepared with a function named `echo', which
+    takes a single object and pretty-prints it as part of the command
+    response. If a command can be parsed as an expression, the value
+    it evaluates to is automatically echoed to the client. If the
+    evalution of the command terminates with an exception, its
+    traceback is echoed to the client.
+
+    The REPL protocol is only intended for interactive usage. In order
+    to interact programmatically with the server process, see the PERF
+    protocol instead.
+    """
     def __init__(self, cl):
         self.cl = cl
         self.mod = types.ModuleType("repl")
@@ -54,6 +76,97 @@ class repl(object):
 protocols["repl"] = repl
 
 class perf(object):
+    """PERF protocol handler
+    
+    The PERF protocol provides an interface for program interaction
+    with the server process. It allows limited remote interactions
+    with Python objects over a few defined interfaces.
+
+    All objects that wish to be available for interaction need to
+    implement a method named `pdm_protocols' which, when called with
+    no arguments, should return a list of strings, each indicating a
+    PERF interface that the object implements. For each such
+    interface, the object must implement additional methods as
+    described below.
+
+    A client can find PERF objects to interact with either by
+    specifying the name of such an object in an existing module, or by
+    using the `dir' interface, described below. Thus, to make a PERF
+    object available for clients, it needs only be bound to a global
+    variable in a module and implement the `pdm_protocols'
+    method. When requesting an object from a module, the module must
+    already be imported. PDM will not import new modules for clients;
+    rather, the daemon process needs to import all modules that
+    clients should be able to interact with. PDM itself always imports
+    the pdm.perf module, which contains a few basic PERF objects. See
+    its documentation for details.
+
+    The following interfaces are currently known to PERF.
+
+     * attr:
+       An object that implements the `attr' interface models an
+       attribute that can be read by clients. The attribute can be
+       anything, as long as its representation can be
+       pickled. Examples of attributes could be such things as the CPU
+       time consumed by the server process, or the number of active
+       connections to whatever clients the program serves. To
+       implement the `attr' interface, an object must implement
+       methods called `readattr' and `attrinfo'. `readattr' is called
+       with no arguments to read the current value of the attribute,
+       and `attrinfo' is called with no arguments to read a
+       description of the attribute. Both should be
+       idempotent. `readattr' can return any pickleable object, and
+       `attrinfo' should return either None to indicate that it has no
+       description, or an instance of the pdm.perf.attrinfo class.
+
+     * dir:
+       The `dir' interface models a directory of other PERF
+       objects. An object implementing it must implement methods
+       called `lookup' and `listdir'. `lookup' is called with a single
+       string argument that names an object, and should either return
+       another PERF object based on the name, or raise KeyError if it
+       does not recognize the name. `listdir' is called with no
+       arguments, and should return a list of known names that can be
+       used as argument to `lookup', but the list is not required to
+       be exhaustive and may also be empty.
+
+     * invoke:
+       The `invoke' interface allows a more arbitrary form of method
+       calls to objects implementing it. Such objects must implement a
+       method called `invoke', which is called with one positional
+       argument naming a method to be called (which it is free to
+       interpret however it wishes), and with any additional
+       positional and keyword arguments that the client wishes to pass
+       to it. Whatever `invoke' returns is pickled and sent back to
+       the client. In case the method name is not recognized, `invoke'
+       should raise an AttributeError.
+
+     * event:
+       The `event' interface allows PERF objects to notify clients of
+       events asynchronously. Objects implementing it must implement
+       methods called `subscribe' and `unsubscribe'. `subscribe' will
+       be called with a single argument, which is a callable of one
+       argument, which should be registered to be called when an event
+       pertaining to the `event' object in question occurs. The
+       `event' object should then call all such registered callables
+       with a single argument describing the event. The argument could
+       be any object that can be pickled, but should be an instance of
+       a subclass of the pdm.perf.event class. If `subscribe' is
+       called with a callback object that it has already registered,
+       it should raise a ValueError. `unsubscribe' is called with a
+       single argument, which is a previously registered callback
+       object, which should then be unregistered to that it is no
+       longer called when an event occurs. If the given callback
+       object is not, in fact, registered, a ValueError should be
+       raised.
+
+    The pdm.perf module contains a few convenience classes which
+    implements the interfaces, but PERF objects are not required to be
+    instances of them. Any object can implement a PERF interface, as
+    long as it does so as described above.
+
+    The pdm.cli.perfclient class is the client-side implementation.
+    """
     def __init__(self, cl):
         self.cl = cl
         self.odtab = {}
@@ -297,11 +410,19 @@ class client(threading.Thread):
             
 
 class listener(threading.Thread):
+    """PDM listener
+
+    This subclass of a thread listens to PDM connections and handles
+    client connections properly. It is intended to be subclassed by
+    providers of specific domains, such as unixlistener and
+    tcplistener.
+    """
     def __init__(self):
         super().__init__(name = "Management listener")
         self.setDaemon(True)
 
     def listen(self, sk):
+        """Listen for and accept connections."""
         self.running = True
         while self.running:
             rfd, wfd, efd = select.select([sk], [], [sk], 1)
@@ -311,6 +432,11 @@ class listener(threading.Thread):
                     self.accept(nsk, addr)
 
     def stop(self):
+        """Stop listening for client connections
+
+        Tells the listener thread to stop listening, and then waits
+        for it to terminate.
+        """
         self.running = False
         self.join()
 
@@ -319,7 +445,14 @@ class listener(threading.Thread):
         cl.start()
 
 class unixlistener(listener):
+    """Unix socket listener"""
     def __init__(self, name, mode = 0o600, group = None):
+        """Create a listener that will bind to the Unix socket named
+        by `name'. The socket will not actually be bound until the
+        listener is started. The socket will be chmodded to `mode',
+        and if `group' is given, the named group will be set as the
+        owner of the socket.
+        """
         super().__init__()
         self.name = name
         self.mode = mode
@@ -344,7 +477,12 @@ class unixlistener(listener):
                 os.unlink(self.name)
 
 class tcplistener(listener):
+    """TCP socket listener"""
     def __init__(self, port, bindaddr = "127.0.0.1"):
+        """Create a listener that will bind to the given TCP port, and
+        the given local interface. The socket will not actually be
+        bound until the listener is started.
+        """
         super().__init__()
         self.port = port
         self.bindaddr = bindaddr
@@ -359,6 +497,22 @@ class tcplistener(listener):
             sk.close()
 
 def listen(spec):
+    """Create and start a listener according to a string
+    specification. The string specifications can easily be passed from
+    command-line options, user configuration or the like. Currently,
+    the two following specification formats are recognized:
+
+    PATH[:MODE[:GROUP]] -- PATH must contain at least one slash. A
+    Unix socket listener will be created listening to that path, and
+    the socket will be chmodded to MODE and owned by GROUP. If MODE is
+    not given, it defaults to 0600, and if GROUP is not given, the
+    process' default group is used.
+
+    ADDRESS:PORT -- PORT must be entirely numeric. A TCP socket
+    listener will be created listening to that port, bound to the
+    given local interface address. Since PDM has no authentication
+    support, ADDRESS should probably be localhost.
+    """
     if ":" in spec:
         first = spec[:spec.index(":")]
         last = spec[spec.rindex(":") + 1:]