summaryrefslogtreecommitdiffstats
path: root/Lib/idlelib/protocol.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/idlelib/protocol.py')
-rw-r--r--Lib/idlelib/protocol.py369
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)