#!/usr/local/bin/ruby21
#  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.

## Magic comment: begin bootstrap ##
source_root = File.expand_path("..", File.dirname(__FILE__))
$LOAD_PATH.unshift("#{source_root}/lib")
begin
	require 'rubygems'
rescue LoadError
end
require 'phusion_passenger'
## Magic comment: end bootstrap ##

PhusionPassenger.locate_directories
PhusionPassenger.require_passenger_lib 'platform_info'
PhusionPassenger.require_passenger_lib 'admin_tools/server_instance'
PhusionPassenger.require_passenger_lib 'utils/ansi_colors'
require 'optparse'

include PhusionPassenger::AdminTools
include PhusionPassenger::Utils::AnsiColors

DEFAULT_OPTIONS = { :show => 'pool' }.freeze


##### Show status command #####

def command_show_status(argv, options)
	if argv.empty?
		server_instance = find_sole_server_instance
	else
		server_instance = find_server_instance_on_pid(argv[0].to_i)
	end
	show_status(server_instance, options)
end

def find_sole_server_instance
	server_instances = ServerInstance.list
	if server_instances.empty?
		abort "ERROR: Phusion Passenger doesn't seem to be running."
	elsif server_instances.size == 1
		return server_instances.first
	else
		puts "It appears that multiple Passenger instances are running. Please select a"
		puts "specific one by running:"
		puts
		puts "  passenger-status <PID>"
		puts
		puts "The following Passenger instances are running:"
		server_instances.each do |instance|
			puts "  PID: #{instance.pid}"
		end
		exit 1
	end
end

def find_server_instance_on_pid(pid)
	if server_instance = ServerInstance.for_pid(pid)
		return server_instance
	else
		abort "ERROR: there doesn't seem to be a Phusion Passenger instance running on PID #{pid}."
	end
end

