#!/usr/bin/env ruby require 'stringio' require 'yaml' require 'digest/sha1' require 'socket' require 'cgi' require 'monitor' require 'singleton' module SCGI # :nodoc: all # Modifies CGI so that we can use it. class SCGIFixed < ::CGI # :nodoc: all public :env_table def initialize(params, data, out, *args) @env_table = params @args = *args @input = StringIO.new(data) @out = out super(*args) end def args @args end def env_table @env_table end alias_method :env, :env_table def stdinput @input end def stdoutput @out end end class SCGIProcessor < Monitor # :nodoc: all attr_reader :settings def initialize(server, settings = {}) @conns = 0 @shutdown = false @dead = false @server = server super() configure(settings) end def configure(settings) @settings = settings #@log = LogFactory.instance.create(settings[:logfile]) @log = Logger @log = Logger.new(settings[:logfile]) if settings[:logfile] @maxconns = settings[:maxconns] @started = Time.now if settings[:socket] @socket = settings[:socket] else @host = settings[:host] @port = settings[:port] end @throttle_sleep = 1.0/settings[:conns_second] if settings[:conns_second] end def listen @socket = TCPServer.new(@host, @port) begin while true handle_client(@socket.accept) sleep @throttle_sleep if @throttle_sleep break if @shutdown and @conns <= 0 end rescue Interrupt @log.info("SCGI: Shutting down from SIGINT.") rescue Object @log.warn("SCGI: while listening for connections on #@host:#@port -- #{$!.class} #$! #{$!.backtrace.join("\n")}" ) end @socket.close if not @socket.closed? @dead = true @log.info("SCGI: Exited accept loop. Shutdown complete.") end def handle_client(socket) Thread.new do begin # remember if we were doing a shutdown so we can avoid increment later in_shutdown = @shutdown synchronize { @conns += 1 if not in_shutdown } len = "" # we only read 10 bytes of the length. any request longer than this is invalid while len.length <= 10 c = socket.read(1) if c == ':' # found the terminal, len now has a length in it so read the payload break else len << c end end # we should now either have a payload length to get payload = socket.read(len.to_i) if (c = socket.read(1)) != ',' @log.warn("SCGI: Malformed request, does not end with ','") else read_header(socket, payload, @conns) end rescue IOError @log.warn("SCGI: received IOError #$! when handling client. Your web server doesn't like me.") rescue Object @log.warn("SCGI: after accepting client #@host:#@port -- #{$!.class} #$! #{$!.backtrace.join("\n")}") ensure synchronize { @conns -= 1 if not in_shutdown} socket.close if not socket.closed? end end end def read_header(socket, payload, conns) return if socket.closed? request = split_body(payload) if request["CONTENT_LENGTH"] length = request["CONTENT_LENGTH"].to_i if length > 0 body = socket.read(length) else body = "" end if @shutdown or @conns > @maxconns socket.write("Location: /busy.html\r\n") socket.write("Cache-control: no-cache, must-revalidate\r\n") socket.write("Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n") socket.write("Status: 307 Temporary Redirect\r\n\r\n") else process_request(request, body, socket) end end end def process_request(request, body, socket) return if socket.closed? cgi = SCGIFixed.new(request, body, socket) begin #-- # TODO: remove sync, Nitro *is* thread safe! #++ # guill: and why not ? ;) #synchronize do #-- # FIXME: this is uggly, something better? #++ cgi.stdinput.rewind cgi.env["QUERY_STRING"] = (cgi.env["REQUEST_URI"] =~ /^[^?]+\?(.+)$/ and $1).to_s Nitro::Cgi.process(@server, cgi, cgi.stdinput, cgi.stdoutput) #end ensure Og.manager.put_store if defined?(Og) and Og.respond_to?(:manager) and Og.manager end end def split_body(data) result = {} el = data.split("\0") i = 0 len = el.length while i < len result[el[i]] = el[i+1] i += 2 end return result end def status { :time => Time.now, :pid => Process.pid, :settings => @settings, :env => @settings[:env], :started => @started, :max_conns => @maxconns, :conns => @conns, :systimes => Process.times, :conns_second => @throttle, :shutdown => @shutdown, :dead => @dead } end # Graceful shutdown is done by setting the maxconns to 0 so that all new requests # get the 503 Service Unavailable status. The handler code checks if @shutdown is # set and if so it will not increase the @conns count, but it will decrease. # Once the @conns count is down to 0 it will exit the loop. def shutdown(force = false) @shutdown = true; if force @socket.close @log.info("SCGI: FORCED shutdown requested. Oh well.") else @log.info("SCGI: Shutdown requested. Beginning graceful shutdown with #@conns connected.") end end end class << self def start(server) settings = { :host => server.address, :port => server.port, :logfile => nil, # will use Logger :maxconns => 2**30-1, :socket => nil, :conns_second => nil, :env => nil, :drb_enable => false, :drb_port => server.port - 1000, :drb_password => "" } settings.update(server.options) @nitro = SCGIProcessor.new(server, settings) Logger.info("SCGI: Running on #{settings[:host]}:#{settings[:port]}") @nitro.listen end end end # * Zed A Shaw # * George Moschovitis