| 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 |