# -*- Mode: Python; tab-width: 4 -*-

# Copyright 1999, 2000 by eGroups, Inc.
# 
#                         All Rights Reserved
# 
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of
# eGroups not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
# 
# EGROUPS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL EGROUPS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import marshal
import socket
import string
import time
import exceptions
import select
import string
import sys
import types
import errno

#
# there are three clients in here.
#
# 1) rpc client
# 2) fastrpc client
# 3) async fastrpc client
#
# we hope that *whichever* choice you make, that you will enjoy the
# excellent hand-made construction, and return to do business with us
# again in the near future.
#

class RPC_Error (exceptions.StandardError):
    pass
    
    # ===========================================================================
    #							  RPC Client
    # ===========================================================================
    
    # request types:
    # 0 call
    # 1 getattr
    # 2 setattr
    # 3 repr
    # 4 del
    
    
class rpc_proxy:

    DEBUG = 0
    
    def __init__ (self, conn, oid):
            # route around __setattr__
        self.__dict__['conn'] = conn
        self.__dict__['oid'] = oid
        
        # Warning: be VERY CAREFUL with attribute references, keep
        #             this __getattr__ in mind!
        
    def __getattr__ (self, attr):
            # __getattr__ and __call__
        if attr == '__call__':
                # 0 == __call__
            return self.__remote_call__
        elif attr == '__repr__':
                # 3 == __repr__
            return self.__remote_repr__
        elif attr == '__getitem__':
            return self.__remote_getitem__
        elif attr == '__setitem__':
            return self.__remote_setitem__
        elif attr == '__len__':
            return self.__remote_len__
        else:
                # 1 == __getattr__
            return self.__send_request__ (1, attr)
            
    def __setattr__ (self, attr, value):
        return self.__send_request__ (2, (attr, value))
        
    def __del__ (self):
        try:
            self.__send_request__ (4, None)
        except:
            import who_calls
            info = who_calls.compact_traceback()
            print info
            
    def __remote_repr__ (self):
        r = self.__send_request__ (3, None)
        return '<remote object [%s]>' % r[1:-1]
        
    def __remote_call__ (self, *args):
        return self.__send_request__ (0, args)
        
    def __remote_getitem__ (self, key):
        return self.__send_request__ (5, key)
        
    def __remote_setitem__ (self, key, value):
        return self.__send_request__ (6, (key, value))
        
    def __remote_len__ (self):
        return self.__send_request__ (7, None)
        
    _request_types_ = ['call', 'getattr', 'setattr', 'repr', 'del', 'getitem', 'setitem', 'len']
    
    def __send_request__ (self, *args):
        if self.DEBUG:
            kind = args[0]
            print (
                    'RPC: ==> %s:%08x:%s:%s' % (
                            self.conn.address,
                            self.oid,
                            self._request_types_[kind],
                            repr(args[1:])
                            )
                    )
        packet = marshal.dumps ((self.oid,)+args)
        # send request
        self.conn.send_packet (packet)
        # get response
        data = self.conn.receive_packet()
        # types of response:
        # 0: proxy
        # 1: error
        # 2: marshal'd data
        
        kind, value = marshal.loads (data)
        
        if kind == 0:
                # proxy (value == oid)
            if self.DEBUG:
                print 'RPC: <== proxy(%08x)' % (value)
            return rpc_proxy (self.conn, value)
        elif kind == 1:
            raise RPC_Error, value
        else:
            if self.DEBUG:
                print 'RPC: <== %s' % (repr(value))
            return value
            
class rpc_connection:

    cache = {}
    
    def __init__ (self, address):
        self.address = address
        self.connect ()
        
    def connect (self):
        s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
        s.connect (self.address)
        self.socket = s
        
    def receive_packet (self):
        packet_len = string.atoi (self.socket.recv (8), 16)
        packet = []
        while packet_len:
            data = self.socket.recv (8192)
            packet.append (data)
            packet_len = packet_len - len(data)
        return string.join (packet, '')
        
    def send_packet (self, packet):
        self.socket.send ('%08x%s' % (len(packet), packet))
        
