
"""Select Manager

This module contains basic framework for object-based encapsulation of
the select() system call.  Classes you might want to extend:

AbstractManager - to make your own channel handling logic
SelectChannel - all channels passed to SelectManager should be derived
from this
BufferedChannel - a subclass of SelectChannel, implements basic
(line-oriented) buffering
ChannelHandler - all handlers of channels should extend this class"""

from log import log
import string
import select
import exceptions
import EventQueue

class UnknownStateException( exceptions.Exception ):

    """ UnknownStateException - exception for unexpected states

    This exception is raised in situations that should not happen with
    channels. They are maybe better left uncaught, and rather debug
    the code when an UnknownStateException is raised. """
    
    pass

class AbstractManager:

    def add_channel( s, chan ):

        """ To add channels to manager

        Methods a channel should define are shown in SelectChannel, which
        can be subclassed to form different channels. """

        pass

    def del_channel( s, chan ):

        """ To remove channels from manager """
        
        pass

    def register_write( s, chan ):

	""" To announce that a channel wants to write() """

	pass

    def clear( s ):
        pass

    def loop( s ):

        """ Handle select() events forever """

        while 1:
            s.once()

    def once( s ):

        """ Handle one round of select()

        Calling this function is useful, for example, if you need to
        write your own mainloop to track some conditions amidst the
        calls to select(). """
        
        rl, wl, el = s.select( s.max_timeout )
        s.dispatch( rl, wl, el )    

class SelectManager( AbstractManager ):

    """ SelectManager - high level interface to select.select()

    This is the basic class for bookkeeping needed by select() calls.
    It has approximately the same functionnality as asyncore.loop(),
    but adds the following:

    using instance variables - this way, every thread can have its own
    SelectManager;
    event queues - if you want time-triggered events but still want to
    use select(), they do the bookkeeping for you;
    once() - for doing at most one select();
    and the possibility to subclass."""

    def __init__( s, evq = EventQueue.EventQueue(), max_timeout = None ):

        """ Initial parameters for SelectManager

	If you need timed events, you should pass in your own event
	queue and use its methods to schedule events.  max_timeout is
	the maximum time to sleep in select(); this defaults to None
	(sleep endlessly / until next event). """
        
        s.evq = evq
        s.max_timeout = max_timeout
        s.chans = []

    def add_channel( s, chan ):
        s.chans.append( chan )
        log( 5, "Added channel %s" % chan )

    def del_channel( s, chan ):
        if chan in s.chans:
            s.chans.remove( chan )
            log( 5, "Removed channel %s" % chan )
        else: log( 6, "non-existent channel" )

    def clear( s ):
        s.evq.clear()
        s.chans = []

    def select( s, mto ):
        rl = filter( lambda x: x.can_handle_read(), s.chans )
        wl = filter( lambda x: x.can_handle_write(), s.chans )
        to = s.evq.next_event_delay()
        if to == None: to = mto
        elif mto != None: to = min( to, mto )

        log( 7, "select() with timeout: %s" % to )
        return select.select( rl, wl, s.chans, to )

    def dispatch( s, rl, wl, el ):
	log( 7, "readers=%d writers=%d expt=%d" %
			( len( rl ), len( wl ), len( el )))
        map( lambda x, srv = s: x.handle_expt_event( srv ), el )
        map( lambda x, srv = s: x.handle_write_event( srv ), wl )
        map( lambda x, srv = s: x.handle_read_event( srv ), rl )
        s.evq.process()

    def loop( s ):
        while s.chans or not s.evq.empty():
            s.once()

class ProactiveSelectManager( AbstractManager ):

    """In this select manager, channels proactively register as writers
    instead of being asked whether they want to write.  This supposedly
    leads to performance enhancements."""

    def __init__( s, evq = EventQueue.EventQueue(), max_timeout = None ):
        s.evq = evq
        s.max_timeout = max_timeout
	s.rchans = []
	s.wchans = []

    def add_channel( s, chan ):
	s.rchans.append( chan )
	s.wchans.append( chan )
        log( 5, "Added channel %s" % chan )

    def del_channel( s, chan ):
	if chan in s.rchans: s.rchans.remove( chan )
	if chan in s.wchans: s.wchans.remove( chan )
        log( 5, "Removed channel %s" % chan )

    def register_write( s, chan ):
	if not chan in s.wchans: s.wchans.append( chan )

    def clear( s ):
	s.evq.clear()
	s.rchans = []
	s.wchans = []

    def select( s, mto ):
        to = s.evq.next_event_delay()
        if to == None: to = mto
        elif mto != None: to = min( to, mto )
        log( 7, "select() with timeout: %s" % to )
        result = select.select( s.rchans, s.wchans, [], to )
	s.wchans = []
	return result

    def dispatch( s, rl, wl, el ):
	log( 7, "readers=%d writers=%d expt=%d" %
			( len( rl ), len( wl ), len( el )))
        map( lambda x, srv = s: x.handle_write_event( srv ), wl )
        map( lambda x, srv = s: x.handle_read_event( srv ), rl )
        s.evq.process()

    def loop( s ):
	while s.rchans or s.wchans or not s.evq.empty():
	    s.once()

class SelectChannel:

    def __str__( s ):
        return "<channel for fd %d>" % s.fileno()

    def fileno( s ):

        """ Should return the file descriptor to select(). """

        raise UnknownStateException( "fileno asked for an abstract channel" )

    def can_handle_read( s ):

        """ Should return true if the channel can receive read events. """
        
        return 0

    def can_handle_write( s ):

        """ Should return true if the channel wants to make writes. """
        
        return 0

    def handle_read_event( s, serv ):

        """ Called when select tells that there is data available for read. """
        
        pass

    def handle_write_event( s, serv ):

        """ Called when select tells that write() can be issued. """
        
        pass

    def handle_expt_event( s, serv ):

        """ Called when select reports exceptional events. (OOB etc) """
        
        pass

class BufferedChannel( SelectChannel ):

    """ BufferedChannel - ABC for buffered connections

    BufferedChannel implements basic buffering operations. These are needed,
    for example, in protocols that exchange messages of varying length. """

    terminator = '\r\n'
    max_buffersize = 16000

    def __init__( s, handler ):

        """ Initial parameters for the BufferedChannel

        handler should be an object defining the methods shown in
        ChannelHandler. """
        
        s.handler = handler
        s.rbuf = ''
        s.wbuf = ''

    def reg_handler( s, handler ):

        """ If you want to change the handler object afterwards. """

        log( 5, "changed handler for %s" % s )
        s.handler = handler
        
    def can_handle_read( s ):
        return 1

    def can_handle_write( s ):
        return len( s.wbuf ) > 0

    def write( s, data ):
        s.wbuf = s.wbuf + data + s.terminator

    def handle_buffer_over( s ):
        log( 4, "Over %d bytes w/o terminator, discarding" % s.max_buffersize )
        s.rbuf = ''

    def handle_read_event( s, server ):
        log( 9, "read event on %s" % s )
        data = s.make_receive()
        if not data:	# Close
            server.del_channel( s )
            s.handler.cleanup()
            s.close()
	else:
            s.rbuf = s.rbuf + data
            s.collect()
	if s.wbuf: server.register_write( s )

    def handle_write_event( s, server ):
        log( 9, "write event on %s" % s )
        s.make_send()
	if s.wbuf: server.register_write( s )

    def collect( s ):
	if not s.terminator:
	    s.handler.receive( s.rbuf )
	    s.rbuf = ''
	    return
        while 1:
            term_idx = string.find( s.rbuf, s.terminator )
	    if term_idx < 0: break
            s.handler.receive( s.rbuf[:term_idx] )
            s.rbuf = s.rbuf[term_idx + len( s.terminator ):]
        if len( s.rbuf ) == s.max_buffersize: s.handle_buffer_over()

    def soft_close( s ):
        while ( s.wbuf ):
            s.make_send()
        s.close()

class ChannelHandler:

    """ An abstract example class of a channel handler.

    Note that neither the data passed to receive() contains any record
    termitors nor should the data passed to conn.write() contain them. """

    def receive( s, data ):

        """ A callback which is called upon receiving a complete record. """

        pass

    def cleanup( s ):

        """ A callback to be called upon peer closing connection. """

        pass

    def reg_conn( s, conn ):

        """ Connection registering.

        This method is called to give the handler an associated connection;
        When the handler wants to send data, it should call conn.write(). """

	s.connection = conn

    def write( s, data ):

	""" To send data to the connection. """

	s.connection.write( data )

