diff options
Diffstat (limited to 'Lib/idlelib/protocol.py')
-rw-r--r-- | Lib/idlelib/protocol.py | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/Lib/idlelib/protocol.py b/Lib/idlelib/protocol.py new file mode 100644 index 0000000..10295bf --- /dev/null +++ b/Lib/idlelib/protocol.py @@ -0,0 +1,369 @@ +"""protocol (David Scherer <dscherer@cmu.edu>) + + This module implements a simple RPC or "distributed object" protocol. + I am probably the 100,000th person to write this in Python, but, hey, + it was fun. + + Contents: + + connectionLost is an exception that will be thrown by functions in + the protocol module or calls to remote methods that fail because + the remote program has closed the socket or because no connection + could be established in the first place. + + Server( port=None, connection_hook=None ) creates a server on a + well-known port, to which clients can connect. When a client + connects, a Connection is created for it. If connection_hook + is defined, then connection_hook( socket.getpeername() ) is called + before a Connection is created, and if it returns false then the + connection is refused. connection_hook must be prepared to be + called from any thread. + + Client( ip='127.0.0.1', port=None ) returns a Connection to a Server + object at a well-known address and port. + + Connection( socket ) creates an RPC connection on an arbitrary socket, + which must already be connected to another program. You do not + need to use this directly if you are using Client() or Server(). + + publish( name, connect_function ) provides an object with the + specified name to some or all Connections. When another program + calls Connection.getobject() with the specified name, the + specified connect_function is called with the arguments + + connect_function( conn, addr ) + + where conn is the Connection object to the requesting client and + addr is the address returned by socket.getpeername(). If that + function returns an object, that object becomes accessible to + the caller. If it returns None, the caller's request fails. + + Connection objects: + + .close() refuses additional RPC messages from the peer, and notifies + the peer that the connection has been closed. All pending remote + method calls in either program will fail with a connectionLost + exception. Further remote method calls on this connection will + also result in errors. + + .getobject(name) returns a proxy for the remote object with the + specified name, if it exists and the peer permits us access. + Otherwise, it returns None. It may throw a connectionLost + exception. The returned proxy supports basic attribute access + and method calls, and its methods have an extra attribute, + .void, which is a function that has the same effect but always + returns None. This last capability is provided as a performance + hack: object.method.void(params) can return without waiting for + the remote process to respond, but object.method(params) needs + to wait for a return value or exception. + + .rpc_loop(block=0) processes *incoming* messages for this connection. + If block=1, it continues processing until an exception or return + value is received, which is normally forever. Otherwise it + returns when all currently pending messages have been delivered. + It may throw a connectionLost exception. + + .set_close_hook(f) specifies a function to be called when the remote + object closes the connection during a call to rpc_loop(). This + is a good way for servers to be notified when clients disconnect. + + .set_shutdown_hook(f) specifies a function called *immediately* when + the receive loop detects that the connection has been lost. The + provided function must be prepared to run in any thread. + + Server objects: + + .rpc_loop() processes incoming messages on all connections, and + returns when all pending messages have been processed. It will + *not* throw connectionLost exceptions; the + Connection.set_close_hook() mechanism is much better for servers. +""" + +import sys, os, string, types +import socket +from threading import Thread +from Queue import Queue, Empty +from cPickle import Pickler, Unpickler, PicklingError + +class connectionLost: + def __init__(self, what=""): self.what = what + def __repr__(self): return self.what + def __str__(self): return self.what + +def getmethods(cls): + "Returns a list of the names of the methods of a class." + methods = [] + for b in cls.__bases__: + methods = methods + getmethods(b) + d = cls.__dict__ + for k in d.keys(): + if type(d[k])==types.FunctionType: + methods.append(k) + return methods + +class methodproxy: + "Proxy for a method of a remote object." + def __init__(self, classp, name): + self.classp=classp + self.name=name + self.client = classp.client + def __call__(self, *args, **keywords): + return self.client.call( 'm', self.classp.name, self.name, args, keywords ) + + def void(self, *args, **keywords): + self.client.call_void( 'm', self.classp.name,self.name,args,keywords) + +class classproxy: + "Proxy for a remote object." + def __init__(self, client, name, methods): + self.__dict__['client'] = client + self.__dict__['name'] = name + + for m in methods: + prox = methodproxy( self, m ) + self.__dict__[m] = prox + + def __getattr__(self, attr): + return self.client.call( 'g', self.name, attr ) + + def __setattr__(self, attr, value): + self.client.call_void( 's', self.name, attr, value ) + +local_connect = {} +def publish(name, connect_function): + local_connect[name]=connect_function + +class socketFile: + "File emulator based on a socket. Provides only blocking semantics for now." + + def __init__(self, socket): + self.socket = socket + self.buffer = '' + + def _recv(self,bytes): + try: + r=self.socket.recv(bytes) + except: + raise connectionLost() + if not r: + raise connectionLost() + return r + + def write(self, string): + try: + self.socket.send( string ) + except: + raise connectionLost() + + def read(self,bytes): + x = bytes-len(self.buffer) + while x>0: + self.buffer=self.buffer+self._recv(x) + x = bytes-len(self.buffer) + s = self.buffer[:bytes] + self.buffer=self.buffer[bytes:] + return s + + def readline(self): + while 1: + f = string.find(self.buffer,'\n') + if f>=0: + s = self.buffer[:f+1] + self.buffer=self.buffer[f+1:] + return s + self.buffer = self.buffer + self._recv(1024) + + +class Connection (Thread): + debug = 0 + def __init__(self, socket): + self.local_objects = {} + self.socket = socket + self.name = socket.getpeername() + self.socketfile = socketFile(socket) + self.queue = Queue(-1) + self.refuse_messages = 0 + self.cmds = { 'm': self.r_meth, + 'g': self.r_get, + 's': self.r_set, + 'o': self.r_geto, + 'e': self.r_exc, + #'r' handled by rpc_loop + } + + Thread.__init__(self) + self.setDaemon(1) + self.start() + + def getobject(self, name): + methods = self.call( 'o', name ) + if methods is None: return None + return classproxy(self, name, methods) + + # close_hook is called from rpc_loop(), like a normal remote method + # invocation + def set_close_hook(self,hook): self.close_hook = hook + + # shutdown_hook is called directly from the run() thread, and needs + # to be "thread safe" + def set_shutdown_hook(self,hook): self.shutdown_hook = hook + + close_hook = None + shutdown_hook = None + + def close(self): + self._shutdown() + self.refuse_messages = 1 + + def call(self, c, *args): + self.send( (c, args, 1 ) ) + return self.rpc_loop( block = 1 ) + + def call_void(self, c, *args): + try: + self.send( (c, args, 0 ) ) + except: + pass + + # the following methods handle individual RPC calls: + + def r_geto(self, obj): + c = local_connect.get(obj) + if not c: return None + o = c(self, self.name) + if not o: return None + self.local_objects[obj] = o + return getmethods(o.__class__) + + def r_meth(self, obj, name, args, keywords): + return apply( getattr(self.local_objects[obj],name), args, keywords) + + def r_get(self, obj, name): + return getattr(self.local_objects[obj],name) + + def r_set(self, obj, name, value): + setattr(self.local_objects[obj],name,value) + + def r_exc(self, e, v): + raise e, v + + def rpc_exec(self, cmd, arg, ret): + if self.refuse_messages: return + if self.debug: print cmd,arg,ret + if ret: + try: + r=apply(self.cmds.get(cmd), arg) + self.send( ('r', r, 0) ) + except: + try: + self.send( ('e', sys.exc_info()[:2], 0) ) + except PicklingError: + self.send( ('e', (TypeError, 'Unpicklable exception.'), 0 ) ) + else: + # we cannot report exceptions to the caller, so + # we report them in this process. + r=apply(self.cmds.get(cmd), arg) + + # the following methods implement the RPC and message loops: + + def rpc_loop(self, block=0): + if self.refuse_messages: raise connectionLost('(already closed)') + try: + while 1: + try: + cmd, arg, ret = self.queue.get( block ) + except Empty: + return None + if cmd=='r': return arg + self.rpc_exec(cmd,arg,ret) + except connectionLost: + if self.close_hook: + self.close_hook() + self.close_hook = None + raise + + def run(self): + try: + while 1: + data = self.recv() + self.queue.put( data ) + except: + self.queue.put( ('e', sys.exc_info()[:2], 0) ) + + # The following send raw pickled data to the peer + + def send(self, data): + try: + Pickler(self.socketfile,1).dump( data ) + except connectionLost: + self._shutdown() + if self.shutdown_hook: self.shutdown_hook() + raise + + def recv(self): + try: + return Unpickler(self.socketfile).load() + except connectionLost: + self._shutdown() + if self.shutdown_hook: self.shutdown_hook() + raise + except: + raise + + def _shutdown(self): + try: + self.socket.shutdown(1) + self.socket.close() + except: + pass + + +class Server (Thread): + default_port = 0x1D1E # "IDlE" + + def __init__(self, port=None, connection_hook=None): + self.connections = [] + self.port = port or self.default_port + self.connection_hook = connection_hook + + try: + self.wellknown = s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind('', self.port) + s.listen(3) + except: + raise connectionLost + + Thread.__init__(self) + self.setDaemon(1) + self.start() + + def run(self): + s = self.wellknown + while 1: + conn, addr = s.accept() + if self.connection_hook and not self.connection_hook(addr): + try: + conn.shutdown(1) + except: + pass + continue + self.connections.append( Connection(conn) ) + + def rpc_loop(self): + cns = self.connections[:] + for c in cns: + try: + c.rpc_loop(block = 0) + except connectionLost: + if c in self.connections: + self.connections.remove(c) + +def Client(ip='127.0.0.1', port=None): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(ip,port or Server.default_port) + except socket.error, what: + raise connectionLost(str(what)) + except: + raise connectionLost() + return Connection(s) |