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