def show_status(server_instance, options)
	if options[:show] != 'xml'
		puts "Version : #{PhusionPassenger::VERSION_STRING}"
		puts "Date    : #{Time.now}"
		puts "Instance: #{server_instance.pid}"
	end
	case options[:show]
	when 'pool'
		client = server_instance.connect(:role => :passenger_status)
		begin
			puts client.pool_status(:verbose => options[:verbose], :colorize => true)
		rescue SystemCallError => e
			STDERR.puts "*** ERROR: Cannot query status for Phusion Passenger instance #{server_instance.pid}:"
			STDERR.puts e.to_s
			exit 2
		end

	when 'requests'
		client = server_instance.connect(:role => :passenger_status)
		begin
			puts client.helper_agent_requests
		rescue SystemCallError => e
			STDERR.puts "*** ERROR: Cannot query status for Phusion Passenger instance #{server_instance.pid}:"
			STDERR.puts e.to_s
			exit 2
		end

	when 'backtraces'
		client = server_instance.connect(:role => :passenger_status)
		begin
			text = client.helper_agent_backtraces
		rescue SystemCallError => e
			STDERR.puts "*** ERROR: Cannot query status for Phusion Passenger instance #{control_process.pid}:"
			STDERR.puts e.to_s
			exit 2
		end
	
		# Colorize output
		text.gsub!(/^(Thread .*:)$/, BLACK_BG + YELLOW + '\1' + RESET)
		text.gsub!(/^( +in '.*? )(.*?)\(/, '\1' + BOLD + '\2' + RESET + '(')
	
		puts text
	
	when 'xml'
		client = server_instance.connect(:role => :passenger_status)
		begin
			xml = client.pool_xml
		rescue SystemCallError => e
			STDERR.puts "*** ERROR: Cannot query status for Phusion Passenger instance #{control_process.pid}:"
			STDERR.puts e.to_s
			exit 2
		end
		
		indented = format_with_xmllint(xml)
		if indented
			puts indented
		else
			puts xml
			STDERR.puts "*** Tip: if you install the 'xmllint' command then the XML output will be indented."
		end

	when 'union_station'
		client = server_instance.connect(:role => :passenger_status, :socket_name => 'logging_admin')
		begin
			response = client.logging_agent_status
		rescue SystemCallError => e
			STDERR.puts "*** ERROR: Cannot query status for Phusion Passenger instance #{control_process.pid}:"
			STDERR.puts e.to_s
			exit 2
		end

		puts response
	end
rescue ServerInstance::RoleDeniedError
	PhusionPassenger.require_passenger_lib 'platform_info/ruby'
	STDERR.puts "*** ERROR: You are not authorized to query the status for this Phusion " <<
		"Passenger instance. Please try again with '#{PhusionPassenger::PlatformInfo.ruby_sudo_command}'."
	exit 2
rescue ServerInstance::CorruptedDirectoryError
	STDERR.puts "*** ERROR: The server instance directory #{server_instance.path} is corrupted. " <<
		"This could have two causes:\n" <<
		"\n" <<
		"  1. The Phusion Passenger instance is no longer running, but failed to cleanup the directory. " <<
			"Please delete this directory and ignore the problem.\n" <<
		"  2. An external program corrupted the directory. Please restart Phusion Passenger.\n"
	exit 2
ensure
	client.close if client
end

def format_with_xmllint(xml)
	return nil if !PhusionPassenger::PlatformInfo.find_command('xmllint')
	require 'open3'
	require 'thread'
	ENV['XMLLINT_INDENT'] = '   '
	Open3.popen3("xmllint", "--format", "-") do |stdin, stdout, stderr|
		stdout_text = nil
		stderr_text = nil
		thread1 = Thread.new do
			stdin.write(xml)
			stdin.close
		end
		thread2 = Thread.new do
			stdout_text = stdout.read
			stdout.close
		end
		thread3 = Thread.new do
			stderr_text = stderr.read
			stderr.close
		end
		thread1.join
		thread2.join
		thread3.join
		
		if stdout_text.nil? || stdout_text.empty?
			if stderr_text !~ /No such file or directory/ && stderr_text !~ /command not found/
				STDERR.puts stderr_text
			end
			return nil
		else
			return stdout_text
		end
	end
end


##### Main command dispatcher #####

def create_option_parser(options)
	return OptionParser.new do |opts|
		opts.banner = "Usage: passenger-status [options] [Phusion Passenger's PID]"
		opts.separator ""
		opts.separator "Tool for inspecting Phusion Passenger's internal status."
		opts.separator ""

		opts.separator "Options:"
		opts.on("--show=pool|requests|backtraces|xml|union_station", String,
		        "Whether to show the pool's contents,\n" <<
		        "#{' ' * 37}the currently running requests,\n" <<
		        "#{' ' * 37}the backtraces of all threads or an XML\n" <<
		        "#{' ' * 37}description of the pool.") do |what|
			if what !~ /\A(pool|requests|backtraces|xml|union_station)\Z/
				STDERR.puts "Invalid argument for --show."
				exit 1
			else
				options[:show] = what
			end
		end
		opts.on("--verbose", "-v", "Show verbose information.") do
			options[:verbose] = true
		end
	end
end

def parse_argv
	options = DEFAULT_OPTIONS.dup
	parser = create_option_parser(options)
	begin
		parser.parse!
	rescue OptionParser::ParseError => e
		puts e
		puts
		puts "Please see '--help' for valid options."
		exit 1
	end

	return options
end

def infer_command
	if !ARGV[0] || ARGV[0] =~ /\A[0-9]+\Z/
		return [:show_status, ARGV.dup]
	else
		command_name, *argv = ARGV
		if respond_to?("command_#{command_name}")
			return [command_name, argv]
		else
			abort "ERROR: unrecognized command '#{command_name}'"
		end
	end
end

def start
	options = parse_argv
	command, argv = infer_command
	send("command_#{command}", argv, options)
end

start
