Python Socket Server

From PeformIQ Upgrade
Jump to navigation Jump to search

Socket Server

#!/usr/bin/python
#
#  Copyright (c) 1998, 1999, Sean Reifschneider, tummy.com, ltd.
#  All Rights Reserved
#
#  High-level classes for implementing command/response clients and
#  servers in Python.  Though the server is much more highly developed
#  than the client at this point.
#
#  http://www.tummy.com/sockserv/
#  ftp://ftp.tummy.com/pub/tummy/sockserv/
######################################################################
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.0 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is sockserv, released September 20, 1998. 
# The Initial Developer of the Original Code is tummy.com, ltd.
# Portions created by tummy.com, ltd. are Copyright (C) 1998, 1999
# tummy.com, ltd. All Rights Reserved.
#
# Contributor(s): ______________________________________. 
#
######################################################################

revision = "$Revision: 1.5 $"
rcsid = "$Id: sockserv.py,v 1.5 2002/10/08 16:30:41 jafo Exp $"

'''Base-class for server side of client/server programs.

This class manages the server side of a client/server program.  It uses a
very simple command-oriented protocol.  The server manages receiving
connections from clients and processing the commands they may send.
With the exception of a couple of stubs, all of the dealing with
these events is done by user-supplied routines (typically provided
via inheritance).

Commands are dispatched to methods named "do_COMMAND", where "COMMAND" is
the command name (and is always converted to upper-case).  These methods
should normally return 0.  Returning 1 indicates that the client socket
should be shut down (useful in a "do_QUIT" method).

CLIENTS:
	Clients (as passed to the do_* and other routines) aren't pure socket
	objects.  In order to support some additional functionality, they have
	been wrapped by a thin wrapper object called "connection".  Therefore.
	these objects have a few additional attributes to make setting
	client-specific data easier.

	The connection types support 'addGroup' and 'delGroup' methods for
	adding and removing a client from a group.  Clients may belong to
	multiple groups.  A list of clients in a given group can be found
	by using the sockserv.getGroupSockList() method, and data may be
	sent to clients in a group using sockserv.sendGroup().

	User-data may be associated with a client by setting it's
	"user_data" attribute (which defaults to "None".  This is data
	that's associated with an individual socket (for example, the
	socket object of another connection which we are proxying for).

	Command processing can be overridden by specifying a "processor"
	function for a client.  A processor function takes a single
	argument (data ready to be processed), and returns the remaining
	(unprocessed) data, or '' if all was processed

	To set the processor, call "connection.processor()", passing
	the function to be invoked.  To clear this (and return to the
	normal "do_*" processing, call "connection.processor()" with
	no arguments.

	This "processor" function is meant for implementing data phases:

		helpinfo_DATA = ( '200-  DATA', 'Enter data phase, terminated with ".".' )
		def do_DATA(self, client, cmd, args):
			client.processor(readUntilDot)

		def readUntilDot(client, data)
			#  process data looking for '.' to terminate data.
			if foundDot: client.processor()
			return(bytesProcessed)

ATTRIBUTES:
	- newLine -- String containing line termination suitable for socket
		communication.
	- portNum -- Port number server is running on, or "None" if not running.

Simple socket server example:

	import sockserv

	class server(sockserv.sockserv):
		def clientClosed(self, client):
			print 'Client %d closed' % client.fileno()

		helpinfo_QUIT = ( '200-  QUIT', 'Terminate the connection.' )
		def do_QUIT(self, client, cmd, args):
			client.send('Bye now!', nl = 1)
			return(1)

		helpinfo_SHUTDOWN = ( '200-  SHUTDOWN', 'Terminate the server.' )
		def do_SHUTDOWN(self, client, cmd, args):
			client.send('Server terminating', nl = 1)
			self.shutdownServer = 1
			return(1)

		helpinfo_HELP = ( '200-  HELP', 'Display this message.' )
		def do_HELP(self, client, cmd, args):
			client.send('200-Help information', nl = 1)
			client.send('200-', nl = 1)
			sockserv.sockserv.do_HELP(self, client, cmd, args)
			client.send('200-', nl = 1)
			client.send('200 End of help information.', nl = 1)

	s = server(( 5000, 5001, 5002, 5003))
	print 'Socket opened on %d' % s.portNum
	s.mainloop()
'''

revision = '$Revision: 1.5 $'

import socket
import string
import os

newLine = '\r\n'			#  socket line termination

