class Process::Daemon::Controller

This module contains functionality related to starting and stopping the @daemon, and code for processing command line input.

Constants

STOP_ATTEMPTS

The number of attempts to stop the daemon using SIGTERM. On the last attempt, SIGKILL is used.

STOP_PERIOD

How long to wait between checking the daemon process when shutting down:

STOP_WAIT_FACTOR

The factor which controls how long we sleep between attempts to kill the process. Only applies to processes which don’t stop immediately.

Public Class Methods

new(daemon, options = {}) click to toggle source

options` specifies where to write textual output describing what is going on.

# File lib/process/daemon/controller.rb, line 31
def initialize(daemon, options = {})
        @daemon = daemon
        
        @output = options[:output] || $stdout
        
        # How long to wait until sending SIGTERM and eventually SIGKILL to the daemon process group when asking it to stop:
        @stop_timeout = options[:stop_timeout] || 10.0
end

Public Instance Methods

daemonize(argv = ARGV) click to toggle source

This function is called from the daemon executable. It processes ARGV and checks whether the user is asking for ‘start`, `stop`, `restart`, `status`.

# File lib/process/daemon/controller.rb, line 41
def daemonize(argv = ARGV)
        case (argv.shift || :default).to_sym
        when :start
                start
                show_status
        when :stop
                stop
                show_status
                ProcessFile.cleanup(@daemon)
        when :restart
                stop
                ProcessFile.cleanup(@daemon)
                start
                show_status
        when :status
                show_status
        else
                @output.puts Rainbow("Invalid command. Please specify start, restart, stop or status.").red
        end
end
pid() click to toggle source

The pid of the daemon if it is available. The pid may be invalid if the daemon has crashed.

# File lib/process/daemon/controller.rb, line 158
def pid
        ProcessFile.recall(@daemon)
end
show_status() click to toggle source
# File lib/process/daemon/controller.rb, line 138
def show_status
        case self.status
        when :running
                @output.puts Rainbow("Daemon status: running pid=#{ProcessFile.recall(@daemon)}").green
        when :unknown
                if @daemon.crashed?
                        @output.puts Rainbow("Daemon status: crashed").red

                        @output.flush
                        @output.puts Rainbow("Dumping daemon crash log:").red
                        @daemon.tail_log(@output)
                else
                        @output.puts Rainbow("Daemon status: unknown").red
                end
        when :stopped
                @output.puts Rainbow("Daemon status: stopped").blue
        end
end
spawn() click to toggle source

Fork a child process, detatch it and run the daemon code.

# File lib/process/daemon/controller.rb, line 63
def spawn
        @daemon.prefork
        @daemon.mark_log

        fork do
                Process.setsid
                exit if fork

                ProcessFile.store(@daemon, Process.pid)

                File.umask 0000
                Dir.chdir @daemon.working_directory

                $stdin.reopen '/dev/null'
                $stdout.reopen @daemon.log_file_path, 'a'
                $stdout.sync = true
        
                $stderr.reopen $stdout
                $stderr.sync = true

                begin
                        @daemon.spawn
                rescue Exception => error
                        $stderr.puts "=== Daemon Exception Backtrace @ #{Time.now.to_s} ==="
                        $stderr.puts "#{error.class}: #{error.message}"
                        $!.backtrace.each { |at| $stderr.puts at }
                        $stderr.puts "=== Daemon Crashed ==="
                        $stderr.flush
                ensure
                        $stderr.puts "=== Daemon Stopping @ #{Time.now.to_s} ==="
                        $stderr.flush
                end
        end
end
start() click to toggle source

This function starts the daemon process in the background.

