############################################################################# ## This file is part of Ostendi. ## ##-------------------------------------------------------------------------## ## Copyright 2007-2009 Bryce Schroeder ## ## bryce.schroeder@gmail.com ## ## http://www.ferazelhosting.net/~bryce/ ## ##-------------------------------------------------------------------------## ## This program is free software: you can redistribute it and/or modify ## ## it under the terms of the GNU General Public License as published by ## ## the Free Software Foundation, either version 3 of the License, or ## ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## ## along with this program. If not, see . ## ############################################################################# from select import select import thread, socket, time server=0 class NetException(Exception): pass ############################################################################## ############################################################################## # Message class # ############################################################################## ############################################################################## class Message(object): """ This class implements a network-transparent message. See the file network.txt for more info about this concept. The gist is that instances of this class (or, more liekely, derrived classes implementing the message interface) are created on one machine and then moved over the network to others. Derrived classes must override the kind class property with their offical name. The derrived class should be named 'Msg%s', with %s equal to the offical name. If you wish to use the built in serialize and deserialize functions (rather than overriding them), a class property named 'parameters' must be supplied. An example is given. """ kind = "MESSAGE" # example. # the key is the name of the parameter, # the value is the function used to decode it from its # string representation. # For serialization, __repr__ is assumed. parameters = { 'keyname': int } def __init__(self, **parameters): """ Creates a new message. It is assumed that there is an init() method (part of the Message interface) that will properly intialize the message-dependent parts of the instance. If called without additional parameters, it is expected that this instance is being created with the intent to make it valid by deserializing it later. """ self.sender = None if parameters: self.init(**parameters) def init(self, **kwargs): """ Initalize the message in whatever ways are specific to this kind of message. (Optional) Only called when a new message is sent, not when receiving. OVERRIDE THIS METHOD. """ for k,v in kwargs.items(): if not k in self.parameters: print "Warning, unknown parameter %s won't be transmitted."%k continue self.__dict__[k]=v def set_sender(self, sender): """ INTERNAL. Set the sender of the message. """ self.sender = sender def get_sender(self): """ Get the client name of the message's sender. (Highly useful if you want to reply!) """ return self.sender def serialize(self): """OVERRIDE THIS METHOD Turn this instance into a string, which will be sent over the network and passed to a deserialize method of another instance of this class. """ temp = [] for key,tfunc in self.parameters.items(): temp.append("%s\x1F%s"%(key, encoders.get(self.parameters[key],repr)(self.__dict__[key]) )) return "\x1E".join(temp) def deserialize(self, sob): """OVERRIDE THIS METHOD Turn this object into the one represented by the string sob, which came from the serialize method of another instance. """ for parameter in sob.split("\x1E"): if not parameter: continue k,v = parameter.split("\x1F") self.__dict__[k] = self.parameters[k](v) #print "***", self.parameters[k],(v),self.parameters[k](v) ############################################################################## # Individual message object class # ############################################################################## # A literal string # TODO: replace by safe_eval for everything litstr = (lambda x: x[1:-1]) def typed(x): type, value = x[0:5].strip(), x[6:] #print "***x:<%s>, type=<%s>, value=<%s>"%(x,type,value) if type == 'int': return int(value) elif type == 'float': return float(value) elif type == 'str': return litstr(value) def mktyped(x): return "%5s=%s"%(x.__class__.__name__, x.__repr__()) encoders = { typed: mktyped } class MsgChat(Message): kind="CHAT" parameters={ 'text': litstr } # these two prop messages should really be moved into th2 class MsgNewProp(Message): kind="PROP_NEW" parameters={ 'name': litstr, 'kind': litstr } class MsgEdgeProp(Message): kind="PROP_EDGE" parameters={ 'name': litstr, 'edge': litstr } class MsgConnect(Message): kind="CONNECT" parameters={} class MsgRename(Message): kind='RENAME' parameters={'newname': litstr} ############################################################################## ############################################################################## # Net class # ############################################################################## ############################################################################## class Net(object): """ This class hides the nasty realites of the network from its user and implements a message-passing interface. The class then ensures that the object, which must conform to the Message interface, ends up on the other side transparently. One of the Net class instances involved must be a router, which provides the central location that the other instances will connect to, and, unsurprisngly, does the routing of messages to their proper destinations. - CONCEPTS - Client names are strings identifying the instances of the Net class that are in communication with each other. Messages are instances of classes conforming to the Message interface (documented in that class), which are transmitted over the network. """ # Set the version here. 0-9999 VERSION = 0 # this is the manifest of classes we are allowed to make MANIFEST = [MsgChat, MsgConnect, MsgRename, MsgEdgeProp, MsgNewProp] def __init__(self, server=0): """ Create a new Net object. By default, it will have no connections to other objects. If the keyword argument server is an integer != 0, it will result in this machine becoming a message router on the port specified by the value of the argument. The router is the central hub through which messages pass. The server should always be the message router, clients should never be, but the class is client/server agnostic, that is it presents the same interface and works the same way from the user's pov for both. """ # message /error handler (a NetHandler or derrived class) self.handler = None # bound oodb instance, if applicable self.db = None # important: these hold strings, not message objects, # so no messing with them directly please self.inbox = [] self.outbox = [] # this, on the other hand, does contain messages - # it gets filled when fetch pumps the outbox. self.mailbox = [] #process the manifest self.manifest = {} for item in self.MANIFEST: self.manifest[item.kind] = item # sources for messages # contains name:socket pairs. self.sources = {} # spawning router if server: self.servermode = True self.client_id = 0 self.port = server self.name = 'server' #print "DBG starting acceptor" thread.start_new_thread(self.acceptor, ()) #time.sleep(2) #print "DBG done" else: self.servermode = False self.name = 'temporary' #print "DBG starting carrier" # no more threads, alas.... #thread.start_new_thread(self.mailcarrier, ()) #time.sleep(2) #print "DBG done2" # spawn mailman is done in connect. self.partial_buffer = [] def attach_handler(self, handler): """ Attach a handler object to this net object. The handler will deal with exceptional conditions and has efficent message handler dispatch. It should be an instance of some class inherriting from NetHandler. """ self.handler = handler self.handler.net = self def acceptor(self): """ INTERNAL. This is the server thread that accepts connections. """ #print "ACCPETOR STARTING" serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.bind(('',self.port)) serversocket.listen(10) while 1: #print "ACCEPTOR LISTENING" conn, address = serversocket.accept() try: name = self.accept_client(conn, address) print "Connection with", address, "ready." # now we can add it to our sources. self.sources[name] = conn if self.handler: self.handler.new_client(name) except socket.error: print "Client gave bad handshake:", address def accept_client(self, conn, address): print "New client", address # get the handshake message message = self.decode_message(self.read_from(conn)) oldname = name = message.get_sender() if name == 'temporary': self.client_id += 1 name = 'client'+str(self.client_id) # transmit the rename message self.write_to(conn, self.encode_message(MsgRename(newname=name), oldname)) return name def emit(self, message, destination=None): """ Send a message. If the keyword arg destination is None, as it is by default, then the message is broadcast to all other instances linked to this one. Destination can also be a list of client names. """ if destination is None and self.servermode: destination = '*' self.outbox.append((destination,self.encode_message(message, destination))) return def encode_message(self, message, destination=None): """ INTERNAL encodes a message with destination, kind, etc and serializes the message object. """ if not destination: destination = '*' return "%s\x1C%s\x1C%s\x1C%s"%( destination,self.name,message.kind, message.serialize()) def write_to(self, router, text, pre_generated=False): """ Write the entire message text to the router socket, transmitting the version and length information. If the parameter pre_generated is true, then it is assumed that the version/length info is already there. """ if pre_generated: message = text else: message = "TH2v%04d\x1C%08X\x1C%s"%(self.VERSION,len(text),text) i = 0 while i < len(message): i += router.send(message[i:]) def read_from(self, router, block = False): """ Read from the router, returning the content portion of the message (version is checked, and length is of no relevence anymore, so that is not returned either.) """ # The elaborate reading code here (rather than saying # for instance f = ...recv(18) is because theoretically # the message might get broken up into pieces and f would # be only part of it. if not self.partial_buffer: f = '' while len(f) < 18: # we assume this little bit won't be badly broken up # if the server is actually going too slow, fix this fixme try: tmp = router.recv(18-len(f)) if not tmp: return None f += tmp except socket.error: if f: # leaving it in a partial state like that is a # terminal error print "GRAVE ERROR", router, f return None self.r_program = f[0:4] self.r_version = int(f[4:8]) self.r_length = int(f[10:17],16) if self.r_program != 'TH2v': raise NetException("Invalid message from server (%s)"%( f.__repr__())) if self.r_version != self.VERSION: raise NetException("Version Mismatch - %04d != %04d"%( self.r_version, self.version)) self.read_so_far = 0 self.partial_buffer = [] if self.read_so_far < self.r_length: segment = router.recv(self.r_length-self.read_so_far) if not segment: return None self.partial_buffer.append(segment) self.read_so_far += len(segment) if self.read_so_far == self.r_length: tmp = self.partial_buffer self.partial_buffer = [] return ''.join(tmp) else: return None def fetch(self, kind=None): """ Get messages from this Net instance's mailbox. A list of all the messages currently there will be returned, optionally they may be filtered by kind (a string is supplied via the keyword argument 'kind'), in which case only the messages of that kind will be returned. (The others will remain in the mailbox.) If there are no messages, the function returns an empty list immediatly (it is nonblocking). You can use this function to empty the mailbox by ignoring the return value, e.g. myNet.fetch() """ # If there are any new messages, process them. if self.inbox: self.pump_messages() matching = [] leftover = [] for message in self.mailbox: if not kind or kind == message.kind: matching.append(message) else: leftover.append(message) self.mailbox = leftover return matching def pump_messages(self): """ INTERNAL. This function reads message strings from the inbox after they are turned into Message objects. """ for message in self.inbox: self.mailbox.append(self.decode_message(message)) self.inbox = [] def decode_message(self, message, return_routing=False): """ INTERNAL. This function decodes strings and turns them into message objects. If the return_routing flag is set, it only analyzes routing information, returning a tuple (destination,sender) Otherwise it makes a message object and returns that. """ if not message: return None #destination,self.name,message.kind, # message.serialize() destination, sendername, kind, data = message.split('\x1C') if return_routing: return destination,sendername # the manifest is a dictionary with the classes for messages # as values, indexed by the message names. newmsg = self.manifest[kind]() newmsg.deserialize(data) newmsg.set_sender(sendername) return newmsg def connect(self, ip, port, name='temporary'): """ Connect to another Net instance at the specified ip and port. The other instance must be a router, i.e. it was started with a nonzero 'server' argument. (Otherwise the connection will be refused, because nothing is there to accept connections.) You may be connected to only one Net class at a time. Implementation - this adds a message source for the ip/port after handshaking is complete. """ self.name = name tsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tsock.connect((ip,port)) # greet the server self.write_to(tsock, self.encode_message(MsgConnect(), 'server')) # get our new name message = self.decode_message(self.read_from(tsock)) servername = message.get_sender() # Should probablya lways be 'server' self.name = message.newname # having completed the handshake, we can add the server to our sources self.sources[servername] = tsock #thread.start_new_thread(self.receiver, ('server',)) #OLD CODE #self.sources.append #self.router_sockets[0].connect((ip,port)) # #thread.start_new_thread(self.mailcarrier, () ) ##TODO: replace with lock? #time.sleep(0.3) #self.emit(MsgConnect()) def extend_manifest(self, newmsg): """ This method adds new classes to the list of things the messages is permitted to deal in. """ self.MANIFEST.extend(newmsg) for item in newmsg: self.manifest[item.kind] = item def process(self): """ This method simplifies the use of the net class when a handler class is employed (using attach_handler). It calls the mailcarrier and runs the handler's process method. """ self.mailcarrier() return self.handler.process() def mailcarrier(self): """ Call this method in the main loop if you want to get messages. It listens for messages from the router and puts them into the mailbox. """ remlist = [] for destination, outgoing in self.outbox: #print "DBG sending outgoing message" #print "destination: ", destination #print outgoing.__repr__() if destination == '*' and self.servermode: #if self.sources: print "___", self.sources for name,source in self.sources.items(): try: self.write_to(source, outgoing) except socket.error: print "Disconnected:", source remlist.append(name) elif self.sources.has_key(destination): try: self.write_to(self.sources[destination], outgoing) except socket.error: print "Disconnected (S):", destination remlist.append(destination) elif not destination: try: self.write_to(self.sources.values()[0], outgoing) except socket.error: print "Disconnected (N):", destination remlist.append(destination) else: print "DBG couldn't route the message." print self.sources self.outbox = [] # debuggeing!! #for s in self.sources.values(): # print s #end debuggin infiles, outfiles, errfiles = select(self.sources.values(),[],[],0.001) # support maybe having more than one router in the future for source in infiles: #print "DBG getting a message" f = self.read_from(source, False) if f: self.inbox.append(f) else: # drop client print "Disconnect", source remlist.append(source) for r in remlist: print "Handle diconnection:",r if not isinstance(r,str): for k,v in self.sources.items(): if v == r: r = k break if self.handler: self.handler.dropped_client(r) del self.sources[r] ############################################################################## ############################################################################## # NetHandler class # ############################################################################## ############################################################################## class NetHandler(object): """ This class is for efficently handling messages, net clients, disconnections and so forth by overloading methods in a derrived class. """ def new_client(self, name): """ This is called when a new client connects. The parameter is the name, by which it can be found in self.sources. The handshake is already complete and the client has a true name. """ return def dropped_client(self, name): """ The named client has just /unexpectedly/ disconnected, that is to say it is being dropped because of an exception/read error, not a controlled process (in which case the client would be removed from self.sources before any read errors occured.) It is still in sources and will be removed after this function returns. """ return def process(self): """ Fetches events from the bound handler and dispatches them. Run this in the main loop, after the call to mail_carrier which pumps events in to the queue. Do not use both a handler and attempt to manually access events from the net class' fetch method. """ kmsgs = self.net.fetch() for msg in kmsgs: self.__class__.__dict__.get(msg.kind, self.unknown_message)(self,msg) return bool(kmsgs) def unknown_message(self, msg): print "received unknown message", msg