######################
def extractLine(data):
	'''Return next line from "data", and the rest of data.
	Extract the next line from "data" and return a tuple containing it and
	the remainder of the data.

	RETURNS: Tuple containing: next line, remainder of data.  Next line will
		be None if there is no line terminator.
	EXCEPTIONS: None generated internally.
	ARGUMENTS:
		- data -- string of bytes to look for line in.
	'''
	pos = string.find(data, '\r')
	pos2 = string.find(data, '\n')

	if pos2 >= 0 and (pos2 < pos or pos < 0): pos = pos2
	if pos < 0: return(( None, data ))
	if data[pos] == '\r' and data[pos + 1] == '\n':
		return(data[:pos], data[pos + 2:])
	return(data[:pos], data[pos + 1:])

class sockserv:
	helpinfo = {}

	#######################################################################
	def __init__(self, port, host = '', name = 'GENERIC', portFile = None):
		'''Create an instance of a server.
		This initializes the server, and calls an "__localinit__" method if
		it exists.

		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- portList -- a single numeric port, or a list of ports to try to start
				the server on.
			- host -- Interface to bind server on.  Defaults to '', meaning server
				should bind on all interfaces.
			- name -- String representing server name, used in initial connect
				message.
			- portFile -- File name that port of server is written to.  Meant for
				allowing clients to locate server when a list of ports are given.
				Defaults to None which indicates no file.  File is deleted
				when server is closed.
		'''

		self.serverName = name
		self.host = host
		try:
			self.portList = port[0]
			self.portList = port
		except:
			self.portList = ( port, )
		self.portFile = portFile
		self.portNum = None
		self.clientList = []
		self.clientData = {}
		if hasattr(self, '__localinit__'):
			getattr(self, '__localinit__')()
		self.open()


	###################
	def mainloop(self):
		'''Loop forever processing data on sockets.
		If the server has a "shutdownServer" attribute, the loop will stop
		and this function will return.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		'''
		import select

		while not hasattr(self, 'shutdownServer'):
			rfdl = []
			self.rfdlAdd(rfdl)
			rfdl, wfdl, efdl = select.select(rfdl, [], [], None)
			self.process(rfdl)


	###############
	def open(self):
		'''FOR INTERNAL USE ONLY
		Opens the socket, creating portFile if necessary, etc....

		RETURNS: Port that the server was started on
		EXCEPTIONS: Raises an exception from socket.bind() on failure.
		'''

		self.close()
		self.portNum = None
		self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		failWith = None
		for port in self.portList:
			try:
				self.server.bind(( self.host, port ))
				self.portNum = port
				failWith = None
				break
			except Exception, e:
				failWith = e

		if self.portFile != None:
			file = open(self.portFile, 'w')
			file.write('%d\n' % self.portNum)
			file.close()

		if self.portNum != None:
			self.server.listen(10)
		if failWith != None:
			raise failWith
		return(self.portNum)


	################
	def close(self):
		'''FOR INTERNAL USE ONLY
		Closes the socket (only if open).  Sets internal attributes to
		sane values.

		RETURNS: Port that the server was started on
		EXCEPTIONS: None generated internally.
		'''

		if self.portFile != None:
			os.unlink(self.portFile)
		try: self.server.close()
		except: pass
		self.server = None

		for client in self.clientList:
			try: client.close()
			except: pass
		self.clientList = []
		self.clientData = {}

		self.portNum = None


	########################
	def rfdlAdd(self, list):
		'''Add internal sockets to select() list.
		Add to "list", the sockets that we want listened on during a select().

		RETURNS: Reference to list passed in.
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- list -- (MODIFIED) Mutable list type which sockets should be
					appended to.
		'''

		try: list.append(self.server.fileno())
		except: pass
		for client in self.clientList:
			fn = client.fileno()
			if fn < 0: self.clientShutdown(client)
			else: list.append(fn)
		return(list)


	########################
	def process(self, rfdl):
		'''Iterate over "rfdl", processing our sockets if listed.

		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- rfdl -- List of sockets as returned by select().  Data (or
				connections in the case of the server socket) is processed,
				commands are decoded and dispatched.
		'''

		try: foo = rfdl[0]
		except: rfdl = ( rfdl )

		for sock in rfdl:
			#  new connections to server
			if self.server != None and sock == self.server.fileno():
				conn, addr = self.server.accept()
				conn = connection(conn)
				self.clientList.append(conn)
				self.clientReady(conn, addr)

			#  data from a client
			for client in self.clientList:
				fileno = client.fileno()
				if sock == fileno:
					try: data = client.recv(1024)
					except: data = ''
					if len(data) < 1:
						self.clientShutdown(client)
						continue
					if self.clientData.has_key(fileno):
						data = self.clientData.get(fileno, '') + data

					while 1:
						if client.dataProcessor:
							#  If processor returns -1, it consumed all data.
							#  Otherwise, it returns number of bytes processed.
							data = client.dataProcessor(client, data)
							break

						line, data = extractLine(data)
						if line == None: break
						line = string.strip(line)

						if len(line) > 0:
							cmd = line
							args = ''
							try: cmd, args = string.split(line, None, 1)
							except: pass
							cmd = string.upper(string.strip(cmd))
							args = string.strip(args)
							mname = 'do_' + cmd

							if hasattr(self, mname):
								method = getattr(self, mname)
							elif hasattr(self, 'do_unknownCommand'):
								method = getattr(self, 'do_unknownCommand')
							else:
								client.send('500 Unknown command "%s"%s'
										% ( cmd, newLine ))
								continue
							if method(client, cmd, args):
								self.clientShutdown(client)
								break

					if data: self.clientData[fileno] = data
					else:
						try: del(self.clientData[fileno])
						except KeyError: pass


	#################################
	def clientShutdown(self, client):
		'''Close specified client connection.
		Used to remove a socket from the list of clients and do other misc
		clean-up.

		RETURNS: Nothing
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- client -- "socket" to be removed.
		'''

		self.clientClosed(client)
		try: client.close()
		except IOError: pass
		try:
			self.clientList.remove(client)
			del(self.clientData[client.fileno()])
			client.close()
		except:
			pass


	###############################
	def clientClosed(self, client):
		'''STUB: Called when socket is being closed.
		All attempts are made to call this routine before the socket is
		actually closed.  However, data sent on the "client" socket may
		cause an exception.  "client.fileno()" can be used as a connection
		identifier, but in certain cases this may be -1.

		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- client -- "socket" which is being closed.
		'''
		pass


	##################################
	def clientReady(self, sock, addr):
		'''STUB: Called when new connection is opened.
		This routine is called right after a connection is opened, displaying
		a default connection message.

		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- sock -- "socket" that client is on.
			- addr -- Address as returned by "socket.accept()".  Typically, a
				2-element list containing host and address of client side.
		'''
		sock.send('200 %s server ready, connection from %s:%d...%s' % 
				( self.serverName, addr[0], addr[1], newLine ))


	####################################
	def getGroupSockList(self, groupId):
		'''Returns a list of all sockets in specified group.
		Returns a list of all sockets in specified group.

		RETURNS: List of socket objects in group "groupId".
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- ID of group to send message to.
		'''
		list = []
		for client in self.clientList:
			try:
				if client.isinGroup(groupId): list.append(client)
			except: pass
		return(list)


	########################################################
	def sendGroup(self, groupId, string, appendNewLine = 0):
		'''Sends a string to all clients in a group.
		Sends a string to all clients in the specified group, with optional
		terminating newLine.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- ID of group to send message to.
			- string -- String to send to clients.
			- appendNewLine -- write a newLine after string?
		'''
		for client in self.getGroupSockList(groupId):
			try:
				client.send(string)
				if appendNewLine: client.send(newLine)
			except socket.error: pass


	#####################################
	def do_HELP(self, client, cmd, args):
		'''Prints help message based on "helpinfo_*" data elements.
		Sends a list of command information based on "helpinfo_*" member
		elements to client.  Headers and footers are to be provided by
		subclasses.

		RETURNS: None
		Exceptions: None generated internally.
		ARGUMENTS:
			- client -- Socket which client is on.
			- cmd -- Command entered by user.
			- args -- Arguments.
		'''

		list = {}
		maxLen = 0
		for key in self.__class__.__dict__.keys():
			if key[:9] == 'helpinfo_':
				element = self.__class__.__dict__[key]
				list[element[0]] = element[1]
				thisLen = len(element[0])
				if thisLen > maxLen: maxLen = thisLen
		keys = list.keys()
		keys.sort()
		if len(keys) > 0:
			for key in keys:
				client.send(('%-*s  %s' % ( maxLen, key, list[key] )) + newLine)
		else:
			client.send('No help available...' + newLine)

