X-Git-Url: http://dolda2000.com/gitweb/?a=blobdiff_plain;f=pdm%2Fcli.py;h=667e2ed5f851a64189b92921b0297fc7bc0e000b;hb=16e7fd3d4abbec9f5e6d976a2feca87176e79e29;hp=da6bcc53d42d27fa9c37b05e46c0aa7e1ca176cd;hpb=7f97a47e579701c8e033ad73259434777f70ef3e;p=pdm.git diff --git a/pdm/cli.py b/pdm/cli.py index da6bcc5..667e2ed 100644 --- a/pdm/cli.py +++ b/pdm/cli.py @@ -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: