OLD | NEW |
| (Empty) |
1 | |
2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """L{twisted.manhole} L{PB<twisted.spread.pb>} service implementation. | |
7 """ | |
8 | |
9 # twisted imports | |
10 from twisted import copyright | |
11 from twisted.spread import pb | |
12 from twisted.python import log, failure | |
13 from twisted.cred import portal | |
14 from twisted.application import service | |
15 from zope.interface import implements, Interface | |
16 | |
17 # sibling imports | |
18 import explorer | |
19 | |
20 # system imports | |
21 from cStringIO import StringIO | |
22 | |
23 import string | |
24 import sys | |
25 import traceback | |
26 import types | |
27 | |
28 | |
29 class FakeStdIO: | |
30 def __init__(self, type_, list): | |
31 self.type = type_ | |
32 self.list = list | |
33 | |
34 def write(self, text): | |
35 log.msg("%s: %s" % (self.type, string.strip(str(text)))) | |
36 self.list.append((self.type, text)) | |
37 | |
38 def flush(self): | |
39 pass | |
40 | |
41 def consolidate(self): | |
42 """Concatenate adjacent messages of same type into one. | |
43 | |
44 Greatly cuts down on the number of elements, increasing | |
45 network transport friendliness considerably. | |
46 """ | |
47 if not self.list: | |
48 return | |
49 | |
50 inlist = self.list | |
51 outlist = [] | |
52 last_type = inlist[0] | |
53 block_begin = 0 | |
54 for i in xrange(1, len(self.list)): | |
55 (mtype, message) = inlist[i] | |
56 if mtype == last_type: | |
57 continue | |
58 else: | |
59 if (i - block_begin) == 1: | |
60 outlist.append(inlist[block_begin]) | |
61 else: | |
62 messages = map(lambda l: l[1], | |
63 inlist[block_begin:i]) | |
64 message = string.join(messages, '') | |
65 outlist.append((last_type, message)) | |
66 last_type = mtype | |
67 block_begin = i | |
68 | |
69 | |
70 class IManholeClient(Interface): | |
71 def console(list_of_messages): | |
72 """Takes a list of (type, message) pairs to display. | |
73 | |
74 Types include: | |
75 - \"stdout\" -- string sent to sys.stdout | |
76 | |
77 - \"stderr\" -- string sent to sys.stderr | |
78 | |
79 - \"result\" -- string repr of the resulting value | |
80 of the expression | |
81 | |
82 - \"exception\" -- a L{failure.Failure} | |
83 """ | |
84 | |
85 def receiveExplorer(xplorer): | |
86 """Receives an explorer.Explorer | |
87 """ | |
88 | |
89 def listCapabilities(): | |
90 """List what manholey things I am capable of doing. | |
91 | |
92 i.e. C{\"Explorer\"}, C{\"Failure\"} | |
93 """ | |
94 | |
95 def runInConsole(command, console, globalNS=None, localNS=None, | |
96 filename=None, args=None, kw=None, unsafeTracebacks=False): | |
97 """Run this, directing all output to the specified console. | |
98 | |
99 If command is callable, it will be called with the args and keywords | |
100 provided. Otherwise, command will be compiled and eval'd. | |
101 (Wouldn't you like a macro?) | |
102 | |
103 Returns the command's return value. | |
104 | |
105 The console is called with a list of (type, message) pairs for | |
106 display, see L{IManholeClient.console}. | |
107 """ | |
108 output = [] | |
109 fakeout = FakeStdIO("stdout", output) | |
110 fakeerr = FakeStdIO("stderr", output) | |
111 errfile = FakeStdIO("exception", output) | |
112 code = None | |
113 val = None | |
114 if filename is None: | |
115 filename = str(console) | |
116 if args is None: | |
117 args = () | |
118 if kw is None: | |
119 kw = {} | |
120 if localNS is None: | |
121 localNS = globalNS | |
122 if (globalNS is None) and (not callable(command)): | |
123 raise ValueError("Need a namespace to evaluate the command in.") | |
124 | |
125 try: | |
126 out = sys.stdout | |
127 err = sys.stderr | |
128 sys.stdout = fakeout | |
129 sys.stderr = fakeerr | |
130 try: | |
131 if callable(command): | |
132 val = apply(command, args, kw) | |
133 else: | |
134 try: | |
135 code = compile(command, filename, 'eval') | |
136 except: | |
137 code = compile(command, filename, 'single') | |
138 | |
139 if code: | |
140 val = eval(code, globalNS, localNS) | |
141 finally: | |
142 sys.stdout = out | |
143 sys.stderr = err | |
144 except: | |
145 (eType, eVal, tb) = sys.exc_info() | |
146 fail = failure.Failure(eVal, eType, tb) | |
147 del tb | |
148 # In CVS reversion 1.35, there was some code here to fill in the | |
149 # source lines in the traceback for frames in the local command | |
150 # buffer. But I can't figure out when that's triggered, so it's | |
151 # going away in the conversion to Failure, until you bring it back. | |
152 errfile.write(pb.failure2Copyable(fail, unsafeTracebacks)) | |
153 | |
154 if console: | |
155 fakeout.consolidate() | |
156 console(output) | |
157 | |
158 return val | |
159 | |
160 def _failureOldStyle(fail): | |
161 """Pre-Failure manhole representation of exceptions. | |
162 | |
163 For compatibility with manhole clients without the \"Failure\" | |
164 capability. | |
165 | |
166 A dictionary with two members: | |
167 - \'traceback\' -- traceback.extract_tb output; a list of tuples | |
168 (filename, line number, function name, text) suitable for | |
169 feeding to traceback.format_list. | |
170 | |
171 - \'exception\' -- a list of one or more strings, each | |
172 ending in a newline. (traceback.format_exception_only output) | |
173 """ | |
174 import linecache | |
175 tb = [] | |
176 for f in fail.frames: | |
177 # (filename, line number, function name, text) | |
178 tb.append((f[1], f[2], f[0], linecache.getline(f[1], f[2]))) | |
179 | |
180 return { | |
181 'traceback': tb, | |
182 'exception': traceback.format_exception_only(fail.type, fail.value) | |
183 } | |
184 | |
185 # Capabilities clients are likely to have before they knew how to answer a | |
186 # "listCapabilities" query. | |
187 _defaultCapabilities = { | |
188 "Explorer": 'Set' | |
189 } | |
190 | |
191 class Perspective(pb.Avatar): | |
192 lastDeferred = 0 | |
193 def __init__(self, service): | |
194 self.localNamespace = { | |
195 "service": service, | |
196 "avatar": self, | |
197 "_": None, | |
198 } | |
199 self.clients = {} | |
200 self.service = service | |
201 | |
202 def __getstate__(self): | |
203 state = self.__dict__.copy() | |
204 state['clients'] = {} | |
205 if state['localNamespace'].has_key("__builtins__"): | |
206 del state['localNamespace']['__builtins__'] | |
207 return state | |
208 | |
209 def attached(self, client, identity): | |
210 """A client has attached -- welcome them and add them to the list. | |
211 """ | |
212 self.clients[client] = identity | |
213 | |
214 host = ':'.join(map(str, client.broker.transport.getHost()[1:])) | |
215 | |
216 msg = self.service.welcomeMessage % { | |
217 'you': getattr(identity, 'name', str(identity)), | |
218 'host': host, | |
219 'longversion': copyright.longversion, | |
220 } | |
221 | |
222 client.callRemote('console', [("stdout", msg)]) | |
223 | |
224 client.capabilities = _defaultCapabilities | |
225 client.callRemote('listCapabilities').addCallbacks( | |
226 self._cbClientCapable, self._ebClientCapable, | |
227 callbackArgs=(client,),errbackArgs=(client,)) | |
228 | |
229 def detached(self, client, identity): | |
230 try: | |
231 del self.clients[client] | |
232 except KeyError: | |
233 pass | |
234 | |
235 def runInConsole(self, command, *args, **kw): | |
236 """Convience method to \"runInConsole with my stuff\". | |
237 """ | |
238 return runInConsole(command, | |
239 self.console, | |
240 self.service.namespace, | |
241 self.localNamespace, | |
242 str(self.service), | |
243 args=args, | |
244 kw=kw, | |
245 unsafeTracebacks=self.service.unsafeTracebacks) | |
246 | |
247 | |
248 ### Methods for communicating to my clients. | |
249 | |
250 def console(self, message): | |
251 """Pass a message to my clients' console. | |
252 """ | |
253 clients = self.clients.keys() | |
254 origMessage = message | |
255 compatMessage = None | |
256 for client in clients: | |
257 try: | |
258 if not client.capabilities.has_key("Failure"): | |
259 if compatMessage is None: | |
260 compatMessage = origMessage[:] | |
261 for i in xrange(len(message)): | |
262 if ((message[i][0] == "exception") and | |
263 isinstance(message[i][1], failure.Failure)): | |
264 compatMessage[i] = ( | |
265 message[i][0], | |
266 _failureOldStyle(message[i][1])) | |
267 client.callRemote('console', compatMessage) | |
268 else: | |
269 client.callRemote('console', message) | |
270 except pb.ProtocolError: | |
271 # Stale broker. | |
272 self.detached(client, None) | |
273 | |
274 def receiveExplorer(self, objectLink): | |
275 """Pass an Explorer on to my clients. | |
276 """ | |
277 clients = self.clients.keys() | |
278 for client in clients: | |
279 try: | |
280 client.callRemote('receiveExplorer', objectLink) | |
281 except pb.ProtocolError: | |
282 # Stale broker. | |
283 self.detached(client, None) | |
284 | |
285 | |
286 def _cbResult(self, val, dnum): | |
287 self.console([('result', "Deferred #%s Result: %r\n" %(dnum, val))]) | |
288 return val | |
289 | |
290 def _cbClientCapable(self, capabilities, client): | |
291 log.msg("client %x has %s" % (id(client), capabilities)) | |
292 client.capabilities = capabilities | |
293 | |
294 def _ebClientCapable(self, reason, client): | |
295 reason.trap(AttributeError) | |
296 log.msg("Couldn't get capabilities from %s, assuming defaults." % | |
297 (client,)) | |
298 | |
299 ### perspective_ methods, commands used by the client. | |
300 | |
301 def perspective_do(self, expr): | |
302 """Evaluate the given expression, with output to the console. | |
303 | |
304 The result is stored in the local variable '_', and its repr() | |
305 string is sent to the console as a \"result\" message. | |
306 """ | |
307 log.msg(">>> %s" % expr) | |
308 val = self.runInConsole(expr) | |
309 if val is not None: | |
310 self.localNamespace["_"] = val | |
311 from twisted.internet.defer import Deferred | |
312 # TODO: client support for Deferred. | |
313 if isinstance(val, Deferred): | |
314 self.lastDeferred += 1 | |
315 self.console([('result', "Waiting for Deferred #%s...\n" % self.
lastDeferred)]) | |
316 val.addBoth(self._cbResult, self.lastDeferred) | |
317 else: | |
318 self.console([("result", repr(val) + '\n')]) | |
319 log.msg("<<<") | |
320 | |
321 def perspective_explore(self, identifier): | |
322 """Browse the object obtained by evaluating the identifier. | |
323 | |
324 The resulting ObjectLink is passed back through the client's | |
325 receiveBrowserObject method. | |
326 """ | |
327 object = self.runInConsole(identifier) | |
328 if object: | |
329 expl = explorer.explorerPool.getExplorer(object, identifier) | |
330 self.receiveExplorer(expl) | |
331 | |
332 def perspective_watch(self, identifier): | |
333 """Watch the object obtained by evaluating the identifier. | |
334 | |
335 Whenever I think this object might have changed, I will pass | |
336 an ObjectLink of it back to the client's receiveBrowserObject | |
337 method. | |
338 """ | |
339 raise NotImplementedError | |
340 object = self.runInConsole(identifier) | |
341 if object: | |
342 # Return an ObjectLink of this right away, before the watch. | |
343 oLink = self.runInConsole(self.browser.browseObject, | |
344 object, identifier) | |
345 self.receiveExplorer(oLink) | |
346 | |
347 self.runInConsole(self.browser.watchObject, | |
348 object, identifier, | |
349 self.receiveExplorer) | |
350 | |
351 | |
352 class Realm: | |
353 | |
354 implements(portal.IRealm) | |
355 | |
356 def __init__(self, service): | |
357 self.service = service | |
358 self._cache = {} | |
359 | |
360 def requestAvatar(self, avatarId, mind, *interfaces): | |
361 if pb.IPerspective not in interfaces: | |
362 raise NotImplementedError("no interface") | |
363 if avatarId in self._cache: | |
364 p = self._cache[avatarId] | |
365 else: | |
366 p = Perspective(self.service) | |
367 p.attached(mind, avatarId) | |
368 def detached(): | |
369 p.detached(mind, avatarId) | |
370 return (pb.IPerspective, p, detached) | |
371 | |
372 | |
373 class Service(service.Service): | |
374 | |
375 welcomeMessage = ( | |
376 "\nHello %(you)s, welcome to Manhole " | |
377 "on %(host)s.\n" | |
378 "%(longversion)s.\n\n") | |
379 | |
380 def __init__(self, unsafeTracebacks=False, namespace=None): | |
381 self.unsafeTracebacks = unsafeTracebacks | |
382 self.namespace = { | |
383 '__name__': '__manhole%x__' % (id(self),), | |
384 'sys': sys | |
385 } | |
386 if namespace: | |
387 self.namespace.update(namespace) | |
388 | |
389 def __getstate__(self): | |
390 """This returns the persistent state of this shell factory. | |
391 """ | |
392 # TODO -- refactor this and twisted.reality.author.Author to | |
393 # use common functionality (perhaps the 'code' module?) | |
394 dict = self.__dict__.copy() | |
395 ns = dict['namespace'].copy() | |
396 dict['namespace'] = ns | |
397 if ns.has_key('__builtins__'): | |
398 del ns['__builtins__'] | |
399 return dict | |
OLD | NEW |