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 | |
e7b433ee | 13 | from . import perf as mperf |
7f97a47e | 14 | |
6fde0e19 | 15 | __all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"] |
7f97a47e FT |
16 | |
17 | protocols = {} | |
18 | ||
19 | class 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:] | |
79 | protocols["repl"] = repl | |
80 | ||
81 | class 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 | ||
357 | protocols["perf"] = perf | |
358 | ||
359 | class 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 | ||
415 | class 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 | ||
450 | class 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 | ||
482 | class 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 | ||
502 | def 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 | ||
545 | import pdm.perf |