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