1 """Python Daemon Management -- Server functions
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.
10 import os, sys, socket, threading, grp, select
11 import types, pprint, traceback
13 from . import perf as mperf
15 __all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"]
20 """REPL protocol handler
22 Provides a read-eval-print loop. The primary client-side interface
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.
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.
35 The REPL protocol is only intended for interactive usage. In order
36 to interact programmatically with the server process, see the PERF
39 def __init__(self, cl):
41 self.mod = types.ModuleType("repl")
42 self.mod.echo = self.echo
43 self.printer = pprint.PrettyPrinter(indent = 4, depth = 6)
46 def sendlines(self, text):
47 for line in text.split("\n"):
48 self.cl.send(b" " + line.encode("utf-8") + b"\n")
51 self.sendlines(self.printer.pformat(ob))
53 def command(self, cmd):
54 cmd = cmd.decode("utf-8")
57 ccode = compile(cmd, "PDM Input", "eval")
59 ccode = compile(cmd, "PDM Input", "exec")
60 exec(ccode, self.mod.__dict__)
61 self.cl.send(b"+OK\n")
63 self.echo(eval(ccode, self.mod.__dict__))
64 self.cl.send(b"+OK\n")
66 lines = ("".join(traceback.format_exception(*sys.exc_info()))).split("\n")
67 while len(lines) > 0 and lines[-1] == "": lines = lines[:-1]
69 self.cl.send(b" " + line.encode("utf-8") + b"\n")
70 self.cl.send(b"+EXC\n")
72 def handle(self, buf):
79 protocols["repl"] = repl
82 """PERF protocol handler
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.
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
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
104 the L{pdm.perf} module, which contains a few basic PERF
105 objects. See its documentation for details.
107 The following interfaces are currently known to PERF.
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
123 description, or an instance of the L{pdm.perf.attrinfo} class.
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.
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.
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
157 a subclass of the L{pdm.perf.event} class. If `subscribe' is
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
166 The L{pdm.perf} module contains a few convenience classes which
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.
171 The L{pdm.cli.perfclient} class is the client-side implementation.
173 def __init__(self, cl):
178 self.lock = threading.Lock()
182 for id, recv in self.subscribed.items():
184 if ob is None: continue
190 def send(self, *args):
193 buf = pickle.dumps(args)
194 buf = struct.pack(">l", len(buf)) + buf
199 def bindob(self, id, ob):
200 if not hasattr(ob, "pdm_protocols"):
201 raise mperf.nosuchname("Object does not support PDM introspection")
203 proto = ob.pdm_protocols()
204 except Exception as exc:
205 raise ValueError("PDM introspection failed", exc)
206 self.odtab[id] = ob, proto
209 def bind(self, id, module, obnm):
210 resmod = sys.modules.get(module)
212 self.send("-", mperf.nosuchname("No such module: %s" % module))
215 ob = getattr(resmod, obnm)
216 except AttributeError:
217 self.send("-", mperf.nosuchname("No such object: %s" % obnm))
220 proto = self.bindob(id, ob)
221 except Exception as exc:
224 self.send("+", proto)
226 def getob(self, id, proto):
227 ob = self.odtab.get(id)
229 self.send("-", ValueError("No such bound ID: %r" % id))
232 if proto not in protos:
233 self.send("-", mperf.nosuchproto("Object does not support that protocol: " + proto))
237 def lookup(self, tgtid, srcid, obnm):
238 src = self.getob(srcid, "dir")
242 ob = src.lookup(obnm)
243 except KeyError as exc:
244 self.send("-", mperf.nosuchname(obnm))
247 proto = self.bindob(tgtid, ob)
248 except Exception as exc:
251 self.send("+", proto)
253 def unbind(self, id):
254 ob = self.odtab.get(id)
256 self.send("-", KeyError("No such name bound: %r" % id))
260 recv = self.subscribed.get(id)
263 del self.subscribed[id]
266 def listdir(self, id):
267 ob = self.getob(id, "dir")
270 self.send("+", ob.listdir())
272 def readattr(self, id):
273 ob = self.getob(id, "attr")
278 except Exception as exc:
279 self.send("-", Exception("Could not read attribute"))
283 def attrinfo(self, id):
284 ob = self.getob(id, "attr")
287 self.send("+", ob.attrinfo())
289 def invoke(self, id, method, args, kwargs):
290 ob = self.getob(id, "invoke")
294 self.send("+", ob.invoke(method, *args, **kwargs))
295 except Exception as exc:
298 def event(self, id, ob, ev):
299 self.send("*", id, ev)
301 def subscribe(self, id):
302 ob = self.getob(id, "event")
305 if id in self.subscribed:
306 self.send("-", ValueError("Already subscribed"))
308 self.event(id, ob, ev)
310 self.subscribed[id] = recv
313 def unsubscribe(self, id):
314 ob = self.getob(id, "event")
317 recv = self.subscribed.get(id)
319 self.send("-", ValueError("Not subscribed"))
321 del self.subscribed[id]
324 def command(self, data):
328 elif cmd == "unbind":
329 self.unbind(*data[1:])
330 elif cmd == "lookup":
331 self.lookup(*data[1:])
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:])
341 self.subscribe(*data[1:])
342 elif cmd == "unsubs":
343 self.unsubscribe(*data[1:])
345 self.send("-", Exception("Unknown command: %r" % (cmd,)))
347 def handle(self, buf):
350 dlen = struct.unpack(">l", buf[:4])[0]
351 if len(buf) < dlen + 4:
353 data = pickle.loads(buf[4:dlen + 4])
355 return buf[dlen + 4:]
357 protocols["perf"] = perf
359 class client(threading.Thread):
360 def __init__(self, sk):
361 super().__init__(name = "Management client")
366 def send(self, data):
367 return self.sk.send(data)
369 def choose(self, proto):
371 proto = proto.decode("ascii")
374 if proto in protocols:
375 self.handler = protocols[proto](self)
377 self.send("-ERR Unknown protocol: %s\n" % proto)
380 def handle(self, buf):
391 self.send(b"+PDM1\n")
393 ret = self.sk.recv(1024)
399 nbuf = self.handler.handle(buf)
401 #for line in traceback.format_exception(*sys.exc_info()):
411 if hasattr(self.handler, "closed"):
412 self.handler.closed()
415 class listener(threading.Thread):
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
424 super().__init__(name = "Management listener")
427 def listen(self, sk):
428 """Listen for and accept connections."""
431 rfd, wfd, efd = select.select([sk], [], [sk], 1)
434 nsk, addr = sk.accept()
435 self.accept(nsk, addr)
438 """Stop listening for client connections
440 Tells the listener thread to stop listening, and then waits
446 def accept(self, sk, addr):
450 class unixlistener(listener):
451 """Unix socket listener"""
452 def __init__(self, name, mode = 0o600, group = None):
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
465 sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
468 if os.path.exists(self.name) and os.path.stat.S_ISSOCK(os.stat(self.name).st_mode):
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)
482 class tcplistener(listener):
483 """TCP socket listener"""
484 def __init__(self, port, bindaddr = "127.0.0.1"):
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.
491 self.bindaddr = bindaddr
494 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
496 sk.bind((self.bindaddr, self.port))
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:
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.
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.
520 first = spec[:spec.index(":")]
521 last = spec[spec.rindex(":") + 1:]
526 parts = spec.split(":")
530 mode = int(parts[1], 8)
533 ret = unixlistener(parts[0], mode = mode, group = group)
539 port = int(spec[p + 1:])
540 ret = tcplistener(port, bindaddr = host)
543 raise ValueError("Unparsable listener specification: %r" % spec)