DOPY Users Manual

This is a poor excuse for a user manual, but it does cover some important topics in depth.

Getting Tracebacks Into the Server Code

When an exception is raised from within a remote method, the DOPY server code catches the exception and passes it back to the client. On the client side, the proxy code re-raises the exception object so that it appears to the client as if the exception were raised seamlessly across the remote method invocation.

The only problem with this approach is that traceback information (which is not associated with the exception object itself) gets lost when the exception is passed back to the client environment.

To remedy this, the DOPY server stores the traceback information in the exception in an attribute called "dopy_traceback". This attribute contains the traceback information in the form returned by the traceback.extract_tb().

The information in "dopy_traceback" is automatically appended to the "real" traceback if you use the errorText() function in the dopy.tb module:

   import dopy.tb
   
   try:
   
      # call a method on a remote object
      remoteObject.someMethod()
      
   except Exception, ex:
   
      # print out the _full_ traceback and the exception info.
      print dopy.tb.errorText(ex)

Using DOPY With rsh

It is possible to run dopy over rsh, ssh or any communication program that uses standard input and output. The dopy.rsh module is used for this purpose. On the client side, just create a remote object from the rsh module instead of the tcp module. The first parameter is the command to run, the second is the object key:

   import dopy
   from dopy import rsh
   foo = dopy.remote('rsh my.host.com "dopyserver 2>err.out"', 'foo')

On the server side, we just invoke the makeServer() method and wait for the end of input:

   import dopy
   from dopy import rsh
   
   rsh.makeServer()
   rsh.wait()

See the rshclient and rshserver programs for an example.

It is also possible to start a dopy rsh server with inetd to serve DOPY tcp clients. Simply add a line similar to this to your /etc/inetd.conf file:

   13477 stream tcp nowait mike /home/mike/w/dopy/myserver myserver

"13477" is the port number to listen for. "mike" is the account that it is run under (I recommend that you use an account other than "root" to minimize the impact of any security holes that might arise). "/home/mike/w/dopy/myserver" is the name of the server program, and "myserver" is the 0th argument.

Do not write to standard output from within a dopy rsh server or any method called by it: standard input and output are the communication channels back to the client. Do not write to standard error either (unless it has been redirected) because this is usually merged with standard output.

Keep in mind that the rsh approach creates a new instance of the server program with every client connection. Applications making use of persistence will need to coordinate access to the persistent objects explicitly.

The DOPY Naming Service

The DOPY naming service is intended to provide the same functionality as a CORBA naming service through a Python dictionary interface. The basic classes of this service are housed in the dopy.naming module.

At this time, there is no dedicated "name server": any server can become a name server by registering an instance of StandardNamingContext:


   import dopy
   from dopy.naming import StandardNamingContext

   hub = dopy.getHub()
   nameService = StandardNamingContext()
   hub.addObject('naming', nameService)

We recommend the convention of using the object key "naming" for the name service.

At this time, the naming service is really just a dictionary. In fact, from a purely functional point of view, there is no reason why we could not have replaced the lines in which the StandardNamingContext was created and registered with the following:

   hub.addObject('naming', {})

It is only appropriate to store RemoteObject instances in the name server: however, there is nothing to prevent you from storing any kind of object in the name server. A copy of whatever is stored under a particular key will be returned to the client, so a naming service can (to some extent) be used as a storage repository. An attempt to store an object implementation in the naming service will produce unexpected results: retrieval of the object will return a copy of the object, not a proxy.

This brings up the broader issue of what the naming service really is. To some extent, the hub's registry is a naming service: it maps keys to object implementations. Likewise, the persistence service is a naming service that mirrors the directory tree. It seems as though there should be a more generic way of representing the tree of objects served by a particular server. At this point the system is still very much in flux, so there is no reason not to expect such a thing to evolve.

The DOPY Persistence System

The dopy.pos module is similar to the dopy.naming module described above, only instead of a tree of object references, the persistence system represents a directory tree full of pickled python object instances.

Use of the system is extremely simple: just create an instance of FileSysDirectory pointed at the root directory of the file system that you want to publish:

   import dopy
   from dopy.pos import FileSysDirectory
   
   hub = dopy.getHub()
   pos = FileSysDirectory('/home/mike/etc/pickled', 'pos')
   hub.addObject('pos', pos)

In the example above, /home/mike/etc/pickled is the root directory of the pickled object tree, and 'pos' is the object key of the persistence repository (again, we recommend the key 'pos' just for this purpose). The object key passed into the FileSysDirectory constructor must be the same as the key used in the hub.addObject() call.

Clients can store and retrieve copies of objects using the standard list operators. To manipulate them remotely, use the getRemote() method:


   # get the persistence repository
   pos = dopy.tcp.remote('somehost.bogus.net', 9600, 'pos')

   # store an object in the repository
   pos['foo'] = Foo()
   
   # get the remote instance and modify it
   remoteFoo = pos.getRemote('foo')
   remoteFoo.setValue(100)
   
   # get a local copy of foo and modify it
   localFoo = pos['foo']
   localFoo.setValue(200)

Note that in the example above, localFoo is not remoteFoo. The remoteFoo object is a proxy object for the foo instance stored on the server. The localFoo object is a copy of the object stored on the server. When we call the setValue() method, we are changing the local value; this does not effect the remote instance.

Normal, DOPY does not impose any constraints upon the objects that it provides access to. However, in the case of persistence, the system must be able to tell the object when it is done calling a method so that the object can save itself. To deal with this, it is recommended that persistent objects derive from dopy.pos.PersistentObject. This class implements three special methods, dopy_beforeMethod(), dopy_afterMethod() and dopy_setPath(), which are used to automatically write the file after any method is called and to manage a single instance in memory.

Users may wish to provide their own versions of these methods to gain greater control over when objects are written: for example, it is not necessary for methods that do not modify an object to cause the object to be written. However, they should still derive from PersistentObject and call the _acquire() and _release() methods, which manage the object instance in a cache for persistent objects.

The persistence system is smart enough to maintain a single instance of an object in memory: if two different clients call a method on the same object, it will not be loaded twice. However, this system is not smart enough to take into account things like overlapping directory trees and symbolic links. For this reason, we recommend the following:

In summary, the system epects that a file be known by only one name.

The object locking mechanism is still rather flimsy in other respects: when an object is initially loaded, it is given a "use count" of 0. This use count is incremented prior to method invocation and decremented after method invocation. If the use count is zero after method invocation, the object is deleted and removed from the cache. Obviously, this creates a region of exposure between the point at which the object is loaded and the begining of the invocation of the method for which it has been loaded.

Special Methods

DOPY objects may have the following special methods:

dopy_beforeMethod(self, request)

Called before a remote method invocation. If an exception occurs, it is returned to the caller.

dopy_afterMethod(self, request, response)

Called after a remote method invocation. If an exception occurs, it is not returned to the caller. It is ignored.

dopy_setPath(self, pathname)

If the object is stored using the persistence service, this method will be called when the object is loaded so that the source file's path name can be given to it.

These methods will only be called if they are present.

Copyright Info

Copyright (C) 1999 Michael A. Muller

Permission is granted to use, modify and redistribute this document, providing that the following conditions are met:

This code comes with ABSOLUTELY NO WARRANTEE, not even the implied warrantee of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.