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

1  2 
pdm/cli.py
pdm/srv.py

diff --combined pdm/cli.py
@@@ -6,9 -6,10 +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):
      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 = ""
 +        self.buf = b""
          line = self.readline()
 -        if line != "+PDM1":
 +        if line != b"+PDM1":
              raise protoerr("Illegal protocol signature")
          if proto is not None:
              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("\n")
 +            p = self.buf.find(b"\n")
              if p >= 0:
                  ret = self.buf[:p]
                  self.buf = self.buf[p + 1:]
                  return ret
              ret = self.sk.recv(1024)
 -            if ret == "":
 +            if ret == b"":
                  return None
              self.buf += ret
  
      def select(self, proto):
 -        if "\n" in proto:
+         """Negotiate the given subprotocol with the server"""
 +        if isinstance(proto, str):
 +            proto = proto.encode("ascii")
 +        if b"\n" in proto:
              raise Exception("Illegal protocol specified: %r" % proto)
 -        self.sk.send(proto + "\n")
 +        self.sk.send(proto + b"\n")
          rep = self.readline()
 -        if len(rep) < 1 or rep[0] != "+":
 +        if len(rep) < 1 or rep[0] != b"+"[0]:
              raise protoerr("Error reply when selecting protocol %s: %s" % (proto, rep[1:]))
  
      def __enter__(self):
          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):
 -        super(replclient, self).__init__(sk, "repl")