class connection:
	#########################
	def __init__(self, sock):
		self.sock = sock
		self.user_data = None
		self.groupIdList = {}
		self.dataProcessor = None

	############################
	def __getattr__(self, name):
		'''FOR INTERNAL USE ONLY
		Unknown attributes are pulled from socket "object".

		RETURNS: Named attribute pulled from "self.socket".
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- name -- Attribute to look up in "self.socket".
		'''
		return(getattr(self.sock, name))


	########################################
	def send(self, data, flags = 0, nl = 0):
		'''Send data over the socket.
		This is the same as the socket.send function, except that it adds the
		"nl" argument.  If "nl" is set, a newline is appended to the string
		being sent.

		RETURNS: Number of bytes sent (same as socket object send() method).
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- data -- Data to be sent over socket.
			- flags -- (optional, defaults to 0) Same as argument to socket
				object send method.  "See Unix manual page recv(2) for the
				meaning of the optional argument flags."
			- nl -- (optional, defaults to 0) If set, sockserv.newLine is
				appended to "data".
		'''
		if nl: return(self.sock.send(data + newLine, flags))
		return(self.sock.send(data, flags))


	#################################
	def processor(self, func = None):
		'''Set up function to process all incoming data.
		Specify function to be called which handles all incoming data.
		Default handler processes data into lines and calls do_* methods
		of sockserv class.  This overrides that processing (used typicly
		for data-phase processing).

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- func -- Function to be called, passing data to process as argument.
				If not present, processor function is cleared, returning to
				default do_* command processing.
		'''
		self.dataProcessor = func


	############################
	def addGroup(self, groupId):
		'''Add the client to a given group.
		Allows grouping of sockets for different tasks.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- Group ID to place client in.
		'''
		self.groupIdList[groupId] = 1


	###################################
	def delGroup(self, groupId = None):
		'''Add the client to a given group.
		Allows grouping of sockets for different tasks.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- Group ID to place client in.  If not specified, removes
				client from ALL groups.
		'''
		if groupId: del(self.groupIdList[groupId])
		else: self.groupIdList = {}


	#############################
	def isinGroup(self, groupId):
		'''Is this client in the specified group?

		RETURNS: 1 if client is in specified group, 0 otherwise.
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- Group ID to check client for membership.
		'''
		return(self.groupIdList.has_key(groupId))


