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