#!/usr/bin/ruby
# = chatserver.rb
# 
# This is an extremely crude and simple single-threaded multiplexing chat
# server. It (hopefully) demonstrates how to use a Poll object to do IO
# multiplexing with events.
#
# == Synopsis
#
#   $ chatserver.rb [HOST [PORT [POLLDELAY]]]
#
# [HOST]
#   The host or IP the server will bind to
#
# [PORT]
#   The port the server will listen on
#
# [POLLDELAY]
#   The number of floating-point seconds between polls. Specifying -1 (or any
#   negative number, really) here will make the server call poll() in blocking
#   mode.
#
# == Author
# 
# Michael Granger <ged@FaerieMUD.org>
# 
# Copyright (c) 2002 The FaerieMUD Consortium. All rights reserved.
# 
# This program is free software. You may use, modify, and/or redistribute this
# software under the same terms as Ruby itself.
# 
# 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.
#
# == Version
#
#  $Id: chatserver.rb,v 1.2 2002/07/20 16:03:01 deveiant Exp $
# 

require 'poll'
require 'socket'


### Chatserver user class -- part of the chatserver example.
class User

	MTU	 = 4096
	CR   = "\015"
	LF   = "\012"
	EOL  = CR + LF
	
	PROMPT = 'chat> '

	### Create and return a user object which will use the specified
	### <tt>socket</tt> and <tt>pollObj</tt>.
	def initialize( socket, server )
		@socket = socket
		@server = server
		@obuffer = ''
		@ibuffer = ''
		@peerHost = @socket.peeraddr[2]
		@peerPort = @socket.peeraddr[1]
		@connected = true
	end

	# Object attribute
	attr_reader :socket, :server, :ibuffer, :obuffer


	### Return a stringified version of the user
	def to_s
		"%s:%d" % [ @peerHost, @peerPort ]
	end


	### Add the specified string to the user's output buffer and turn on
	### output events.
	def addOutput( string )
		@obuffer << string.chomp << EOL
		@server.pollObj.addMask( @socket, Poll::WRNORM )
	end
	alias :<< :addOutput


	### Write as much of the output buffer to the socket as possible, and return
	### the number of bytes remaining to be sent.
	def writeOutput
		bytes = @socket.syswrite( @obuffer )
		@obuffer[ 0, bytes ] = '' if bytes.nonzero?
		return @obuffer.length
	end


	### Write a prompt to the user
	def prompt
		@obuffer << PROMPT
		@server.pollObj.addMask( @socket, Poll::WRNORM )
	end


	### Read at most MTU bytes from the socket and append them to the input
	### buffer. Split off any complete lines (one that end with EOL) and return
	### them as an Array of Strings.
	def readInput
		rary = []
		@ibuffer << @socket.sysread( MTU )
		$stderr.puts "Input buffer for user #{self} now: #@ibuffer" if $VERBOSE
		while (( pos = @ibuffer.index EOL ))
			$stderr.puts "Found terminating EOL. Splitting off 0..#{pos} of the input buffer." if $VERBOSE
			rary << @ibuffer[ 0, pos ]
			@ibuffer[ 0, pos + EOL.length ] = ''
		end

		return rary
	rescue EOFError
		@server.disconnectUser( self )
		return []
	end


	### Handle poll events on the socket
	def handlePollEvent( io, evmask )
		case evmask

		when Poll::ERR|Poll::HUP|Poll::NVAL
			@server.disconnectUser( self )
			
		when Poll::RDNORM
			input = readInput()
			@server.processInput( self, *input ) unless input.empty?

		when Poll::WRNORM
			bytesLeft = writeOutput()
			@server.pollObj.removeMask( @socket, Poll::WRNORM ) if bytesLeft.zero?

		end

	end


	### Disconnect the user
	def disconnect( msg='' )
		@connected = false
		unless msg.empty?
			@obuffer = ">>> Disconnected: #{msg} <<<" + EOL
		else
			@obuffer = ">>> Disconnected <<<" + EOL
		end
		writeOutput()
		@socket.close
	end


	### Returns true if the user is still connected
	def connected?
		@connected
	end
end


