lib/servolux/server.rb



# == Synopsis
# The Server class makes it simple to create a server-type application in
# Ruby. A server in this context is any process that should run for a long
# period of time either in the foreground or as a daemon.
#
# == Details
# The Server class provides for standard server features: process ID file
# management, signal handling, run loop, logging, etc. All that you need to
# provide is a +run+ method that will be called by the server's run loop.
# Optionally, you can provide a block to the +new+ method and it will be
# called within the run loop instead of a run method.
#
# SIGINT and SIGTERM are handled by default. These signals will gracefully
# shutdown the server by calling the +shutdown+ method (provided by default,
# too). A few other signals can be handled by defining a few methods on your
# server instance. For example, SIGINT is hanlded by the +int+ method (an
# alias for +shutdown+). Likewise, SIGTERM is handled by the +term+ method
# (another alias for +shutdown+). The following signal methods are
# recognized by the Server class:
#
#    Method  |  Signal  |  Default Action
#    --------+----------+----------------
#    hup        SIGHUP     none
#    int        SIGINT     shutdown
#    term       SIGTERM    shutdown
#    usr1       SIGUSR1    none
#    usr2       SIGUSR2    none
#
# In order to handle SIGUSR1 you would define a <tt>usr1</tt> method for your
# server.
#
# There are a few other methods that are useful and should be mentioned. Two
# methods are called before and after the run loop starts: +before_starting+
# and +after_starting+. The first is called just before the run loop thread
# is created and started. The second is called just after the run loop
# thread has been created (no guarantee is made that the run loop thread has
# actually been scheduled).
#
# Likewise, two other methods are called before and after the run loop is
# shutdown: +before_stopping+ and +after_stopping+. The first is called just
# before the run loop thread is signaled for shutdown. The second is called
# just after the run loop thread has died; the +after_stopping+ method is
# guarnteed to NOT be called till after the run loop thread is well and
# truly dead.
# 
# == Usage
# For simple, quick and dirty servers just pass a block to the Server
# initializer. This block will be used as the run method.
#
#    server = Servolux::Server.new('Basic', :interval => 1) {
#      puts "I'm alive and well @ #{Time.now}"
#    }
#    server.startup
#
# For more complex services you will need to define your own server methods:
# the +run+ method, signal handlers, and before/after methods. Any pattern
# that Ruby provides for defining methods on objects can be used to define
# these methods. In a nutshell:
#
# Inheritance
#
#    class MyServer < Servolux::Server
#      def run
#        puts "I'm alive and well @ #{Time.now}"
#      end
#    end
#    server = MyServer.new('MyServer', :interval => 1)
#    server.startup
#
# Extension
#
#    module MyServer
#      def run
#        puts "I'm alive and well @ #{Time.now}"
#      end
#    end
#    server = Servolux::Server.new('Module', :interval => 1)
#    server.extend MyServer
#    server.startup
#
# Singleton Class
#
#    server = Servolux::Server.new('Singleton', :interval => 1)
#    class << server
#      def run
#        puts "I'm alive and well @ #{Time.now}"
#      end
#    end
#    server.startup
#
# == Examples
#
# === Signals
# This example shows how to change the log level of the server when SIGUSR1
# is sent to the process. The log level toggles between "debug" and the
# original log level each time SIGUSR1 is sent to the server process. Since
# this is a module, it can be used with any Servolux::Server instance.
#
#    module DebugSignal
#      def usr1
#        @old_log_level ||= nil
#        if @old_log_level
#          logger.level = @old_log_level
#          @old_log_level = nil
#        else
#          @old_log_level = logger.level
#          logger.level = :debug
#        end
#      end
#    end
#
#    server = Servolux::Server.new('Debugger', :interval => 2) {
#      logger.info "Running @ #{Time.now}"
#      logger.debug "hey look - a debug message"
#    }
#    server.extend DebugSignal
#    server.startup
#
class Servolux::Server
  include ::Servolux::Threaded

  # :stopdoc:
  SIGNALS = %w[HUP INT TERM USR1 USR2] & Signal.list.keys
  SIGNALS.each {|sig| sig.freeze}.freeze
  # :startdoc:

  Error = Class.new(::Servolux::Error)

  attr_reader   :name
  attr_accessor :logger
  attr_writer   :pid_file

  # call-seq:
  #    Server.new( name, options = {} ) { block }
  #
  # Creates a new server identified by _name_ and configured from the
  # _options_ hash. The _block_ is run inside a separate thread that will
  # loop at the configured interval.
  #
  # ==== Options
  # * logger <Logger> :: The logger instance this server will use
  # * pid_file <String> :: Location of the PID file
  # * interval <Numeric> :: Sleep interval between invocations of the _block_
  #
  def initialize( name, opts = {}, &block )
    @name = name
    @activity_thread = nil
    @activity_thread_running = false

    self.logger   = opts.getopt :logger
    self.pid_file = opts.getopt :pid_file
    self.interval = opts.getopt :interval, 0

    if block
      eg = class << self; self; end
      eg.__send__(:define_method, :run, &block)
    end

    ary = %w[name logger pid_file].map { |var|
      self.send(var).nil? ? var : nil
    }.compact
    raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
  end

  # Start the server running using it's own internal thread. This method
  # will not return until the server is shutdown.
  #
  # Startup involves creating a PID file, registering signal handlers to
  # shutdown the server, starting and joining the server thread. The PID
  # file is deleted when this method returns.
  #
  def startup
    return self if running?
    begin
      create_pid_file
      trap_signals
      start
      join
    ensure
      delete_pid_file
    end
    return self
  end

  alias :shutdown :stop     # for symmetry with the startup method
  alias :int :stop          # handles the INT signal
  alias :term :stop         # handles the TERM signal
  private :start, :stop

  # Returns the PID file name used by the server. If none was given, then
  # the server name is used to create a PID file name.
  #
  def pid_file
    @pid_file ||= name.downcase.tr(' ','_') + '.pid'
  end

  private

  def create_pid_file
    logger.debug "Server #{name.inspect} creating pid file #{pid_file.inspect}"
    File.open(pid_file, 'w') {|fd| fd.write(Process.pid.to_s)}
  end

  def delete_pid_file
    if test(?f, pid_file)
      logger.debug "Server #{name.inspect} removing pid file #{pid_file.inspect}"
      File.delete(pid_file)
    end
  end

  def trap_signals
    SIGNALS.each do |sig|
      m = sig.downcase.to_sym
      Signal.trap(sig) { self.send(m) rescue nil } if self.respond_to? m
    end
  end

end  # class Servolux::Server

# EOF