Properly split exception reports into lines.
[pdm.git] / pdm / srv.py
CommitLineData
6fde0e19 1"""Python Daemon Management -- Server functions
7f97a47e 2
6fde0e19
FT
3This module implements the server part of the PDM protocols. The
4primary object of interest herein is the listen() function, which is
5the most generic way to create PDM listeners based on user
6configuration, and the documentation for the repl and perf classes,
7which describes the functioning of the REPL and PERF protocols.
7f97a47e
FT
8"""
9
10import os, sys, socket, threading, grp, select
11import types, pprint, traceback
12import pickle, struct
13
6fde0e19 14__all__ = ["repl", "perf", "listener", "unixlistener", "tcplistener", "listen"]
7f97a47e
FT
15
16protocols = {}
17
18class repl(object):
6fde0e19
FT
19 """REPL protocol handler
20
21 Provides a read-eval-print loop. The primary client-side interface
57808152
FT
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.
6fde0e19
FT
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 """
7f97a47e
FT
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:
0eaf2431
FT
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")
7f97a47e
FT
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:]
77protocols["repl"] = repl
78
79class perf(object):
6fde0e19
FT
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
57808152
FT
102 the L{pdm.perf} module, which contains a few basic PERF
103 objects. See its documentation for details.
6fde0e19
FT
104
105 The following interfaces are currently known to PERF.
106
57808152 107 - attr:
6fde0e19
FT
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
57808152 121 description, or an instance of the L{pdm.perf.attrinfo} class.
6fde0e19 122
57808152 123 - dir:
6fde0e19
FT
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
57808152 134 - invoke:
6fde0e19
FT
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
57808152 145 - event:
6fde0e19
FT
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
57808152 155 a subclass of the L{pdm.perf.event} class. If `subscribe' is
6fde0e19
FT
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
57808152 164 The L{pdm.perf} module contains a few convenience classes which
6fde0e19
FT
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
57808152 169 The L{pdm.cli.perfclient} class is the client-side implementation.
6fde0e19 170 """
7f97a47e
FT
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
355protocols["perf"] = perf
356
357class 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
409class listener(threading.Thread):
6fde0e19
FT
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 """
7f97a47e
FT
417 def __init__(self):
418 super(listener, self).__init__(name = "Management listener")
419 self.setDaemon(True)
420
421 def listen(self, sk):
6fde0e19 422 """Listen for and accept connections."""
7f97a47e
FT
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):
6fde0e19
FT
432 """Stop listening for client connections
433
434 Tells the listener thread to stop listening, and then waits
435 for it to terminate.
436 """
7f97a47e
FT
437 self.running = False
438 self.join()
439
440 def accept(self, sk, addr):
441 cl = client(sk)
442 cl.start()
443
444class unixlistener(listener):
6fde0e19 445 """Unix socket listener"""
7f97a47e 446 def __init__(self, name, mode = 0600, group = None):
6fde0e19
FT
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 """
7f97a47e
FT
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
476class tcplistener(listener):
6fde0e19 477 """TCP socket listener"""
7f97a47e 478 def __init__(self, port, bindaddr = "127.0.0.1"):
6fde0e19
FT
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 """
7f97a47e
FT
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
496def listen(spec):
6fde0e19
FT
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 """
7f97a47e
FT
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:
cfc372bf 524 mode = int(parts[1], 8)
7f97a47e
FT
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
539import pdm.perf