ranmocy/guard-rails

View on GitHub
lib/guard/rails/runner.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
99%
require 'fileutils'

module Guard
  class Rails
    class Runner
      MAX_WAIT_COUNT = 10

      attr_reader :options

      def initialize(options)
        @options = options
        @root = options[:root] ? File.expand_path(options[:root]) : Dir.pwd
      end

      def start
        kill_unmanaged_pid! if options[:force_run]

        if options[:zeus] && !wait_for_zeus
          UI.info "[Guard::Rails::Error] Could not find zeus socket file."
          return false
        end

        run_rails_command!
        wait_for_pid
      end

      def stop
        return unless has_pid?

        if (pid = read_pid)
          sig_sent = kill_process("INT", pid)
          wait_for_no_pid if sig_sent

          # If you lost your pid_file, you are already died.
          kill_process("KILL", pid)
        end
        remove_pid_file_and_wait_for_no_pid
      end

      def restart
        stop
        start
      end

      def build_command
        command = build_cli_command if options[:CLI]
        command ||= build_zeus_command if options[:zeus]
        command ||= build_rails_command
        "sh -c 'cd \"#{@root}\" && #{command} &'"
      end

      def environment
        rails_env = if options[:zeus]
                      nil
                    else
                      options[:environment]
                    end

        { "RAILS_ENV" => rails_env }
      end

      def pid_file
        @pid_file ||= File.expand_path(options[:pid_file] || File.join(@root, "tmp/pids/#{options[:environment]}.pid"))
      end

      def pid
        has_pid? ? read_pid : nil
      end

      def sleep_time
        options[:timeout].to_f / MAX_WAIT_COUNT.to_f
      end

      def wait_for_zeus
        wait_for_loop { File.exist?(zeus_sockfile) }
      end

      private

      # command builders
      def build_options
        rails_options = [
          options[:daemon] ? '-d' : nil,
          options[:debugger] ? '-u' : nil,
          '-e', options[:environment],
          '--pid', "\"#{pid_file}\"",
          '-b', options[:host],
          '-p', options[:port],
          options[:server],
        ]

        rails_options.join(' ')
      end

      def build_cli_command
        "#{options[:CLI]} --pid \"#{pid_file}\""
      end

      def build_zeus_command
        zeus_options = [
          options[:zeus_plan] || 'server',
        ]
        "zeus #{zeus_options.join(' ')} #{build_options}"
      end

      def build_rails_command
        "rails server #{build_options}"
      end

      def without_bundler_env
        if defined?(::Bundler)
          ::Bundler.with_clean_env { yield }
        else
          yield
        end
      end

      def run_rails_command!
        if options[:CLI] || options[:zeus]
          without_bundler_env { system(environment, build_command) }
        else
          system(environment, build_command)
        end
      end

      def has_pid?
        File.file?(pid_file)
      end

      def wait_for_pid_action
        sleep sleep_time
      end

      def kill_unmanaged_pid!
        if pid = unmanaged_pid
          kill_process("KILL", pid)
          remove_pid_file_and_wait_for_no_pid
        end
      end

      def unmanaged_pid
        file_list = `lsof -n -i TCP:#{options[:port]}`
        file_list.each_line { |line|
          if line["*:#{options[:port]} "]
            return line.split("\s")[1].to_i
          end
        }
        nil
      end

      def wait_for_pid
        wait_for_loop { has_pid? }
      end

      def wait_for_no_pid
        wait_for_loop { !has_pid? }
      end

      def remove_pid_file_and_wait_for_no_pid
        wait_for_loop do
          FileUtils.rm pid_file, force: true
          !has_pid?
        end
      end

      def wait_for_loop
        count = 0
        while !yield && count < MAX_WAIT_COUNT
          wait_for_pid_action
          count += 1
        end
        !(count == MAX_WAIT_COUNT)
      end

      def kill_process(signal, pid)
        begin
          ::Process.kill(signal, pid)
          true
        rescue Errno::EPERM
          UI.info "[Guard::Rails::Error] Don't have permission to KILL!"
          false
        rescue Errno::EINVAL, ArgumentError, Errno::ESRCH, RangeError
          false
        end
      end

      def read_pid
        Integer(File.read(pid_file))
      rescue ArgumentError
        nil
      end

      def zeus_sockfile
        unless ENV['ZEUSSOCK'].to_s.empty?
          ENV['ZEUSSOCK']
        else
          File.join(@root, '.zeus.sock')
        end
      end

    end
  end
end