##########################################
#  client which connects to socket servers

class sockcli:
	############################################################
	def __init__(self, port, host = 'localhost', reconnect = 0):
		'''Creates an instance of a client.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- port -- Port server is running on.
			- host -- Host server is running on.
			- reconnect -- If 1, client will try re-connecting if the connection
				is broken.
		'''
		self.host = host
		self.port = port
		self.reconnect = reconnect
		self.sock = None
		self.open()
		self.data = ''
		self.setCallback()


	###############
	def open(self):
		'''Open socket if it's not already open.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		'''
		if self.sock == None:
			try:
				self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
				self.sock.connect(self.host, self.port)
			except Exception, e:
				self.sock = None


	####################
	def getSocket(self):
		'''Returns a list of sockects that should be selected on.

		RETURNS: A list of sockects that should be selected on.
		EXCEPTIONS: None generated internally.
		'''
		if self.sock == None and self.reconnect: self.open()
		if self.sock == None: return([ ])
		return([ self.sock ])


	#######################################
	def setCallback(self, callback = None):
		'''Sets function to be called when data arrives.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- callback -- Specifies routine to call when data arrives on the
				socket.  If ommited, the callback is removed.
		'''
		self.callback = callback


	##################
	def process(self):
		'''Process data on the socket.
		Processes any data on socket, dispatching lines to the callback when
		appropriate.  Returns 1 while socket is still available.

		RETURNS: 1 if socket is still open, or 0 if connection terminated.
		EXCEPTIONS: None generated internally.
		'''
		if self.sock == None and self.reconnect: self.open()
		if self.sock == None:
			if self.reconnect: return(1)
			return(0)

		data = self.sock.recv(1024);
		if len(data) < 1:
			self.sock = None
			return self.reconnect

		self.data = self.data + data

		#  locate lines in input
		while 1:
			line, data = extractLine(data)
			if line == None: break
			line = string.strip(line)

			if len(line) > 0:
				if self.callback:
					self.callback(self.sock, line)

		return(1)


#####################################
#  test code
#####################################

if __name__ == '__main__':
	class server(sockserv):
		def clientClosed(self, client):
			print 'Client %d closed' % client.fileno()

		helpinfo_QUIT = ( '200-  QUIT', 'Terminate the connection.' )
		def do_QUIT(self, client, cmd, args):
			client.send('Bye now!', nl = 1)
			return(1)

		helpinfo_SHUTDOWN = ( '200-  SHUTDOWN', 'Terminate the server.' )
		def do_SHUTDOWN(self, client, cmd, args):
			client.send('Server terminating', nl = 1)
			self.shutdownServer = 1
			return(1)

		helpinfo_HELP = ( '200-  HELP', 'Display this message.' )
		def do_HELP(self, client, cmd, args):
			client.send('200-Help information', nl = 1)
			client.send('200-', nl = 1)
			sockserv.do_HELP(self, client, cmd, args)
			client.send('200-', nl = 1)
			client.send('200 End of help information.', nl = 1)

	s = server(( 5000, 5001, 5002, 5003))
	print 'Socket opened on %d' % s.portNum
	s.mainloop()

Variant

#!/usr/bin/python
#
#  Copyright (c) 1998, 1999, Sean Reifschneider, tummy.com, ltd.
#  All Rights Reserved
#
#  High-level classes for implementing command/response clients and
#  servers in Python.  Though the server is much more highly developed
#  than the client at this point.
#
#  http://www.tummy.com/sockserv/
#  ftp://ftp.tummy.com/pub/tummy/sockserv/
######################################################################
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.0 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is sockserv, released September 20, 1998. 
# The Initial Developer of the Original Code is tummy.com, ltd.
# Portions created by tummy.com, ltd. are Copyright (C) 1998, 1999
# tummy.com, ltd. All Rights Reserved.
#
# Contributor(s): ______________________________________. 
#
######################################################################

