Throw more informative error classes from perf.
[pdm.git] / pdm / srv.py
... / ...
CommitLineData
1"""Python Daemon Management -- Server functions
2
3This module implements the server part of the PDM protocols. The
4primary object of interest herein is the listen() function, which is
5the most generic way to create PDM listeners based on user
6configuration, and the documentation for the repl and perf classes,
7which describes the functioning of the REPL and PERF protocols.
8"""
9
10import os, sys, socket, threading, grp, select
11import types, pprint, traceback
12import pickle, struct
13from . import perf as mperf
14
15__all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"]
16
17protocols = {}
18
19class repl(object):
20 """REPL protocol handler
21
22 Provides a read-eval-print loop. The primary client-side interface
23 is the L{pdm.cli.replclient} class. Clients can send arbitrary
24 code, which is compiled and run on its own thread in the server
25 process, and output responses that are echoed back to the client.
26
27 Each client is provided with its own module, in which the code
28 runs. The module is prepared with a function named `echo', which
29 takes a single object and pretty-prints it as part of the command
30 response. If a command can be parsed as an expression, the value
31 it evaluates to is automatically echoed to the client. If the
32 evalution of the command terminates with an exception, its
33 traceback is echoed to the client.
34
35 The REPL protocol is only intended for interactive usage. In order
36 to interact programmatically with the server process, see the PERF
37 protocol instead.
38 """
39 def __init__(self, cl):
40 self.cl = cl
41 self.mod = types.ModuleType("repl")
42 self.mod.echo = self.echo
43 self.printer = pprint.PrettyPrinter(indent = 4, depth = 6)
44 cl.send(b"+REPL\n")
45
46 def sendlines(self, text):
47 for line in text.split("\n"):
48 self.cl.send(b" " + line.encode("utf-8") + b"\n")
49
50 def echo(self, ob):
51 self.sendlines(self.printer.pformat(ob))
52
53 def command(self, cmd):
54 cmd = cmd.decode("utf-8")
55 try:
56 try:
57 ccode = compile(cmd, "PDM Input", "eval")
58 except SyntaxError:
59 ccode = compile(cmd, "PDM Input", "exec")
60 exec(ccode, self.mod.__dict__)
61 self.cl.send(b"+OK\n")
62 else:
63 self.echo(eval(ccode, self.mod.__dict__))
64 self.cl.send(b"+OK\n")
65 except:
66 lines = ("".join(traceback.format_exception(*sys.exc_info()))).split("\n")
67 while len(lines) > 0 and lines[-1] == "": lines = lines[:-1]
68 for line in lines:
69 self.cl.send(b" " + line.encode("utf-8") + b"\n")
70 self.cl.send(b"+EXC\n")
71
72 def handle(self, buf):
73 p = buf.find(b"\n\n")
74 if p < 0:
75 return buf
76 cmd = buf[:p + 1]
77 self.command(cmd)
78 return buf[p + 2:]
79protocols["repl"] = repl
80
81class perf(object):
82 """PERF protocol handler
83
84 The PERF protocol provides an interface for program interaction
85 with the server process. It allows limited remote interactions
86 with Python objects over a few defined interfaces.
87
88 All objects that wish to be available for interaction need to
89 implement a method named `pdm_protocols' which, when called with
90 no arguments, should return a list of strings, each indicating a
91 PERF interface that the object implements. For each such
92 interface, the object must implement additional methods as
93 described below.
94
95 A client can find PERF objects to interact with either by
96 specifying the name of such an object in an existing module, or by
97 using the `dir' interface, described below. Thus, to make a PERF
98 object available for clients, it needs only be bound to a global
99 variable in a module and implement the `pdm_protocols'
100 method. When requesting an object from a module, the module must
101 already be imported. PDM will not import new modules for clients;
102 rather, the daemon process needs to import all modules that
103 clients should be able to interact with. PDM itself always imports
104 the L{pdm.perf} module, which contains a few basic PERF
105 objects. See its documentation for details.
106
107 The following interfaces are currently known to PERF.
108
109 - attr:
110 An object that implements the `attr' interface models an
111 attribute that can be read by clients. The attribute can be
112 anything, as long as its representation can be
113 pickled. Examples of attributes could be such things as the CPU
114 time consumed by the server process, or the number of active
115 connections to whatever clients the program serves. To
116 implement the `attr' interface, an object must implement
117 methods called `readattr' and `attrinfo'. `readattr' is called
118 with no arguments to read the current value of the attribute,
119 and `attrinfo' is called with no arguments to read a
120 description of the attribute. Both should be
121 idempotent. `readattr' can return any pickleable object, and
122 `attrinfo' should return either None to indicate that it has no
123 description, or an instance of the L{pdm.perf.attrinfo} class.
124
125 - dir:
126 The `dir' interface models a directory of other PERF
127 objects. An object implementing it must implement methods
128 called `lookup' and `listdir'. `lookup' is called with a single
129 string argument that names an object, and should either return
130 another PERF object based on the name, or raise KeyError if it
131 does not recognize the name. `listdir' is called with no
132 arguments, and should return a list of known names that can be
133 used as argument to `lookup', but the list is not required to
134 be exhaustive and may also be empty.
135
136 - invoke:
137 The `invoke' interface allows a more arbitrary form of method
138 calls to objects implementing it. Such objects must implement a
139 method called `invoke', which is called with one positional
140 argument naming a method to be called (which it is free to
141 interpret however it wishes), and with any additional
142 positional and keyword arguments that the client wishes to pass
143 to it. Whatever `invoke' returns is pickled and sent back to
144 the client. In case the method name is not recognized, `invoke'
145 should raise an AttributeError.
146
147 - event:
148 The `event' interface allows PERF objects to notify clients of
149 events asynchronously. Objects implementing it must implement
150 methods called `subscribe' and `unsubscribe'. `subscribe' will
151 be called with a single argument, which is a callable of one
152 argument, which should be registered to be called when an event
153 pertaining to the `event' object in question occurs. The
154 `event' object should then call all such registered callables
155 with a single argument describing the event. The argument could
156 be any object that can be pickled, but should be an instance of
157 a subclass of the L{pdm.perf.event} class. If `subscribe' is
158 called with a callback object that it has already registered,
159 it should raise a ValueError. `unsubscribe' is called with a
160 single argument, which is a previously registered callback
161 object, which should then be unregistered to that it is no
162 longer called when an event occurs. If the given callback
163 object is not, in fact, registered, a ValueError should be
164 raised.
165
166 The L{pdm.perf} module contains a few convenience classes which
167 implements the interfaces, but PERF objects are not required to be
168 instances of them. Any object can implement a PERF interface, as
169 long as it does so as described above.
170
171 The L{pdm.cli.perfclient} class is the client-side implementation.
172 """
173 def __init__(self, cl):
174 self.cl = cl
175 self.odtab = {}
176 cl.send(b"+PERF1\n")
177 self.buf = ""
178 self.lock = threading.Lock()
179 self.subscribed = {}
180
181 def closed(self):
182 for id, recv in self.subscribed.items():
183 ob = self.odtab[id]
184 if ob is None: continue
185 ob, protos = ob
186 try:
187 ob.unsubscribe(recv)
188 except: pass
189
190 def send(self, *args):
191 self.lock.acquire()
192 try:
193 buf = pickle.dumps(args)
194 buf = struct.pack(">l", len(buf)) + buf
195 self.cl.send(buf)
196 finally:
197 self.lock.release()
198
199 def bindob(self, id, ob):
200 if not hasattr(ob, "pdm_protocols"):
201 raise mperf.nosuchname("Object does not support PDM introspection")
202 try:
203 proto = ob.pdm_protocols()
204 except Exception as exc:
205 raise ValueError("PDM introspection failed", exc)
206 self.odtab[id] = ob, proto
207 return proto
208
209 def bind(self, id, module, obnm):
210 resmod = sys.modules.get(module)
211 if resmod is None:
212 self.send("-", mperf.nosuchname("No such module: %s" % module))
213 return
214 try:
215 ob = getattr(resmod, obnm)
216 except AttributeError:
217 self.send("-", mperf.nosuchname("No such object: %s" % obnm))
218 return
219 try:
220 proto = self.bindob(id, ob)
221 except Exception as exc:
222 self.send("-", exc)
223 return
224 self.send("+", proto)
225
226 def getob(self, id, proto):
227 ob = self.odtab.get(id)
228 if ob is None:
229 self.send("-", ValueError("No such bound ID: %r" % id))
230 return None
231 ob, protos = ob
232 if proto not in protos:
233 self.send("-", mperf.nosuchproto("Object does not support that protocol: " + proto))
234 return None
235 return ob
236
237 def lookup(self, tgtid, srcid, obnm):
238 src = self.getob(srcid, "dir")
239 if src is None:
240 return
241 try:
242 ob = src.lookup(obnm)
243 except KeyError as exc:
244 self.send("-", mperf.nosuchname(obnm))
245 return
246 try:
247 proto = self.bindob(tgtid, ob)
248 except Exception as exc:
249 self.send("-", exc)
250 return
251 self.send("+", proto)
252
253 def unbind(self, id):
254 ob = self.odtab.get(id)
255 if ob is None:
256 self.send("-", KeyError("No such name bound: %r" % id))
257 return
258 ob, protos = ob
259 del self.odtab[id]
260 recv = self.subscribed.get(id)
261 if recv is not None:
262 ob.unsubscribe(recv)
263 del self.subscribed[id]
264 self.send("+")
265
266 def listdir(self, id):
267 ob = self.getob(id, "dir")
268 if ob is None:
269 return
270 self.send("+", ob.listdir())
271
272 def readattr(self, id):
273 ob = self.getob(id, "attr")
274 if ob is None:
275 return
276 try:
277 ret = ob.readattr()
278 except Exception as exc:
279 self.send("-", Exception("Could not read attribute"))
280 return
281 self.send("+", ret)
282
283 def attrinfo(self, id):
284 ob = self.getob(id, "attr")
285 if ob is None:
286 return
287 self.send("+", ob.attrinfo())
288
289 def invoke(self, id, method, args, kwargs):
290 ob = self.getob(id, "invoke")
291 if ob is None:
292 return
293 try:
294 self.send("+", ob.invoke(method, *args, **kwargs))
295 except Exception as exc:
296 self.send("-", exc)
297
298 def event(self, id, ob, ev):
299 self.send("*", id, ev)
300
301 def subscribe(self, id):
302 ob = self.getob(id, "event")
303 if ob is None:
304 return
305 if id in self.subscribed:
306 self.send("-", ValueError("Already subscribed"))
307 def recv(ev):
308 self.event(id, ob, ev)
309 ob.subscribe(recv)
310 self.subscribed[id] = recv
311 self.send("+")
312
313 def unsubscribe(self, id):
314 ob = self.getob(id, "event")
315 if ob is None:
316 return
317 recv = self.subscribed.get(id)
318 if recv is None:
319 self.send("-", ValueError("Not subscribed"))
320 ob.unsubscribe(recv)
321 del self.subscribed[id]
322 self.send("+")
323
324 def command(self, data):
325 cmd = data[0]
326 if cmd == "bind":
327 self.bind(*data[1:])
328 elif cmd == "unbind":
329 self.unbind(*data[1:])
330 elif cmd == "lookup":
331 self.lookup(*data[1:])
332 elif cmd == "ls":
333 self.listdir(*data[1:])
334 elif cmd == "readattr":
335 self.readattr(*data[1:])
336 elif cmd == "attrinfo":
337 self.attrinfo(*data[1:])
338 elif cmd == "invoke":
339 self.invoke(*data[1:])
340 elif cmd == "subs":
341 self.subscribe(*data[1:])
342 elif cmd == "unsubs":
343 self.unsubscribe(*data[1:])
344 else:
345 self.send("-", Exception("Unknown command: %r" % (cmd,)))
346
347 def handle(self, buf):
348 if len(buf) < 4:
349 return buf
350 dlen = struct.unpack(">l", buf[:4])[0]
351 if len(buf) < dlen + 4:
352 return buf
353 data = pickle.loads(buf[4:dlen + 4])
354 self.command(data)
355 return buf[dlen + 4:]
356
357protocols["perf"] = perf
358
359class client(threading.Thread):
360 def __init__(self, sk):
361 super().__init__(name = "Management client")
362 self.setDaemon(True)
363 self.sk = sk
364 self.handler = self
365
366 def send(self, data):
367 return self.sk.send(data)
368
369 def choose(self, proto):
370 try:
371 proto = proto.decode("ascii")
372 except UnicodeError:
373 proto = None
374 if proto in protocols:
375 self.handler = protocols[proto](self)
376 else:
377 self.send("-ERR Unknown protocol: %s\n" % proto)
378 raise Exception()
379
380 def handle(self, buf):
381 p = buf.find(b"\n")
382 if p >= 0:
383 proto = buf[:p]
384 buf = buf[p + 1:]
385 self.choose(proto)
386 return buf
387
388 def run(self):
389 try:
390 buf = b""
391 self.send(b"+PDM1\n")
392 while True:
393 ret = self.sk.recv(1024)
394 if ret == b"":
395 return
396 buf += ret
397 while True:
398 try:
399 nbuf = self.handler.handle(buf)
400 except:
401 #for line in traceback.format_exception(*sys.exc_info()):
402 # print(line)
403 return
404 if nbuf == buf:
405 break
406 buf = nbuf
407 finally:
408 try:
409 self.sk.close()
410 finally:
411 if hasattr(self.handler, "closed"):
412 self.handler.closed()
413
414
415class listener(threading.Thread):
416 """PDM listener
417
418 This subclass of a thread listens to PDM connections and handles
419 client connections properly. It is intended to be subclassed by
420 providers of specific domains, such as unixlistener and
421 tcplistener.
422 """
423 def __init__(self):
424 super().__init__(name = "Management listener")
425 self.setDaemon(True)
426
427 def listen(self, sk):
428 """Listen for and accept connections."""
429 self.running = True
430 while self.running:
431 rfd, wfd, efd = select.select([sk], [], [sk], 1)
432 for fd in rfd:
433 if fd == sk:
434 nsk, addr = sk.accept()
435 self.accept(nsk, addr)
436
437 def stop(self):
438 """Stop listening for client connections
439
440 Tells the listener thread to stop listening, and then waits
441 for it to terminate.
442 """
443 self.running = False
444 self.join()
445
446 def accept(self, sk, addr):
447 cl = client(sk)
448 cl.start()
449
450class unixlistener(listener):
451 """Unix socket listener"""
452 def __init__(self, name, mode = 0o600, group = None):
453 """Create a listener that will bind to the Unix socket named
454 by `name'. The socket will not actually be bound until the
455 listener is started. The socket will be chmodded to `mode',
456 and if `group' is given, the named group will be set as the
457 owner of the socket.
458 """
459 super().__init__()
460 self.name = name
461 self.mode = mode
462 self.group = group
463
464 def run(self):
465 sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
466 ul = False
467 try:
468 if os.path.exists(self.name) and os.path.stat.S_ISSOCK(os.stat(self.name).st_mode):
469 os.unlink(self.name)
470 sk.bind(self.name)
471 ul = True
472 os.chmod(self.name, self.mode)
473 if self.group is not None:
474 os.chown(self.name, os.getuid(), grp.getgrnam(self.group).gr_gid)
475 sk.listen(16)
476 self.listen(sk)
477 finally:
478 sk.close()
479 if ul:
480 os.unlink(self.name)
481
482class tcplistener(listener):
483 """TCP socket listener"""
484 def __init__(self, port, bindaddr = "127.0.0.1"):
485 """Create a listener that will bind to the given TCP port, and
486 the given local interface. The socket will not actually be
487 bound until the listener is started.
488 """
489 super().__init__()
490 self.port = port
491 self.bindaddr = bindaddr
492
493 def run(self):
494 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
495 try:
496 sk.bind((self.bindaddr, self.port))
497 sk.listen(16)
498 self.listen(sk)
499 finally:
500 sk.close()
501
502def listen(spec):
503 """Create and start a listener according to a string
504 specification. The string specifications can easily be passed from
505 command-line options, user configuration or the like. Currently,
506 the two following specification formats are recognized:
507
508 PATH[:MODE[:GROUP]] -- PATH must contain at least one slash. A
509 Unix socket listener will be created listening to that path, and
510 the socket will be chmodded to MODE and owned by GROUP. If MODE is
511 not given, it defaults to 0600, and if GROUP is not given, the
512 process' default group is used.
513
514 ADDRESS:PORT -- PORT must be entirely numeric. A TCP socket
515 listener will be created listening to that port, bound to the
516 given local interface address. Since PDM has no authentication
517 support, ADDRESS should probably be localhost.
518 """
519 if ":" in spec:
520 first = spec[:spec.index(":")]
521 last = spec[spec.rindex(":") + 1:]
522 else:
523 first = spec
524 last = spec
525 if "/" in first:
526 parts = spec.split(":")
527 mode = 0o600
528 group = None
529 if len(parts) > 1:
530 mode = int(parts[1], 8)
531 if len(parts) > 2:
532 group = parts[2]
533 ret = unixlistener(parts[0], mode = mode, group = group)
534 ret.start()
535 return ret
536 if last.isdigit():
537 p = spec.rindex(":")
538 host = spec[:p]
539 port = int(spec[p + 1:])
540 ret = tcplistener(port, bindaddr = host)
541 ret.start()
542 return ret
543 raise ValueError("Unparsable listener specification: %r" % spec)
544
545import pdm.perf