Throw more informative error classes from perf.
[pdm.git] / pdm / cli.py
CommitLineData
ebd7a8ba 1"""Python Daemon Management -- Client functions
7f97a47e 2
ebd7a8ba
FT
3This module implements the client part of the PDM protocols. The
4primary objects of interest are the replclient and perfclient classes,
5which implement support for their respective protocols. See their
6documentation for details.
7f97a47e
FT
7"""
8
9import socket, pickle, struct, select, threading
10
ac4f5166 11__all__ = ["client", "replclient", "perfclient"]
7f97a47e
FT
12
13class protoerr(Exception):
73c1ef8b 14 """Raised on protocol errors"""
7f97a47e
FT
15 pass
16
17def resolve(spec):
18 if isinstance(spec, socket.socket):
19 return spec
20 sk = None
21 try:
c30be9c9
FT
22 if ":" in spec:
23 p = spec.rindex(":")
24 first, second = spec[:p], spec[p + 1:]
25 if "/" in second:
16e7fd3d 26 from . import sshsock
c30be9c9
FT
27 sk = sshsock.sshsocket(first, second)
28 else:
29 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
30 sk.connect((first, second))
31 elif "/" in spec:
7f97a47e
FT
32 sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
33 sk.connect(spec)
34 elif spec.isdigit():
35 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
36 sk.connect(("localhost", int(spec)))
7f97a47e
FT
37 else:
38 raise Exception("Unknown target specification %r" % spec)
39 rv = sk
40 sk = None
41 finally:
42 if sk is not None: sk.close()
43 return rv
44
45class client(object):
73c1ef8b
FT
46 """PDM client
47
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.
51
52 `client' instances can be passed as arguments to select.select(),
53 and can be used in `with' statements.
54 """
7f97a47e 55 def __init__(self, sk, proto = None):
73c1ef8b
FT
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
57808152
FT
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
64 the select() method).
73c1ef8b 65 """
7f97a47e 66 self.sk = resolve(sk)
11d50d09 67 self.buf = b""
7f97a47e 68 line = self.readline()
11d50d09 69 if line != b"+PDM1":
7f97a47e
FT
70 raise protoerr("Illegal protocol signature")
71 if proto is not None:
72 self.select(proto)
73
dc1b10b8
FT
74 @property
75 def closed(self):
76 return self.sk is None
77
7f97a47e 78 def close(self):
73c1ef8b 79 """Close this connection"""
dc1b10b8
FT
80 if self.sk is not None:
81 self.sk.close()
82 self.sk = None
7f97a47e 83
9928d247 84 def fileno(self):
73c1ef8b 85 """Return the file descriptor of the underlying socket."""
dc1b10b8 86 return self.sk.fileno() if self.sk else None
9928d247 87
7f97a47e 88 def readline(self):
73c1ef8b 89 """Read a single NL-terminated line and return it."""
7f97a47e 90 while True:
11d50d09 91 p = self.buf.find(b"\n")
7f97a47e
FT
92 if p >= 0:
93 ret = self.buf[:p]
94 self.buf = self.buf[p + 1:]
95 return ret
96 ret = self.sk.recv(1024)
11d50d09 97 if ret == b"":
7f97a47e
FT
98 return None
99 self.buf += ret
100
101 def select(self, proto):
73c1ef8b 102 """Negotiate the given subprotocol with the server"""
11d50d09
FT
103 if isinstance(proto, str):
104 proto = proto.encode("ascii")
105 if b"\n" in proto:
7f97a47e 106 raise Exception("Illegal protocol specified: %r" % proto)
11d50d09 107 self.sk.send(proto + b"\n")
7f97a47e 108 rep = self.readline()
11d50d09 109 if len(rep) < 1 or rep[0] != b"+"[0]:
7f97a47e
FT
110 raise protoerr("Error reply when selecting protocol %s: %s" % (proto, rep[1:]))
111
112 def __enter__(self):
113 return self
114
115 def __exit__(self, *excinfo):
116 self.close()
117 return False
118
119class replclient(client):
73c1ef8b
FT
120 """REPL protocol client
121
57808152
FT
122 Implements the client side of the REPL protocol; see
123 L{pdm.srv.repl} for details on the protocol and its functionality.
73c1ef8b 124 """
7f97a47e 125 def __init__(self, sk):
73c1ef8b 126 """Create a connected client as documented in the `client' class."""
ed115f48 127 super().__init__(sk, "repl")
7f97a47e
FT
128
129 def run(self, code):
73c1ef8b 130 """Run a single block of Python code on the server. Returns
57808152
FT
131 the output of the command (as documented in L{pdm.srv.repl})
132 as a string.
73c1ef8b 133 """
7f97a47e
FT
134 while True:
135 ncode = code.replace("\n\n", "\n")
136 if ncode == code: break
137 code = ncode
138 while len(code) > 0 and code[-1] == "\n":
139 code = code[:-1]
11d50d09
FT
140 self.sk.send((code + "\n\n").encode("utf-8"))
141 buf = b""
7f97a47e
FT
142 while True:
143 ln = self.readline()
11d50d09
FT
144 if ln[0] == b" "[0]:
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"))
7f97a47e
FT
150 else:
151 raise protoerr("Illegal reply: %s" % ln)
152
153class perfproxy(object):
154 def __init__(self, cl, id, proto):
155 self.cl = cl
156 self.id = id
157 self.proto = proto
158 self.subscribers = set()
159
160 def lookup(self, name):
161 self.cl.lock.acquire()
162 try:
163 id = self.cl.nextid
164 self.cl.nextid += 1
165 finally:
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
170 return proxy
171
172 def listdir(self):
173 return self.cl.run("ls", self.id)[0]
174
175 def readattr(self):
176 return self.cl.run("readattr", self.id)[0]
177
178 def attrinfo(self):
179 return self.cl.run("attrinfo", self.id)[0]
180
181 def invoke(self, method, *args, **kwargs):
182 return self.cl.run("invoke", self.id, method, args, kwargs)[0]
183
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)
190
441160a2 191 def unsubscribe(self, cb):
7f97a47e
FT
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)
197
198 def notify(self, ev):
199 for cb in self.subscribers:
200 try:
201 cb(ev)
202 except: pass
203
204 def close(self):
4fcf3c74 205 if self.id is not None:
dc1b10b8
FT
206 if not self.cl.closed:
207 self.cl.run("unbind", self.id)
4fcf3c74
FT
208 del self.cl.proxies[self.id]
209 self.id = None
210
211 def __del__(self):
212 self.close()
7f97a47e
FT
213
214 def __enter__(self):
215 return self
216
217 def __exit__(self, *excinfo):
218 self.close()
219 return False
220
221class perfclient(client):
73c1ef8b
FT
222 """PERF protocol client
223
57808152
FT
224 Implements the client side of the PERF protocol; see
225 L{pdm.srv.perf} for details on the protocol and its functionality.
73c1ef8b
FT
226
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.
234
57808152
FT
235 See L{pdm.srv.perf} for details on the various PERF interfaces
236 that the proxy objects might implement.
73c1ef8b 237 """
7f97a47e 238 def __init__(self, sk):
73c1ef8b 239 """Create a connected client as documented in the `client' class."""
ed115f48 240 super().__init__(sk, "perf")
7f97a47e
FT
241 self.nextid = 0
242 self.lock = threading.Lock()
243 self.proxies = {}
244 self.names = {}
245
246 def send(self, ob):
247 buf = pickle.dumps(ob)
248 buf = struct.pack(">l", len(buf)) + buf
249 self.sk.send(buf)
250
251 def recvb(self, num):
11d50d09 252 buf = b""
7f97a47e
FT
253 while len(buf) < num:
254 data = self.sk.recv(num - len(buf))
11d50d09 255 if data == b"":
7f97a47e
FT
256 raise EOFError()
257 buf += data
258 return buf
259
260 def recv(self):
261 return pickle.loads(self.recvb(struct.unpack(">l", self.recvb(4))[0]))
262
263 def event(self, id, ev):
264 proxy = self.proxies.get(id)
265 if proxy is None: return
266 proxy.notify(ev)
267
268 def dispatch(self, timeout = None):
73c1ef8b
FT
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().
274 """
7f97a47e
FT
275 rfd, wfd, efd = select.select([self.sk], [], [], timeout)
276 if self.sk in rfd:
277 msg = self.recv()
278 if msg[0] == "*":
279 self.event(msg[1], msg[2])
280 else:
281 raise ValueError("Unexpected non-event message: %r" % msg[0])
282
283 def recvreply(self):
284 while True:
285 reply = self.recv()
286 if reply[0] in ("+", "-"):
287 return reply
288 elif reply[0] == "*":
289 self.event(reply[1], reply[2])
290 else:
291 raise ValueError("Illegal reply header: %r" % reply[0])
292
293 def run(self, cmd, *args):
294 self.lock.acquire()
295 try:
296 self.send((cmd,) + args)
297 reply = self.recvreply()
298 if reply[0] == "+":
299 return reply[1:]
300 else:
301 raise reply[1]
302 finally:
303 self.lock.release()
304
305 def lookup(self, module, obnm):
73c1ef8b
FT
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.
309 """
7f97a47e
FT
310 self.lock.acquire()
311 try:
312 id = self.nextid
313 self.nextid += 1
314 finally:
315 self.lock.release()
316 (proto,) = self.run("bind", id, module, obnm)
317 proxy = perfproxy(self, id, proto)
318 self.proxies[id] = proxy
319 return proxy
320
321 def find(self, name):
73c1ef8b
FT
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.
ebd7a8ba
FT
332
333 The proxy objects returned by this function are cached and the
3e02e6b5 334 same object is returned the next time the same name is
ebd7a8ba
FT
335 requested, which means that they are kept live until the
336 client connection is closed.
73c1ef8b 337 """
7f97a47e
FT
338 ret = self.names.get(name)
339 if ret is None:
340 if "/" in name:
341 p = name.rindex("/")
342 ret = self.find(name[:p]).lookup(name[p + 1:])
343 else:
344 p = name.rindex(".")
345 ret = self.lookup(name[:p], name[p + 1:])
346 self.names[name] = ret
347 return ret