revision = "$Revision: 1.5 $"
rcsid = "$Id: sockserv.py,v 1.5 2002/10/08 16:30:41 jafo Exp $"

'''Base-class for server side of client/server programs.

This class manages the server side of a client/server program.  It uses a
very simple command-oriented protocol.  The server manages receiving
connections from clients and processing the commands they may send.
With the exception of a couple of stubs, all of the dealing with
these events is done by user-supplied routines (typically provided
via inheritance).

Commands are dispatched to methods named "do_COMMAND", where "COMMAND" is
the command name (and is always converted to upper-case).  These methods
should normally return 0.  Returning 1 indicates that the client socket
should be shut down (useful in a "do_QUIT" method).

CLIENTS:
	Clients (as passed to the do_* and other routines) aren't pure socket
	objects.  In order to support some additional functionality, they have
	been wrapped by a thin wrapper object called "connection".  Therefore.
	these objects have a few additional attributes to make setting
	client-specific data easier.

	The connection types support 'addGroup' and 'delGroup' methods for
	adding and removing a client from a group.  Clients may belong to
	multiple groups.  A list of clients in a given group can be found
	by using the sockserv.getGroupSockList() method, and data may be
	sent to clients in a group using sockserv.sendGroup().

	User-data may be associated with a client by setting it's
	"user_data" attribute (which defaults to "None".  This is data
	that's associated with an individual socket (for example, the
	socket object of another connection which we are proxying for).

	Command processing can be overridden by specifying a "processor"
	function for a client.  A processor function takes a single
	argument (data ready to be processed), and returns the remaining
	(unprocessed) data, or '' if all was processed

	To set the processor, call "connection.processor()", passing
	the function to be invoked.  To clear this (and return to the
	normal "do_*" processing, call "connection.processor()" with
	no arguments.

	This "processor" function is meant for implementing data phases:

		helpinfo_DATA = ( '200-  DATA', 'Enter data phase, terminated with ".".' )
		def do_DATA(self, client, cmd, args):
			client.processor(readUntilDot)

		def readUntilDot(client, data)
			#  process data looking for '.' to terminate data.
			if foundDot: client.processor()
			return(bytesProcessed)

ATTRIBUTES:
	- newLine -- String containing line termination suitable for socket
		communication.
	- portNum -- Port number server is running on, or "None" if not running.

Simple socket server example:

	import sockserv

	class server(sockserv.sockserv):
		def clientClosed(self, client):
			print 'Client %d closed' % client.fileno()

		helpinfo_QUIT = ( '200-  QUIT', 'Terminate the connection.' )
		def do_QUIT(self, client, cmd, args):
			client.send('Bye now!', nl = 1)
			return(1)

		helpinfo_SHUTDOWN = ( '200-  SHUTDOWN', 'Terminate the server.' )
		def do_SHUTDOWN(self, client, cmd, args):
			client.send('Server terminating', nl = 1)
			self.shutdownServer = 1
			return(1)

		helpinfo_HELP = ( '200-  HELP', 'Display this message.' )
		def do_HELP(self, client, cmd, args):
			client.send('200-Help information', nl = 1)
			client.send('200-', nl = 1)
			sockserv.sockserv.do_HELP(self, client, cmd, args)
			client.send('200-', nl = 1)
			client.send('200 End of help information.', nl = 1)

	s = server(( 5000, 5001, 5002, 5003))
	print 'Socket opened on %d' % s.portNum
	s.mainloop()
'''

revision = '$Revision: 1.5 $'

import socket
import string
import os

newLine = '\r\n'			#  socket line termination

######################
def extractLine(data):
	'''Return next line from "data", and the rest of data.
	Extract the next line from "data" and return a tuple containing it and
	the remainder of the data.

	RETURNS: Tuple containing: next line, remainder of data.  Next line will
		be None if there is no line terminator.
	EXCEPTIONS: None generated internally.
	ARGUMENTS:
		- data -- string of bytes to look for line in.
	'''
	pos = string.find(data, '\r')
	pos2 = string.find(data, '\n')

	if pos2 >= 0 and (pos2 < pos or pos < 0): pos = pos2
	if pos < 0: return(( None, data ))
	if data[pos] == '\r' and data[pos + 1] == '\n':
		return(data[:pos], data[pos + 2:])
	return(data[:pos], data[pos + 1:])

