OLD | NEW |
| (Empty) |
1 | |
2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """ | |
7 This module represents flavors of remotely acessible objects. | |
8 | |
9 Currently this is only objects accessible through Perspective Broker, but will | |
10 hopefully encompass all forms of remote access which can emulate subsets of PB | |
11 (such as XMLRPC or SOAP). | |
12 | |
13 Future Plans: Optimization. Exploitation of new-style object model. | |
14 Optimizations to this module should not affect external-use semantics at all, | |
15 but may have a small impact on users who subclass and override methods. | |
16 | |
17 @author: U{Glyph Lefkowitz<mailto:glyph@twistedmatrix.com>} | |
18 """ | |
19 | |
20 __version__ = "$Revision: 1.32 $"[11:-2] | |
21 | |
22 # NOTE: this module should NOT import pb; it is supposed to be a module which | |
23 # abstractly defines remotely accessible types. Many of these types expect to | |
24 # be serialized by Jelly, but they ought to be accessible through other | |
25 # mechanisms (like XMLRPC) | |
26 | |
27 # system imports | |
28 import sys | |
29 from zope.interface import implements, Interface | |
30 | |
31 # twisted imports | |
32 from twisted.python import log, reflect | |
33 | |
34 # sibling imports | |
35 from jelly import setUnjellyableForClass, setUnjellyableForClassTree, setUnjelly
ableFactoryForClass, unjellyableRegistry | |
36 from jelly import Jellyable, Unjellyable, _Dummy, _DummyNewStyle | |
37 from jelly import setInstanceState, getInstanceState | |
38 | |
39 # compatibility | |
40 setCopierForClass = setUnjellyableForClass | |
41 setCopierForClassTree = setUnjellyableForClassTree | |
42 setFactoryForClass = setUnjellyableFactoryForClass | |
43 copyTags = unjellyableRegistry | |
44 | |
45 copy_atom = "copy" | |
46 cache_atom = "cache" | |
47 cached_atom = "cached" | |
48 remote_atom = "remote" | |
49 | |
50 | |
51 class NoSuchMethod(AttributeError): | |
52 """Raised if there is no such remote method""" | |
53 | |
54 | |
55 class IPBRoot(Interface): | |
56 """Factory for root Referenceable objects for PB servers.""" | |
57 | |
58 def rootObject(broker): | |
59 """Return root Referenceable for broker.""" | |
60 | |
61 | |
62 class Serializable(Jellyable): | |
63 """An object that can be passed remotely. | |
64 | |
65 I am a style of object which can be serialized by Perspective | |
66 Broker. Objects which wish to be referenceable or copied remotely | |
67 have to subclass Serializable. However, clients of Perspective | |
68 Broker will probably not want to directly subclass Serializable; the | |
69 Flavors of transferable objects are listed below. | |
70 | |
71 What it means to be \"Serializable\" is that an object can be | |
72 passed to or returned from a remote method. Certain basic types | |
73 (dictionaries, lists, tuples, numbers, strings) are serializable by | |
74 default; however, classes need to choose a specific serialization | |
75 style: L{Referenceable}, L{Viewable}, L{Copyable} or L{Cacheable}. | |
76 | |
77 You may also pass C{[lists, dictionaries, tuples]} of L{Serializable} | |
78 instances to or return them from remote methods, as many levels deep | |
79 as you like. | |
80 """ | |
81 | |
82 def processUniqueID(self): | |
83 """Return an ID which uniquely represents this object for this process. | |
84 | |
85 By default, this uses the 'id' builtin, but can be overridden to | |
86 indicate that two values are identity-equivalent (such as proxies | |
87 for the same object). | |
88 """ | |
89 | |
90 return id(self) | |
91 | |
92 class Referenceable(Serializable): | |
93 perspective = None | |
94 """I am an object sent remotely as a direct reference. | |
95 | |
96 When one of my subclasses is sent as an argument to or returned | |
97 from a remote method call, I will be serialized by default as a | |
98 direct reference. | |
99 | |
100 This means that the peer will be able to call methods on me; | |
101 a method call xxx() from my peer will be resolved to methods | |
102 of the name remote_xxx. | |
103 """ | |
104 | |
105 def remoteMessageReceived(self, broker, message, args, kw): | |
106 """A remote message has been received. Dispatch it appropriately. | |
107 | |
108 The default implementation is to dispatch to a method called | |
109 'remote_messagename' and call it with the same arguments. | |
110 """ | |
111 args = broker.unserialize(args) | |
112 kw = broker.unserialize(kw) | |
113 method = getattr(self, "remote_%s" % message, None) | |
114 if method is None: | |
115 raise NoSuchMethod("No such method: remote_%s" % (message,)) | |
116 try: | |
117 state = method(*args, **kw) | |
118 except TypeError: | |
119 log.msg("%s didn't accept %s and %s" % (method, args, kw)) | |
120 raise | |
121 return broker.serialize(state, self.perspective) | |
122 | |
123 def jellyFor(self, jellier): | |
124 """(internal) | |
125 | |
126 Return a tuple which will be used as the s-expression to | |
127 serialize this to a peer. | |
128 """ | |
129 | |
130 return "remote", jellier.invoker.registerReference(self) | |
131 | |
132 | |
133 class Root(Referenceable): | |
134 """I provide a root object to L{pb.Broker}s for a L{pb.BrokerFactory}. | |
135 | |
136 When a L{pb.BrokerFactory} produces a L{pb.Broker}, it supplies that | |
137 L{pb.Broker} with an object named \"root\". That object is obtained | |
138 by calling my rootObject method. | |
139 | |
140 See also: L{pb.getObjectAt} | |
141 """ | |
142 | |
143 implements(IPBRoot) | |
144 | |
145 def rootObject(self, broker): | |
146 """A L{pb.BrokerFactory} is requesting to publish me as a root object. | |
147 | |
148 When a L{pb.BrokerFactory} is sending me as the root object, this | |
149 method will be invoked to allow per-broker versions of an | |
150 object. By default I return myself. | |
151 """ | |
152 return self | |
153 | |
154 | |
155 class ViewPoint(Referenceable): | |
156 """ | |
157 I act as an indirect reference to an object accessed through a | |
158 L{pb.Perspective}. | |
159 | |
160 Simply put, I combine an object with a perspective so that when a | |
161 peer calls methods on the object I refer to, the method will be | |
162 invoked with that perspective as a first argument, so that it can | |
163 know who is calling it. | |
164 | |
165 While L{Viewable} objects will be converted to ViewPoints by default | |
166 when they are returned from or sent as arguments to a remote | |
167 method, any object may be manually proxied as well. (XXX: Now that | |
168 this class is no longer named C{Proxy}, this is the only occourance | |
169 of the term 'proxied' in this docstring, and may be unclear.) | |
170 | |
171 This can be useful when dealing with L{pb.Perspective}s, L{Copyable}s, | |
172 and L{Cacheable}s. It is legal to implement a method as such on | |
173 a perspective:: | |
174 | |
175 | def perspective_getViewPointForOther(self, name): | |
176 | defr = self.service.getPerspectiveRequest(name) | |
177 | defr.addCallbacks(lambda x, self=self: ViewPoint(self, x), log.msg) | |
178 | return defr | |
179 | |
180 This will allow you to have references to Perspective objects in two | |
181 different ways. One is through the initial 'attach' call -- each | |
182 peer will have a L{pb.RemoteReference} to their perspective directly. The | |
183 other is through this method; each peer can get a L{pb.RemoteReference} to | |
184 all other perspectives in the service; but that L{pb.RemoteReference} will | |
185 be to a L{ViewPoint}, not directly to the object. | |
186 | |
187 The practical offshoot of this is that you can implement 2 varieties | |
188 of remotely callable methods on this Perspective; view_xxx and | |
189 C{perspective_xxx}. C{view_xxx} methods will follow the rules for | |
190 ViewPoint methods (see ViewPoint.L{remoteMessageReceived}), and | |
191 C{perspective_xxx} methods will follow the rules for Perspective | |
192 methods. | |
193 """ | |
194 | |
195 def __init__(self, perspective, object): | |
196 """Initialize me with a Perspective and an Object. | |
197 """ | |
198 self.perspective = perspective | |
199 self.object = object | |
200 | |
201 def processUniqueID(self): | |
202 """Return an ID unique to a proxy for this perspective+object combinatio
n. | |
203 """ | |
204 return (id(self.perspective), id(self.object)) | |
205 | |
206 def remoteMessageReceived(self, broker, message, args, kw): | |
207 """A remote message has been received. Dispatch it appropriately. | |
208 | |
209 The default implementation is to dispatch to a method called | |
210 'C{view_messagename}' to my Object and call it on my object with | |
211 the same arguments, modified by inserting my Perspective as | |
212 the first argument. | |
213 """ | |
214 args = broker.unserialize(args, self.perspective) | |
215 kw = broker.unserialize(kw, self.perspective) | |
216 method = getattr(self.object, "view_%s" % message) | |
217 try: | |
218 state = apply(method, (self.perspective,)+args, kw) | |
219 except TypeError: | |
220 log.msg("%s didn't accept %s and %s" % (method, args, kw)) | |
221 raise | |
222 rv = broker.serialize(state, self.perspective, method, args, kw) | |
223 return rv | |
224 | |
225 | |
226 class Viewable(Serializable): | |
227 """I will be converted to a L{ViewPoint} when passed to or returned from a r
emote method. | |
228 | |
229 The beginning of a peer's interaction with a PB Service is always | |
230 through a perspective. However, if a C{perspective_xxx} method returns | |
231 a Viewable, it will be serialized to the peer as a response to that | |
232 method. | |
233 """ | |
234 | |
235 def jellyFor(self, jellier): | |
236 """Serialize a L{ViewPoint} for me and the perspective of the given brok
er. | |
237 """ | |
238 return ViewPoint(jellier.invoker.serializingPerspective, self).jellyFor(
jellier) | |
239 | |
240 | |
241 | |
242 class Copyable(Serializable): | |
243 """Subclass me to get copied each time you are returned from or passed to a
remote method. | |
244 | |
245 When I am returned from or passed to a remote method call, I will be | |
246 converted into data via a set of callbacks (see my methods for more | |
247 info). That data will then be serialized using Jelly, and sent to | |
248 the peer. | |
249 | |
250 The peer will then look up the type to represent this with; see | |
251 L{RemoteCopy} for details. | |
252 """ | |
253 | |
254 def getStateToCopy(self): | |
255 """Gather state to send when I am serialized for a peer. | |
256 | |
257 I will default to returning self.__dict__. Override this to | |
258 customize this behavior. | |
259 """ | |
260 | |
261 return self.__dict__ | |
262 | |
263 def getStateToCopyFor(self, perspective): | |
264 """ | |
265 Gather state to send when I am serialized for a particular | |
266 perspective. | |
267 | |
268 I will default to calling L{getStateToCopy}. Override this to | |
269 customize this behavior. | |
270 """ | |
271 | |
272 return self.getStateToCopy() | |
273 | |
274 def getTypeToCopy(self): | |
275 """Determine what type tag to send for me. | |
276 | |
277 By default, send the string representation of my class | |
278 (package.module.Class); normally this is adequate, but | |
279 you may override this to change it. | |
280 """ | |
281 | |
282 return reflect.qual(self.__class__) | |
283 | |
284 def getTypeToCopyFor(self, perspective): | |
285 """Determine what type tag to send for me. | |
286 | |
287 By default, defer to self.L{getTypeToCopy}() normally this is | |
288 adequate, but you may override this to change it. | |
289 """ | |
290 | |
291 return self.getTypeToCopy() | |
292 | |
293 def jellyFor(self, jellier): | |
294 """Assemble type tag and state to copy for this broker. | |
295 | |
296 This will call L{getTypeToCopyFor} and L{getStateToCopy}, and | |
297 return an appropriate s-expression to represent me. | |
298 """ | |
299 | |
300 if jellier.invoker is None: | |
301 return getInstanceState(self, jellier) | |
302 p = jellier.invoker.serializingPerspective | |
303 t = self.getTypeToCopyFor(p) | |
304 state = self.getStateToCopyFor(p) | |
305 sxp = jellier.prepare(self) | |
306 sxp.extend([t, jellier.jelly(state)]) | |
307 return jellier.preserve(self, sxp) | |
308 | |
309 | |
310 class Cacheable(Copyable): | |
311 """A cached instance. | |
312 | |
313 This means that it's copied; but there is some logic to make sure | |
314 that it's only copied once. Additionally, when state is retrieved, | |
315 it is passed a "proto-reference" to the state as it will exist on | |
316 the client. | |
317 | |
318 XXX: The documentation for this class needs work, but it's the most | |
319 complex part of PB and it is inherently difficult to explain. | |
320 """ | |
321 | |
322 def getStateToCacheAndObserveFor(self, perspective, observer): | |
323 """ | |
324 Get state to cache on the client and client-cache reference | |
325 to observe locally. | |
326 | |
327 This is similiar to getStateToCopyFor, but it additionally | |
328 passes in a reference to the client-side RemoteCache instance | |
329 that will be created when it is unserialized. This allows | |
330 Cacheable instances to keep their RemoteCaches up to date when | |
331 they change, such that no changes can occur between the point | |
332 at which the state is initially copied and the client receives | |
333 it that are not propogated. | |
334 """ | |
335 | |
336 return self.getStateToCopyFor(perspective) | |
337 | |
338 def jellyFor(self, jellier): | |
339 """Return an appropriate tuple to serialize me. | |
340 | |
341 Depending on whether this broker has cached me or not, this may | |
342 return either a full state or a reference to an existing cache. | |
343 """ | |
344 if jellier.invoker is None: | |
345 return getInstanceState(self, jellier) | |
346 luid = jellier.invoker.cachedRemotelyAs(self, 1) | |
347 if luid is None: | |
348 luid = jellier.invoker.cacheRemotely(self) | |
349 p = jellier.invoker.serializingPerspective | |
350 type_ = self.getTypeToCopyFor(p) | |
351 observer = RemoteCacheObserver(jellier.invoker, self, p) | |
352 state = self.getStateToCacheAndObserveFor(p, observer) | |
353 l = jellier.prepare(self) | |
354 jstate = jellier.jelly(state) | |
355 l.extend([type_, luid, jstate]) | |
356 return jellier.preserve(self, l) | |
357 else: | |
358 return cached_atom, luid | |
359 | |
360 def stoppedObserving(self, perspective, observer): | |
361 """This method is called when a client has stopped observing me. | |
362 | |
363 The 'observer' argument is the same as that passed in to | |
364 getStateToCacheAndObserveFor. | |
365 """ | |
366 | |
367 | |
368 | |
369 class RemoteCopy(Unjellyable): | |
370 """I am a remote copy of a Copyable object. | |
371 | |
372 When the state from a L{Copyable} object is received, an instance will | |
373 be created based on the copy tags table (see setUnjellyableForClass) and | |
374 sent the L{setCopyableState} message. I provide a reasonable default | |
375 implementation of that message; subclass me if you wish to serve as | |
376 a copier for remote data. | |
377 | |
378 NOTE: copiers are invoked with no arguments. Do not implement a | |
379 constructor which requires args in a subclass of L{RemoteCopy}! | |
380 """ | |
381 | |
382 def setCopyableState(self, state): | |
383 """I will be invoked with the state to copy locally. | |
384 | |
385 'state' is the data returned from the remote object's | |
386 'getStateToCopyFor' method, which will often be the remote | |
387 object's dictionary (or a filtered approximation of it depending | |
388 on my peer's perspective). | |
389 """ | |
390 | |
391 self.__dict__ = state | |
392 | |
393 def unjellyFor(self, unjellier, jellyList): | |
394 if unjellier.invoker is None: | |
395 return setInstanceState(self, unjellier, jellyList) | |
396 self.setCopyableState(unjellier.unjelly(jellyList[1])) | |
397 return self | |
398 | |
399 | |
400 | |
401 class RemoteCache(RemoteCopy, Serializable): | |
402 """A cache is a local representation of a remote L{Cacheable} object. | |
403 | |
404 This represents the last known state of this object. It may | |
405 also have methods invoked on it -- in order to update caches, | |
406 the cached class generates a L{pb.RemoteReference} to this object as | |
407 it is originally sent. | |
408 | |
409 Much like copy, I will be invoked with no arguments. Do not | |
410 implement a constructor that requires arguments in one of my | |
411 subclasses. | |
412 """ | |
413 | |
414 def remoteMessageReceived(self, broker, message, args, kw): | |
415 """A remote message has been received. Dispatch it appropriately. | |
416 | |
417 The default implementation is to dispatch to a method called | |
418 'C{observe_messagename}' and call it on my with the same arguments. | |
419 """ | |
420 | |
421 args = broker.unserialize(args) | |
422 kw = broker.unserialize(kw) | |
423 method = getattr(self, "observe_%s" % message) | |
424 try: | |
425 state = apply(method, args, kw) | |
426 except TypeError: | |
427 log.msg("%s didn't accept %s and %s" % (method, args, kw)) | |
428 raise | |
429 return broker.serialize(state, None, method, args, kw) | |
430 | |
431 def jellyFor(self, jellier): | |
432 """serialize me (only for the broker I'm for) as the original cached ref
erence | |
433 """ | |
434 if jellier.invoker is None: | |
435 return getInstanceState(self, jellier) | |
436 assert jellier.invoker is self.broker, "You cannot exchange cached proxi
es between brokers." | |
437 return 'lcache', self.luid | |
438 | |
439 | |
440 def unjellyFor(self, unjellier, jellyList): | |
441 if unjellier.invoker is None: | |
442 return setInstanceState(self, unjellier, jellyList) | |
443 self.broker = unjellier.invoker | |
444 self.luid = jellyList[1] | |
445 if isinstance(self.__class__, type): #new-style class | |
446 cProxy = _DummyNewStyle() | |
447 else: | |
448 cProxy = _Dummy() | |
449 cProxy.__class__ = self.__class__ | |
450 cProxy.__dict__ = self.__dict__ | |
451 # XXX questionable whether this was a good design idea... | |
452 init = getattr(cProxy, "__init__", None) | |
453 if init: | |
454 init() | |
455 unjellier.invoker.cacheLocally(jellyList[1], self) | |
456 cProxy.setCopyableState(unjellier.unjelly(jellyList[2])) | |
457 # Might have changed due to setCopyableState method; we'll assume that | |
458 # it's bad form to do so afterwards. | |
459 self.__dict__ = cProxy.__dict__ | |
460 # chomp, chomp -- some existing code uses "self.__dict__ =", some uses | |
461 # "__dict__.update". This is here in order to handle both cases. | |
462 self.broker = unjellier.invoker | |
463 self.luid = jellyList[1] | |
464 return cProxy | |
465 | |
466 ## def __really_del__(self): | |
467 ## """Final finalization call, made after all remote references have bee
n lost. | |
468 ## """ | |
469 | |
470 def __cmp__(self, other): | |
471 """Compare me [to another RemoteCache. | |
472 """ | |
473 if isinstance(other, self.__class__): | |
474 return cmp(id(self.__dict__), id(other.__dict__)) | |
475 else: | |
476 return cmp(id(self.__dict__), other) | |
477 | |
478 def __hash__(self): | |
479 """Hash me. | |
480 """ | |
481 return int(id(self.__dict__) % sys.maxint) | |
482 | |
483 broker = None | |
484 luid = None | |
485 | |
486 def __del__(self): | |
487 """Do distributed reference counting on finalize. | |
488 """ | |
489 try: | |
490 # log.msg( ' --- decache: %s %s' % (self, self.luid) ) | |
491 if self.broker: | |
492 self.broker.decCacheRef(self.luid) | |
493 except: | |
494 log.deferr() | |
495 | |
496 def unjellyCached(unjellier, unjellyList): | |
497 luid = unjellyList[1] | |
498 cNotProxy = unjellier.invoker.cachedLocallyAs(luid) | |
499 | |
500 cProxy = _Dummy() | |
501 cProxy.__class__ = cNotProxy.__class__ | |
502 cProxy.__dict__ = cNotProxy.__dict__ | |
503 return cProxy | |
504 | |
505 setUnjellyableForClass("cached", unjellyCached) | |
506 | |
507 def unjellyLCache(unjellier, unjellyList): | |
508 luid = unjellyList[1] | |
509 obj = unjellier.invoker.remotelyCachedForLUID(luid) | |
510 return obj | |
511 | |
512 setUnjellyableForClass("lcache", unjellyLCache) | |
513 | |
514 def unjellyLocal(unjellier, unjellyList): | |
515 obj = unjellier.invoker.localObjectForID(unjellyList[1]) | |
516 return obj | |
517 | |
518 setUnjellyableForClass("local", unjellyLocal) | |
519 | |
520 class RemoteCacheMethod: | |
521 """A method on a reference to a L{RemoteCache}. | |
522 """ | |
523 | |
524 def __init__(self, name, broker, cached, perspective): | |
525 """(internal) initialize. | |
526 """ | |
527 self.name = name | |
528 self.broker = broker | |
529 self.perspective = perspective | |
530 self.cached = cached | |
531 | |
532 def __cmp__(self, other): | |
533 return cmp((self.name, self.broker, self.perspective, self.cached), othe
r) | |
534 | |
535 def __hash__(self): | |
536 return hash((self.name, self.broker, self.perspective, self.cached)) | |
537 | |
538 def __call__(self, *args, **kw): | |
539 """(internal) action method. | |
540 """ | |
541 cacheID = self.broker.cachedRemotelyAs(self.cached) | |
542 if cacheID is None: | |
543 from pb import ProtocolError | |
544 raise ProtocolError("You can't call a cached method when the object
hasn't been given to the peer yet.") | |
545 return self.broker._sendMessage('cache', self.perspective, cacheID, self
.name, args, kw) | |
546 | |
547 class RemoteCacheObserver: | |
548 """I am a reverse-reference to the peer's L{RemoteCache}. | |
549 | |
550 I am generated automatically when a cache is serialized. I | |
551 represent a reference to the client's L{RemoteCache} object that | |
552 will represent a particular L{Cacheable}; I am the additional | |
553 object passed to getStateToCacheAndObserveFor. | |
554 """ | |
555 | |
556 def __init__(self, broker, cached, perspective): | |
557 """(internal) Initialize me. | |
558 | |
559 @param broker: a L{pb.Broker} instance. | |
560 | |
561 @param cached: a L{Cacheable} instance that this L{RemoteCacheObserver} | |
562 corresponds to. | |
563 | |
564 @param perspective: a reference to the perspective who is observing this
. | |
565 """ | |
566 | |
567 self.broker = broker | |
568 self.cached = cached | |
569 self.perspective = perspective | |
570 | |
571 def __repr__(self): | |
572 return "<RemoteCacheObserver(%s, %s, %s) at %s>" % ( | |
573 self.broker, self.cached, self.perspective, id(self)) | |
574 | |
575 def __hash__(self): | |
576 """Generate a hash unique to all L{RemoteCacheObserver}s for this broker
/perspective/cached triplet | |
577 """ | |
578 | |
579 return ( (hash(self.broker) % 2**10) | |
580 + (hash(self.perspective) % 2**10) | |
581 + (hash(self.cached) % 2**10)) | |
582 | |
583 def __cmp__(self, other): | |
584 """Compare me to another L{RemoteCacheObserver}. | |
585 """ | |
586 | |
587 return cmp((self.broker, self.perspective, self.cached), other) | |
588 | |
589 def callRemote(self, _name, *args, **kw): | |
590 """(internal) action method. | |
591 """ | |
592 cacheID = self.broker.cachedRemotelyAs(self.cached) | |
593 if cacheID is None: | |
594 from pb import ProtocolError | |
595 raise ProtocolError("You can't call a cached method when the " | |
596 "object hasn't been given to the peer yet.") | |
597 return self.broker._sendMessage('cache', self.perspective, cacheID, | |
598 _name, args, kw) | |
599 | |
600 def remoteMethod(self, key): | |
601 """Get a L{pb.RemoteMethod} for this key. | |
602 """ | |
603 return RemoteCacheMethod(key, self.broker, self.cached, self.perspective
) | |
OLD | NEW |