#############################################################################
#
#	Pyro Core Library
#
#	This is part of "Pyro" - Python Remote Objects
#	which is (c)1999 Irmen de Jong - irmen@bigfoot.com.
#
#############################################################################

import sys, types, time, string
import socket, select
import SocketServer
import Pyro.util

# Fall back on pickle if cPickle isn't available
try:
	import cPickle; pickle = cPickle
except ImportError:
	import pickle
	


#### Pyro exception object - see also PyroException ####

class PyroError(Exception):     pass

class ProtocolError(PyroError): pass
class URIError(PyroError):      pass
class DaemonError(PyroError):   pass

	
class RemotePyroError(PyroError):
	def __init__(self,exception):
		PyroError.__init__(self,exception)
	def __str__(self):
		return '[REMOTE] '+self.args[0].__class__.__name__+': '+str(self.args[0])

#### Remote Invocation Flags ####

RIF_Varargs  = (1<<0)		# for '*args' syntax
RIF_Keywords = (1<<1)		# for '**keywords' syntax


#############################################################################
#
#	ObjBase		- server-side object implementation base class
#
#############################################################################

class ObjBase:
	def __init__(self):
		self.objectUUID=Pyro.util.getUUID()
	def UUID(self):
		return self.objectUUID
	def setPersistentUUID(self,UUID):
		# only to be used for persistent objects which should get a fixed UUID
		self.objectUUID=UUID
	def Pyro_dyncall(self, method, flags, args):
		# find the method in this object, and call it with the supplied args.
		keywords={}
		if flags & RIF_Keywords:
			# reconstruct the varargs from a tuple like (a,b,(va1,va2,va3...),{kw1:?,...})
			keywords=args[-1]
			args=args[:-1]
		if flags & RIF_Varargs:
			# reconstruct the varargs from a tuple like (a,b,(va1,va2,va3...))
			args=args[:-1]+args[-1]
		return apply(getattr(self,method),args,keywords)


#############################################################################
#
#	Protocol Adapters
#
#############################################################################

#------ NULL: adapter

class NULLAdapter:
	def bindToURI(self,URI):
		if URI.protocol!='NULL':
			raise ProtocolError('incompatible protocol in URI')
	def remoteInvocation(self, method, flags, *args):
		pass
	def acceptInvocation(self,sock):
		pass


#------ PYRO: adapter

class PYROAdapter:
	server=None		# link back to the server that's using us
	def bindToURI(self,URI):
		if URI.protocol!='PYRO':
			raise ProtocolError('incompatible protocol in URI')
		self.host = URI.host
		self.port = URI.port
		self.objectID = URI.objectID
	def remoteInvocation(self, method, flags, *args):
		s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		s.connect(self.host, self.port)
		s.send(pickle.dumps((self.objectID,method,flags,args)))
		s.shutdown(1)    # send EOF & disallow further sends
		answer=''
		while 1:
			data=s.recv(1000)
			if not data:
				break
			answer=answer+data
		answer = pickle.loads(answer)
		if isinstance(answer,PyroException):
			answer.raiseEx()
		return answer
	def handleInvocation(self,sock):
		req=''
		while 1:
			data=sock.recv(1000)
			if not data:
				break
			req=req+data
		# Unpickle the request, which is a tuple:
		#  (object ID, method name, flags, (arg1,arg2,...))
		req=pickle.loads(req)
		try:
			# find the object in the implementation database of our server
			o = self.server.implementations[req[0]]
			# call the method on this object
			res = o[0].Pyro_dyncall(req[1],req[2],req[3])	# (method,flags,args)
			# reply the result to the caller
			sock.send(pickle.dumps(res))
		except KeyError,x:
			print 'invocation to unknown object ignored',x
		
def getProtocolAdapter(protocol):
	if protocol=='PYRO':
		return PYROAdapter()
	if protocol=='NULL':
		return NULLAdapter()
	else:
		raise ProtocolError('unsupported protocol')


