Throw more informative error classes from perf.
[pdm.git] / pdm / srv.py
CommitLineData
6fde0e19 1"""Python Daemon Management -- Server functions
7f97a47e 2
6fde0e19
FT
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.
7f97a47e
FT
8"""
9
10import os, sys, socket, threading, grp, select
11import types, pprint, traceback
12import pickle, struct
e7b433ee 13from . import perf as mperf
7f97a47e 14
6fde0e19 15__all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"]
7f97a47e
FT
16
17protocols = {}
18
19class repl(object):
6fde0e19
FT
20 """REPL protocol handler
21
22 Provides a read-eval-print loop. The primary client-side interface
57808152
FT
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.
6fde0e19
FT
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 """
7f97a47e
FT
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)
11d50d09 44 cl.send(b"+REPL\n")
7f97a47e
FT
45
46 def sendlines(self, text):
47 for line in text.split("\n"):
11d50d09 48 self.cl.send(b" " + line.encode("utf-8") + b"\n")
7f97a47e
FT
49
50 def echo(self, ob):
51 self.sendlines(self.printer.pformat(ob))
52
53 def command(self, cmd):
11d50d09 54 cmd = cmd.decode("utf-8")
7f97a47e
FT
55 try:
56 try:
57 ccode = compile(cmd, "PDM Input", "eval")
58 except SyntaxError:
59 ccode = compile(cmd, "PDM Input", "exec")
afd9f04c 60 exec(ccode, self.mod.__dict__)
11d50d09 61 self.cl.send(b"+OK\n")
7f97a47e
FT
62 else:
63 self.echo(eval(ccode, self.mod.__dict__))
11d50d09 64 self.cl.send(b"+OK\n")
7f97a47e 65 except:
0eaf2431
FT
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:
9b6845a6 69 self.cl.send(b" " + line.encode("utf-8") + b"\n")
11d50d09 70 self.cl.send(b"+EXC\n")
7f97a47e
FT
71
72 def handle(self, buf):
11d50d09 73 p = buf.find(b"\n\n")
7f97a47e
FT
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):
6fde0e19
FT
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
57808152
FT
104 the L{pdm.perf} module, which contains a few basic PERF
105 objects. See its documentation for details.
6fde0e19
FT
106
107 The following interfaces are currently known to PERF.
108
57808152 109 - attr:
6fde0e19
FT
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
57808152 123 description, or an instance of the L{pdm.perf.attrinfo} class.
6fde0e19 124
57808152 125 - dir:
6fde0e19
FT
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
57808152 136 - invoke:
6fde0e19
FT
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
57808152 147 - event:
6fde0e19
FT
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
57808152 157 a subclass of the L{pdm.perf.event} class. If `subscribe' is
6fde0e19
FT
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
57808152 166 The L{pdm.perf} module contains a few convenience classes which
6fde0e19
FT
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
57808152 171 The L{pdm.cli.perfclient} class is the client-side implementation.
6fde0e19 172 """
7f97a47e
FT
173 def __init__(self, cl):
174 self.cl = cl
175 self.odtab = {}
11d50d09 176 cl.send(b"+PERF1\n")
7f97a47e
FT
177 self.buf = ""
178 self.lock = threading.Lock()
179 self.subscribed = {}
180
181 def closed(self):
afd9f04c 182 for id, recv in self.subscribed.items():
7f97a47e
FT
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"):
e7b433ee 201 raise mperf.nosuchname("Object does not support PDM introspection")
7f97a47e
FT
202 try:
203 proto = ob.pdm_protocols()
afd9f04c 204 except Exception as exc:
7f97a47e
FT
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:
e7b433ee 212 self.send("-", mperf.nosuchname("No such module: %s" % module))
7f97a47e
FT
213 return
214 try:
215 ob = getattr(resmod, obnm)
216 except AttributeError:
e7b433ee 217 self.send("-", mperf.nosuchname("No such object: %s" % obnm))
7f97a47e
FT
218 return
219 try:
220 proto = self.bindob(id, ob)
afd9f04c 221 except Exception as exc:
7f97a47e
FT
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:
e7b433ee 233 self.send("-", mperf.nosuchproto("Object does not support that protocol: " + proto))
7f97a47e
FT
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)
afd9f04c 243 except KeyError as exc:
e7b433ee 244 self.send("-", mperf.nosuchname(obnm))
7f97a47e
FT
245 return
246 try:
247 proto = self.bindob(tgtid, ob)
afd9f04c 248 except Exception as exc:
7f97a47e
FT
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()
afd9f04c 278 except Exception as exc:
7f97a47e
FT
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))
afd9f04c 295 except Exception as exc:
7f97a47e
FT
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):
ed115f48 361 super().__init__(name = "Management client")
7f97a47e
FT
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):
11d50d09
FT
370 try:
371 proto = proto.decode("ascii")
372 except UnicodeError:
373 proto = None
7f97a47e
FT
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):
11d50d09 381 p = buf.find(b"\n")
7f97a47e
FT
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:
11d50d09
FT
390 buf = b""
391 self.send(b"+PDM1\n")
7f97a47e
FT
392 while True:
393 ret = self.sk.recv(1024)
11d50d09 394 if ret == b"":
7f97a47e
FT
395 return
396 buf += ret
397 while True:
398 try:
399 nbuf = self.handler.handle(buf)
400 except:
11d50d09
FT
401 #for line in traceback.format_exception(*sys.exc_info()):
402 # print(line)
7f97a47e
FT
403 return
404 if nbuf == buf:
405 break
406 buf = nbuf
407 finally:
7f97a47e
FT
408 try:
409 self.sk.close()
410 finally:
411 if hasattr(self.handler, "closed"):
412 self.handler.closed()
413
414
415class listener(threading.Thread):
6fde0e19
FT
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 """
7f97a47e 423 def __init__(self):
ed115f48 424 super().__init__(name = "Management listener")
7f97a47e
FT
425 self.setDaemon(True)
426
427 def listen(self, sk):
6fde0e19 428 """Listen for and accept connections."""
7f97a47e
FT
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):
6fde0e19
FT
438 """Stop listening for client connections
439
440 Tells the listener thread to stop listening, and then waits
441 for it to terminate.
442 """
7f97a47e
FT
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):
6fde0e19 451 """Unix socket listener"""
afd9f04c 452 def __init__(self, name, mode = 0o600, group = None):
6fde0e19
FT
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 """
ed115f48 459 super().__init__()
7f97a47e
FT
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):
6fde0e19 483 """TCP socket listener"""
7f97a47e 484 def __init__(self, port, bindaddr = "127.0.0.1"):
6fde0e19
FT
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 """
ed115f48 489 super().__init__()
7f97a47e
FT
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):
6fde0e19
FT
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 """
7f97a47e
FT
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(":")
afd9f04c 527 mode = 0o600
7f97a47e
FT
528 group = None
529 if len(parts) > 1:
59f47152 530 mode = int(parts[1], 8)
7f97a47e
FT
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