# File lib/process/daemon/controller.rb, line 99
def start
        @output.puts Rainbow("Starting #{@daemon.name} daemon...").blue

        case self.status
        when :running
                @output.puts Rainbow("Daemon already running!").blue
                return
        when :stopped
                # We are good to go...
        else
                @output.puts Rainbow("Daemon in unknown state! Will clear previous state and continue.").red
                ProcessFile.clear(@daemon)
        end

        spawn

        sleep 0.1
        timer = TIMEOUT
        pid = ProcessFile.recall(@daemon)

        while pid == nil and timer > 0
                # Wait a moment for the forking to finish...
                @output.puts Rainbow("Waiting for daemon to start (#{timer}/#{TIMEOUT})").blue
                sleep 1

                # If the @daemon has crashed, it is never going to start...
                break if @daemon.crashed?

                pid = ProcessFile.recall(@daemon)

                timer -= 1
        end
end
status() click to toggle source

Prints out the status of the daemon

# File lib/process/daemon/controller.rb, line 134
def status
        ProcessFile.status(@daemon)
end
stop() click to toggle source

Stops the daemon process. This function initially sends SIGINT. It waits STOP_PERIOD and checks if the daemon is still running. If it is, it sends SIGTERM, and then waits a bit longer. It tries STOP_ATTEMPTS times until it basically assumes the daemon is stuck and sends SIGKILL.

# File lib/process/daemon/controller.rb, line 172
def stop
        @output.puts Rainbow("Stopping #{@daemon.name} daemon...").blue

        # Check if the pid file exists...
        unless File.file?(@daemon.process_file_path)
                @output.puts Rainbow("Pid file not found. Is the daemon running?").red
                return
        end

        pid = ProcessFile.recall(@daemon)

        # Check if the @daemon is already stopped...
        unless ProcessFile.running(@daemon)
                @output.puts Rainbow("Pid #{pid} is not running. Has daemon crashed?").red
                @daemon.tail_log($stderr)
                return
        end

        pgid = -Process.getpgid(pid)
        
        # Stop by interrupt sends a single interrupt and waits for the process to terminate:
        unless stop_by_interrupt(pgid)
                # If the process is still running, we try sending SIGTERM followed by SIGKILL:
                @output.puts Rainbow("** Daemon did not stop gracefully after #{@stop_timeout}s **").red
                
                stop_by_terminate_or_kill(pgid)
        end

        # If after doing our best the @daemon is still running (pretty odd)...
        if ProcessFile.running(@daemon)
                @output.puts Rainbow("Daemon appears to be still running!").red
                return
        else
                @output.puts Rainbow("Daemon has left the building.").green
        end

        # Otherwise the @daemon has been stopped.
        ProcessFile.clear(@daemon)
end

Private Instance Methods

stop_by_interrupt(pgid) click to toggle source

Returns true if the process was stopped.

# File lib/process/daemon/controller.rb, line 215
def stop_by_interrupt(pgid)
        running = true
        
        # Interrupt the process group:
        Process.kill("INT", pgid)

        (@stop_timeout / STOP_PERIOD).ceil.times do
                if running = ProcessFile.running(@daemon)
                        sleep STOP_PERIOD
                end
        end
        
        return !running
end
stop_by_terminate_or_kill(pgid) click to toggle source
# File lib/process/daemon/controller.rb, line 230
def stop_by_terminate_or_kill(pgid)
        # TERM/KILL loop - if the daemon didn't die easily, shoot it a few more times.
        (STOP_ATTEMPTS+1).times do |attempt|
                break unless ProcessFile.running(@daemon)

                # SIGKILL gets sent on the last attempt.
                signal_name = (attempt < STOP_ATTEMPTS) ? "TERM" : "KILL"

                @output.puts Rainbow("Sending #{signal_name} to process group #{pgid}...").red

                Process.kill(signal_name, pgid)

                # We iterate quickly to start with, and slow down if the process seems unresponsive.
                timeout = STOP_PERIOD + (attempt.to_f / STOP_ATTEMPTS) * STOP_WAIT_FACTOR
                @output.puts Rainbow("Waiting for #{timeout.round(1)}s for daemon to terminate...").blue
                sleep(timeout)
        end
end