#!/usr/bin/env ruby require 'stringio' require 'yaml' require 'digest/sha1' require 'socket' require 'cgi' require 'monitor' require 'singleton' module SCGI # :nodoc: all class LogFactory < Monitor include Singleton def initialize super() @@logs = {} end def create(file) result = nil synchronize do result = @@logs[file] if not result result = Log.new(file) @@logs[file] = result end end return result end end class Log < Monitor def initialize(file) super() @out = open(file, "a+") @out.sync = true @pid = Process.pid @info = "[INF][#{Process.pid}] " @error = "[ERR][#{Process.pid}] " end def info(msg) synchronize do @out.print @info, msg,"\n" end end def error(msg, exc=nil) if exc synchronize do @out.print @error, "#{msg}: #{exc}\n" @out.print @error, exc.backtrace.join("\n"), "\n" end else synchronize do @out.print @error, msg,"\n" end end end end # 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 def stdinput @input end def stdoutput @out end end class SCGIProcessor < Monitor # :nodoc: all attr_reader :settings def initialize(settings = {}) @conns = 0 @shutdown = false @dead = false super() configure(settings) end def configure(settings) @settings = settings @log = LogFactory.instance.create(settings[:logfile] || "log/scgi.log") @maxconns = settings[:maxconns] || 2**30-1 @started = Time.now if settings[:socket] @socket = settings[:socket] else @host = settings[:host] || "127.0.0.1" @port = settings[:port] || "9999" 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("Shutting down from SIGINT.") rescue Object @log.error("while listening for connections on #@host:#@port -- #{$!.class}", $!) end @socket.close if not @socket.closed? @dead = true @log.info("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.error("Malformed request, does not end with ','") else read_header(socket, payload, @conns) end rescue IOError @log.error("received IOError #$! when handling client. Your web server doesn't like me.") rescue Object @log.error("after accepting client #@host:#@port -- #{$!.class}", $!) 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) raise "You must implement process_request" 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("FORCED shutdown requested. Oh well.") else @log.info("Shutdown requested. Beginning graceful shutdown with #@conns connected.") end end end end # * Zed A Shaw # * George Moschovitis