+         """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
              code = ncode
          while len(code) > 0 and code[-1] == "\n":
              code = code[:-1]
 -        self.sk.send(code + "\n\n")
 -        buf = ""
 +        self.sk.send((code + "\n\n").encode("utf-8"))
 +        buf = b""
          while True:
              ln = self.readline()
 -            if ln[0] == " ":
 -                buf += ln[1:] + "\n"
 -            elif ln[0] == "+":
 -                return buf
 -            elif ln[0] == "-":
 -                raise protoerr("Error reply: %s" % ln[1:])
 +            if ln[0] == b" "[0]:
 +                buf += ln[1:] + b"\n"
 +            elif ln[0] == b"+"[0]:
 +                return buf.decode("utf-8")
 +            elif ln[0] == b"-"[0]:
 +                raise protoerr("Error reply: %s" % ln[1:].decode("utf-8"))
              else:
                  raise protoerr("Illegal reply: %s" % ln)
  
@@@ -155,8 -187,13 +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
          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):
 -        super(perfclient, self).__init__(sk, "perf")
+         """Create a connected client as documented in the `client' class."""
 +        super().__init__(sk, "perf")
          self.nextid = 0
          self.lock = threading.Lock()
          self.proxies = {}
          self.sk.send(buf)
  
      def recvb(self, num):
 -        buf = ""
 +        buf = b""
          while len(buf) < num:
              data = self.sk.recv(num - len(buf))
 -            if data == "":
 +            if data == b"":
                  raise EOFError()
              buf += data
          return buf
          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()
              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
          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:
diff --combined pdm/srv.py
@@@ -1,51 -1,72 +1,73 @@@
- """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")
          self.mod.echo = self.echo
          self.printer = pprint.PrettyPrinter(indent = 4, depth = 6)
 -        cl.send("+REPL\n")
 +        cl.send(b"+REPL\n")
  
      def sendlines(self, text):
          for line in text.split("\n"):
 -            self.cl.send(" " + line + "\n")
 +            self.cl.send(b" " + line.encode("utf-8") + b"\n")
  
      def echo(self, ob):
          self.sendlines(self.printer.pformat(ob))
  
      def command(self, cmd):
 +        cmd = cmd.decode("utf-8")
          try:
              try:
                  ccode = compile(cmd, "PDM Input", "eval")
              except SyntaxError:
                  ccode = compile(cmd, "PDM Input", "exec")
 -                exec ccode in self.mod.__dict__
 -                self.cl.send("+OK\n")
 +                exec(ccode, self.mod.__dict__)
 +                self.cl.send(b"+OK\n")
              else:
                  self.echo(eval(ccode, self.mod.__dict__))
 -                self.cl.send("+OK\n")
 +                self.cl.send(b"+OK\n")
          except:
              for line in traceback.format_exception(*sys.exc_info()):
 -                self.cl.send(" " + line)
 -            self.cl.send("+EXC\n")
 +                self.cl.send(b" " + line.encode("utf-8"))
 +            self.cl.send(b"+EXC\n")
  
      def handle(self, buf):
 -        p = buf.find("\n\n")
 +        p = buf.find(b"\n\n")
          if p < 0:
              return buf
          cmd = buf[:p + 1]
  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 = {}
 -        cl.send("+PERF1\n")
 +        cl.send(b"+PERF1\n")
          self.buf = ""
          self.lock = threading.Lock()
          self.subscribed = {}
  
      def closed(self):
 -        for id, recv in self.subscribed.iteritems():
 +        for id, recv in self.subscribed.items():
              ob = self.odtab[id]
              if ob is None: continue
              ob, protos = ob
              raise ValueError("Object does not support PDM introspection")
          try:
              proto = ob.pdm_protocols()
 -        except Exception, exc:
 +        except Exception as exc:
              raise ValueError("PDM introspection failed", exc)
          self.odtab[id] = ob, proto
          return proto
              return
          try:
              proto = self.bindob(id, ob)
 -        except Exception, exc:
 +        except Exception as exc:
              self.send("-", exc)
              return
          self.send("+", proto)
              return
          try:
              ob = src.lookup(obnm)
 -        except KeyError, exc:
 +        except KeyError as exc:
              self.send("-", exc)
              return
          try:
              proto = self.bindob(tgtid, ob)
 -        except Exception, exc:
 +        except Exception as exc:
              self.send("-", exc)
              return
          self.send("+", proto)
              return
          try:
              ret = ob.readattr()
 -        except Exception, exc:
 +        except Exception as exc:
              self.send("-", Exception("Could not read attribute"))
              return
          self.send("+", ret)
              return
          try:
              self.send("+", ob.invoke(method, *args, **kwargs))
 -        except Exception, exc:
 +        except Exception as exc:
              self.send("-", exc)
  
      def event(self, id, ob, ev):
@@@ -242,7 -354,7 +355,7 @@@ protocols["perf"] = per
  
  class client(threading.Thread):
      def __init__(self, sk):
 -        super(client, self).__init__(name = "Management client")
 +        super().__init__(name = "Management client")
          self.setDaemon(True)
          self.sk = sk
          self.handler = self
          return self.sk.send(data)
  
      def choose(self, proto):
 +        try:
 +            proto = proto.decode("ascii")
 +        except UnicodeError:
 +            proto = None
          if proto in protocols:
              self.handler = protocols[proto](self)
          else:
              raise Exception()
  
      def handle(self, buf):
 -        p = buf.find("\n")
 +        p = buf.find(b"\n")
          if p >= 0:
              proto = buf[:p]
              buf = buf[p + 1:]
  
      def run(self):
          try:
 -            buf = ""
 -            self.send("+PDM1\n")
 +            buf = b""
 +            self.send(b"+PDM1\n")
              while True:
                  ret = self.sk.recv(1024)
 -                if ret == "":
 +                if ret == b"":
                      return
                  buf += ret
                  while True:
                      try:
                          nbuf = self.handler.handle(buf)
                      except:
 +                        #for line in traceback.format_exception(*sys.exc_info()):
 +                        #    print(line)
                          return
                      if nbuf == buf:
                          break
                      buf = nbuf
          finally:
 -            #for line in traceback.format_exception(*sys.exc_info()):
 -            #    print line
              try:
                  self.sk.close()
              finally:
              
  
  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(listener, self).__init__(name = "Management listener")
 +        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)
                      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()
  
          cl.start()
  
  class unixlistener(listener):
 -    def __init__(self, name, mode = 0600, group = None):
+     """Unix socket listener"""
 -        super(unixlistener, self).__init__()
 +    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
          self.group = group
                  os.unlink(self.name)
  
  class tcplistener(listener):
+     """TCP socket listener"""
      def __init__(self, port, bindaddr = "127.0.0.1"):
 -        super(tcplistener, self).__init__()
+         """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
  
              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:]
          last = spec
      if "/" in first:
          parts = spec.split(":")
 -        mode = 0600
 +        mode = 0o600
          group = None
          if len(parts) > 1:
              mode = int(parts[1], 8)