### Example chatserver class -- an extremely crude and simple chat server that
### demonstrates how to use Poll to do multiplexing IO in a single thread.
class Server

	BANNER = <<-EOF
	[[ Ruby-Poll Example Chatserver ]]
	Commands: '/quit' to quit, '/shutdown' to shut the server down
	EOF

	### Instantiate and return a chatserver on the specified host and port
	def initialize( listenHost="0.0.0.0", listenPort=1138, interval=0.20 )
		raise "This server requires the POLLRDNORM and POLLWRNORM constants, which " +
			"don't seem to be defined by your machine's implementation. Sorry. " unless
			Poll.const_defined?( :RDNORM ) && Poll.const_defined?( :WRNORM )

		@socket			= TCPServer::new( listenHost, listenPort )
		@users			= []
		@pollObj		= Poll::new
		@pollInterval	= interval
		@shuttingDown	= false

		@pollObj.register @socket, Poll::RDNORM, method(:handlePollEvent)
	end

	# Server attributes
	attr_reader :pollObj, :users, :socket


	### Main server loop
	def pollLoop

		trap( "INT" ) { shutdown("Server caught SIGINT") }
		trap( "TERM" ) { shutdown("Server caught SIGTERM") }
		trap( "HUP" ) { disconnectAllUsers(">>> Server reset <<<") }

		until @shuttingDown
			eventCount = @pollObj.poll( @pollInterval )
			$stderr.puts "#{eventCount} poll events..." if eventCount.nonzero?
		end

	rescue StandardError => e
		shutdown( "Server error: #{e.message}" )
	rescue SignalException => e
		shutdown( "Server caught #{e.type.name}" )
	ensure
		trap( "INT", "SIG_IGN" )
		trap( "TERM", "SIG_IGN" )
		trap( "HUP", "SIG_IGN" )

		$stderr.puts "Server exiting poll loop."
	end


	### Handle a poll event specified by <tt>evmask</tt> on the specified
	### <tt>socket</tt>
	def handlePollEvent( socket, evmask )
		case evmask

		when Poll::ERR|Poll::HUP|Poll::NVAL
			shutdown()

		when Poll::RDNORM
			clSock = socket.accept
			user = User::new( clSock, self )
			$stderr.puts "Accepted connection from #{user}"
			@pollObj.register clSock, Poll::RDNORM, user.method(:handlePollEvent)
			user.addOutput( BANNER )
			user.prompt
			broadcastMsg( "[New connection: #{user}]" )
			@users << user

		end
	end


	### Process the specified input from the specified user
	def processInput( user, *inputStrings )
		inputStrings.each {|str|
			case str

			when %r{^/(\w+)\s*(.*)}
				handleCommand( user, $1, $2 )

			else
				user.addOutput( "You>> #{str}" )
				broadcastMsgFrom( user, str )
			end
		}

		user.prompt if user.connected?
	end


	### Handle the specified command from the specified user
	def handleCommand( user, command, args )
		case command

		when /quit/
			disconnectUser( user, 'Quit' )
			
		when /shutdown/
			shutdown()

		when /who/
			user.addOutput( self.wholist(user) )

		else
			user.addOutput("Unknown command '#{command}'")
		end
	end


	### Broadcast the specified message to all connected users
	def broadcastMsg( msg )
		@users.each {|cl|
			cl.addOutput( msg )
		}
	end


	### Broadcast the specified message from the specified user
	def broadcastMsgFrom( user, msg )
		userDesc = user.to_s

		@users.each {|cl|
			next if cl == user
			cl.addOutput( "#{userDesc}>> #{msg}" )
		}
	end


	### Disconnect the specified user
	def disconnectUser( user, msg='' )
		@users -= [ user ]
		@pollObj.unregister user.socket
		user.disconnect( msg )
		broadcastMsg( "#{user.to_s} Disconnected." )
	end


	### Disconnect all connected users
	def disconnectAllUsers( msg )
		@users.each {|cl| cl.disconnect(msg) }
		@users.clear
	end


	### Shut the server down
	def shutdown( msg="Server shutdown" )
		@shuttingDown = true
		@pollObj.clear
		begin
			@socket.shutdown
		rescue
		end
		disconnectAllUsers( msg )
		begin
			@socket.close
		rescue
		end
	end

	### Build and return a list of connected users for the specified user.
	def wholist( user )
		rval = "[Connected Users]\n" <<
			"  *#{user}*\n"
		@users.each {|u|
			next if u == user
			rval << "  #{u}\n"
		}

		return rval
	end

end

srv = Server::new( *ARGV )
$stderr.puts "Chat server listening on #{srv.socket.addr[2]} port #{srv.socket.addr[1]}"
srv.pollLoop
$stderr.puts "Chat server finished."



syntax highlighted by Code2HTML, v. 0.9.1