~landscape/zope3/newer-from-ztk

« back to all changes in this revision

Viewing changes to src/twisted/manhole/service.py

  • Committer: Thomas Hervé
  • Date: 2009-07-08 13:52:04 UTC
  • Revision ID: thomas@canonical.com-20090708135204-df5eesrthifpylf8
Remove twisted copy

Show diffs side-by-side

added added

removed removed

Lines of Context:
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