#############################################################################
#
#	PyroException		- Pyro exception.
#
#	This class represents a Pyro exception which can be transported
#	across the network, and raised on the other side (by invoking raiseEx).
#	NOTE: the 'real' exception class must be 'known' on the other side!
#	NOTE2: this class is adapted from exceptions.Exception.
#	NOTE3: USE PyroError FOR OTHER PYRO EXCEPTIONS. PyroException IS ONLY
#          TO BE USED TO TRANSPORT AN EXCEPTION ACROSS THE NETWORK.
#
#############################################################################

class PyroException:
	def __init__(self,excObj):
		self.excObj = excObj
	def raiseEx(self):
		raise self.excObj
	def __str__(self):
		s=self.excClass.__name__
		if not self.args:
			return s
		elif len(self.args) == 1:
			return s+': '+str(self.args[0])
		else:
			return s+': '+str(self.args)
	def __getitem__(self, i):
		return self.args[i]	


#############################################################################
#
#	PyroURI		- Pyro Unique Resource Identifier
#
#	This class represents a Pyro URI (which consists of four parts,
#	a protocol identifier, a hostname, a portnumber, and an object ID.
#	
#	The URI can be converted to a string representation (str converter).
#	The URI can also be read back from such a string (initFromString).
#	The URI can be initialised from its parts (init).
#	The URI can be initialised from a string directly, if the init
#	 code detects a ':' and '/' in the host argument (which is then
#        assumed to be a string URI, not a hostname).
#
#############################################################################

class PyroURI:
	def __init__(self,host,objectID=0,port=Pyro.config.PYRO_PORT,protocol='PYRO'):
		# If the 'host' arg contains ':' and '/', assume it's a URI string.
		if string.find(host,':')>=0 and string.find(host,'/')>=0:
			self.initFromString(host)
		else:
			self.host=host
			self.port=port
			self.protocol=protocol
			self.objectID=objectID
	def __str__(self):
		s = self.protocol+'://'+self.host
		if self.port!=Pyro.config.PYRO_PORT:
			s=s+':'+str(self.port)
		return s+'/'+str(self.objectID)
	def __repr__(self):
		return '<PyroURI \''+str(self)+'\'>'
	def init(self,host,objectID,port=Pyro.config.PYRO_PORT,protocol='PYRO'):
		self.host=host
		self.objectID=objectID
		self.port=port
		self.protocol=protocol
	def initFromString(self,arg):
		colon = string.find(arg,':')
		try:
			if colon>=1:
				self.protocol = arg[:colon]
				if arg[colon+1:colon+3]=='//':
					arg=arg[colon+3:]
					colon = string.find(arg,':')
					if colon>=1:
						self.host=arg[:colon]
						arg=arg[colon+1:]
						slash=string.find(arg,'/')
						if slash>=1:
							self.port=int(arg[:slash])
							self.objectID=arg[slash+1:]
							return
					else:
						slash=string.find(arg,'/')
						if slash>=1:
							self.host=arg[:slash]
							self.port=Pyro.config.PYRO_PORT
							self.objectID=arg[slash+1:]
							return
			
		except ValueError, IndexError:
			pass
		raise URIError('illegal URI format')


#############################################################################
#
#	DynamicProxy	- dynamic Pyro proxy
#
#	Can be used by clients to invoke objects for which they have no
#	precompiled proxy.
#
#############################################################################

def getProxyForURI(URI):
	return DynamicProxy(URI)

