lib/process/daemon.rb
# 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