Imported Upstream version 12.1.0
[contrib/python-twisted.git] / twisted / manhole / service.py
1
2 # Copyright (c) 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