#!/usr/bin/env ruby
#
# Samizdat Mongrel server
#
#   Copyright (c) 2002-2009  Dmitry Borodaenko <angdraug@debian.org>
#
#   This program is free software.
#   You can distribute/modify this program under the terms of
#   the GNU General Public License version 3 or later.
#
# vim: et sw=2 sts=2 ts=8 tw=0

require 'samizdat'
require 'mongrel'
require 'getoptlong'
require 'etc'
require 'syslog'

class SamizdatMongrelHandler < Mongrel::HttpHandler
  def process(request, response)
    cgi = Mongrel::CGIWrapper.new(request, response)
    Dispatcher.dispatch(cgi)
  end
end

class SamizdatMongrelServer
  PNAME = 'samizdatd'

  def usage
    puts %q{
Options:

  --help
      Display this message and quit.

  -H
  --host HOST
      Hostname or IP address to bind to. Default is localhost.

  -P
  --ports PORT1,PORT2
      Comma-separated list of ports to bind to, one process per port.
      For optimal performance, start at least one process per CPU core.
      Default is 3000.

  -u
  --user USER
      Run as USER if started as root. Default is nobody.

  -e
  --error-log ERROR_LOG_PATH
      File to write errors to. Default is /dev/null. When run as root,
      the file is chowned to USER:adm.

  --pidfile PID_FILE_PATH
      Path to the pidfile. By default, pidfile is created under
      /var/run/samizdat/ when run as root, or under $TMPDIR
      otherwise. Location should be writeable by USER.

}
    exit
  end

  def set_options
    opts = GetoptLong.new(
      [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
      [ '--host', '-H', GetoptLong::REQUIRED_ARGUMENT ],
      [ '--ports', '-P', GetoptLong::REQUIRED_ARGUMENT ],
      [ '--user', '-u', GetoptLong::REQUIRED_ARGUMENT ],
      [ '--error-log', '-e', GetoptLong::REQUIRED_ARGUMENT ],
      [ '--pidfile', '-p', GetoptLong::REQUIRED_ARGUMENT ]
    )

    @host = 'localhost'
    @ports = ['3000']
    @user = 'nobody'
    @error_log = '/dev/null'
    @pidfile = (0 == Process.uid) ?
      "/var/run/samizdat/#{PNAME}.pid" :
      (ENV.has_key?('TMPDIR') ? ENV['TMPDIR'] : '/tmp') + "/#{PNAME}.pid"

    opts.each do |opt, arg|
      case opt
      when '--help'
        usage
      when '--host'
        @host = arg.dup.untaint
      when '--ports'
        @ports = arg.dup.untaint.split(',')
      when '--user'
        @user = arg.dup.untaint
      when '--error-log'
        @error_log = arg.dup.untaint
      when '--pidfile'
        @pidfile = arg
      end
    end

    @pidfile = @pidfile.dup.untaint
    @user = Etc.getpwnam(@user)
  end

  def daemonize
    $0 = PNAME
    ENV.clear
    exit if fork
    Process.setsid
    exit if fork
    Dir.chdir '/'
    File.umask 0022

    STDIN.reopen '/dev/null'
    STDOUT.reopen '/dev/null', 'a'
    STDERR.reopen @error_log, 'a'

    if 0 == Process.uid
      if @error_log != '/dev/null'
        File.chown(@user.uid, Etc.getgrnam('adm').gid, @error_log)
        File.chmod(0640, @error_log)
      end

      # drop root priviledge
      Process::Sys.setgid(@user.gid)
      Process::Sys.setuid(@user.uid)
    end

    File.open(@pidfile, 'w') {|f| f.puts Process.pid }
  end

  def open_syslog
    @log = Syslog.open PNAME
  end

  def trap_signals
    shutdown = lambda do
      # stop the children
      @keep_running = false
      @children.each_key do |pid|
        Process.kill('TERM', pid)
      end
    end
    Signal.trap('INT', shutdown)
    Signal.trap('TERM', shutdown)
  end

  def run_server(port)
    @server = Mongrel::HttpServer.new(@host, port)
    @server.register("/", SamizdatMongrelHandler.new)

    shutdown = lambda do
      @server.stop(true)
      @log.info 'stopped'
    end
    Signal.trap('INT', shutdown)
    Signal.trap('TERM', shutdown)

    thread = @server.run
    @log.info 'started'
    thread.join
  end

  def start_child(port)
    if pid = fork
      @children[pid] = port
    else
      @children = {}
      run_server(port)
      exit
    end
  end

  def start_servers
    $SAFE = 1 if $SAFE < 1
    @children = {}
    @ports.each do |port|
      start_child(port)
    end
    @keep_running = true
    @log.info 'ready to serve requests'
  end

  def wait_for_children
    while not @children.empty?
      pid = Process.wait
      port = @children.delete(pid)
      start_child(port) if @keep_running
    end
    File.delete @pidfile
    @log.info 'shut down'
  end

  def run
    set_options
    daemonize
    open_syslog
    begin
      trap_signals
      start_servers
      wait_for_children
    rescue => error
      @log.err "#{error.class.to_s}: #{error.to_s}"
      error.backtrace.each {|line| @log.err '  ' + line }
      raise
    end
  end
end

SamizdatMongrelServer.new.run