def rpc_connect (address = ('localhost', 8746)):
    if not rpc_connection.cache.has_key (address):
        conn = rpc_connection (address)
        # get oid of remote object
        data = conn.receive_packet()
        (oid,) = marshal.loads (data)
        rpc_connection.cache[address] = rpc_proxy (conn, oid)
    return rpc_connection.cache[address]
    
    # ===========================================================================
    #			fastrpc client
    # ===========================================================================
    
class fastrpc_proxy:

    def __init__ (self, conn, path=()):
        self.conn = conn
        self.path = path
        
    def __getattr__ (self, attr):
        if attr == '__call__':
            return self.__method_caller__
        else:
            return fastrpc_proxy (self.conn, self.path + (attr,))
            
    def __method_caller__ (self, *args):
            # send request
        packet = marshal.dumps ((self.path, args))
        self.conn.send_packet (packet)
        # get response
        data = self.conn.receive_packet()
        error, result = marshal.loads (data)
        if error is None:
            return result
        else:
            raise RPC_Error, error
            
    def __repr__ (self):
        return '<remote-method-%s at %x>' % (string.join (self.path, '.'), id (self))
        
def fastrpc_connect (address = ('localhost', 8748)):
    if not rpc_connection.cache.has_key (address):
        conn = rpc_connection (address)
        rpc_connection.cache[address] = fastrpc_proxy (conn)
    return rpc_connection.cache[address]
    
    # ===========================================================================
    #						 async fastrpc client
    # ===========================================================================
    
import asynchat
import fifo

class async_fastrpc_client (asynchat.async_chat):

    STATE_LENGTH = 'length state'
    STATE_PACKET = 'packet state'
    
    def __init__ (self, address=('idb', 3001)):
    
        asynchat.async_chat.__init__ (self)
        
        if type(address) is type(''):
            family = socket.AF_UNIX
        else:
            family = socket.AF_INET
            
        self.create_socket (family, socket.SOCK_STREAM)
        self.address = address
        self.request_fifo = fifo.fifo()
        self.buffer = []
        self.pstate = self.STATE_LENGTH
        self.set_terminator (8)
        self._connected = 0
        self.connect (self.address)
        
    def log (self, *args):
        pass
        
    def handle_connect (self):
        self._connected = 1
        
    def close (self):
        self._connected = 0
        self.flush_pending_requests ('lost connection to rpc server')
        asynchat.async_chat.close(self)
        
    def flush_pending_requests (self, why):
        f = self.request_fifo
        while len(f):
            callback = f.pop()
            callback (why, None)
            
    def collect_incoming_data (self, data):
        self.buffer.append (data)
        
    def found_terminator (self):
        self.buffer, data = [], string.join (self.buffer, '')
        
        if self.pstate is self.STATE_LENGTH:
            packet_length = string.atoi (data, 16)
            self.set_terminator (packet_length)
            self.pstate = self.STATE_PACKET
        else:
                # modified to fix socket leak in chat server, 2000-01-27, schiller@eGroups.net
                #self.set_terminator (8)
                #self.pstate = self.STATE_LENGTH
            error, result = marshal.loads (data)
            callback = self.request_fifo.pop()
            callback (error, result)
            self.close()	# for chat server
            
    def call_method (self, method, args, callback):
        if not self._connected:
                # might be a unix socket...
            family, type = self.family_and_type
            self.create_socket (family, type)
            self.connect (self.address)
            # push the request out the socket
        path = string.split (method, '.')
        packet = marshal.dumps ((path, args))
        self.push ('%08x%s' % (len(packet), packet))
        self.request_fifo.push (callback)
        
        
if __name__ == '__main__':
    import sys
    if '-f' in sys.argv:
        connect = fastrpc_connect
    else:
        connect = rpc_connect
        
    print 'connecting...'
    c = connect()
    print 'calling <remote>.calc.sum (1,2,3)'
    print c.calc.sum (1,2,3)
    print 'calling <remote>.calc.nonexistent(), expect an exception!'
    print c.calc.nonexistent()
