2ddb9e754f063da74341770ca88e2eda1304b4c7
[pdm.git] / pdm / srv.py
1 """Python Daemon Management -- Server functions
2
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.
8 """
9
10 import os, sys, socket, threading, grp, select
11 import types, pprint, traceback
12 import pickle, struct
13 from . import perf as mperf
14
15 __all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"]
16
17 protocols = {}
18
19 class repl(object):
20     """REPL protocol handler
21     
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.
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     """
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)
44         cl.send(b"+REPL\n")
45
46     def sendlines(self, text):
47         for line in text.split("\n"):
48             self.cl.send(b" " + line.encode("utf-8") + b"\n")
49
50     def echo(self, ob):
51         self.sendlines(self.printer.pformat(ob))
52
53     def command(self, cmd):
54         cmd = cmd.decode("utf-8")
55         try:
56             try:
57                 ccode = compile(cmd, "PDM Input", "eval")
58             except SyntaxError:
59                 ccode = compile(cmd, "PDM Input", "exec")
60                 exec(ccode, self.mod.__dict__)
61                 self.cl.send(b"+OK\n")
62             else:
63                 self.echo(eval(ccode, self.mod.__dict__))
64                 self.cl.send(b"+OK\n")
65         except:
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:
69                 self.cl.send(b" " + line.encode("utf-8") + b"\n")
70             self.cl.send(b"+EXC\n")
71
72     def handle(self, buf):
73         p = buf.find(b"\n\n")
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):
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
104     the L{pdm.perf} module, which contains a few basic PERF
105     objects. See its documentation for details.
106
107     The following interfaces are currently known to PERF.
108
109      - attr:
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.
124
125      - dir:
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
136      - invoke:
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
147      - event:
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
164        raised.
165
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.
170
171     The L{pdm.cli.perfclient} class is the client-side implementation.
172     """
173     def __init__(self, cl):
174         self.cl = cl
175         self.odtab = {}
176         cl.send(b"+PERF1\n")
177         self.buf = ""
178         self.lock = threading.Lock()
179         self.subscribed = {}
180
181     def closed(self):
182         for id, recv in self.subscribed.items():
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"):
201             raise mperf.nosuchname("Object does not support PDM introspection")
202         try:
203             proto = ob.pdm_protocols()
204         except Exception as exc:
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:
212             self.send("-", mperf.nosuchname("No such module: %s" % module))
213             return
214         try:
215             ob = getattr(resmod, obnm)
216         except AttributeError:
217             self.send("-", mperf.nosuchname("No such object: %s" % obnm))
218             return
219         try:
220             proto = self.bindob(id, ob)
221         except Exception as exc:
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:
233             self.send("-", mperf.nosuchproto("Object does not support that protocol: " + proto))
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)
243         except KeyError as exc:
244             self.send("-", mperf.nosuchname(obnm))
245             return
246         try:
247             proto = self.bindob(tgtid, ob)
248         except Exception as exc:
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()
278         except Exception as exc:
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))
295         except Exception as exc:
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):
361         super().__init__(name = "Management client")
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):
370         try:
371             proto = proto.decode("ascii")
372         except UnicodeError:
373             proto = None
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):
381         p = buf.find(b"\n")
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:
390             buf = b""
391             self.send(b"+PDM1\n")
392             while True:
393                 ret = self.sk.recv(1024)
394                 if ret == b"":
395                     return
396                 buf += ret
397                 while True:
398                     try:
399                         nbuf = self.handler.handle(buf)
400                     except:
401                         #for line in traceback.format_exception(*sys.exc_info()):
402                         #    print(line)
403                         return
404                     if nbuf == buf:
405                         break
406                     buf = nbuf
407         finally:
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):
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     """
423     def __init__(self):
424         super().__init__(name = "Management listener")
425         self.setDaemon(True)
426
427     def listen(self, sk):
428         """Listen for and accept connections."""
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):
438         """Stop listening for client connections
439
440         Tells the listener thread to stop listening, and then waits
441         for it to terminate.
442         """
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):
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
457         owner of the socket.
458         """
459         super().__init__()
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):
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.
488         """
489         super().__init__()
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):
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     """
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(":")
527         mode = 0o600
528         group = None
529         if len(parts) > 1:
530             mode = int(parts[1], 8)
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