2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 """L{twisted.manhole} L{PB<twisted.spread.pb>} service implementation.
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
21 from cStringIO import StringIO
30 def __init__(self, type_, list):
34 def write(self, text):
35 log.msg("%s: %s" % (self.type, string.strip(str(text))))
36 self.list.append((self.type, text))
41 def consolidate(self):
42 """Concatenate adjacent messages of same type into one.
44 Greatly cuts down on the number of elements, increasing
45 network transport friendliness considerably.
54 for i in xrange(1, len(self.list)):
55 (mtype, message) = inlist[i]
56 if mtype == last_type:
59 if (i - block_begin) == 1:
60 outlist.append(inlist[block_begin])
62 messages = map(lambda l: l[1],
63 inlist[block_begin:i])
64 message = string.join(messages, '')
65 outlist.append((last_type, message))
70 class IManholeClient(Interface):
71 def console(list_of_messages):
72 """Takes a list of (type, message) pairs to display.
75 - \"stdout\" -- string sent to sys.stdout
77 - \"stderr\" -- string sent to sys.stderr
79 - \"result\" -- string repr of the resulting value
82 - \"exception\" -- a L{failure.Failure}
85 def receiveExplorer(xplorer):
86 """Receives an explorer.Explorer
89 def listCapabilities():
90 """List what manholey things I am capable of doing.
92 i.e. C{\"Explorer\"}, C{\"Failure\"}
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.
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?)
103 Returns the command's return value.
105 The console is called with a list of (type, message) pairs for
106 display, see L{IManholeClient.console}.
109 fakeout = FakeStdIO("stdout", output)
110 fakeerr = FakeStdIO("stderr", output)
111 errfile = FakeStdIO("exception", output)
115 filename = str(console)
122 if (globalNS is None) and (not callable(command)):
123 raise ValueError("Need a namespace to evaluate the command in.")
131 if callable(command):
132 val = apply(command, args, kw)
135 code = compile(command, filename, 'eval')
137 code = compile(command, filename, 'single')
140 val = eval(code, globalNS, localNS)
145 (eType, eVal, tb) = sys.exc_info()
146 fail = failure.Failure(eVal, eType, 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))
155 fakeout.consolidate()
160 def _failureOldStyle(fail):
161 """Pre-Failure manhole representation of exceptions.
163 For compatibility with manhole clients without the \"Failure\"
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.
171 - \'exception\' -- a list of one or more strings, each
172 ending in a newline. (traceback.format_exception_only output)
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])))
182 'exception': traceback.format_exception_only(fail.type, fail.value)
185 # Capabilities clients are likely to have before they knew how to answer a
186 # "listCapabilities" query.
187 _defaultCapabilities = {
191 class Perspective(pb.Avatar):
193 def __init__(self, service):
194 self.localNamespace = {
200 self.service = service
202 def __getstate__(self):
203 state = self.__dict__.copy()
204 state['clients'] = {}
205 if state['localNamespace'].has_key("__builtins__"):
206 del state['localNamespace']['__builtins__']
209 def attached(self, client, identity):
210 """A client has attached -- welcome them and add them to the list.
212 self.clients[client] = identity
214 host = ':'.join(map(str, client.broker.transport.getHost()[1:]))
216 msg = self.service.welcomeMessage % {
217 'you': getattr(identity, 'name', str(identity)),
219 'longversion': copyright.longversion,
222 client.callRemote('console', [("stdout", msg)])
224 client.capabilities = _defaultCapabilities
225 client.callRemote('listCapabilities').addCallbacks(
226 self._cbClientCapable, self._ebClientCapable,
227 callbackArgs=(client,),errbackArgs=(client,))
229 def detached(self, client, identity):
231 del self.clients[client]
235 def runInConsole(self, command, *args, **kw):
236 """Convience method to \"runInConsole with my stuff\".
238 return runInConsole(command,
240 self.service.namespace,
245 unsafeTracebacks=self.service.unsafeTracebacks)
248 ### Methods for communicating to my clients.
250 def console(self, message):
251 """Pass a message to my clients' console.
253 clients = self.clients.keys()
254 origMessage = message
256 for client in clients:
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)):
266 _failureOldStyle(message[i][1]))
267 client.callRemote('console', compatMessage)
269 client.callRemote('console', message)
270 except pb.ProtocolError:
272 self.detached(client, None)
274 def receiveExplorer(self, objectLink):
275 """Pass an Explorer on to my clients.
277 clients = self.clients.keys()
278 for client in clients:
280 client.callRemote('receiveExplorer', objectLink)
281 except pb.ProtocolError:
283 self.detached(client, None)
286 def _cbResult(self, val, dnum):
287 self.console([('result', "Deferred #%s Result: %r\n" %(dnum, val))])
290 def _cbClientCapable(self, capabilities, client):
291 log.msg("client %x has %s" % (id(client), capabilities))
292 client.capabilities = capabilities
294 def _ebClientCapable(self, reason, client):
295 reason.trap(AttributeError)
296 log.msg("Couldn't get capabilities from %s, assuming defaults." %
299 ### perspective_ methods, commands used by the client.
301 def perspective_do(self, expr):
302 """Evaluate the given expression, with output to the console.
304 The result is stored in the local variable '_', and its repr()
305 string is sent to the console as a \"result\" message.
307 log.msg(">>> %s" % expr)
308 val = self.runInConsole(expr)
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)
318 self.console([("result", repr(val) + '\n')])
321 def perspective_explore(self, identifier):
322 """Browse the object obtained by evaluating the identifier.
324 The resulting ObjectLink is passed back through the client's
325 receiveBrowserObject method.
327 object = self.runInConsole(identifier)
329 expl = explorer.explorerPool.getExplorer(object, identifier)
330 self.receiveExplorer(expl)
332 def perspective_watch(self, identifier):
333 """Watch the object obtained by evaluating the identifier.
335 Whenever I think this object might have changed, I will pass
336 an ObjectLink of it back to the client's receiveBrowserObject
339 raise NotImplementedError
340 object = self.runInConsole(identifier)
342 # Return an ObjectLink of this right away, before the watch.
343 oLink = self.runInConsole(self.browser.browseObject,
345 self.receiveExplorer(oLink)
347 self.runInConsole(self.browser.watchObject,
349 self.receiveExplorer)
354 implements(portal.IRealm)
356 def __init__(self, service):
357 self.service = service
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]
366 p = Perspective(self.service)
367 p.attached(mind, avatarId)
369 p.detached(mind, avatarId)
370 return (pb.IPerspective, p, detached)
373 class Service(service.Service):
376 "\nHello %(you)s, welcome to Manhole "
378 "%(longversion)s.\n\n")
380 def __init__(self, unsafeTracebacks=False, namespace=None):
381 self.unsafeTracebacks = unsafeTracebacks
383 '__name__': '__manhole%x__' % (id(self),),
387 self.namespace.update(namespace)
389 def __getstate__(self):
390 """This returns the persistent state of this shell factory.
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__']