#! /usr/bin/ruby # # Copyright (C) 2002 Yoshinori K. Okuji # # This file is part of BugCommunicator. # # BugCommunicator is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # BugCommunicator is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Foobar; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Load modules. require 'getoptlong' require 'mysql' require 'drb/drb' require 'acl' require 'cgi' # Define the method `shadow=' specific to $stdin, to hide a password. begin require 'termios' def $stdin.shadow=(bool) tios = Termios.getattr($stdin) if bool tios.c_lflag &= ~Termios::ECHO else tios.c_lflag |= Termios::ECHO end Termios.setattr($stdin, TCSANOW, tios) end rescue LoadError def $stdin.shadow=(bool) system('stty', if bool then '-echo' else 'echo' end) end end # Define some classes under the module `BugCommunicator'. module BugCommunicator class BugCommError < Exception; end # Configuration Manager. class Config def initialize(file) m = Module.new m.module_eval(IO.readlines(file, nil).first, file) # Set the defaults. @host = 'localhost' @port = 8844 @acl = nil @size_limit = nil @address = nil @smtp_host = 'localhost' @site_admins = nil @url = nil @db_host = 'localhost' @db_name = 'bugcomm' @db_user = 'bugcomm' @db_password = nil if defined?(m::HOST) unless m::HOST.kind_of?(String) raise TypeError, 'HOST must be a string' end @host = m::HOST end if defined?(m::PORT) unless m::PORT.kind_of?(Integer) raise TypeError, 'PORT must be a string' end @port = m::PORT end if defined?(m::ACL) unless m::ACL.kind_of?(Array) raise TypeError, 'ACL must be an array' end @acl = m::ACL end if defined?(m::SIZE_LIMIT) unless m::SIZE_LIMIT.kind_of?(Integer) raise TypeError, 'SIZE_LIMIT must be an integer' end @size_limit = m::SIZE_LIMIT end if defined?(m::ADDRESS) unless m::ADDRESS.kind_of?(String) raise TypeError, 'ADDRESS must be an string' end @address = m::ADDRESS else raise RuntimeError, 'ADDRESS must be defined' end if defined?(m::SMTP_HOST) unless m::SMTP_HOST.kind_of?(String) raise TypeError, 'SMTP_HOST must be an string' end @smtp_host = m::SMTP_HOST end if defined?(m::SITE_ADMINS) unless m::SITE_ADMINS.kind_of?(Array) raise TypeError, 'SITE_ADMINS must be an array' end @site_admins = m::SITE_ADMINS else raise RuntimeError, 'SITE_ADMINS must be defined' end if defined?(m::URL) unless m::URL.kind_of?(String) raise TypeError, 'URL must be a string' end @url = m::URL else raise RuntimeError, 'URL must be defined' end if defined?(m::DB_HOST) unless m::DB_HOST.kind_of?(String) raise TypeError, 'DB_HOST must be an string' end @db_host = m::DB_HOST end if defined?(m::DB_NAME) unless m::DB_NAME.kind_of?(String) raise TypeError, 'DB_NAME must be an string' end @db_name = m::DB_NAME end if defined?(m::DB_USER) unless m::DB_USER.kind_of?(String) raise TypeError, 'DB_USER must be an string' end @db_user = m::DB_USER end if defined?(m::DB_PASSWORD) unless m::DB_PASSWORD.kind_of?(String) raise TypeError, 'DB_PASSWORD must be an string' end @db_password = m::DB_PASSWORD end end attr_reader :host, :port, :acl, :size_limit attr_reader :address, :smtp_host, :site_admins attr_reader :url attr_reader :db_host, :db_name, :db_user attr_accessor :db_password end # End of class Config. # Command Processor. class Command def initialize(config) @config = config end # Helper method to disable signals temporarily. def critical_region t = Thread.current begin t[:critical] = true yield ensure t[:critical] = false end end private :critical_region def h(str) CGI.escapeHTML(str) end private :h def u(str) CGI.escape(str) end private :u def error_log(exception) error("#{exception.message} (#{exception.class})\n" + exception.backtrace.join("\n")) end private :error_log def mail(message) begin rescue BugCommError, NoMemoryError, ScriptError, StandardError => e error_log(e) return "#{e.message} (#{e.class})\n" + e.backtrace.join("\n") end nil end def cgi(metavars, request) begin rescue BugCommError => e error_log(e) return "Content-Type: text/html\r\n\r\n" + "Error\r\n" + "

Error

#{h(e.message)}

\r\n" + "" rescue NoMemoryError, ScriptError, StandardError => e error_log(e) return "Content-Type: text/html\r\n\r\n" + "Server internal error\r\n" + "

Server internal error

\r\n" + "
#{h(e.message)} (#{h(e.class)})\r\n" +
	  h(e.backtrace.join("\r\n")) + "

