375 lines
12 KiB
Ruby
Executable File
375 lines
12 KiB
Ruby
Executable File
#!/usr/bin/ruby
|
|
# Phusion Passenger - https://www.phusionpassenger.com/
|
|
# Copyright (c) 2010-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.
|
|
|
|
|
|
ENV["PASSENGER_LOCATION_CONFIGURATION_FILE"] = "/usr/share/passenger/phusion_passenger/locations.ini"
|
|
begin
|
|
require 'rubygems'
|
|
rescue LoadError
|
|
end
|
|
require '/usr/share/passenger/phusion_passenger'
|
|
|
|
|
|
PhusionPassenger.locate_directories
|
|
PhusionPassenger.require_passenger_lib 'constants'
|
|
PhusionPassenger.require_passenger_lib 'platform_info'
|
|
PhusionPassenger.require_passenger_lib 'admin_tools/instance_registry'
|
|
PhusionPassenger.require_passenger_lib 'config/utils'
|
|
PhusionPassenger.require_passenger_lib 'utils/ansi_colors'
|
|
require 'optparse'
|
|
require 'socket'
|
|
require 'json'
|
|
require 'net/http'
|
|
|
|
include PhusionPassenger::SharedConstants
|
|
include PhusionPassenger::AdminTools
|
|
include PhusionPassenger::Utils::AnsiColors
|
|
|
|
DEFAULT_OPTIONS = { :show => 'pool', :color => STDOUT.tty? }.freeze
|
|
|
|
|
|
##### Show status command #####
|
|
|
|
def command_show_status(argv, options)
|
|
if argv.empty?
|
|
instance = find_sole_instance
|
|
elsif options[:pid_identifier]
|
|
instance = find_instance_by_watchdog_pid(argv[0].to_i)
|
|
else
|
|
instance = find_instance_by_name_prefix(argv[0])
|
|
end
|
|
show_status(instance, options)
|
|
end
|
|
|
|
def find_sole_instance
|
|
instances = InstanceRegistry.new.list
|
|
if instances.empty?
|
|
abort "ERROR: #{PROGRAM_NAME} doesn't seem to be running. If you " +
|
|
"are sure that it is running, then the causes of this problem could be:\n\n" +
|
|
"1. You customized the instance registry directory using Apache's " +
|
|
"PassengerInstanceRegistryDir option, Nginx's passenger_instance_registry_dir " +
|
|
"option, or #{PROGRAM_NAME} Standalone's --instance-registry-dir command line " +
|
|
"argument. If so, please set the environment variable PASSENGER_INSTANCE_REGISTRY_DIR " +
|
|
"to that directory and run passenger-status again.\n" +
|
|
"2. The instance directory has been removed by an operating system background service. " +
|
|
"Please set a different instance registry directory using Apache's " +
|
|
"PassengerInstanceRegistryDir option, Nginx's passenger_instance_registry_dir " +
|
|
"option, or #{PROGRAM_NAME} Standalone's --instance-registry-dir command line " +
|
|
"argument."
|
|
elsif instances.size == 1
|
|
return instances.first
|
|
else
|
|
puts "It appears that multiple #{PROGRAM_NAME} instances are running. Please"
|
|
puts "select a specific one by running:"
|
|
puts
|
|
puts " passenger-status <NAME>"
|
|
puts
|
|
PhusionPassenger::Config::Utils.list_all_passenger_instances(instances)
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
def find_instance_by_name_prefix(name)
|
|
instance = InstanceRegistry.new.find_by_name_prefix(name)
|
|
if instance == :ambigious
|
|
abort "ERROR: there are multiple instances whose name start with '#{name}'. Please specify the full name."
|
|
elsif instance
|
|
return instance
|
|
else
|
|
abort "ERROR: there doesn't seem to be a #{PROGRAM_NAME} instance running with the name '#{name}'."
|
|
end
|
|
end
|
|
|
|
def find_instance_by_watchdog_pid(pid)
|
|
instance = InstanceRegistry.new.find_by_watchdog_pid(pid)
|
|
|
|
if instance
|
|
return instance
|
|
else
|
|
abort "ERROR: there doesn't seem to be a #{PROGRAM_NAME} instance running with the pid #{pid}."
|
|
end
|
|
end
|
|
|
|
def show_status(instance, options)
|
|
# if the noshow override is not specified, the default is to show the header, unless show=xml
|
|
if options[:noheader] != true && !['xml','json'].include?(options[:show])
|
|
print_header(STDOUT, instance)
|
|
end
|
|
|
|
case options[:show]
|
|
when 'pool'
|
|
request = Net::HTTP::Get.new("/pool.txt?colorize=#{options[:color]}&verbose=#{options[:verbose]}")
|
|
try_performing_ro_admin_basic_auth(request, instance)
|
|
response = instance.http_request("agents.s/core_api", request)
|
|
if response.code.to_i / 100 == 2
|
|
puts response.body
|
|
elsif response.code.to_i == 401
|
|
if response["pool-empty"] == "true"
|
|
puts "#{PROGRAM_NAME} is currently not serving any applications."
|
|
else
|
|
print_permission_error_message
|
|
exit 2
|
|
end
|
|
else
|
|
STDERR.puts "*** An error occured."
|
|
STDERR.puts "#{response.code}: #{response.body}"
|
|
exit 2
|
|
end
|
|
|
|
when 'requests', 'server'
|
|
request = Net::HTTP::Get.new("/server.json")
|
|
try_performing_ro_admin_basic_auth(request, instance)
|
|
response = instance.http_request("agents.s/core_api", request)
|
|
if response.code.to_i / 100 == 2
|
|
puts response.body
|
|
elsif response.code.to_i == 401
|
|
print_permission_error_message
|
|
exit 2
|
|
else
|
|
STDERR.puts "*** An error occured."
|
|
STDERR.puts "#{response.code}: #{response.body}"
|
|
exit 2
|
|
end
|
|
|
|
when 'backtraces'
|
|
request = Net::HTTP::Get.new("/backtraces.txt")
|
|
try_performing_ro_admin_basic_auth(request, instance)
|
|
response = instance.http_request("agents.s/core_api", request)
|
|
if response.code.to_i / 100 == 2
|
|
text = response.body
|
|
# Colorize output
|
|
color = PhusionPassenger::Utils::AnsiColors.new
|
|
text.gsub!(/^(Thread .*:)$/, color.black_bg + color.yellow + '\1' + color.reset)
|
|
text.gsub!(/^( +in '.*? )(.*?)\(/, '\1' + color.bold + '\2' + color.reset + '(')
|
|
puts text
|
|
elsif response.code.to_i == 401
|
|
print_permission_error_message
|
|
exit 2
|
|
else
|
|
STDERR.puts "*** An error occured."
|
|
STDERR.puts "#{response.code}: #{response.body}"
|
|
exit 2
|
|
end
|
|
|
|
when 'xml'
|
|
request = Net::HTTP::Get.new("/pool.xml?secrets=#{options[:verbose]}")
|
|
try_performing_ro_admin_basic_auth(request, instance)
|
|
response = instance.http_request("agents.s/core_api", request)
|
|
if response.code.to_i / 100 == 2
|
|
indented = format_with_xmllint(response.body)
|
|
if indented
|
|
puts indented
|
|
else
|
|
puts response.body
|
|
STDERR.puts "*** Tip: if you install the 'xmllint' command then the XML output will be indented."
|
|
end
|
|
elsif response.code.to_i == 401
|
|
if response["pool-empty"] == "true"
|
|
puts "#{PROGRAM_NAME} is currently not serving any applications."
|
|
else
|
|
print_permission_error_message
|
|
exit 2
|
|
end
|
|
else
|
|
STDERR.puts "*** An error occured."
|
|
STDERR.puts "#{response.code}: #{response.body}"
|
|
exit 2
|
|
end
|
|
|
|
when 'json'
|
|
request = Net::HTTP::Get.new("/pool.json?secrets=#{options[:verbose]}")
|
|
try_performing_ro_admin_basic_auth(request, instance)
|
|
response = instance.http_request("agents.s/core_api", request)
|
|
if response.code.to_i / 100 == 2
|
|
indented = JSON.pretty_generate(JSON.parse(response.body))
|
|
if indented
|
|
puts indented
|
|
else
|
|
puts response.body
|
|
end
|
|
elsif response.code.to_i == 401
|
|
if response["pool-empty"] == "true"
|
|
puts "#{PROGRAM_NAME} is currently not serving any applications."
|
|
else
|
|
print_permission_error_message
|
|
exit 2
|
|
end
|
|
else
|
|
STDERR.puts "*** An error occured."
|
|
STDERR.puts "#{response.code}: #{response.body}"
|
|
exit 2
|
|
end
|
|
|
|
when 'union_station'
|
|
request = Net::HTTP::Get.new("/server.json")
|
|
try_performing_ro_admin_basic_auth(request, instance)
|
|
response = instance.http_request("agents.s/ust_router_api", request)
|
|
if response.code.to_i / 100 == 2
|
|
puts response.body
|
|
elsif response.code.to_i == 401
|
|
print_permission_error_message
|
|
exit 2
|
|
else
|
|
STDERR.puts "*** An error occured."
|
|
STDERR.puts "#{response.code}: #{response.body}"
|
|
exit 2
|
|
end
|
|
end
|
|
end
|
|
|
|
def print_header(io, instance)
|
|
io.puts "Version : #{PhusionPassenger::VERSION_STRING}"
|
|
io.puts "Date : #{Time.now}"
|
|
io.puts "Instance: #{instance.name} (#{instance.server_software})"
|
|
io.puts
|
|
end
|
|
|
|
def try_performing_ro_admin_basic_auth(request, instance)
|
|
begin
|
|
password = instance.read_only_admin_password
|
|
rescue Errno::EACCES
|
|
return
|
|
end
|
|
request.basic_auth("ro_admin", password)
|
|
end
|
|
|
|
def print_permission_error_message
|
|
PhusionPassenger.require_passenger_lib 'platform_info/ruby'
|
|
STDERR.puts "*** ERROR: You are not authorized to query the status for this " <<
|
|
"#{PROGRAM_NAME} instance. Please try again with '#{PhusionPassenger::PlatformInfo.ruby_sudo_command}'."
|
|
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|
|
|
nl = "\n" << " " * 37
|
|
opts.banner = "Usage: passenger-status [options] [instance name]"
|
|
opts.separator ""
|
|
opts.separator "Tool for inspecting Phusion Passenger's internal status."
|
|
opts.separator ""
|
|
|
|
opts.separator "Options:"
|
|
opts.on("--show=pool|server|backtraces|xml|json|union_station", String,
|
|
"Whether to show the pool's contents,#{nl}" <<
|
|
"the currently running requests,#{nl}" <<
|
|
"the backtraces of all threads or an XML#{nl}" <<
|
|
"or JSON description of the pool.") do |what|
|
|
if what !~ /\A(pool|server|requests|backtraces|xml|json|union_station)\Z/
|
|
STDERR.puts "Invalid argument for --show."
|
|
exit 1
|
|
else
|
|
options[:show] = what
|
|
end
|
|
end
|
|
opts.on("--no-header", "Do not display an informative header#{nl}" <<
|
|
"containing the timestamp, version number,#{nl}" <<
|
|
"etc.") do
|
|
options[:noheader] = true
|
|
end
|
|
opts.on("--force-colors", "Display colors even if stdout is not a TTY") do
|
|
options[:color] = true
|
|
end
|
|
opts.on("--pid-identifier", "Identify instance by [Watchdog] PID, rather than name.") do
|
|
options[:pid_identifier] = true
|
|
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] !~ /^-/
|
|
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
|