# encoding: binary
#  Phusion Passenger - https://www.phusionpassenger.com/
#  Copyright (c) 2011-2014 Phusion
#
#  "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
#
#  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 'socket'
require 'tmpdir'
PhusionPassenger.require_passenger_lib 'utils'
PhusionPassenger.require_passenger_lib 'native_support'

module PhusionPassenger

  # Provides shared functions for preloader apps.
  module PreloaderSharedHelpers
    extend self

    def init(options)
      if !Kernel.respond_to?(:fork)
        message = "Smart spawning is not available on this Ruby " +
          "implementation because it does not support `Kernel.fork`. "
        if ENV['SERVER_SOFTWARE'].to_s =~ /nginx/i
          message << "Please set `passenger_spawn_method` to `direct`."
        else
          message << "Please set `PassengerSpawnMethod` to `direct`."
        end
        raise(message)
      end
      return options
    end

    def accept_and_process_next_client(server_socket)
      original_pid = Process.pid
      client = server_socket.accept
      client.binmode
      begin
        command = client.readline
      rescue EOFError
        return nil
      end
      if command !~ /\n\Z/
        STDERR.puts "Command must end with a newline"
      elsif command == "spawn\n"
        while client.readline != "\n"
          # Do nothing.
        end

        # Improve copy-on-write friendliness.
        GC.start

        pid = fork
        if pid.nil?
          $0 = "#{$0} (forking...)"
          client.puts "OK"
          client.puts Process.pid
          client.flush
          client.sync = true
          return [:forked, client]
        elsif defined?(NativeSupport)
          NativeSupport.detach_process(pid)
        else
          Process.detach(pid)
        end
      else
        STDERR.puts "Unknown command '#{command.inspect}'"
      end
      return nil
    ensure
      if client && Process.pid == original_pid
        begin
          client.close
        rescue Errno::EINVAL
          # Work around OS X bug.
          # https://code.google.com/p/phusion-passenger/issues/detail?id=854
        end
      end
    end

    def run_main_loop(options)
      $0 = "Passenger AppPreloader: #{options['app_root']}"
      client = nil
      original_pid = Process.pid

      if defined?(NativeSupport)
        unix_path_max = NativeSupport::UNIX_PATH_MAX
      else
        unix_path_max = options.fetch('UNIX_PATH_MAX', 100).to_i
      end
      if options['socket_dir']
        socket_dir = options['socket_dir']
        socket_prefix = "preloader"
      else
        socket_dir = Dir.tmpdir
        socket_prefix = "PsgPreloader"
      end

      socket_filename = nil
      server = nil
      Utils.retry_at_most(128, Errno::EADDRINUSE) do
        socket_filename = "#{socket_dir}/#{socket_prefix}.#{rand(0xFFFFFFFF).to_s(36)}"
        socket_filename = socket_filename.slice(0, unix_path_max - 10)
        server = UNIXServer.new(socket_filename)
      end
      server.close_on_exec!
      File.chmod(0600, socket_filename)

      # Update the dump information just before telling the preloader that we're
      # ready because the HelperAgent will read and memorize this information.
      LoaderSharedHelpers.dump_all_information(options)

      puts "!> Ready"
      puts "!> socket: unix:#{socket_filename}"
      puts "!> "

      while true
        # We call ::select just in case someone overwrites the global select()
        # function by including ActionView::Helpers in the wrong place.
        # https://code.google.com/p/phusion-passenger/issues/detail?id=915
        ios = Kernel.select([server, STDIN])[0]
        if ios.include?(server)
          result, client = accept_and_process_next_client(server)
          if result == :forked
            STDIN.reopen(client)
            STDOUT.reopen(client)
            STDOUT.sync = true
            client.close
            return :forked
          end
        end
        if ios.include?(STDIN)
          if STDIN.tty?
            begin
              # Prevent bash from exiting when we press Ctrl-D.
              STDIN.read_nonblock(1)
            rescue Errno::EAGAIN
              # Do nothing.
            end
          end
          break
        end
      end
      return nil
    ensure
      server.close if server
      if original_pid == Process.pid
        File.unlink(socket_filename) rescue nil
      end
    end
  end

end # module PhusionPassenger