\r\n" + "Please send bug reports to " + @config.site_admins.collect {|admin| h(admin)}.join(", ") + ".

" end end end # End of class Command. end # Constants. BUGCOMM_VERSION = '@VERSION@' # Variables. $verbosity = 0 $log = nil config_file = '@BUGCOMMD_CONFIG_FILE@' log_file = nil pid_file = nil passwd = nil ask_passwd = false daemonized = false ## Start the main routine. # Parse arguments. parser = GetoptLong.new parser.set_options(['--help', '-h', GetoptLong::NO_ARGUMENT], ['--version', '-V', GetoptLong::NO_ARGUMENT], ['--config', '-c', GetoptLong::REQUIRED_ARGUMENT], ['--log', '-l', GetoptLong::REQUIRED_ARGUMENT], ['--daemon', '-d', GetoptLong::NO_ARGUMENT], ['--pid-file', '-p', GetoptLong::REQUIRED_ARGUMENT], ['--password', '-P', GetoptLong::OPTIONAL_ARGUMENT], ['--verbose', '-v', GetoptLong::NO_ARGUMENT]) # FIXME: should catch errors gracefully. parser.each do |name, arg| case name when '--help' puts 'Usage: bugcommd [OPTION]...' puts '' puts 'BugCommunicator server.' puts '' puts ' -c, --config=FILE use FILE as a configuration file' puts ' -d, --daemon run as a daemon' puts ' -h, --help display this help and exit' puts ' -p, --pid=FILE write a process id to FILE' puts ' -P, --password[=PASSWD] set the database password' puts ' -l, --log=FILE write log messages to FILE' puts ' -v, --verbose print verbose messages' puts ' -V, --version print version information and exit' puts '' puts 'Report bugs to .' exit 0 when '--version' puts "bugcommd (BugCommunicator #{BUGCOMM_VERSION})" exit 0 when '--verbose' $verbosity += 1 when '--config' config_file = File.expand_path(arg) when '--log' log_file = File.expand_path(arg) when '--daemon' daemonized = true when '--pid-file' pid_file = File.expand_path(arg) when '--password' if arg.empty? ask_passwd = true else passwd = arg end end end # Initialize the configuration. config = BugCommunicator::Config.new(config_file) if ask_passwd begin $stdin.shadow = true $stderr.print "Enter the password for the database user `#{config.db_user}': " passwd = gets.chomp $stderr.print "\n" ensure $stdin.shadow = false end end config.db_password = passwd unless passwd.nil? # Open the log. if log_file.nil? and daemonized require 'syslog' $log = Syslog.open('bugcommd', Syslog::Constants::LOG_PID | Syslog::Constants::LOG_CONS, Syslog::Constants::LOG_DAEMON) else if log_file.nil? $log = $stderr else $log = File.open(log_file, 'a') end def $log.err(str, *args) self.puts("bugcommd [error]: " + sprintf(str, *args)) end def $log.info(str, *args) self.puts("bugcommd [info]: " + sprintf(str, *args)) end def $log.debug(str, *args) self.puts("bugcommd [debug]: " + sprintf(str, *args)) end end at_exit { $log.close } def error(str) $log.err('%s', str) end def info(str) $log.info('%s', str) if $verbosity > 0 end def debug(str) $log.debug('%s', str) if $verbosity > 1 end begin # Signal handler. def handler(sig) s = DRb.primary_server unless s.nil? s.stop_service group = ThreadGroup.new Thread.list.each do |t| next if t == Thread.current or t == Thread.main or t == s.thread group.add(t) end 10.times do |i| group.list.each do |t| t.kill unless t[:critical] end sleep 1 unless group.list.empty? end group.list.each do |t| t.kill end end info("interrupted by the signal `#{sig}'") exit end trap(:INT) { handler('SIGINT') } trap(:HUP) { handler('SIGHUP') } trap(:TERM) { handler('SIGTERM') } trap(:USR1, "IGNORE") trap(:USR2, "IGNORE") # Run as a daemon, if necessary. if daemonized exit! if fork Process.setsid if pid = fork unless pid_file.nil? File.open(pid_file, 'w') do |f| f.write pid end end exit! end Dir.chdir('/') File.umask 0 $stdin.close $stdout.close $stderr.close end # Run a dRuby service. acl = ACL.new(config.acl, ACL::DENY_ALLOW) uri = "druby://#{config.host}:#{config.port}" begin s = DRb::DRbServer.new(uri, BugCommunicator::Command.new(config), acl) s.argc_limit = 2 s.load_limit = config.size_limit unless config.size_limit.nil? rescue SignalException, Interrupt end DRb.thread.join rescue BugCommunicator::BugCommError, NoMemoryError, ScriptError, StandardError => e error("#{e.message} (#{e.class})") error(e.backtrace.join("\n")) ensure File.delete(pid_file) if pid_file and File.exist?(pid_file) end