Made relevant Python3 changes.
[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
14 __all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"]
15
16 protocols = {}
17
18 class repl(object):
19     """REPL protocol handler
20     
21     Provides a read-eval-print loop. The primary client-side interface
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.
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     """
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)
43         cl.send(b"+REPL\n")
44
45     def sendlines(self, text):
46         for line in text.split("\n"):
47             self.cl.send(b" " + line.encode("utf-8") + b"\n")
48
49     def echo(self, ob):
50         self.sendlines(self.printer.pformat(ob))
51
52     def command(self, cmd):
53         cmd = cmd.decode("utf-8")
54         try:
55             try:
56                 ccode = compile(cmd, "PDM Input", "eval")
57             except SyntaxError:
58                 ccode = compile(cmd, "PDM Input", "exec")
59                 exec(ccode, self.mod.__dict__)
60                 self.cl.send(b"+OK\n")
61             else:
62                 self.echo(eval(ccode, self.mod.__dict__))
63                 self.cl.send(b"+OK\n")
64         except:
65             for line in traceback.format_exception(*sys.exc_info()):
66                 self.cl.send(b" " + line.encode("utf-8"))
67             self.cl.send(b"+EXC\n")
68
69     def handle(self, buf):
70         p = buf.find(b"\n\n")
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):
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
101     the L{pdm.perf} module, which contains a few basic PERF
102     objects. See its documentation for details.
103
104     The following interfaces are currently known to PERF.
105
106      - attr:
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
120        description, or an instance of the L{pdm.perf.attrinfo} class.
121
122      - dir:
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
133      - invoke:
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
144      - event:
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
154        a subclass of the L{pdm.perf.event} class. If `subscribe' is
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
163     The L{pdm.perf} module contains a few convenience classes which
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
168     The L{pdm.cli.perfclient} class is the client-side implementation.
169     """
170     def __init__(self, cl):
171         self.cl = cl
172         self.odtab = {}
173         cl.send(b"+PERF1\n")
174         self.buf = ""
175         self.lock = threading.Lock()
176         self.subscribed = {}
177
178     def closed(self):
179         for id, recv in self.subscribed.items():
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()
201         except Exception as exc:
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)
218         except Exception as exc:
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)
240         except KeyError as exc:
241             self.send("-", exc)
242             return
243         try:
244             proto = self.bindob(tgtid, ob)
245         except Exception as exc:
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()
275         except Exception as exc:
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))
292         except Exception as exc:
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):
358         super().__init__(name = "Management client")
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):
367         try:
368             proto = proto.decode("ascii")
369         except UnicodeError:
370             proto = None
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):
378         p = buf.find(b"\n")
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:
387             buf = b""
388             self.send(b"+PDM1\n")
389             while True:
390                 ret = self.sk.recv(1024)
391                 if ret == b"":
392                     return
393                 buf += ret
394                 while True:
395                     try:
396                         nbuf = self.handler.handle(buf)
397                     except:
398                         #for line in traceback.format_exception(*sys.exc_info()):
399                         #    print(line)
400                         return
401                     if nbuf == buf:
402                         break
403                     buf = nbuf
404         finally:
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):
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     """
420     def __init__(self):
421         super().__init__(name = "Management listener")
422         self.setDaemon(True)
423
424     def listen(self, sk):
425         """Listen for and accept connections."""
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):
435         """Stop listening for client connections
436
437         Tells the listener thread to stop listening, and then waits
438         for it to terminate.
439         """
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):
448     """Unix socket listener"""
449     def __init__(self, name, mode = 0o600, group = None):
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         """
456         super().__init__()
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):
480     """TCP socket listener"""
481     def __init__(self, port, bindaddr = "127.0.0.1"):
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         """
486         super().__init__()
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):
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     """
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(":")
524         mode = 0o600
525         group = None
526         if len(parts) > 1:
527             mode = int(parts[1], 8)
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