#  Phusion Passenger - https://www.phusionpassenger.com/
#  Copyright (c) 2010-2013 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 'thread'
require 'etc'
require 'phusion_passenger'
require 'phusion_passenger/plugin'
require 'phusion_passenger/standalone/command'

# We lazy load as many libraries as possible not only to improve startup performance,
# but also to ensure that we don't require libraries before we've passed the dependency
# checking stage of the runtime installer.

module PhusionPassenger
module Standalone

class StartCommand < Command
	def self.description
		return "Start Phusion Passenger Standalone."
	end

	def initialize(args)
		super(args)
		@console_mutex = Mutex.new
		@termination_pipe = IO.pipe
		@threads = []
		@interruptable_threads = []
		@plugin = PhusionPassenger::Plugin.new('standalone/start_command', self, @options)
	end

	def run
		parse_my_options
		sanity_check_options

		require 'phusion_passenger/standalone/runtime_locator'
		@runtime_locator = RuntimeLocator.new(@options[:runtime_dir],
			@options[:nginx_version])
		ensure_runtime_installed
		exit if @options[:runtime_check_only]
		determine_various_resource_locations
		require_app_finder
		@app_finder = AppFinder.new(@args, @options)
		@apps = @app_finder.scan
		@plugin.call_hook(:found_apps, @apps)

		extra_controller_options = {}
		@plugin.call_hook(:before_creating_nginx_controller, extra_controller_options)
		create_nginx_controller(extra_controller_options)

		begin
			start_nginx
			show_intro_message
			if @options[:daemonize]
				if PlatformInfo.ruby_supports_fork?
					daemonize
				else
					daemonize_without_fork
				end
			end
			Thread.abort_on_exception = true
			@plugin.call_hook(:nginx_started, @nginx)
			########################
			########################
			touch_temp_dir_in_background
			watch_log_files_in_background if should_watch_logs?
			wait_until_nginx_has_exited if should_wait_until_nginx_has_exited?
		rescue Interrupt
			stop_threads
			stop_nginx
			exit 2
		rescue SignalException => signal
			stop_threads
			stop_nginx
			if signal.message == 'SIGINT' || signal.message == 'SIGTERM'
				exit 2
			else
				raise
			end
		rescue Exception => e
			stop_threads
			stop_nginx
			raise
		ensure
			stop_touching_temp_dir_in_background if should_wait_until_nginx_has_exited?
			stop_threads
		end
	ensure
		if @temp_dir
			FileUtils.remove_entry_secure(@temp_dir) rescue nil
		end
		@plugin.call_hook(:cleanup)
	end

