ioquatix/process-daemon

View on GitHub
lib/process/daemon.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

require 'fileutils'

require_relative 'daemon/controller'

require_relative 'daemon/notification'

require_relative 'daemon/log_file'
require_relative 'daemon/process_file'

module Process
    # Provides the infrastructure for spawning a daemon.
    class Daemon
        # Initialize the daemon in the given working root.
        def initialize(working_directory = ".")
            @working_directory = working_directory
            
            @shutdown_notification = Notification.new
        end
        
        # Return the name of the daemon
        def name
            return self.class.name.gsub(/[^a-zA-Z0-9]+/, '-')
        end

        # The directory the daemon will run in.
        attr :working_directory

        # Return the directory to store log files in.
        def log_directory
            File.join(working_directory, "log")
        end

        # Standard log file for stdout and stderr.
        def log_file_path
            File.join(log_directory, "#{name}.log")
        end

        # Runtime data directory for the daemon.
        def runtime_directory
            File.join(working_directory, "run")
        end

        # Standard location of process pid file.
        def process_file_path
            File.join(runtime_directory, "#{name}.pid")
        end

        # Mark the output log.
        def mark_log
            File.open(log_file_path, "a") do |log_file|
                log_file.puts "=== Log Marked @ #{Time.now.to_s} [#{Process.pid}] ==="
            end
        end

        # Prints some information relating to daemon startup problems.
        def tail_log(output)
            lines = LogFile.open(log_file_path).tail_log do |line|
                line.match("=== Log Marked") || line.match("=== Daemon Exception Backtrace")
            end
            
            output.puts lines
        end

        # Check the last few lines of the log file to find out if the daemon crashed.
        def crashed?
            count = 3
            
            LogFile.open(log_file_path).tail_log do |line|
                return true if line.match("=== Daemon Crashed")

                break if (count -= 1) == 0
            end

            return false
        end
        
        # The main function to setup any environment required by the daemon
        def prefork
            # Ignore any previously setup signal handler for SIGINT:
            trap(:INT, :DEFAULT)
            
            # We update the working directory to a full path:
            @working_directory = File.expand_path(working_directory)
            
            FileUtils.mkdir_p(log_directory)
            FileUtils.mkdir_p(runtime_directory)
        end
        
        # The process title of the daemon.
        attr :title
        
        # Set the process title - only works after daemon has forked.
        def title= title
            @title = title
            
            if Process.respond_to? :setproctitle
                Process.setproctitle(@title)
            else
                $0 = @title
            end
        end
        
        # Request that the sleep_until_interrupted function call returns.
        def request_shutdown
            @shutdown_notification.signal
        end
        
        # Call this function to sleep until the daemon is sent SIGINT.
        def sleep_until_interrupted
            trap(:INT) do
                self.request_shutdown
            end

            @shutdown_notification.wait
        end
        
        # This function must setup the daemon quickly and return.
        def startup
        end
        
        # If you want to implement a long running process you override this method. You may like to call super but it is not necessary to use the supplied interruption machinery.
        def run
            sleep_until_interrupted
        end
        
        # This function should terminate any active processes in the daemon and return as quickly as possible.
        def shutdown
        end
        
        # The entry point from the newly forked process.
        def spawn
            self.title = self.name
            
            self.startup
            
            begin
                self.run
            rescue Interrupt
                $stderr.puts "Daemon interrupted, proceeding to shutdown."
            end
            
            self.shutdown
        end
        
        class << self
            # A shared instance of the daemon.
            def instance
                @instance ||= self.new
            end
            
            # The process controller, responsible for managing the daemon process start, stop, restart, etc.
            def controller(options = {})
                @controller ||= Controller.new(instance, options)
            end
            
            # The main entry point for daemonized scripts.
            def daemonize(*args, **options)
                args = ARGV if args.empty?
                
                controller(options).daemonize(args)
            end
            
            # Start the shared daemon instance.
            def start
                controller.start
            end
            
            # Stop the shared daemon instance.
            def stop
                controller.stop
            end
            
            # Check if the shared daemon instance is runnning or not.
            def status
                controller.status
            end
        end
    end
end