#
# server.rb -- GenericServer Class
#
# Author: IPR -- Internet Programming with Ruby -- writers
# Copyright (C) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
#
# $IPR: server.rb,v 1.36 2002/02/14 10:01:01 gotoyuzo Exp $

require 'thread'
require 'socket'
require 'webrick/config'
require 'webrick/log'

module WEBrick

  class ServerError < StandardError; end

  class SimpleServer
    def SimpleServer.start
      yield
    end
  end

  class Daemon
    def Daemon.start
      fork do
        Process::setsid
        fork do
          Dir::chdir("/")
          File::umask(0)
          [ STDIN, STDOUT, STDERR ].each{|io|
            io.reopen("/dev/null", "r+")
          }
          yield
        end
      end
      exit!(0)
    end
  end

  class GenericServer
    attr_reader :status, :config, :logger

    def initialize(config={}, default=Config::General)
      @config = default.dup.update(config)
      @status = :Stop
      @config[:Logger] ||= Log::new
      @logger = @config[:Logger]
      @listeners = []
      listen if @config[:ListenImmediately]
    end

    def listen
      res = Socket::getaddrinfo(@config[:BindAddress], @config[:Port],
                                Socket::AF_UNSPEC,   # address family
                                Socket::SOCK_STREAM, # socket type
                                0,                   # protocol
                                Socket::AI_PASSIVE)  # flag
      last_error = nil
      res.each{|ai|
        begin
          @logger.debug("TCPServer.new(#{ai[3]}, #{@config[:Port]})")
          @listeners << TCPServer.new(ai[3], @config[:Port])
        rescue => ex
          @logger.warn("TCPServer Error: #{ex}")
          last_error  = ex
        end
      }
      raise last_error if @listeners.empty?
    end

    def start
      raise ServerError, "already started." if status != :Stop
      server_type = @config[:ServerType] || SimpleServer
      start_threads = @config[:StartThreads]

      server_type.start{
        thgroup = Array.new
        queue   = SizedQueue.new(start_threads)

        @logger.info "#{self.type}#start: pid=#{$$} port=#{@config[:Port]}"
        start_hook()

        start_threads.times{ |i|
          th = Thread.start(i){ |j|
            @logger.info "thread(#{j}) start."
            loop do
              sock = queue.pop
              case sock
              when IPSocket
                begin
                  addr = sock.peeraddr
                  @logger.debug "accept: #{addr[3]}:#{addr[1]}"
                  accept_hook(sock)
                  block_given? ? yield(sock) : run(sock)
                rescue Exception => ex
                  @logger.error ex
                ensure
                  @logger.debug "close: #{addr[3]}:#{addr[1]}"
                  sock.close
                end
              else
                @logger.info "thread(#{j}) exit."
                Thread.exit
              end
            end
          }
          thgroup.push(th)
        }

        @status = :Running
        while @status == :Running
          begin
            if socks = IO.select(@listeners, nil, nil, 2.0)
              socks[0].each{|s|
                ns = s.accept
                ns.sync = true
                queue.push(ns)
              }
            end
          rescue Errno::ECONNRESET, Errno::ECONNABORTED => ex
            msg = "#{ex.type}: #{ex.message}\n\t#{ex.backtrace[0]}"
            @logger.error msg
          rescue => ex
            @logger.error ex
            break
          end
        end

        @logger.info "going to shutdown ..."
        start_threads.times{ queue.push(nil) }
        thgroup.each{|th| th.join }
        stop_hook()
        @logger.info "#{self.type}#start done."
        @status = :Stop
      }
    end

    def stop
      if @status == :Running
        @status = :Shutdown
      end
    end

    def shutdown
      stop
      @listeners.each{|s| s.close }
      @listeners.clear
    end

    def run(sock)
      @logger.fatal "run() must be provided by user."
    end

    def accept_hook(sock); end
    def start_hook(); end
    def stop_hook(); end

  end

end
