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