class sockserv:
	helpinfo = {}

	#######################################################################
	def __init__(self, port, host = '', name = 'GENERIC', portFile = None):
		'''Create an instance of a server.
		This initializes the server, and calls an "__localinit__" method if
		it exists.

		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- portList -- a single numeric port, or a list of ports to try to start
				the server on.
			- host -- Interface to bind server on.  Defaults to '', meaning server
				should bind on all interfaces.
			- name -- String representing server name, used in initial connect
				message.
			- portFile -- File name that port of server is written to.  Meant for
				allowing clients to locate server when a list of ports are given.
				Defaults to None which indicates no file.  File is deleted
				when server is closed.
		'''

		self.serverName = name
		self.host = host
		try:
			self.portList = port[0]
			self.portList = port
		except:
			self.portList = ( port, )
		self.portFile = portFile
		self.portNum = None
		self.clientList = []
		self.clientData = {}
		if hasattr(self, '__localinit__'):
			getattr(self, '__localinit__')()
		self.open()


	###################
	def mainloop(self):
		'''Loop forever processing data on sockets.
		If the server has a "shutdownServer" attribute, the loop will stop
		and this function will return.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		'''
		import select

		while not hasattr(self, 'shutdownServer'):
			rfdl = []
			self.rfdlAdd(rfdl)
			rfdl, wfdl, efdl = select.select(rfdl, [], [], None)
			self.process(rfdl)


	###############
	def open(self):
		'''FOR INTERNAL USE ONLY
		Opens the socket, creating portFile if necessary, etc....

		RETURNS: Port that the server was started on
		EXCEPTIONS: Raises an exception from socket.bind() on failure.
		'''

		self.close()
		self.portNum = None
		self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		failWith = None
		for port in self.portList:
			try:
				self.server.bind(( self.host, port ))
				self.portNum = port
				failWith = None
				break
			except Exception, e:
				failWith = e

		if self.portFile != None:
			file = open(self.portFile, 'w')
			file.write('%d\n' % self.portNum)
			file.close()

		if self.portNum != None:
			self.server.listen(10)
		if failWith != None:
			raise failWith
		return(self.portNum)


	################
	def close(self):
		'''FOR INTERNAL USE ONLY
		Closes the socket (only if open).  Sets internal attributes to
		sane values.

		RETURNS: Port that the server was started on
		EXCEPTIONS: None generated internally.
		'''

		if self.portFile != None:
			os.unlink(self.portFile)
		try: self.server.close()
		except: pass
		self.server = None

		for client in self.clientList:
			try: client.close()
			except: pass
		self.clientList = []
		self.clientData = {}

		self.portNum = None


	########################
	def rfdlAdd(self, list):
		'''Add internal sockets to select() list.
		Add to "list", the sockets that we want listened on during a select().

		RETURNS: Reference to list passed in.
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- list -- (MODIFIED) Mutable list type which sockets should be
					appended to.
		'''

		try: list.append(self.server.fileno())
		except: pass
		for client in self.clientList:
			fn = client.fileno()
			if fn < 0: self.clientShutdown(client)
			else: list.append(fn)
		return(list)


	########################
	def process(self, rfdl):
		'''Iterate over "rfdl", processing our sockets if listed.

		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- rfdl -- List of sockets as returned by select().  Data (or
				connections in the case of the server socket) is processed,
				commands are decoded and dispatched.
		'''

		try: foo = rfdl[0]
		except: rfdl = ( rfdl )

		for sock in rfdl:
			#  new connections to server
			if self.server != None and sock == self.server.fileno():
				conn, addr = self.server.accept()
				conn = connection(conn)
				self.clientList.append(conn)
				self.clientReady(conn, addr)

			#  data from a client
			for client in self.clientList:
				fileno = client.fileno()
				if sock == fileno:
					try: data = client.recv(1024)
					except: data = ''
					if len(data) < 1:
						self.clientShutdown(client)
						continue
					if self.clientData.has_key(fileno):
						data = self.clientData.get(fileno, '') + data

					while 1:
						if client.dataProcessor:
							#  If processor returns -1, it consumed all data.
							#  Otherwise, it returns number of bytes processed.
							data = client.dataProcessor(client, data)
							break

						line, data = extractLine(data)
						if line == None: break
						line = string.strip(line)

						if len(line) > 0:
							cmd = line
							args = ''
							try: cmd, args = string.split(line, None, 1)
							except: pass
							cmd = string.upper(string.strip(cmd))
							args = string.strip(args)
							mname = 'do_' + cmd

							if hasattr(self, mname):
								method = getattr(self, mname)
							elif hasattr(self, 'do_unknownCommand'):
								method = getattr(self, 'do_unknownCommand')
							else:
								client.send('500 Unknown command "%s"%s'
										% ( cmd, newLine ))
								continue
							if method(client, cmd, args):
								self.clientShutdown(client)
								break

					if data: self.clientData[fileno] = data
					else:
						try: del(self.clientData[fileno])
						except KeyError: pass


	#################################
	def clientShutdown(self, client):
		'''Close specified client connection.
		Used to remove a socket from the list of clients and do other misc
		clean-up.

		RETURNS: Nothing
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- client -- "socket" to be removed.
		'''

		self.clientClosed(client)
		try: client.close()
		except IOError: pass
		try:
			self.clientList.remove(client)
			del(self.clientData[client.fileno()])
			client.close()
		except:
			pass


	###############################
	def clientClosed(self, client):
		'''STUB: Called when socket is being closed.
		All attempts are made to call this routine before the socket is
		actually closed.  However, data sent on the "client" socket may
		cause an exception.  "client.fileno()" can be used as a connection
		identifier, but in certain cases this may be -1.

		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- client -- "socket" which is being closed.
		'''
		pass


	##################################
	def clientReady(self, sock, addr):
		'''STUB: Called when new connection is opened.
		This routine is called right after a connection is opened, displaying
		a default connection message.

		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- sock -- "socket" that client is on.
			- addr -- Address as returned by "socket.accept()".  Typically, a
				2-element list containing host and address of client side.
		'''
		sock.send('200 %s server ready, connection from %s:%d...%s' % 
				( self.serverName, addr[0], addr[1], newLine ))


	####################################
	def getGroupSockList(self, groupId):
		'''Returns a list of all sockets in specified group.
		Returns a list of all sockets in specified group.

		RETURNS: List of socket objects in group "groupId".
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- ID of group to send message to.
		'''
		list = []
		for client in self.clientList:
			try:
				if client.isinGroup(groupId): list.append(client)
			except: pass
		return(list)


	########################################################
	def sendGroup(self, groupId, string, appendNewLine = 0):
		'''Sends a string to all clients in a group.
		Sends a string to all clients in the specified group, with optional
		terminating newLine.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- ID of group to send message to.
			- string -- String to send to clients.
			- appendNewLine -- write a newLine after string?
		'''
		for client in self.getGroupSockList(groupId):
			try:
				client.send(string)
				if appendNewLine: client.send(newLine)
			except socket.error: pass


	#####################################
	def do_HELP(self, client, cmd, args):
		'''Prints help message based on "helpinfo_*" data elements.
		Sends a list of command information based on "helpinfo_*" member
		elements to client.  Headers and footers are to be provided by
		subclasses.

		RETURNS: None
		Exceptions: None generated internally.
		ARGUMENTS:
			- client -- Socket which client is on.
			- cmd -- Command entered by user.
			- args -- Arguments.
		'''

		list = {}
		maxLen = 0
		for key in self.__class__.__dict__.keys():
			if key[:9] == 'helpinfo_':
				element = self.__class__.__dict__[key]
				list[element[0]] = element[1]
				thisLen = len(element[0])
				if thisLen > maxLen: maxLen = thisLen
		keys = list.keys()
		keys.sort()
		if len(keys) > 0:
			for key in keys:
				client.send(('%-*s  %s' % ( maxLen, key, list[key] )) + newLine)
		else:
			client.send('No help available...' + newLine)

