# encoding: utf-8
#  Phusion Passenger - https://www.phusionpassenger.com/
#  Copyright (c) 2014-2025 Asynchronous B.V.
#
#  "Passenger", "Phusion Passenger" and "Union Station" are registered
#  trademarks of Asynchronous B.V.
#
#  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'
PhusionPassenger.require_passenger_lib 'admin_tools/instance'

module PhusionPassenger
  module AdminTools

    class InstanceRegistry
      def initialize(paths = nil)
        @paths = [paths || default_paths].flatten.uniq
      end

      def list(options = {})
        options = {
          :clean_stale_or_corrupted => true
        }.merge(options)

        instances = []

        @paths.each do |path|
          Dir["#{path}/passenger.*"].each do |dir|
            instance = Instance.new(dir)
            case instance.state
            when :good
              if instance.locked?
                instances << instance
              elsif options[:clean_stale_or_corrupted]
                cleanup(dir)
              end
            when :structure_version_unsupported
              next
            when :corrupted
              if !instance.locked? && options[:clean_stale_or_corrupted]
                cleanup(dir)
              end
            when :not_finalized
              if instance.stale? && options[:clean_stale_or_corrupted]
                cleanup(dir)
              end
            end
          end
        end

        instances
      end

      def find_by_name(name, options = {})
        return list(options).find { |instance| instance.name == name }
      end

      def find_by_name_prefix(name, options = {})
        prefix = /^#{Regexp.escape name}/
        results = list(options).find_all { |instance| instance.name =~ prefix }
        if results.size <= 1
          return results.first
        else
          return :ambiguous
        end
      end

      # :watchdog_pid: expected Integer.
      #
      # return: the matching instance, if found; nil otherwise.
      #
      def find_by_watchdog_pid(pid, options = {})
        return list(options).detect { |instance| instance.watchdog_pid == pid }
      end

    private
      def default_paths
        if result = string_env("PASSENGER_INSTANCE_REGISTRY_DIR")
          return [result]
        end

        # On OSX, TMPDIR is set to a different value per-user. But Apache
        # is launched through Launchctl and runs without TMPDIR (and thus
        # uses the default /tmp).
        #
        # The RPM packages configure Apache and Nginx to use /var/run/passenger-instreg
        # as the instance registry dir. See https://github.com/phusion/passenger/issues/1475
        #
        # systemd's PrivateTmp feature works like an inverted OSX, apache gets its own
        # TMPDIR and users use /tmp, however the path is often too long because socket paths can
        # only be up to 108 characters long.
        [string_env("TMPDIR"), "/tmp", "/var/run/passenger-instreg",*Dir['/tmp/systemd-private-*-{httpd,nginx,apache2}.service-*/tmp']].compact
      end

      def string_env(name)
        if (result = ENV[name]) && !result.empty?
          result
        else
          nil
        end
      end

      def cleanup(path)
        puts "*** Cleaning stale instance directory #{path}"
        begin
          FileUtils.chmod_R(0700, path) rescue nil
          FileUtils.remove_entry_secure(path)
        rescue SystemCallError => e
          puts "    Warning: #{e}"
        end
      end
    end

  end # module AdminTools
end # module PhusionPassenger
