Gurpartap/cognizant

View on GitHub
lib/cognizant/daemon.rb

Summary

Maintainability
C
7 hrs
Test Coverage
require "eventmachine"

require "cognizant"
require "cognizant/log"
require "cognizant/process"
require "cognizant/application"
require "cognizant/system"
require "cognizant/util/transform_hash_keys"

module Cognizant
  class Daemon
    # Whether or not to the daemon in the background.
    # @return [true, false] Defaults to true
    attr_accessor :daemonize

    attr_accessor :sockfile

    # The pid lock file for the daemon.
    # e.g. /Users/Gurpartap/.cognizant/cognizantd.pid
    # @return [String] Defaults to /var/run/cognizant/cognizantd.pid
    attr_accessor :pidfile

    # The file to log the daemon's operations information into.
    # e.g. /Users/Gurpartap/.cognizant/cognizantd.log
    # @return [String] Defaults to /var/log/cognizant/cognizantd.log
    attr_accessor :logfile

    # Whether or not to log to syslog daemon instead of file.
    # @return [true, false] Defaults to true
    attr_accessor :syslog

    # The level of information to log. This does not affect the log
    # level of managed processes.
    # @note The possible values must be one of the following:
    #   DEBUG, INFO, WARN, ERROR and FATAL or 0, 1, 2, 3, 4 (respectively).
    # @return [Logger::Severity] Defaults to Logger::INFO
    attr_accessor :loglevel

    # Hash of applications being managed.
    # @private
    # @return [Hash]
    attr_accessor :applications

    # @private
    attr_accessor :socket

    def start(options = {})
      self.reset!

      load_files = []
      rb_files   = []
      yml_files  = []
      apps       = []

      load_files = [*options.delete(:load)] if options and options.has_key?(:load)
      load_files.each do |include|
        Dir[File.expand_path(include)].each do |file|
          Log[self].info "Including config from #{file}."
          if file.end_with?(".yml")
            yml_files << file
          else
            rb_files << file
          end
        end
      end
      apps << options.delete(:applications) if options and options.has_key?(:applications)

      yml_files.each do |file|
        file_opts = YAML.load_file(file)
        if file_opts
          file_opts.deep_symbolize_keys!
          apps << file_opts.delete(:applications) if file_opts.has_key?(:applications)
        end
      end

      # Attributes.
      options.each do |key, value|
        self.send("#{key}=", options.delete(key)) if self.respond_to?("#{key}=")
      end

      self.sockfile = File.expand_path(self.sockfile)
      self.pidfile  = File.expand_path(self.pidfile)
      self.logfile  = File.expand_path(self.logfile)

      Log[self].info "Booting up Cognizant daemon..."
      setup_directories
      setup_logging
      stop_previous_daemon
      stop_previous_socket
      trap_signals
      daemonize_process
      write_pid

      EventMachine.run do
        # Applications.
        Log[self].info "Cognizant daemon running successfully."

        apps.each do |app|
          app.each do |key, value|
            self.create_application(key, value)
          end
        end

        [*rb_files].each do |rb_file|
          load_file(rb_file)
        end

        EventMachine.start_unix_domain_server(self.sockfile, Cognizant::Interface)

        EventMachine.add_periodic_timer(1) do
          Cognizant::System.reset_data!
          self.applications.values.each(&:tick)
        end
      end
    end

    def load_file(file)
      if file.end_with?(".yml")
        yml_config = YAML.load_file(file)
        if yml_config
          yml_config.deep_symbolize_keys!
          yml_config_apps = yml_config.delete(:applications) if yml_config.has_key?(:applications)
          yml_config_apps.each { |key, value| self.create_application(key, value) }
        end
      else
        rb_file = File.expand_path(file)
        Kernel.load(rb_file) if File.exists?(rb_file)
      end
    end

    def reset!
      self.daemonize    = true
      self.sockfile     = "/var/run/cognizant/cognizantd.sock"
      self.pidfile      = "/var/run/cognizant/cognizantd.pid"
      self.syslog       = false
      self.logfile      = "/var/log/cognizant/cognizantd.log"
      self.loglevel     = Logging::LEVELS["INFO"]
      self.applications.values.each(&:reset!) if self.applications.is_a?(Hash)
      self.applications = {}
    end

    def create_application(name, options = {}, &block)
      app = Cognizant::Application.new(name, options, &block)
      self.applications[app.name.to_sym] = app
      app
    end

    def setup_logging
      Cognizant::Log.logger.root.level = self.loglevel

      unless self.daemonize
        stdout_appender = Cognizant::Log.stdout
        Cognizant::Log.logger.root.add_appenders(stdout_appender)
      end

      if self.syslog
        # TODO: Choose a non-default facility? (default: LOG_USR).
        syslog_appender = Cognizant::Log.syslog("cognizantd")
        Cognizant::Log.logger.root.add_appenders(syslog_appender)
      elsif self.logfile
        logfile_appender = Cognizant::Log.file(self.logfile)
        Cognizant::Log.logger.root.add_appenders(logfile_appender)
      end
    end

    def setup_directories
      # Create the require directories.
      System.mkdir(File.dirname(self.sockfile), File.dirname(self.pidfile), File.dirname(self.logfile))
    end

    # Stops the socket server and the tick loop, and performs cleanup.
    def shutdown!
      Log[self].info "Shutting down Cognizant daemon..."
      EventMachine.next_tick do
        EventMachine.add_shutdown_hook do
          self.applications.values.each(&:shutdown!)
          unlink_pid
          # TODO: Close logger?
        end
        EventMachine.stop
      end
    end

    def trap_signals
      terminator = Proc.new do
        Log[self].info "Received signal to shutdown."
        shutdown!
      end

      Signal.trap('TERM', &terminator)
      Signal.trap('INT',  &terminator)
      Signal.trap('QUIT', &terminator)
    end

    def stop_previous_daemon
      if self.pidfile and File.exists?(self.pidfile)
        if previous_daemon_pid = File.read(self.pidfile).to_i
          # Only attempt to stop automatically if the daemon will run in background.
          if self.daemonize and Cognizant::System.pid_running?(previous_daemon_pid)
            # Ensure that the process stops within 5 seconds or force kill.
            signals = ["TERM", "KILL"]
            timeout = 2
            catch :stopped do
              signals.each do |stop_signal|
                # Send the stop signal and wait for it to stop.
                Cognizant::System.signal(stop_signal, previous_daemon_pid)

                # Poll to see if it has stopped yet. Minimum 2 so that we check at least once again.
                ([timeout / signals.size, 2].max).times do
                  throw :stopped unless Cognizant::System.pid_running?(previous_daemon_pid)
                  sleep 1
                end
              end
            end
          end
        end

        # Alert the user to manually stop the previous daemon, if it is [still] alive.
        if Cognizant::System.pid_running?(previous_daemon_pid)
          raise "There is already a daemon running with pid #{previous_daemon_pid}."
        else
          unlink_pid # This will be overwritten anyways.
        end
      end
    end

    def stop_previous_socket
      # Socket isn't actually owned by anyone.
      begin
        sock = UNIXSocket.new(self.sockfile)
      rescue Errno::ECONNREFUSED
        # This happens with non-socket files and when the listening
        # end of a socket has exited.
      rescue Errno::ENOENT
        # Socket doesn't exist.
        return
      else
        # Rats, it's still active.
        sock.close
        raise Errno::EADDRINUSE.new("Another process or application is likely already listening on the socket at #{self.sockfile}.")
      end
    
      # Socket should still exist, so don't need to handle error.
      stat = File.stat(self.sockfile)
      unless stat.socket?
        raise Errno::EADDRINUSE.new("Non-socket file present at socket file path #{self.sockfile}. Either remove that file and restart Cognizant, or change the socket file path.")
      end
    
      Log[self].info("Blowing away old socket file at #{self.sockfile}. This likely indicates a previous Cognizant application which did not shutdown gracefully.")

      # Whee, blow it away.
      unlink_sockfile
    end

    # Daemonize the current process and save it pid in a file.
    def daemonize_process
      if self.daemonize
        Log[self].info "Daemonizing into the background..."
        ::Process.daemon
      end
    end

    def write_pid
      pid = ::Process.pid
      if self.pidfile
        Log[self].info "Writing the daemon pid (#{pid}) to the pidfile..."
        File.open(self.pidfile, "w") { |f| f.write(pid) }
      end
    end

    def unlink_pid
      Cognizant::System.unlink_file(self.pidfile) if self.pidfile
    end

    def unlink_sockfile
      Cognizant::System.unlink_file(self.sockfile) if self.sockfile
    end
  end
end