class connection:
	#########################
	def __init__(self, sock):
		self.sock = sock
		self.user_data = None
		self.groupIdList = {}
		self.dataProcessor = None

	############################
	def __getattr__(self, name):
		'''FOR INTERNAL USE ONLY
		Unknown attributes are pulled from socket "object".

		RETURNS: Named attribute pulled from "self.socket".
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- name -- Attribute to look up in "self.socket".
		'''
		return(getattr(self.sock, name))


	########################################
	def send(self, data, flags = 0, nl = 0):
		'''Send data over the socket.
		This is the same as the socket.send function, except that it adds the
		"nl" argument.  If "nl" is set, a newline is appended to the string
		being sent.

		RETURNS: Number of bytes sent (same as socket object send() method).
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- data -- Data to be sent over socket.
			- flags -- (optional, defaults to 0) Same as argument to socket
				object send method.  "See Unix manual page recv(2) for the
				meaning of the optional argument flags."
			- nl -- (optional, defaults to 0) If set, sockserv.newLine is
				appended to "data".
		'''
		if nl: return(self.sock.send(data + newLine, flags))
		return(self.sock.send(data, flags))


	#################################
	def processor(self, func = None):
		'''Set up function to process all incoming data.
		Specify function to be called which handles all incoming data.
		Default handler processes data into lines and calls do_* methods
		of sockserv class.  This overrides that processing (used typicly
		for data-phase processing).

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- func -- Function to be called, passing data to process as argument.
				If not present, processor function is cleared, returning to
				default do_* command processing.
		'''
		self.dataProcessor = func


	############################
	def addGroup(self, groupId):
		'''Add the client to a given group.
		Allows grouping of sockets for different tasks.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- Group ID to place client in.
		'''
		self.groupIdList[groupId] = 1


	###################################
	def delGroup(self, groupId = None):
		'''Add the client to a given group.
		Allows grouping of sockets for different tasks.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- Group ID to place client in.  If not specified, removes
				client from ALL groups.
		'''
		if groupId: del(self.groupIdList[groupId])
		else: self.groupIdList = {}


	#############################
	def isinGroup(self, groupId):
		'''Is this client in the specified group?

		RETURNS: 1 if client is in specified group, 0 otherwise.
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- groupId -- Group ID to check client for membership.
		'''
		return(self.groupIdList.has_key(groupId))


##########################################
#  client which connects to socket servers

class sockcli:
	############################################################
	def __init__(self, port, host = 'localhost', reconnect = 0):
		'''Creates an instance of a client.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- port -- Port server is running on.
			- host -- Host server is running on.
			- reconnect -- If 1, client will try re-connecting if the connection
				is broken.
		'''
		self.host = host
		self.port = port
		self.reconnect = reconnect
		self.sock = None
		self.open()
		self.data = ''
		self.setCallback()


	###############
	def open(self):
		'''Open socket if it's not already open.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		'''
		if self.sock == None:
			try:
				self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
				self.sock.connect(self.host, self.port)
			except Exception, e:
				self.sock = None


	####################
	def getSocket(self):
		'''Returns a list of sockects that should be selected on.

		RETURNS: A list of sockects that should be selected on.
		EXCEPTIONS: None generated internally.
		'''
		if self.sock == None and self.reconnect: self.open()
		if self.sock == None: return([ ])
		return([ self.sock ])


	#######################################
	def setCallback(self, callback = None):
		'''Sets function to be called when data arrives.

		RETURNS: None
		EXCEPTIONS: None generated internally.
		ARGUMENTS:
			- callback -- Specifies routine to call when data arrives on the
				socket.  If ommited, the callback is removed.
		'''
		self.callback = callback


	##################
	def process(self):
		'''Process data on the socket.
		Processes any data on socket, dispatching lines to the callback when
		appropriate.  Returns 1 while socket is still available.

		RETURNS: 1 if socket is still open, or 0 if connection terminated.
		EXCEPTIONS: None generated internally.
		'''
		if self.sock == None and self.reconnect: self.open()
		if self.sock == None:
			if self.reconnect: return(1)
			return(0)

		data = self.sock.recv(1024);
		if len(data) < 1:
			self.sock = None
			return self.reconnect

		self.data = self.data + data

		#  locate lines in input
		while 1:
			line, data = extractLine(data)
			if line == None: break
			line = string.strip(line)

			if len(line) > 0:
				if self.callback:
					self.callback(self.sock, line)

		return(1)


#####################################
#  test code
#####################################

if __name__ == '__main__':
	class server(sockserv):
		def clientClosed(self, client):
			print 'Client %d closed' % client.fileno()

		helpinfo_QUIT = ( '200-  QUIT', 'Terminate the connection.' )
		def do_QUIT(self, client, cmd, args):
			client.send('Bye now!', nl = 1)
			return(1)

		helpinfo_SHUTDOWN = ( '200-  SHUTDOWN', 'Terminate the server.' )
		def do_SHUTDOWN(self, client, cmd, args):
			client.send('Server terminating', nl = 1)
			self.shutdownServer = 1
			return(1)

		helpinfo_HELP = ( '200-  HELP', 'Display this message.' )
		def do_HELP(self, client, cmd, args):
			client.send('200-Help information', nl = 1)
			client.send('200-', nl = 1)
			sockserv.do_HELP(self, client, cmd, args)
			client.send('200-', nl = 1)
			client.send('200 End of help information.', nl = 1)

	s = server(( 5000, 5001, 5002, 5003))
	print 'Socket opened on %d' % s.portNum
	s.mainloop()

uptimed

#!/usr/bin/env python
#
#  sockserv (http://www.tummy.com/sockserv/) example.
#
#  Implemnts remote access to "uptime" data

import sockserv, os, string

class uptimed(sockserv.sockserv):
	helpinfo_QUIT = ( '200-  QUIT', 'Terminate the connection.' )
	def do_QUIT(self, client, cmd, args):
		client.send('Bye now!', nl = 1)
		return(1)

	helpinfo_UPTIME = ( '200-  UPTIME', 'Display hosts uptime information.' )
	def do_UPTIME(self, client, cmd, args):
		uptime = string.strip(os.popen('uptime', 'r').readlines()[0])
		client.send(str(uptime), nl = 1)

s = uptimed(5000)
s.mainloop()