Merge branch 'master' into python3
[pdm.git] / pdm / cli.py
CommitLineData
7f97a47e
FT
1"""Management for daemon processes
2
3This module provides some client support for the daemon management
4provided in the pdm.srv module.
5"""
6
7import socket, pickle, struct, select, threading
8
ac4f5166 9__all__ = ["client", "replclient", "perfclient"]
7f97a47e
FT
10
11class protoerr(Exception):
73c1ef8b 12 """Raised on protocol errors"""
7f97a47e
FT
13 pass
14
15def resolve(spec):
16 if isinstance(spec, socket.socket):
17 return spec
18 sk = None
19 try:
20 if "/" in spec:
21 sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
22 sk.connect(spec)
23 elif spec.isdigit():
24 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
25 sk.connect(("localhost", int(spec)))
26 elif ":" in spec:
27 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
28 p = spec.rindex(":")
29 sk.connect((spec[:p], int(spec[p + 1:])))
30 else:
31 raise Exception("Unknown target specification %r" % spec)
32 rv = sk
33 sk = None
34 finally:
35 if sk is not None: sk.close()
36 return rv
37
38class client(object):
73c1ef8b
FT
39 """PDM client
40
41 This class provides general facilities to speak to PDM servers,
42 and is mainly intended to be subclassed to provide for the
43 specific protocols, such as replclient and perfclient do.
44
45 `client' instances can be passed as arguments to select.select(),
46 and can be used in `with' statements.
47 """
7f97a47e 48 def __init__(self, sk, proto = None):
73c1ef8b
FT
49 """Create a client object connected to the specified
50 server. `sk' can either be a socket object, which is used as
51 it is, or a string specification very similar to the
52 specification for pdm.srv.listen, so see its documentation for
53 details. The differences are only that this function does not
54 take arguments specific to socket creation, like the mode and
55 group arguments for Unix sockets. If `proto' is given, that
56 subprotocol will negotiated with the server (by calling the
57 select() method).
58 """
7f97a47e 59 self.sk = resolve(sk)
11d50d09 60 self.buf = b""
7f97a47e 61 line = self.readline()
11d50d09 62 if line != b"+PDM1":
7f97a47e
FT
63 raise protoerr("Illegal protocol signature")
64 if proto is not None:
65 self.select(proto)
66
67 def close(self):
73c1ef8b 68 """Close this connection"""
7f97a47e
FT
69 self.sk.close()
70
9928d247 71 def fileno(self):
73c1ef8b 72 """Return the file descriptor of the underlying socket."""
9928d247
FT
73 return self.sk.fileno()
74
7f97a47e 75 def readline(self):
73c1ef8b 76 """Read a single NL-terminated line and return it."""
7f97a47e 77 while True:
11d50d09 78 p = self.buf.find(b"\n")
7f97a47e
FT
79 if p >= 0:
80 ret = self.buf[:p]
81 self.buf = self.buf[p + 1:]
82 return ret
83 ret = self.sk.recv(1024)
11d50d09 84 if ret == b"":
7f97a47e
FT
85 return None
86 self.buf += ret
87
88 def select(self, proto):
73c1ef8b 89 """Negotiate the given subprotocol with the server"""
11d50d09
FT
90 if isinstance(proto, str):
91 proto = proto.encode("ascii")
92 if b"\n" in proto:
7f97a47e 93 raise Exception("Illegal protocol specified: %r" % proto)
11d50d09 94 self.sk.send(proto + b"\n")
7f97a47e 95 rep = self.readline()
11d50d09 96 if len(rep) < 1 or rep[0] != b"+"[0]:
7f97a47e
FT
97 raise protoerr("Error reply when selecting protocol %s: %s" % (proto, rep[1:]))
98
99 def __enter__(self):
100 return self
101
102 def __exit__(self, *excinfo):
103 self.close()
104 return False
105
106class replclient(client):
73c1ef8b
FT
107 """REPL protocol client
108
109 Implements the client side of the REPL protocol; see pdm.srv.repl
110 for details on the protocol and its functionality.
111 """
7f97a47e 112 def __init__(self, sk):
73c1ef8b 113 """Create a connected client as documented in the `client' class."""
ed115f48 114 super().__init__(sk, "repl")
7f97a47e
FT
115
116 def run(self, code):
73c1ef8b
FT
117 """Run a single block of Python code on the server. Returns
118 the output of the command (as documented in pdm.srv.repl) as a
119 string.
120 """
7f97a47e
FT
121 while True:
122 ncode = code.replace("\n\n", "\n")
123 if ncode == code: break
124 code = ncode
125 while len(code) > 0 and code[-1] == "\n":
126 code = code[:-1]
11d50d09
FT
127 self.sk.send((code + "\n\n").encode("utf-8"))
128 buf = b""
7f97a47e
FT
129 while True:
130 ln = self.readline()
11d50d09
FT
131 if ln[0] == b" "[0]:
132 buf += ln[1:] + b"\n"
133 elif ln[0] == b"+"[0]:
134 return buf.decode("utf-8")
135 elif ln[0] == b"-"[0]:
136 raise protoerr("Error reply: %s" % ln[1:].decode("utf-8"))
7f97a47e
FT
137 else:
138 raise protoerr("Illegal reply: %s" % ln)
139
140class perfproxy(object):
141 def __init__(self, cl, id, proto):
142 self.cl = cl
143 self.id = id
144 self.proto = proto
145 self.subscribers = set()
146
147 def lookup(self, name):
148 self.cl.lock.acquire()
149 try:
150 id = self.cl.nextid
151 self.cl.nextid += 1
152 finally:
153 self.cl.lock.release()
154 (proto,) = self.cl.run("lookup", id, self.id, name)
155 proxy = perfproxy(self.cl, id, proto)
156 self.cl.proxies[id] = proxy
157 return proxy
158
159 def listdir(self):
160 return self.cl.run("ls", self.id)[0]
161
162 def readattr(self):
163 return self.cl.run("readattr", self.id)[0]
164
165 def attrinfo(self):
166 return self.cl.run("attrinfo", self.id)[0]
167
168 def invoke(self, method, *args, **kwargs):
169 return self.cl.run("invoke", self.id, method, args, kwargs)[0]
170
171 def subscribe(self, cb):
172 if cb in self.subscribers:
173 raise ValueError("Already subscribed")
174 if len(self.subscribers) == 0:
175 self.cl.run("subs", self.id)
176 self.subscribers.add(cb)
177
441160a2 178 def unsubscribe(self, cb):
7f97a47e
FT
179 if cb not in self.subscribers:
180 raise ValueError("Not subscribed")
181 self.subscribers.remove(cb)
182 if len(self.subscribers) == 0:
183 self.cl.run("unsubs", self.id)
184
185 def notify(self, ev):
186 for cb in self.subscribers:
187 try:
188 cb(ev)
189 except: pass
190
191 def close(self):
4fcf3c74
FT
192 if self.id is not None:
193 self.cl.run("unbind", self.id)
194 del self.cl.proxies[self.id]
195 self.id = None
196
197 def __del__(self):
198 self.close()
7f97a47e
FT
199
200 def __enter__(self):
201 return self
202
203 def __exit__(self, *excinfo):
204 self.close()
205 return False
206
207class perfclient(client):
73c1ef8b
FT
208 """PERF protocol client
209
210 Implements the client side of the PERF protocol; see pdm.srv.perf
211 for details on the protocol and its functionality.
212
213 This client class implements functions for finding PERF objects on
214 the server, and returns, for each server-side object looked up, a
215 proxy object that mimics exactly the PERF interfaces that the
216 object implements. As the proxy objects reference live objects on
217 the server, they should be released when they are no longer used;
218 they implement a close() method for that purpose, and can also be
219 used in `with' statements.
220
221 See pdm.srv.perf for details on the various PERF interfaces that
222 the proxy objects might implement.
223 """
7f97a47e 224 def __init__(self, sk):
73c1ef8b 225 """Create a connected client as documented in the `client' class."""
ed115f48 226 super().__init__(sk, "perf")
7f97a47e
FT
227 self.nextid = 0
228 self.lock = threading.Lock()
229 self.proxies = {}
230 self.names = {}
231
232 def send(self, ob):
233 buf = pickle.dumps(ob)
234 buf = struct.pack(">l", len(buf)) + buf
235 self.sk.send(buf)
236
237 def recvb(self, num):
11d50d09 238 buf = b""
7f97a47e
FT
239 while len(buf) < num:
240 data = self.sk.recv(num - len(buf))
11d50d09 241 if data == b"":
7f97a47e
FT
242 raise EOFError()
243 buf += data
244 return buf
245
246 def recv(self):
247 return pickle.loads(self.recvb(struct.unpack(">l", self.recvb(4))[0]))
248
249 def event(self, id, ev):
250 proxy = self.proxies.get(id)
251 if proxy is None: return
252 proxy.notify(ev)
253
254 def dispatch(self, timeout = None):
73c1ef8b
FT
255 """Wait for an incoming notification from the server, and
256 dispatch it to the callback functions that have been
257 registered for it. If `timeout' is specified, wait no longer
258 than so many seconds; otherwise, wait forever. This client
259 object may also be used as argument to select.select().
260 """
7f97a47e
FT
261 rfd, wfd, efd = select.select([self.sk], [], [], timeout)
262 if self.sk in rfd:
263 msg = self.recv()
264 if msg[0] == "*":
265 self.event(msg[1], msg[2])
266 else:
267 raise ValueError("Unexpected non-event message: %r" % msg[0])
268
269 def recvreply(self):
270 while True:
271 reply = self.recv()
272 if reply[0] in ("+", "-"):
273 return reply
274 elif reply[0] == "*":
275 self.event(reply[1], reply[2])
276 else:
277 raise ValueError("Illegal reply header: %r" % reply[0])
278
279 def run(self, cmd, *args):
280 self.lock.acquire()
281 try:
282 self.send((cmd,) + args)
283 reply = self.recvreply()
284 if reply[0] == "+":
285 return reply[1:]
286 else:
287 raise reply[1]
288 finally:
289 self.lock.release()
290
291 def lookup(self, module, obnm):
73c1ef8b
FT
292 """Look up a single server-side object by the given name in
293 the given module. Will return a new proxy object for each
294 call when called multiple times for the same name.
295 """
7f97a47e
FT
296 self.lock.acquire()
297 try:
298 id = self.nextid
299 self.nextid += 1
300 finally:
301 self.lock.release()
302 (proto,) = self.run("bind", id, module, obnm)
303 proxy = perfproxy(self, id, proto)
304 self.proxies[id] = proxy
305 return proxy
306
307 def find(self, name):
73c1ef8b
FT
308 """Convenience function for looking up server-side objects
309 through PERF directories and for multiple uses. The object
310 name can be given as "MODULE.OBJECT", which will look up the
311 named OBJECT in the named MODULE, and can be followed by any
312 number of slash-separated names, which will assume that the
313 object to the left of the slash is a PERF directory, and will
314 return the object in that directory by the name to the right
315 of the slash. For instance, find("pdm.perf.sysres/cputime")
316 will return the built-in attribute for reading the CPU time
317 used by the server process.
318 """
7f97a47e
FT
319 ret = self.names.get(name)
320 if ret is None:
321 if "/" in name:
322 p = name.rindex("/")
323 ret = self.find(name[:p]).lookup(name[p + 1:])
324 else:
325 p = name.rindex(".")
326 ret = self.lookup(name[:p], name[p + 1:])
327 self.names[name] = ret
328 return ret