A small ruby fastcgi stub which can evaluate ruby scripts in much the same way PHP does.

Based heavily off Derrick Pallas' work, at http://derrick.pallas.us/ruby-cgi/ with additional work to support logging and (optionally) displaying ruby exceptions in the browser, instead 500ing all over the place.

#!/usr/bin/ruby
###############

##########################
# FastCGI Ruby dispatcher
# (C) Derrick Pallas
#
# Authors: Derrick Pallas
# Website: http://derrick.pallas.us/ruby-cgi/
# License: Academic Free License 3.0
# Version: 2005-12-23a
#
# Remember to add 'puts cgi.header' at the beginning of your script!

require "fcgi"
require "mmap"
require "thread"

maxscripts = 128
maxscripts.freeze

ERROR_NOISY = true
ERROR_BACKTRACE = true
ERROR_LOGFILE = '/srv/www/log/ruby-cgi.log'

#def myLog(*args)
#  line = self.send(:sprintf, *args)
#  line = sprintf("%s: %s\n", Time.now.strftime("%Y-%m-%dT%H:%M:%S"), line)
#  $logfilemutex ||= Mutex.new
#  $logfilemutex.synchronize {
#    $logfile ||= File.open("/srv/www/log/ruby-cgi.log", "a")
#    $logfile.flock(File::LOCK_EX)
#    $logfile.write(line)
#    $logfile.flush
#    $logfile.flock(File::LOCK_UN)
#  }
#end

def htmlspecialchars(string)
  string = string.to_s unless string.is_a?(String)
  string.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
end

def error_log(*args)
  nao = Time.now.strftime('%Y-%m-%dT%H:%M:%S')
  begin
    line = self.send(:sprintf, *args)
  rescue Exception => err
    line = sprintf('Cannot format message: %s (%s)', err.message, args.inspect);
  ensure
    line = sprintf("%s %s(%u): %s\n", nao, File.basename(__FILE__), Process.pid, line);
  end
  begin
    $logfilemutex ||= Mutex.new
    $logfilemutex.synchronize {
      $logfile ||= File.open(ERROR_LOGFILE, 'a')
      $logfile.flock(File::LOCK_EX)
      $logfile.write(line)
      $logfile.flush
      $logfile.flock(File::LOCK_UN)
    }
  rescue Exception => err
    nil
  end
end

class Script
  attr_accessor :map
  attr_accessor :mod
  attr_accessor :use
end

scripts = {}
mytime  = File.stat(__FILE__).mtime

def getBinding(cgi,env)
  return binding
end

error_log("Starting")

FCGI.each_cgi do |cgi|
  error_log("CGI: %s", cgi.inspect)

  script = cgi.env_table['SCRIPT_FILENAME']
  script.freeze

  begin
    if ( not scripts.key?script or scripts[script].mod < File.stat(script).mtime )
      if scripts.key?script
        scripts[script].map.munmap
      else
        scripts[script] = Script.new
      end
      scripts[script].mod = File.stat(script).mtime
      scripts[script].map = Mmap.new script, "r"
    end
    scripts[script].use = Time.now

    Dir.chdir( File.dirname(script) )
    eval scripts[script].map, getBinding(cgi,cgi.env_table) if scripts[script].map

    if scripts.length > maxscripts
      begin
        killme = scripts.min { |a,b| a[1].use <=> b[1].use } [0]
        scripts[killme].map.munmap
        scripts.delete(killme)
      rescue Exception
      end
    end

  rescue Exception => bang
    error_log('Uncaught exception: %s', bang.inspect)
    error_log("Backtrace:\n%s\n----", bang.backtrace.join("\n"))
    if ERROR_NOISY
      puts cgi.header
      puts "<hr><b>Uncaught exception:</b>", "<em>", htmlspecialchars(bang), "</em>\n"
      puts "<pre>Backtrace:\n", htmlspecialchars(bang.backtrace.join("\n")), "</pre>\n" if ERROR_BACKTRACE
    end
  end if (script && File.stat(script).readable?)

  if (File.stat(__FILE__).mtime > mytime)
    Process.kill 'SIGHUP', Process.pid
    mytime = File.stat(__FILE__).mtime
  end
end

# END
######