# 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 'etc'
PhusionPassenger.require_passenger_lib 'constants'
PhusionPassenger.require_passenger_lib 'standalone/control_utils'
PhusionPassenger.require_passenger_lib 'platform_info'
PhusionPassenger.require_passenger_lib 'platform_info/operating_system'
PhusionPassenger.require_passenger_lib 'utils/shellwords'
PhusionPassenger.require_passenger_lib 'utils/json'

module PhusionPassenger
  module Standalone
    class StartCommand
      module BuiltinEngine
      private
        def start_engine_real
          Standalone::ControlUtils.require_daemon_controller
          @engine = DaemonController.new(build_daemon_controller_options)
          start_engine_no_create
        end

        def wait_until_engine_has_exited
          read_and_delete_report_file!
          if PlatformInfo.supports_flock?
            lock = DaemonController::LockFile.new(@watchdog_lock_file_path)
            lock.shared_lock do
              # Do nothing
            end
          else
            pid = @engine.pid
            while process_is_alive?(pid)
              sleep 1
            end
          end
        end


        def start_engine_no_create
          begin
            @engine.start
          rescue DaemonController::AlreadyStarted
            begin
              pid = @engine.pid
            rescue SystemCallError, IOError
              pid = nil
            end
            if @can_remove_working_dir
              FileUtils.remove_entry_secure(@working_dir)
              @can_remove_working_dir = false
            end
            if pid
              abort "#{PROGRAM_NAME} Standalone is already running on PID #{pid}."
            else
              abort "#{PROGRAM_NAME} Standalone is already running."
            end
          rescue DaemonController::StartError => e
            abort "Could not start the server engine:\n#{e}"
          end
        end

        def build_daemon_controller_options
          ping_spec = lambda do
            File.exist?(report_file_path) &&
              File.stat(report_file_path).size > 0
          end

          command = String.new

          if !@options[:envvars].empty?
            command = "env "
            @options[:envvars].each_pair do |name, value|
              command << "#{Shellwords.escape name}=#{Shellwords.escape value} "
            end
          end

          command << "#{@agent_exe} watchdog"
          command << " --passenger-root #{Shellwords.escape PhusionPassenger.install_spec}"
          command << " --daemonize"
          command << " --no-user-switching"
          command << " --no-delete-pid-file"
          command << " --cleanup-pidfile #{Shellwords.escape @working_dir}/temp_dir_toucher.pid"
          command << " --report-file #{Shellwords.escape @working_dir}/report.json"
          command << " --ctl prestart_urls=#{Shellwords.escape prestart_urls_json}"
          add_param(command, :user, "--user")
          add_param(command, :log_file, "--log-file")
          add_param(command, :pid_file, "--pid-file")
          add_param(command, :instance_registry_dir, "--instance-registry-dir")
          add_param(command, :spawn_dir, "--spawn-dir")
          add_param(command, :data_buffer_dir, "--data-buffer-dir")
          add_param(command, :log_level, "--log-level")
          add_flag_param(command, :disable_log_prefix, "--disable-log-prefix")
          @options[:ctls].each do |ctl|
            command << " --ctl #{Shellwords.escape ctl}"
          end
          if @options[:user]
            command << " --default-user #{Shellwords.escape @options[:user]}"
          else
            user  = Etc.getpwuid(Process.uid).name
            begin
              group = Etc.getgrgid(Process.gid)
            rescue ArgumentError
              # Do nothing. On Heroku, it's normal that the group
              # database is broken.
            else
              command << " --default-group #{Shellwords.escape group.name}"
            end
            command << " --default-user #{Shellwords.escape user}"
          end

          # Arguments for supervised agent are between --BC/--EC and --BU/--EU
          command << " --BC"
          # The builtin engine cannot be used in combination with Mass Deployment,
          # so we know @apps always has 1 app.
          command << " --listen #{listen_address(@apps[0])}"
          command << " --no-graceful-exit"
          add_param(command, :socket_backlog, "--socket-backlog")
          add_param(command, :environment, "--environment")
          add_param(command, :app_type, "--app-type")
          add_param(command, :startup_file, "--startup-file")
          add_param(command, :app_start_command, "--app-start-command")
          add_param(command, :spawn_method, "--spawn-method")
          add_param(command, :restart_dir, "--restart-dir")
          add_param(command, :custom_error_page, "--custom-error-page")
          if @options.has_key?(:friendly_error_pages)
            if @options[:friendly_error_pages]
              command << " --force-friendly-error-pages"
            else
              command << " --disable-friendly-error-pages"
            end
          end
          if @options[:turbocaching] == false
            command << " --disable-turbocaching"
          end
          add_flag_param(command, :old_routing, "--old-routing")
          if @options[:abort_websockets_on_process_shutdown] == false
            command << " --no-abort-websockets-on-process-shutdown"
          end
          add_param(command, :force_max_concurrent_requests_per_process, "--force-max-concurrent-requests-per-process")
          add_flag_param(command, :load_shell_envvars, "--load-shell-envvars")
          add_flag_param(command, :preload_bundler, "--preload-bundler")
          add_param(command, :max_pool_size, "--max-pool-size")
          add_param(command, :min_instances, "--min-instances")
          add_param(command, :pool_idle_time, "--pool-idle-time")
          add_param(command, :max_preloader_idle_time, "--max-preloader-idle-time")
          add_param(command, :max_request_queue_size, "--max-request-queue-size")
          add_enterprise_param(command, :concurrency_model, "--concurrency-model")
          add_enterprise_param(command, :thread_count, "--app-thread-count")
          add_param(command, :max_requests, "--max-requests")
          add_enterprise_param(command, :max_request_time, "--max-request-time")
          add_enterprise_param(command, :memory_limit, "--memory-limit")
          add_enterprise_flag_param(command, :rolling_restarts, "--rolling-restarts")
          add_enterprise_flag_param(command, :resist_deployment_errors, "--resist-deployment-errors")
          add_enterprise_flag_param(command, :debugger, "--debugger")
          add_flag_param(command, :sticky_sessions, "--sticky-sessions")
          add_param(command, :vary_turbocache_by_cookie, "--vary-turbocache-by-cookie")
          add_param(command, :sticky_sessions_cookie_name, "--sticky-sessions-cookie-name")
          add_param(command, :sticky_sessions_cookie_attributes, "--sticky-sessions-cookie-attributes")
          add_param(command, :ruby, "--ruby")
          add_param(command, :python, "--python")
          add_param(command, :nodejs, "--nodejs")
          add_param(command, :meteor_app_settings, "--meteor-app-settings")
          add_param(command, :core_file_descriptor_ulimit, "--core-file-descriptor-ulimit")
          add_param(command, :app_file_descriptor_ulimit, "--app-file-descriptor-ulimit")
          add_flag_param(command, :disable_security_update_check, "--disable-security-update-check")
          add_param(command, :security_update_check_proxy, "--security-update-check-proxy")
          add_flag_param(command, :disable_anonymous_telemetry, "--disable-anonymous-telemetry")
          add_param(command, :anonymous_telemetry_proxy, "--anonymous-telemetry-proxy")

          command << " #{Shellwords.escape(@apps[0][:root])}"

          command << " --EC --BU"
          add_param(command, :core_file_descriptor_ulimit, "--core-file-descriptor-ulimit")

          {
            identifier: "#{AGENT_EXE} watchdog",
            start_command: command,
            ping_command: ping_spec,
            pid_file: @options[:pid_file],
            log_file: @options[:log_file],
            start_timeout: 25,
            stop_timeout: @options[:stop_timeout],
          }
        end

        def listen_address(options = @options, for_ping_port = false)
          if options[:socket_file]
            "unix:#{options[:socket_file]}"
          else
            "tcp://" + compose_ip_and_port(options[:address], options[:port])
          end
        end

        def prestart_urls_json
          PhusionPassenger::Utils::JSON.generate([ listen_url(@apps[0]) ])
        end

        def add_param(command, option_name, param_name)
          if value = @options[option_name]
            command << " #{param_name} #{Shellwords.escape value.to_s}"
          end
        end

        def add_flag_param(command, option_name, param_name)
          if value = @options[option_name]
            command << " #{param_name}"
          end
        end

        def add_enterprise_param(command, option_name, param_name)
          if value = @options[option_name]
            abort "The '#{option_name}' feature is only available in #{PROGRAM_NAME} " +
              "Enterprise. You are currently running the open source #{PROGRAM_NAME}. " +
              "Please learn more about and/or buy #{PROGRAM_NAME} Enterprise at " +
              "https://www.phusionpassenger.com/features#premium-features"
          end
        end

        def add_enterprise_flag_param(command, option_name, param_name)
          if value = @options[option_name]
            abort "The '#{option_name}' feature is only available in #{PROGRAM_NAME} " +
              "Enterprise. You are currently running the open source #{PROGRAM_NAME}. " +
              "Please learn more about and/or buy #{PROGRAM_NAME} Enterprise at " +
              "https://www.phusionpassenger.com/features#premium-features"
          end
        end

        def report_file_path
          @report_file_path ||= "#{@working_dir}/report.json"
        end

        def read_and_delete_report_file!
          report = File.open(report_file_path, "r:utf-8") do |f|
            Utils::JSON.parse(f.read)
          end
          # The report file may contain sensitive information, so delete it.
          File.unlink(report_file_path)
          @watchdog_lock_file_path = report["instance_dir"] + "/lock"
        end

        def process_is_alive?(pid)
          begin
            Process.kill(0, pid)
            true
          rescue Errno::ESRCH
            false
          rescue SystemCallError => e
            true
          end
        end

        #####################
      end # module BuiltinEngine
    end # module StartCommand
  end # module Standalone
end # module PhusionPassenger
