#!/usr/bin/ruby # # An object-oriented implementation of poll(2) for Ruby # # == Synopsis # # require 'poll' # require 'socket' # # pollobj = Poll::new # # sock = TCPServer::new('localhost', 1138) # pollobj.register( sock, Poll::RDNORM ) {|sock,evmask| # case evmask # when Poll::RDNORM # clsock = sock.accept # pollobj.mask( clsock, Poll::RDNORM, clientHandler ) # # when Poll::HUP|Poll::ERR|Poll::NVAL # pollobj.remove( io ) # $stderr.puts "Server error: Shutting down" # # else # $stderr.puts "Unhandled event: #{evmask}" # end # } # # pollobj.poll( 0.25 ) until poll.handles.empty? # # == Author # # Michael Granger # # Copyright (c) 2002 The FaerieMUD Consortium. All rights reserved. # # This module is free software. You may use, modify, and/or redistribute this # software under the same terms as Ruby itself. # # This library 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. # # == Version # # $Id: poll.rb,v 1.10 2002/10/21 03:47:01 deveiant Exp $ # require 'delegate' require 'poll.so' ### An object-oriented poll() implementation for Ruby class Poll ### A Fixnum derivative that does bitwise AND for ===. class EventMask < DelegateClass( Fixnum ) ### Create and return a new Poll::EventMask object with the specified ### bitmask (an Integer). def initialize( mask ) mask = mask.to_i @mask = mask super( mask ) end ### Returns true if the receiver bitwise ANDed with otherNum is ### non-zero. This is useful for using bitmasks in case blocks. def ===( otherNum ) ( self & otherNum ).nonzero? end ### Returns a new EventMask after ORing the receiver with the specified ### value. def |( otherNum ) otherNum = otherNum.to_i return EventMask::new( @mask | otherNum ) end ### Returns a new EventMask after ANDing the receiver with the specified ### value. def &( otherNum ) otherNum = otherNum.to_i return EventMask::new( @mask & otherNum ) end ### Returns a new EventMask after XORing the receiver with the specified ### value. def ^( otherNum ) otherNum = otherNum.to_i return EventMask::new( @mask ^ otherNum ) end end # class Poll::EventMask ### Class constants Version = /([\d\.]+)/.match( %q$Revision: 1.10 $ )[1] Rcsid = %q$Id: poll.rb,v 1.10 2002/10/21 03:47:01 deveiant Exp $ ### Create and return new poll object. def initialize @masks = {} @events = Hash::new( 0 ) @callbacks = {} end ###### public ###### ### Register the specified IO object with the specified ### eventMask. If the optional callback parameter (a ### Method or Proc object) or a block is given, it will be called ### with io and the mask of the event/s whenever #poll generates ### any events for io. If the callback parameter non-nil, ### any block specified is discarded. Any arguments ### specified are passed to the callback as the third and succeeding ### arguments. The following event masks can be set in the ### eventMask: ### [Poll::IN] ### Data other than high-priority data may be read without blocking. ### [Poll::PRI] ### High-priority data may be received without blocking. ### [Poll::OUT] ### Normal data (priority band equals 0) may be written without blocking. ### ### The following masks are ignored in the eventMask, as they are ### always implicitly set, but they may be specified in the handler ### callback or block to trap the conditions they ### represent: ### [Poll::ERR] ### An error has occurred on the device. ### [Poll::HUP] ### The device has been disconnected. This event and Poll::OUT are ### mutually exclusive; a device can never be writable once a hangup has ### occurred. However, this event and Poll::IN, Poll::RDNORM, ### Poll::RDBAND, or Poll::PRI are not mutually exclusive. ### [Poll::NVAL] ### The io object specified is invalid -- it has been closed, has ### a bad file descriptor, etc. ### ### If your operating system defines them, these masks are also available: ### [Poll::RDNORM] ### Normal data (priority band equals 0) may be read without blocking. ### [Poll::RDBAND] ### Data from a non-zero priority band may be read without blocking. ### [Poll::WRNORM] ### Same as Poll::OUT. ### [Poll::WRBAND] ### Priority data (priority band greater than 0) may be written. def register( io, eventMask, callback=nil, *arguments, &block ) raise TypeError, "#{io.class.name} does not appear to be file-descriptor-based" unless io.respond_to?( :fileno ) && io.fileno # Clear any old events for this handle @events.delete( io ) # Set the mask @masks[ io ] = 0 setMask( io, eventMask ) # Set the callback setCallback( io, (callback||block), *arguments ) end alias :add :register ### Remove the specified io from the receiver's list of registered ### handles, if present. Returns the handle if it was registered, or ### nil if it was not. def unregister( io ) @events.delete( io ) @callbacks.delete( io ) @masks.delete( io ) end alias :remove :unregister ### Returns true if the specified io is registered with the poll ### object. def registered?( io ) return @masks.has_key?( io ) end ### Clear all registered handles from the poll object. Returns the handles ### that were cleared. def clear rv = @masks.keys @events.clear @callbacks.clear @masks.clear return rv end ### Get the EventMask for the specified io. def mask( io ) raise ArgumentError, "Handle #{io.inspect} is not registered" unless @masks.has_key?( io ) return @masks[ io ] end ### Set the EventMask for the specified io to the given ### eventMask. def setMask( io, eventMask ) raise ArgumentError, "Handle #{io.inspect} is not registered" unless @masks.has_key?( io ) return @masks[ io ] = EventMask::new( eventMask.to_i ) end ### Add (bitwise OR) the specified eventMask to the mask for the ### specified io. Returns the new mask. def addMask( io, eventMask ) raise ArgumentError, "Handle #{io.inspect} is not registered" unless @masks.has_key?( io ) @masks[ io ] |= eventMask.to_i end ### Remove (bitwise XOR) the specified eventMask from the mask for ### the specified io. Returns the new mask. def removeMask( io, eventMask ) raise ArgumentError, "Handle #{io.inspect} is not registered" unless @masks.has_key?( io ) @masks[ io ] ^= eventMask.to_i end ### Returns true if the specified io has a callback ### associated with it. def hasCallback?( io ) @callbacks.has_key?( io ) end alias :has_callback? :hasCallback? ### Returns the per-handle callback associated with the specified ### io. If no callback exists for the given io, ### nil is returned. def callback( io ) return nil unless @callbacks.has_key? io return @callbacks[io][:callback] end ### Returns the per-handle callback arguments associated with the specified ### io as an Array. If no callback exists for the given ### io, nil is returned. def args( io ) return nil unless @callbacks.has_key? io return @callbacks[io][:args] end ### Reset the per-handle callback associated with the specified io ### to the specified callback (a Proc or Method object) or ### block, if given, or to nil if not specified. Any arguments ### specified past the second will be passed to the callback as its ### arguments. Returns the old callback. def setCallback( io, callback=nil, *args, &block ) raise ArgumentError, "Handle #{io.inspect} is not registered" unless @masks.has_key?( io ) rv = nil if @callbacks.has_key?( io ) rv = @callbacks[ io ][:callback] end if callback || block @callbacks[ io ] = { :callback => (callback || block), :args => args } else @callbacks.delete( io ) end return rv end ### Call the system-level poll function with the handles registered to the ### receiver. Any callbacks specified when the handles were registered are ### run for those handles with events. If a block is given, it will be ### invoked once for each handle which doesn't have an explicit handler. The ### timeout argument is the number of floating-point seconds to ### wait for an event before returning; negative timeout values will cause ### #poll to block until there is at least one event to report. This method ### returns the number of handles on which one or more events occurred. def poll( timeout=-1 ) # :yields: io, eventMask raise TypeError, "Timeout must be Numeric, not a #{timeout.type.name}" unless timeout.kind_of? Numeric timeout = timeout.to_f @events.clear unless @masks.empty? @events = _poll( @masks.to_a, timeout*1000 ) # For each io that had an event happen, call any callback associated # with it, or failing that, any provided block @events.each {|io,evmask| if @callbacks.has_key?( io ) args = @callbacks[ io ][ :args ] @callbacks[ io ][ :callback ].call( io, EventMask::new(evmask), *args ) elsif block_given? yield( io, EventMask::new(evmask) ) end } end @events.default = EventMask::new( 0 ) return @events.length end ### Fetch an Array of handles which had the events specified by ### eventMask happen to them in the last call to #poll. If ### eventMask is nil, an Array of all handles ### with pending events is returned. def events( eventMask=nil ) if eventMask eventMask = eventMask.to_i @events.find_all {|io,evmask| (evmask & eventMask).nonzero? }.collect {|io,evmask| io} else @events.keys end end ### Fetch an Array of handles that are masked to receive the specified ### eventMask. If eventMask is nil, an Array of all ### registered handles is returned. def handles( eventMask=nil ) if eventMask eventMask = eventMask.to_i @masks.find_all {|io,evmask| (evmask & eventMask).nonzero? }.collect {|io,evmask| io} else @masks.keys end end ### Return a human-readable string describing the poll object. def inspect "" % [@masks.inspect, @events.length] end end # class Poll