private
	def require_file_utils
		require 'fileutils' unless defined?(FileUtils)
	end

	def parse_my_options
		description = "Starts Phusion Passenger Standalone and serve one or more Ruby web applications."
		parse_options!("start [directory]", description) do |opts|
			opts.on("-a", "--address HOST", String,
				wrap_desc("Bind to HOST address (default: #{@options[:address]})")) do |value|
				@options[:address] = value
				@options[:tcp_explicitly_given] = true
			end
			opts.on("-p", "--port NUMBER", Integer,
				wrap_desc("Use the given port number (default: #{@options[:port]})")) do |value|
				@options[:port] = value
				@options[:tcp_explicitly_given] = true
			end
			opts.on("-S", "--socket FILE", String,
				wrap_desc("Bind to Unix domain socket instead of TCP socket")) do |value|
				@options[:socket_file] = value
			end

			opts.separator ""
			opts.on("-e", "--environment ENV", String,
				wrap_desc("Framework environment (default: #{@options[:env]})")) do |value|
				@options[:env] = value
			end
			opts.on("-R", "--rackup FILE", String,
				wrap_desc("If Rack application detected, run this rackup file")) do |value|
				ENV["RACKUP_FILE"] = value
			end
			opts.on("--max-pool-size NUMBER", Integer,
				wrap_desc("Maximum number of application processes (default: #{@options[:max_pool_size]})")) do |value|
				@options[:max_pool_size] = value
			end
			opts.on("--min-instances NUMBER", Integer,
				wrap_desc("Minimum number of processes per application (default: #{@options[:min_instances]})")) do |value|
				@options[:min_instances] = value
			end
			opts.on("--spawn-method NAME", String,
				wrap_desc("The spawn method to use (default: #{@options[:spawn_method]})")) do |value|
				@options[:spawn_method] = value
			end
			opts.on("--rolling-restarts",
				wrap_desc("Enable rolling restarts (Enterprise only)")) do
				@options[:rolling_restarts] = true
			end
			opts.on("--resist-deployment-errors",
				wrap_desc("Enable deployment error resistance (Enterprise only)")) do
				@options[:resist_deployment_errors] = true
			end
			opts.on("--no-friendly-error-pages",
				wrap_desc("Disable passenger_friendly_error_pages")) do
				@options[:friendly_error_pages] = false
			end
			opts.on("--ssl",
				wrap_desc("Enable SSL support")) do
				@options[:ssl] = true
			end
			opts.on("--ssl-certificate PATH", String,
				wrap_desc("Specify the SSL certificate path")) do |val|
				@options[:ssl_certificate] = File.expand_path(val)
			end
			opts.on("--ssl-certificate-key PATH", String,
				wrap_desc("Specify the SSL key path")) do |val|
				@options[:ssl_certificate_key] = File.expand_path(val)
			end
			opts.on("--union-station-gateway HOST:PORT", String,
				wrap_desc("Specify Union Station Gateway host and port")) do |value|
				host, port = value.split(":", 2)
				port = port.to_i
				port = 443 if port == 0
				@options[:union_station_gateway_address] = host
				@options[:union_station_gateway_port] = port.to_i
			end
			opts.on("--union-station-key KEY", String,
				wrap_desc("Specify Union Station key")) do |value|
				@options[:union_station_key] = value
			end

			opts.separator ""
			opts.on("--ping-port NUMBER", Integer,
				wrap_desc("Use the given port number for checking whether Nginx is alive (default: same as the normal port)")) do |value|
				@options[:ping_port] = value
			end
			@plugin.call_hook(:parse_options, opts)

			opts.separator ""
			opts.on("-d", "--daemonize",
				wrap_desc("Daemonize into the background")) do
				@options[:daemonize] = true
			end
			opts.on("--user USERNAME", String,
				wrap_desc("User to run as. Ignored unless running as root.")) do |value|
				@options[:user] = value
			end
			opts.on("--log-file FILENAME", String,
				wrap_desc("Where to write log messages (default: console, or /dev/null when daemonized)")) do |value|
				@options[:log_file] = value
			end
			opts.on("--pid-file FILENAME", String,
				wrap_desc("Where to store the PID file")) do |value|
				@options[:pid_file] = value
			end

			opts.separator ""
			opts.on("--nginx-bin FILENAME", String,
				wrap_desc("Nginx binary to use as core")) do |value|
				@options[:nginx_bin] = value
			end
			opts.on("--nginx-version VERSION", String,
				wrap_desc("Nginx version to use as core (default: #{@options[:nginx_version]})")) do |value|
				@options[:nginx_version] = value
			end
			opts.on("--nginx-tarball FILENAME", String,
				wrap_desc("If Nginx needs to be installed, then the given tarball will " +
				          "be used instead of downloading from the Internet")) do |value|
				@options[:nginx_tarball] = File.expand_path(value)
			end
			opts.on("--binaries-url-root URL", String,
				wrap_desc("If Nginx needs to be installed, then the specified URL will be " +
				          "checked for binaries prior to a local build.")) do |value|
				@options[:binaries_url_root] = value
			end
			opts.on("--no-download-binaries",
				wrap_desc("Never download binaries")) do
				@options[:download_binaries] = false
			end
			opts.on("--runtime-dir DIRECTORY", String,
				wrap_desc("Directory to use for Phusion Passenger Standalone runtime files")) do |value|
				@options[:runtime_dir] = File.expand_path(value)
			end
			opts.on("--runtime-check-only",
				wrap_desc("Quit after checking whether the Phusion Passenger Standalone runtime files are installed")) do
				@options[:runtime_check_only] = true
			end
			opts.on("--no-compile-runtime",
				wrap_desc("Abort if runtime must be compiled")) do
				@options[:dont_compile_runtime] = true
			end
		end
		@plugin.call_hook(:done_parsing_options)
	end

	def sanity_check_options
		if @options[:tcp_explicitly_given] && @options[:socket_file]
			error "You cannot specify both --address/--port and --socket. Please choose either one."
			exit 1
		end
		if @options[:ssl] && !@options[:ssl_certificate]
			error "You specified --ssl. Please specify --ssl-certificate as well."
			exit 1
		end
		if @options[:ssl] && !@options[:ssl_certificate_key]
			error "You specified --ssl. Please specify --ssl-certificate-key as well."
			exit 1
		end
		check_port_bind_permission_and_display_sudo_suggestion
		check_port_availability
	end

	# Most platforms don't allow non-root processes to bind to a port lower than 1024.
	# Check whether this is the case for the current platform and if so, tell the user
	# that it must re-run Phusion Passenger Standalone with sudo.
	def check_port_bind_permission_and_display_sudo_suggestion
		if !@options[:socket_file] && @options[:port] < 1024 && Process.euid != 0
			begin
				TCPServer.new('127.0.0.1', @options[:port]).close
			rescue Errno::EACCES
				require 'phusion_passenger/platform_info/ruby'
				myself = `whoami`.strip
				error "Only the 'root' user can run this program on port #{@options[:port]}. " <<
				      "You are currently running as '#{myself}'. Please re-run this program " <<
				      "with root privileges with the following command:\n\n" <<

				      "  #{PlatformInfo.ruby_sudo_command} passenger start #{@original_args.join(' ')} --user=#{myself}\n\n" <<

				      "Don't forget the '--user' part! That will make Phusion Passenger Standalone " <<
				      "drop root privileges and switch to '#{myself}' after it has obtained " <<
				      "port #{@options[:port]}."
				exit 1
			end
		end
	end

	if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
		require 'java'

		def check_port(host_name, port)
			channel = java.nio.channels.SocketChannel.open
			begin
				address = java.net.InetSocketAddress.new(host_name, port)
				channel.configure_blocking(false)
				if channel.connect(address)
					return true
				end

				deadline = Time.now.to_f + 0.1
				done = false
				while true
					begin
						if channel.finish_connect
							return true
						end
					rescue java.net.ConnectException => e
						if e.message =~ /Connection refused/i
							return false
						else
							throw e
						end
					end

					# Not done connecting and no error.
					sleep 0.01
					if Time.now.to_f >= deadline
						return false
					end
				end
			ensure
				channel.close
			end
		end
	else
		def check_port(address, port)
			begin
				socket = Socket.new(Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0)
				sockaddr = Socket.pack_sockaddr_in(port, address)
				begin
					socket.connect_nonblock(sockaddr)
				rescue Errno::ENOENT, Errno::EINPROGRESS, Errno::EAGAIN, Errno::EWOULDBLOCK
					if select(nil, [socket], nil, 0.1)
						begin
							socket.connect_nonblock(sockaddr)
						rescue Errno::EISCONN
						rescue Errno::EINVAL
							if RUBY_PLATFORM =~ /freebsd/i
								raise Errno::ECONNREFUSED
							else
								raise
							end
						end
					else
						raise Errno::ECONNREFUSED
					end
				end
				return true
			rescue Errno::ECONNREFUSED
				return false
			ensure
				socket.close if socket && !socket.closed?
			end
		end
	end

	def check_port_availability
		if !@options[:socket_file] && check_port(@options[:address], @options[:port])
			error "The address #{@options[:address]}:#{@options[:port]} is already " <<
			      "in use by another process, perhaps another Phusion Passenger " <<
			      "Standalone instance.\n\n" <<
			      "If you want to run this Phusion Passenger Standalone instance on " <<
			      "another port, use the -p option, like this:\n\n" <<
			      "  passenger start -p #{@options[:port] + 1}"
			exit 1
		end
	end

	def should_watch_logs?
		return !@options[:daemonize] && @options[:log_file] != "/dev/null"
	end

	def should_wait_until_nginx_has_exited?
		return !@options[:daemonize]
	end

	# Returns the URL that Nginx will be listening on.
	def listen_url
		if @options[:socket_file]
			return @options[:socket_file]
		else
			if @options[:ssl]
				scheme = "https"
			else
				scheme = "http"
			end
			result = "#{scheme}://#{@options[:address]}"
			if @options[:port] != 80
				result << ":#{@options[:port]}"
			end
			result << "/"
			return result
		end
	end

	def install_runtime(runtime_locator)
		require 'phusion_passenger/standalone/runtime_installer'
		installer = RuntimeInstaller.new(
			:targets     => runtime_locator.install_targets,
			:support_dir => runtime_locator.support_dir_install_destination,
			:nginx_dir   => runtime_locator.nginx_binary_install_destination,
			:lib_dir     => runtime_locator.find_lib_dir || runtime_locator.support_dir_install_destination,
			:nginx_version     => @options[:nginx_version],
			:nginx_tarball     => @options[:nginx_tarball],
			:binaries_url_root => @options[:binaries_url_root],
			:download_binaries => @options.fetch(:download_binaries, true),
			:dont_compile_runtime => @options[:dont_compile_runtime],
			:plugin      => @plugin)
		return installer.run
	end

	def ensure_runtime_installed
		if @runtime_locator.everything_installed?
			if !File.exist?(@runtime_locator.find_nginx_binary)
				error "The Nginx binary '#{@runtime_locator.find_nginx_binary}' does not exist."
				exit 1
			end
		else
			if !@runtime_locator.find_support_dir && PhusionPassenger.natively_packaged?
				error "Your Phusion Passenger Standalone installation is broken: the support " +
					"files could not be found. Please reinstall Phusion Passenger Standalone. " +
					"If this problem persists, please contact your packager."
				exit 1
			end
			install_runtime(@runtime_locator) || exit(1)
			@runtime_locator.reload
		end
	end

	def start_nginx
		begin
			@nginx.start
		rescue DaemonController::AlreadyStarted
			begin
				pid = @nginx.pid
			rescue SystemCallError, IOError
				pid = nil
			end
			if pid
				error "Phusion Passenger Standalone is already running on PID #{pid}."
			else
				error "Phusion Passenger Standalone is already running."
			end
			exit 1
		rescue DaemonController::StartError => e
			error "Could not start Passenger Nginx core:\n#{e}"
			exit 1
		end
	end

	def show_intro_message
		puts "=============== Phusion Passenger Standalone web server started ==============="
		puts "PID file: #{@options[:pid_file]}"
		puts "Log file: #{@options[:log_file]}"
		puts "Environment: #{@options[:env]}"

		puts "Accessible via: #{listen_url}"

		puts
		if @options[:daemonize]
			puts "Serving in the background as a daemon."
		else
			puts "You can stop Phusion Passenger Standalone by pressing Ctrl-C."
		end
		puts "==============================================================================="
	end

	def daemonize_without_fork
		STDERR.puts "Unable to daemonize using the current Ruby interpreter " +
			"(#{PlatformInfo.ruby_command}) because it does not support forking."
		exit 1
	end

	def daemonize
		pid = fork
		if pid
			# Parent
			exit!(0)
		else
			# Child
			trap "HUP", "IGNORE"
			STDIN.reopen("/dev/null", "r")
			STDOUT.reopen(@options[:log_file], "a")
			STDERR.reopen(@options[:log_file], "a")
			STDOUT.sync = true
			STDERR.sync = true
			Process.setsid
		end
	end

	# Wait until the termination pipe becomes readable (a hint for threads
	# to shut down), or until the timeout has been reached. Returns true if
	# the termination pipe became readable, false if the timeout has been reached.
	def wait_on_termination_pipe(timeout)
		ios = select([@termination_pipe[0]], nil, nil, timeout)
		return !ios.nil?
	end

	def watch_log_file(log_file)
		if File.exist?(log_file)
			backward = 0
		else
			# tail bails out if the file doesn't exist, so wait until it exists.
			while !File.exist?(log_file)
				sleep 1
			end
			backward = 10
		end

		IO.popen("tail -f -n #{backward} \"#{log_file}\"", "rb") do |f|
			begin
				while true
					begin
						line = f.readline
						@console_mutex.synchronize do
							STDOUT.write(line)
							STDOUT.flush
						end
					rescue EOFError
						break
					end
				end
			ensure
				Process.kill('TERM', f.pid) rescue nil
			end
		end
	end

	def watch_log_files_in_background
		@apps.each do |app|
			thread = Thread.new do
				watch_log_file("#{app[:root]}/log/#{@options[:env]}.log")
			end
			@threads << thread
			@interruptable_threads << thread
		end
		thread = Thread.new do
			watch_log_file(@options[:log_file])
		end
		@threads << thread
		@interruptable_threads << thread
	end

	def touch_temp_dir_in_background
		require 'shellwords'
		script = Shellwords.escape("#{PhusionPassenger.helper_scripts_dir}/touch-dir.sh")
		dir    = Shellwords.escape(@temp_dir)
		@toucher = IO.popen("sh #{script} #{dir}", "r")
	end

	def stop_touching_temp_dir_in_background
		if @toucher
			begin
				Process.kill('TERM', @toucher.pid)
			rescue Errno::ESRCH, Errno::ECHILD
			end
			@toucher.close
		end
	end

	def wait_until_nginx_has_exited
		# Since Nginx is not our child process (it daemonizes or we daemonize)
		# we cannot use Process.waitpid to wait for it. A busy-sleep-loop with
		# Process.kill(0, pid) isn't very efficient. Instead we do this:
		#
		# Connect to Nginx and wait until Nginx disconnects the socket because of
		# timeout. Keep doing this until we can no longer connect.
		while true
			if @options[:socket_file]
				socket = UNIXSocket.new(@options[:socket_file])
			else
				socket = TCPSocket.new(@options[:address], nginx_ping_port)
			end
			begin
				socket.read rescue nil
			ensure
				socket.close rescue nil
			end
		end
	rescue Errno::ECONNREFUSED, Errno::ECONNRESET
	end

	def stop_nginx
		@console_mutex.synchronize do
			STDOUT.write("Stopping web server...")
			STDOUT.flush
			@nginx.stop
			STDOUT.puts " done"
			STDOUT.flush
		end
	end

	def stop_threads
		if !@termination_pipe[1].closed?
			@termination_pipe[1].write("x")
			@termination_pipe[1].close
		end
		@interruptable_threads.each do |thread|
			thread.terminate
		end
		@interruptable_threads = []
		@threads.each do |thread|
			thread.join
		end
		@threads = []
	end

	#### Config file template helpers ####

	def nginx_listen_address(options = @options, for_ping_port = false)
		if options[:socket_file]
			return "unix:" + File.expand_path(options[:socket_file])
		else
			if for_ping_port
				port = options[:ping_port]
			else
				port = options[:port]
			end
			return "#{options[:address]}:#{port}"
		end
	end

	def default_group_for(username)
		user = Etc.getpwnam(username)
		group = Etc.getgrgid(user.gid)
		return group.name
	end

	#################
end

end # module Standalone
end # module PhusionPassenger
