Made relevant Python3 changes.
[pdm.git] / pdm / cli.py
index da6bcc5..667e2ed 100644 (file)
@@ -1,14 +1,17 @@
-"""Management for daemon processes
+"""Python Daemon Management -- Client functions
 
-This module provides some client support for the daemon management
-provided in the pdm.srv module.
+This module implements the client part of the PDM protocols. The
+primary objects of interest are the replclient and perfclient classes,
+which implement support for their respective protocols. See their
+documentation for details.
 """
 
 import socket, pickle, struct, select, threading
 
-__all__ = ["client", "replclient"]
+__all__ = ["client", "replclient", "perfclient"]
 
 class protoerr(Exception):
+    """Raised on protocol errors"""
     pass
 
 def resolve(spec):
@@ -16,16 +19,21 @@ def resolve(spec):
         return spec
     sk = None
     try:
-        if "/" in spec:
+        if ":" in spec:
+            p = spec.rindex(":")
+            first, second = spec[:p], spec[p + 1:]
+            if "/" in second:
+                from . import sshsock
+                sk = sshsock.sshsocket(first, second)
+            else:
+                sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                sk.connect((first, second))
+        elif "/" in spec:
             sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
             sk.connect(spec)
         elif spec.isdigit():
             sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
             sk.connect(("localhost", int(spec)))
-        elif ":" in spec:
-            sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-            p = spec.rindex(":")
-            sk.connect((spec[:p], int(spec[p + 1:])))
         else:
             raise Exception("Unknown target specification %r" % spec)
         rv = sk
@@ -35,36 +43,64 @@ 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 L{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):
@@ -75,26 +111,36 @@ class client(object):
         return False
 
 class replclient(client):
+    """REPL protocol client
+    
+    Implements the client side of the REPL protocol; see
+    L{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 L{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)
 
@@ -136,7 +182,7 @@ class perfproxy(object):
             self.cl.run("subs", self.id)
         self.subscribers.add(cb)
 
-    def unsubscribe(self):
+    def unsubscribe(self, cb):
         if cb not in self.subscribers:
             raise ValueError("Not subscribed")
         self.subscribers.remove(cb)
@@ -150,8 +196,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
@@ -161,8 +212,25 @@ class perfproxy(object):
         return False
 
 class perfclient(client):
+    """PERF protocol client
+    
+    Implements the client side of the PERF protocol; see
+    L{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 L{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 = {}
@@ -174,10 +242,10 @@ class perfclient(client):
         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
@@ -191,6 +259,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()
@@ -222,6 +296,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
@@ -234,6 +312,22 @@ 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.
+
+        The proxy objects returned by this function are cached and the
+        same object are returned the next time the same name is
+        requested, which means that they are kept live until the
+        client connection is closed.
+        """
         ret = self.names.get(name)
         if ret is None:
             if "/" in name: