From 49c9e2f095fdc9cb815490c8e5afd5453be5fbf5 Mon Sep 17 00:00:00 2001 From: elijah Date: Fri, 17 May 2013 23:08:53 -0700 Subject: rolled custom daemon code to better match the way daemons are supposed to work under debian. --- lib/nickserver/config.rb | 9 +- lib/nickserver/daemon.rb | 260 ++++++++++++++++++++++++++++++++++++++++++++++ lib/nickserver/version.rb | 2 +- 3 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 lib/nickserver/daemon.rb (limited to 'lib/nickserver') diff --git a/lib/nickserver/config.rb b/lib/nickserver/config.rb index 3f92186..b283d8b 100644 --- a/lib/nickserver/config.rb +++ b/lib/nickserver/config.rb @@ -15,7 +15,12 @@ module Nickserver attr_accessor :couch_user attr_accessor :couch_password attr_accessor :port + attr_accessor :pid_file + attr_accessor :user + attr_accessor :log_file + attr_accessor :loaded + attr_accessor :verbose end def self.load @@ -39,9 +44,9 @@ module Nickserver exit(1) end end - puts "Loaded #{file_path}" + puts "Loaded #{file_path}" if Config.verbose rescue Errno::ENOENT => exc - puts "Skipping #{file_path}" + puts "Skipping #{file_path}" if Config.verbose rescue Exception => exc STDERR.puts exc.inspect exit(1) diff --git a/lib/nickserver/daemon.rb b/lib/nickserver/daemon.rb new file mode 100644 index 0000000..9eb2ff5 --- /dev/null +++ b/lib/nickserver/daemon.rb @@ -0,0 +1,260 @@ +require 'etc' +require 'fileutils' + +# +# A simple daemon, in a Debian style. Adapted from gem Dante. +# + +module Nickserver + class Daemon + + def self.run(&block) + self.new.run(&block) + end + + def run(&block) + parse_options + Config.load + send("command_#{@command}", &block) + end + + private + + MAX_WAIT = 2 + + # + # PERMISSIONS + # + + # + # see http://timetobleed.com/5-things-you-dont-know-about-user-ids-that-will-destroy-you/ + # (hint: it is easy to get it wrong) + # + def drop_permissions_to(username) + if username != 'root' + if Process::Sys.getuid == 0 + Process::Sys.setuid(Etc.getpwnam(username).uid) + if root? + bail "failed to drop permissions" + end + else + bail "cannot change process uid to #{username}" + end + end + end + + def root? + begin + Process::Sys.setuid(0) + rescue Errno::EPERM + false + else + true + end + end + + # + # PROCESS STUFF + # + + def daemonize + return bail("Process is already started") if daemon_running? + pid = fork do + exit if fork + Process.setsid + exit if fork + create_pid_file(Config.pid_file, Config.user) + catch_interrupt + redirect_output + drop_permissions_to(Config.user) + File.umask 0000 + yield + end + + if until_true { daemon_running? } + puts "Daemon has started successfully" + exit(0) + else # Failed to start + puts "Daemon couldn't be started" + exit(1) + end + end + + def create_pid_file(file, user) + File.open file, 'w' do |f| + f.write("#{Process.pid}\n") + end + FileUtils.chown(user, nil, file) + rescue Errno::EACCES + bail "insufficient permission to create to pid file `#{file}`" + rescue Errno::ENOENT + bail "bad path for pid file `#{file}`" + rescue Errno::EROFS + bail "can't create pid file `#{file}` on read-only filesystem" + end + + def daemon_running? + return false unless File.exist?(Config.pid_file) + Process.kill 0, File.read(Config.pid_file).to_i + true + rescue Errno::ESRCH + false + end + + def pid_from_file(file) + pid = IO.read(file).chomp + if pid != "" + pid.to_i + else + nil + end + end + + def kill_pid + file = Config.pid_file + if File.exists?(file) + pid = pid_from_file(file) + if pid + Process.kill('TERM', pid) + puts "Stopped #{pid}" + else + bail "Error reading pid file #{file}" + end + begin + FileUtils.rm Config.pid_file + rescue Errno::EACCES + bail 'insufficient permission to remove pid file' + end + else + bail "could not find pid file #{file}" + end + rescue => e + puts "Failed to stop: #{e}" + end + + # + # Gracefully handle Ctrl-C + # + def catch_interrupt + Signal.trap("SIGINT") do + command_stop + $stdout.puts "\nQuit" + $stdout.flush + exit + end + end + + + # + # OUTPUT + # + + def usage(msg) + puts msg + puts + puts "Usage: nickserver [OPTION] COMMAND" + puts "COMMAND is one of: start, stop, restart, status, version" + puts "OPTION is one of: --verbose" + puts + exit 1 + end + + def bail(msg) + puts "Nickserver ERROR: #{msg}." + puts "Bailing out." + exit(1) + end + + # + # Redirect output based on log settings (reopens stdout/stderr to specified logfile) + # If log_path is nil, redirect to /dev/null to quiet output + # + def redirect_output + if log_path = Config.log_file + FileUtils.mkdir_p File.dirname(log_path), :mode => 0755 + FileUtils.touch log_path + File.chmod(0644, log_path) + $stdout.reopen(log_path, 'a') + $stderr.reopen $stdout + $stdout.sync = true + else + # redirect to /dev/null + $stdin.reopen '/dev/null' + $stdout.reopen '/dev/null', 'a' + $stderr.reopen $stdout + end + rescue Errno::EACCES + bail "no permission to create log file #{log_path}" + end + + # + # UTILITY + # + + # + # Runs until the block condition is met or the timeout_seconds is exceeded + # until_true(10) { ...return_condition... } + # + def until_true(timeout_seconds=MAX_WAIT, &block) + elapsed_seconds = 0 + interval = 0.5 + while elapsed_seconds < timeout_seconds && block.call != true + elapsed_seconds += interval + sleep(interval) + end + elapsed_seconds < timeout_seconds + end + + def parse_options + loop do + case ARGV[0] + when 'start' then ARGV.shift; @command = :start + when 'stop' then ARGV.shift; @command = :stop + when 'restart' then ARGV.shift; @command = :restart + when 'status' then ARGV.shift; @command = :status + when 'version' then ARGV.shift; @command = :version + when '--verbose' then ARGV.shift; Config.versbose = true + when /^-/ then usage("Unknown option: #{ARGV[0].inspect}") + else break + end + end + usage("Missing command") unless @command + end + + # + # COMMANDS + # + + def command_version + puts "nickserver #{Nickserver::VERSION}, ruby #{RUBY_VERSION}" + exit(0) + end + + def command_start(&block) + daemonize(&block) + end + + def command_stop + if daemon_running? + kill_pid + until_true { !daemon_running? } + else + puts "No processes are running" + end + end + + def command_restart(&block) + command_stop + command_start(&block) + end + + def command_status + if daemon_running? + puts "Process id #{pid_from_file(Config.pid_file)}" + else + puts 'Not running' + end + end + + end +end diff --git a/lib/nickserver/version.rb b/lib/nickserver/version.rb index 799ca6c..6e86d39 100644 --- a/lib/nickserver/version.rb +++ b/lib/nickserver/version.rb @@ -1,3 +1,3 @@ module Nickserver - VERSION = "0.0.1" + VERSION = "0.2.0" end -- cgit v1.2.3