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