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