class DynamicProxy:
	class _invocation:
		def __init__(self,adapter,method):
			self.adapter = adapter
			self.method = method
		def __call__(self,*vargs, **kargs):
			return self.adapter.remoteInvocation(self.method,RIF_Varargs|RIF_Keywords,vargs,kargs)
			
	def __init__(self, URI):
		self.URI = URI
		self.objectID = URI.objectID
		self.adapter = Pyro.core.getProtocolAdapter(self.URI.protocol)
		self.adapter.bindToURI(URI)	
	def __getattr__(self, name):
		return self._invocation(self.adapter,name)
	def __repr__(self):
		return '<Pyro.core.DynamicProxy instance at '+str(id(self))+'>'
	def __str__(self):
		return self.__repr__()

	# Slightly faster way of calling:
	#  instead of proxy.method(args...) use proxy('method',args...)
	def __call__(self,method,*vargs, **kargs):
		return self.adapter.remoteInvocation(method,RIF_Varargs|RIF_Keywords,vargs,kargs)
	

#############################################################################
#
#	Daemon		- server-side Pyro daemon
#
#	Accepts and dispatches incoming Pyro method calls.
#
#############################################################################

class Daemon(SocketServer.TCPServer):
	def __init__(self,protocol='PYRO',port=Pyro.config.PYRO_PORT):
		self.hostname = socket.gethostname()	
		self.port = port
		self.implementations={}
		self.NameServer = None
		self.protocol = protocol
		self.adapter = getProtocolAdapter(protocol)
		self.adapter.server = self
		SocketServer.TCPServer.__init__(self,(self.hostname,self.port),DaemonSlave)
	
	def __del__(self):
		# server shutting down, unregister all known objects in the NS
		for o in self.implementations.keys():
			self.NameServer.unregister(self.implementations[o][1])
	
	def useNameServer(self,NS):
		self.NameServer=NS

	def connectPersistent(self,object,name):
		# enter the (object,name) in the known implementations dictionary
		self.implementations[object.UUID()]=(object,name)
		# don't register the object with the NS; it's already persistent there!

	def disconnectPersistent(self,object):
		# don't unregister with naming service, only remove it from the impl. directory
		try:
			del self.implementations[object.UUID()]
		except KeyError:
			pass
	
	def connect(self,object,name):
		# enter the (object,name) in the known implementations dictionary
		self.implementations[object.UUID()]=(object,name)
		# register the object with the NS
		if self.NameServer:
			self.NameServer.register(name,PyroURI(self.hostname,object.UUID(),
				protocol=self.protocol,port=self.port))
		else:
			print 'WARNING: connecting object without NS specified'

	def disconnect(self,object):
		try:
			self.NameServer.unregister(self.implementations[object.UUID()][1])
			del self.implementations[object.UUID()]
		except KeyError:
			pass

	def handleRequests(self,timeout,others=[],callback=None):
		ins=1
		while ins:
			(ins,outs,excs) = select.select([self]+others,[],[],timeout)
			if self in ins:
				self.handle_request()
			elif ins:
				callback(ins)

	def handle_error(self, request, client_address):
		(exc_type, exc_value, exc_trb) = sys.exc_info()
		if exc_type==PyroException:
			# It is a 'normal' PyroException, generated on purpose. Return it to the client.
			request.send(pickle.dumps(exc_value))
		else:
			# It is another - unexpected! - exception, return failure to the client
			print '!'*40
			print 'Exception happened during processing of request from',client_address
			print exc_type,
			request.send(pickle.dumps(PyroException(RemotePyroError(exc_value))))
			print '- UNEXPECTED EXCEPTION OCCURED IN SERVER CODE'
			print '!'*40
		
	def server_bind(self):
		try:
			SocketServer.TCPServer.server_bind(self)
		except socket.error:
			raise DaemonError('Pyro daemon already running on '+str(self.server_address))

        
class DaemonSlave(SocketServer.BaseRequestHandler):

	def handle(self):
		self.server.adapter.handleInvocation(self.request)
		


#############################################################################
#
#	init	- the init code
#
#############################################################################

def initClient(banner=1):
	if banner:
		print 'Pyro Client Initialized. Using Pyro V'+Pyro.PYRO_VERSION
	
def initServer(banner=1):
	if banner:
		print 'Pyro Server Initialized. Using Pyro V'+Pyro.PYRO_VERSION

