1 """Python Daemon Management -- Client functions
3 This module implements the client part of the PDM protocols. The
4 primary objects of interest are the replclient and perfclient classes,
5 which implement support for their respective protocols. See their
6 documentation for details.
9 import socket, pickle, struct, select, threading
11 __all__ = ["client", "replclient", "perfclient"]
13 class protoerr(Exception):
14 """Raised on protocol errors"""
18 if isinstance(spec, socket.socket):
24 first, second = spec[:p], spec[p + 1:]
27 sk = sshsock.sshsocket(first, second)
29 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
30 sk.connect((first, second))
32 sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
35 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
36 sk.connect(("localhost", int(spec)))
38 raise Exception("Unknown target specification %r" % spec)
42 if sk is not None: sk.close()
48 This class provides general facilities to speak to PDM servers,
49 and is mainly intended to be subclassed to provide for the
50 specific protocols, such as replclient and perfclient do.
52 `client' instances can be passed as arguments to select.select(),
53 and can be used in `with' statements.
55 def __init__(self, sk, proto = None):
56 """Create a client object connected to the specified
57 server. `sk' can either be a socket object, which is used as
58 it is, or a string specification very similar to the
59 specification for L{pdm.srv.listen}, so see its documentation
60 for details. The differences are only that this function does
61 not take arguments specific to socket creation, like the mode
62 and group arguments for Unix sockets. If `proto' is given,
63 that subprotocol will negotiated with the server (by calling
68 line = self.readline()
70 raise protoerr("Illegal protocol signature")
76 return self.sk is None
79 """Close this connection"""
80 if self.sk is not None:
85 """Return the file descriptor of the underlying socket."""
86 return self.sk.fileno() if self.sk else None
89 """Read a single NL-terminated line and return it."""
91 p = self.buf.find(b"\n")
94 self.buf = self.buf[p + 1:]
96 ret = self.sk.recv(1024)
101 def select(self, proto):
102 """Negotiate the given subprotocol with the server"""
103 if isinstance(proto, str):
104 proto = proto.encode("ascii")
106 raise Exception("Illegal protocol specified: %r" % proto)
107 self.sk.send(proto + b"\n")
108 rep = self.readline()
109 if len(rep) < 1 or rep[0] != b"+"[0]:
110 raise protoerr("Error reply when selecting protocol %s: %s" % (proto, rep[1:]))
115 def __exit__(self, *excinfo):
119 class replclient(client):
120 """REPL protocol client
122 Implements the client side of the REPL protocol; see
123 L{pdm.srv.repl} for details on the protocol and its functionality.
125 def __init__(self, sk):
126 """Create a connected client as documented in the `client' class."""
127 super().__init__(sk, "repl")
130 """Run a single block of Python code on the server. Returns
131 the output of the command (as documented in L{pdm.srv.repl})
135 ncode = code.replace("\n\n", "\n")
136 if ncode == code: break
138 while len(code) > 0 and code[-1] == "\n":
140 self.sk.send((code + "\n\n").encode("utf-8"))
145 buf += ln[1:] + b"\n"
146 elif ln[0] == b"+"[0]:
147 return buf.decode("utf-8")
148 elif ln[0] == b"-"[0]:
149 raise protoerr("Error reply: %s" % ln[1:].decode("utf-8"))
151 raise protoerr("Illegal reply: %s" % ln)
153 class perfproxy(object):
154 def __init__(self, cl, id, proto):
158 self.subscribers = set()
160 def lookup(self, name):
161 self.cl.lock.acquire()
166 self.cl.lock.release()
167 (proto,) = self.cl.run("lookup", id, self.id, name)
168 proxy = perfproxy(self.cl, id, proto)
169 self.cl.proxies[id] = proxy
173 return self.cl.run("ls", self.id)[0]
176 return self.cl.run("readattr", self.id)[0]
179 return self.cl.run("attrinfo", self.id)[0]
181 def invoke(self, method, *args, **kwargs):
182 return self.cl.run("invoke", self.id, method, args, kwargs)[0]
184 def subscribe(self, cb):
185 if cb in self.subscribers:
186 raise ValueError("Already subscribed")
187 if len(self.subscribers) == 0:
188 self.cl.run("subs", self.id)
189 self.subscribers.add(cb)
191 def unsubscribe(self, cb):
192 if cb not in self.subscribers:
193 raise ValueError("Not subscribed")
194 self.subscribers.remove(cb)
195 if len(self.subscribers) == 0:
196 self.cl.run("unsubs", self.id)
198 def notify(self, ev):
199 for cb in self.subscribers:
205 if self.id is not None:
206 if not self.cl.closed:
207 self.cl.run("unbind", self.id)
208 del self.cl.proxies[self.id]
217 def __exit__(self, *excinfo):
221 class perfclient(client):
222 """PERF protocol client
224 Implements the client side of the PERF protocol; see
225 L{pdm.srv.perf} for details on the protocol and its functionality.
227 This client class implements functions for finding PERF objects on
228 the server, and returns, for each server-side object looked up, a
229 proxy object that mimics exactly the PERF interfaces that the
230 object implements. As the proxy objects reference live objects on
231 the server, they should be released when they are no longer used;
232 they implement a close() method for that purpose, and can also be
233 used in `with' statements.
235 See L{pdm.srv.perf} for details on the various PERF interfaces
236 that the proxy objects might implement.
238 def __init__(self, sk):
239 """Create a connected client as documented in the `client' class."""
240 super().__init__(sk, "perf")
242 self.lock = threading.Lock()
247 buf = pickle.dumps(ob)
248 buf = struct.pack(">l", len(buf)) + buf
251 def recvb(self, num):
253 while len(buf) < num:
254 data = self.sk.recv(num - len(buf))
261 return pickle.loads(self.recvb(struct.unpack(">l", self.recvb(4))[0]))
263 def event(self, id, ev):
264 proxy = self.proxies.get(id)
265 if proxy is None: return
268 def dispatch(self, timeout = None):
269 """Wait for an incoming notification from the server, and
270 dispatch it to the callback functions that have been
271 registered for it. If `timeout' is specified, wait no longer
272 than so many seconds; otherwise, wait forever. This client
273 object may also be used as argument to select.select().
275 rfd, wfd, efd = select.select([self.sk], [], [], timeout)
279 self.event(msg[1], msg[2])
281 raise ValueError("Unexpected non-event message: %r" % msg[0])
286 if reply[0] in ("+", "-"):
288 elif reply[0] == "*":
289 self.event(reply[1], reply[2])
291 raise ValueError("Illegal reply header: %r" % reply[0])
293 def run(self, cmd, *args):
296 self.send((cmd,) + args)
297 reply = self.recvreply()
305 def lookup(self, module, obnm):
306 """Look up a single server-side object by the given name in
307 the given module. Will return a new proxy object for each
308 call when called multiple times for the same name.
316 (proto,) = self.run("bind", id, module, obnm)
317 proxy = perfproxy(self, id, proto)
318 self.proxies[id] = proxy
321 def find(self, name):
322 """Convenience function for looking up server-side objects
323 through PERF directories and for multiple uses. The object
324 name can be given as "MODULE.OBJECT", which will look up the
325 named OBJECT in the named MODULE, and can be followed by any
326 number of slash-separated names, which will assume that the
327 object to the left of the slash is a PERF directory, and will
328 return the object in that directory by the name to the right
329 of the slash. For instance, find("pdm.perf.sysres/cputime")
330 will return the built-in attribute for reading the CPU time
331 used by the server process.
333 The proxy objects returned by this function are cached and the
334 same object is returned the next time the same name is
335 requested, which means that they are kept live until the
336 client connection is closed.
338 ret = self.names.get(name)
342 ret = self.find(name[:p]).lookup(name[p + 1:])
345 ret = self.lookup(name[:p], name[p + 1:])
346 self.names[name] = ret