18 |
# along with Foobar; if not, write to the Free Software |
# along with Foobar; if not, write to the Free Software |
19 |
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
20 |
|
|
21 |
# Load modules. |
require 'bugcomm/main' |
22 |
require 'getoptlong' |
require 'bugcomm/utils' |
|
require 'mysql' |
|
|
require 'drb/drb' |
|
|
require 'acl' |
|
|
require 'cgi' |
|
23 |
|
|
24 |
# Syslog isn't essential. |
module BugCommunicator |
|
begin |
|
|
require 'syslog' |
|
|
rescue LoadError |
|
|
end |
|
|
|
|
|
# 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 |
|
|
|
|
|
# The master class. |
|
|
class BugCommunicator |
|
|
|
|
|
# Class variables. |
|
|
@@verbosity = 0 |
|
|
@@log = $stderr |
|
|
|
|
|
# Exceptions. |
|
|
class BugCommError < Exception; end |
|
|
|
|
|
# Helper functions. |
|
|
def self.error(str) |
|
|
if @@log.respond_to?(:err) |
|
|
@@log.err('%s', str) |
|
|
elsif @@log.respond_to?(:puts) |
|
|
@@log.puts("bugcommd [#{Process.pid}]: error: #{str}") |
|
|
end |
|
|
end |
|
|
|
|
|
def self.info(str) |
|
|
if @@verbosity > 0 |
|
|
if @@log.respond_to?(:info) |
|
|
@@log.info('%s', str) |
|
|
elsif @@log.respond_to?(:puts) |
|
|
@@log.puts("bugcommd [#{Process.pid}]: info: #{str}") |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
def self.debug(str) |
|
|
if @@verbosity > 1 |
|
|
if @@log.respond_to?(:debug) |
|
|
@@log.debug('%s', str) |
|
|
elsif @@log.respond_to?(:puts) |
|
|
@@log.puts("bugcommd [#{Process.pid}]: debug: #{str}") |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
def self.clean_up |
|
|
@@log.close if @@log != $stderr |
|
|
@@log = $stderr |
|
|
end |
|
|
# End of helper functions. |
|
|
|
|
|
# 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 |
|
25 |
|
|
26 |
if defined?(m::SITE_ADMINS) |
# These constants will be replaced by the configure script. |
27 |
unless m::SITE_ADMINS.kind_of?(Array) |
BUGCOMM_VERSION = '@VERSION@' |
28 |
raise TypeError, 'SITE_ADMINS must be an array' |
BUGCOMMD_CONFIG_FILE = '@BUGCOMMD_CONFIG_FILE@' |
29 |
end |
DATADIR = '@datadir@'.sub(/$\{prefix\}/, '@prefix@') |
|
@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})") |
|
|
exception.backtrace.each do |s| |
|
|
error(s) |
|
|
end |
|
|
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" + |
|
|
"<html><head><title>Error</title></head><body>\r\n" + |
|
|
"<h1>Error</h1><p>#{h(e.message)}</p>\r\n" + |
|
|
"</body></html>" |
|
|
rescue NoMemoryError, ScriptError, StandardError => e |
|
|
error_log(e) |
|
|
return "Content-Type: text/html\r\n\r\n" + |
|
|
"<html><head><title>Server internal error</title></head><body>\r\n" + |
|
|
"<h1>Server internal error</h1>\r\n" + |
|
|
"<pre>#{h(e.message)} (#{h(e.class)})\r\n" + |
|
|
h(e.backtrace.join("\r\n")) + "</pre><p>\r\n" + |
|
|
"Please send bug reports to " + |
|
|
@config.site_admins.collect {|admin| h(admin)}.join(", ") + |
|
|
".</p></body></html>" |
|
|
end |
|
|
end |
|
|
end |
|
|
# End of class Command. |
|
|
|
|
|
def initialize |
|
|
@config_file = '@BUGCOMMD_CONFIG_FILE@' |
|
|
@log_file = nil |
|
|
@pid_file = nil |
|
|
@passwd = nil |
|
|
@ask_passwd = false |
|
|
@daemonized = false |
|
|
|
|
|
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 <okuji@enbug.org>.' |
|
|
exit 0 |
|
|
when '--version' |
|
|
puts "bugcommd (BugCommunicator @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 "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? |
|
|
|
|
|
# Trap some signals. |
|
|
trap(:INT) { handler('SIGINT') } |
|
|
trap(:HUP) { handler('SIGHUP') } |
|
|
trap(:TERM) { handler('SIGTERM') } |
|
|
trap(:USR1, "IGNORE") |
|
|
trap(:USR2, "IGNORE") |
|
|
|
|
|
# Open the log. |
|
|
if @log_file.nil? and @daemonized |
|
|
unless defined?(Syslog) |
|
|
raise "you must specify the option `--log=FILE', because UNIX syslog interface for Ruby is not available" |
|
|
end |
|
|
|
|
|
@@log = Syslog.open('bugcommd', |
|
|
Syslog::Constants::LOG_PID | |
|
|
Syslog::Constants::LOG_CONS, |
|
|
Syslog::Constants::LOG_DAEMON) |
|
|
else |
|
|
unless @log_file.nil? |
|
|
@@log = File.open(@log_file, 'a') |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
# 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 |
|
|
|
|
|
self.class.info("interrupted by the signal `#{sig}'") |
|
|
exit |
|
|
end |
|
|
private :handler |
|
|
|
|
|
# Run itself as a daemon. |
|
|
def daemonize |
|
|
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 |
|
|
private :daemonize |
|
|
|
|
|
# The main routine. |
|
|
def main |
|
|
begin |
|
|
# Run as a daemon, if necessary. |
|
|
daemonize if @daemonized |
|
|
|
|
|
# Run a dRuby service. |
|
|
acl = ACL.new(@config.acl, ACL::DENY_ALLOW) |
|
|
uri = "druby://#{@config.host}:#{@config.port}" |
|
|
DRb::DRbServer.default_argc_limit(2) |
|
|
unless @config.size_limit.nil? |
|
|
DRb::DRbServer.default_load_limit(@config.size_limit) |
|
|
end |
|
|
DRb::DRbServer.new(uri, Command.new(@config), acl) |
|
|
self.class.info("starting the dRuby service at `#{uri}'") |
|
|
DRb.thread.join |
|
|
rescue BugCommError, NoMemoryError, ScriptError, StandardError => e |
|
|
self.class.error("#{e.message} (#{e.class})") |
|
|
e.backtrace.each do |s| |
|
|
self.class.error(s) |
|
|
end |
|
|
ensure |
|
|
if @pid_file and File.exist?(@pid_file) |
|
|
self.class.info("deleteing the pid file `#{@pid_file}'") |
|
|
File.delete(@pid_file) |
|
|
end |
|
|
end |
|
|
end |
|
30 |
|
|
31 |
end |
end |
32 |
|
|
33 |
begin |
begin |
34 |
BugCommunicator.debug('initializing the server') |
BugCommunicator.debug('initializing the server') |
35 |
bugcomm = BugCommunicator.new |
BugCommunicator.init() |
36 |
BugCommunicator.info('starting the service') |
BugCommunicator.info('starting the service') |
37 |
bugcomm.main |
BugCommunicator.main() |
38 |
ensure |
ensure |
39 |
BugCommunicator.info('stopping the service') |
BugCommunicator.info('stopping the service') |
40 |
BugCommunicator.clean_up |
BugCommunicator.clean_up() |
41 |
end |
end |