Commit | Line | Data |
---|---|---|
6fde0e19 | 1 | """Python Daemon Management -- Server functions |
7f97a47e | 2 | |
6fde0e19 FT |
3 | This module implements the server part of the PDM protocols. The |
4 | primary object of interest herein is the listen() function, which is | |
5 | the most generic way to create PDM listeners based on user | |
6 | configuration, and the documentation for the repl and perf classes, | |
7 | which describes the functioning of the REPL and PERF protocols. | |
7f97a47e FT |
8 | """ |
9 | ||
10 | import os, sys, socket, threading, grp, select | |
11 | import types, pprint, traceback | |
12 | import pickle, struct | |
13 | ||
6fde0e19 | 14 | __all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"] |
7f97a47e FT |
15 | |
16 | protocols = {} | |
17 | ||
18 | class 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:] | |
78 | protocols["repl"] = repl | |
79 | ||
80 | class 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 | ||
356 | protocols["perf"] = perf | |
357 | ||
358 | class 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 | ||
414 | class 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 | ||
449 | class 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 | ||
481 | class 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 | ||
501 | def 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 | ||
544 | import pdm.perf |