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")
75 """Close this connection"""
79 """Return the file descriptor of the underlying socket."""
80 return self.sk.fileno()
83 """Read a single NL-terminated line and return it."""
85 p = self.buf.find(b"\n")
88 self.buf = self.buf[p + 1:]
90 ret = self.sk.recv(1024)
95 def select(self, proto):
96 """Negotiate the given subprotocol with the server"""
97 if isinstance(proto, str):
98 proto = proto.encode("ascii")
100 raise Exception("Illegal protocol specified: %r" % proto)
101 self.sk.send(proto + b"\n")
102 rep = self.readline()
103 if len(rep) < 1 or rep[0] != b"+"[0]:
104 raise protoerr("Error reply when selecting protocol %s: %s" % (proto, rep[1:]))
109 def __exit__(self, *excinfo):
113 class replclient(client):
114 """REPL protocol client
116 Implements the client side of the REPL protocol; see
117 L{pdm.srv.repl} for details on the protocol and its functionality.
119 def __init__(self, sk):
120 """Create a connected client as documented in the `client' class."""
121 super().__init__(sk, "repl")
124 """Run a single block of Python code on the server. Returns
125 the output of the command (as documented in L{pdm.srv.repl})
129 ncode = code.replace("\n\n", "\n")
130 if ncode == code: break
132 while len(code) > 0 and code[-1] == "\n":
134 self.sk.send((code + "\n\n").encode("utf-8"))
139 buf += ln[1:] + b"\n"
140 elif ln[0] == b"+"[0]:
141 return buf.decode("utf-8")
142 elif ln[0] == b"-"[0]:
143 raise protoerr("Error reply: %s" % ln[1:].decode("utf-8"))
145 raise protoerr("Illegal reply: %s" % ln)
147 class perfproxy(object):
148 def __init__(self, cl, id, proto):
152 self.subscribers = set()
154 def lookup(self, name):
155 self.cl.lock.acquire()
160 self.cl.lock.release()
161 (proto,) = self.cl.run("lookup", id, self.id, name)
162 proxy = perfproxy(self.cl, id, proto)
163 self.cl.proxies[id] = proxy
167 return self.cl.run("ls", self.id)[0]
170 return self.cl.run("readattr", self.id)[0]
173 return self.cl.run("attrinfo", self.id)[0]
175 def invoke(self, method, *args, **kwargs):
176 return self.cl.run("invoke", self.id, method, args, kwargs)[0]
178 def subscribe(self, cb):
179 if cb in self.subscribers:
180 raise ValueError("Already subscribed")
181 if len(self.subscribers) == 0:
182 self.cl.run("subs", self.id)
183 self.subscribers.add(cb)
185 def unsubscribe(self, cb):
186 if cb not in self.subscribers:
187 raise ValueError("Not subscribed")
188 self.subscribers.remove(cb)
189 if len(self.subscribers) == 0:
190 self.cl.run("unsubs", self.id)
192 def notify(self, ev):
193 for cb in self.subscribers:
199 if self.id is not None:
200 self.cl.run("unbind", self.id)
201 del self.cl.proxies[self.id]
210 def __exit__(self, *excinfo):
214 class perfclient(client):
215 """PERF protocol client
217 Implements the client side of the PERF protocol; see
218 L{pdm.srv.perf} for details on the protocol and its functionality.
220 This client class implements functions for finding PERF objects on
221 the server, and returns, for each server-side object looked up, a
222 proxy object that mimics exactly the PERF interfaces that the
223 object implements. As the proxy objects reference live objects on
224 the server, they should be released when they are no longer used;
225 they implement a close() method for that purpose, and can also be
226 used in `with' statements.
228 See L{pdm.srv.perf} for details on the various PERF interfaces
229 that the proxy objects might implement.
231 def __init__(self, sk):
232 """Create a connected client as documented in the `client' class."""
233 super().__init__(sk, "perf")
235 self.lock = threading.Lock()
240 buf = pickle.dumps(ob)
241 buf = struct.pack(">l", len(buf)) + buf
244 def recvb(self, num):
246 while len(buf) < num:
247 data = self.sk.recv(num - len(buf))
254 return pickle.loads(self.recvb(struct.unpack(">l", self.recvb(4))[0]))
256 def event(self, id, ev):
257 proxy = self.proxies.get(id)
258 if proxy is None: return
261 def dispatch(self, timeout = None):
262 """Wait for an incoming notification from the server, and
263 dispatch it to the callback functions that have been
264 registered for it. If `timeout' is specified, wait no longer
265 than so many seconds; otherwise, wait forever. This client
266 object may also be used as argument to select.select().
268 rfd, wfd, efd = select.select([self.sk], [], [], timeout)
272 self.event(msg[1], msg[2])
274 raise ValueError("Unexpected non-event message: %r" % msg[0])
279 if reply[0] in ("+", "-"):
281 elif reply[0] == "*":
282 self.event(reply[1], reply[2])
284 raise ValueError("Illegal reply header: %r" % reply[0])
286 def run(self, cmd, *args):
289 self.send((cmd,) + args)
290 reply = self.recvreply()
298 def lookup(self, module, obnm):
299 """Look up a single server-side object by the given name in
300 the given module. Will return a new proxy object for each
301 call when called multiple times for the same name.
309 (proto,) = self.run("bind", id, module, obnm)
310 proxy = perfproxy(self, id, proto)
311 self.proxies[id] = proxy
314 def find(self, name):
315 """Convenience function for looking up server-side objects
316 through PERF directories and for multiple uses. The object
317 name can be given as "MODULE.OBJECT", which will look up the
318 named OBJECT in the named MODULE, and can be followed by any
319 number of slash-separated names, which will assume that the
320 object to the left of the slash is a PERF directory, and will
321 return the object in that directory by the name to the right
322 of the slash. For instance, find("pdm.perf.sysres/cputime")
323 will return the built-in attribute for reading the CPU time
324 used by the server process.
326 The proxy objects returned by this function are cached and the
327 same object are returned the next time the same name is
328 requested, which means that they are kept live until the
329 client connection is closed.
331 ret = self.names.get(name)
335 ret = self.find(name[:p]).lookup(name[p + 1:])
338 ret = self.lookup(name[:p], name[p + 1:])
339 self.names[name] = ret