231 lines
7.0 KiB
Ruby
Executable File
231 lines
7.0 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 'platform_info'
|
|
PhusionPassenger.require_passenger_lib 'platform_info/ruby'
|
|
PhusionPassenger.require_passenger_lib 'admin_tools/memory_stats'
|
|
PhusionPassenger.require_passenger_lib 'utils/ansi_colors'
|
|
require 'optparse'
|
|
|
|
include PhusionPassenger
|
|
|
|
# Container for tabular data.
|
|
class Table
|
|
def initialize(column_names, colors)
|
|
@column_names = column_names
|
|
@rows = []
|
|
@colors = colors
|
|
end
|
|
|
|
def add_row(values)
|
|
@rows << values.to_a
|
|
end
|
|
|
|
def add_rows(list_of_rows)
|
|
list_of_rows.each do |row|
|
|
add_row(row)
|
|
end
|
|
end
|
|
|
|
def remove_column(name)
|
|
i = @column_names.index(name)
|
|
@column_names.delete_at(i)
|
|
@rows.each do |row|
|
|
row.delete_at(i)
|
|
end
|
|
end
|
|
|
|
def to_s(title = nil)
|
|
max_column_widths = [1] * @column_names.size
|
|
(@rows + [@column_names]).each do |row|
|
|
row.each_with_index do |value, i|
|
|
max_column_widths[i] = [value.to_s.size, max_column_widths[i]].max
|
|
end
|
|
end
|
|
|
|
format_string = max_column_widths.map{ |i| "%#{-i}s" }.join(" ")
|
|
header = sprintf(format_string, *@column_names).rstrip << "\n"
|
|
if title
|
|
free_space = header.size - title.size - 2
|
|
if free_space <= 0
|
|
left_bar_size = 3
|
|
right_bar_size = 3
|
|
else
|
|
left_bar_size = free_space / 2
|
|
right_bar_size = free_space - left_bar_size
|
|
end
|
|
result = "#{@colors.blue_bg}#{@colors.bold}#{@colors.yellow}\n"
|
|
result << "#{"-" * left_bar_size} #{title} #{"-" * right_bar_size}\n"
|
|
if !@rows.empty?
|
|
result << @colors.white
|
|
result << header
|
|
end
|
|
else
|
|
result = header.dup
|
|
end
|
|
if @rows.empty?
|
|
result << @colors.reset
|
|
else
|
|
result << ("-" * header.size) << "#{@colors.reset}\n"
|
|
@rows.each do |row|
|
|
result << sprintf(format_string, *row).rstrip << "\n"
|
|
end
|
|
end
|
|
result
|
|
end
|
|
end
|
|
|
|
# Parses the specific commandline options.
|
|
#
|
|
# Modeled after `passenger-status` logic, with minor tweaks.
|
|
#
|
|
class CommandLineOptionsParser
|
|
def parse
|
|
options = {}
|
|
parser = create_option_parser(options)
|
|
|
|
begin
|
|
parser.parse!
|
|
options
|
|
rescue OptionParser::ParseError => e
|
|
STDERR.puts e
|
|
STDERR.puts
|
|
STDERR.puts "Please see '--help' for valid options."
|
|
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def create_option_parser(options)
|
|
OptionParser.new do |opts|
|
|
opts.banner = "Usage: #{File.basename(__FILE__)} [-h|--help] [--no-apache] [--no-nginx]"
|
|
|
|
opts.separator ""
|
|
opts.separator "Tool for inspecting the application server and Phusion Passenger's memory statistics."
|
|
opts.separator ""
|
|
|
|
opts.separator "Options:"
|
|
|
|
opts.on("--no-apache", "Do not display the Apache statistics.") do
|
|
options[:no_apache] = true
|
|
end
|
|
opts.on("--no-nginx", "Do not display the Nginx statistics.") do
|
|
options[:no_nginx] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class App
|
|
def initialize
|
|
@stats = AdminTools::MemoryStats.new
|
|
@colors = Utils::AnsiColors.new
|
|
end
|
|
|
|
def start(options = {})
|
|
print_apache_stats = !options.fetch(:no_apache, false)
|
|
print_nginx_stats = !options.fetch(:no_nginx, false)
|
|
|
|
puts "Version: #{PhusionPassenger::VERSION_STRING}"
|
|
puts "Date : #{Time.now}"
|
|
|
|
if print_apache_stats
|
|
if @stats.apache_processes
|
|
print_process_list("Apache processes", @stats.apache_processes)
|
|
else
|
|
puts "#{@colors.blue_bg}#{@colors.bold}#{@colors.yellow}------------- Apache processes -------------#{@colors.reset}\n"
|
|
STDERR.puts "*** WARNING: The Apache executable cannot be found."
|
|
STDERR.puts "Please set the APXS2 environment variable to your 'apxs2' " <<
|
|
"executable's filename, or set the HTTPD environment variable " <<
|
|
"to your 'httpd' or 'apache2' executable's filename."
|
|
end
|
|
end
|
|
|
|
if print_nginx_stats
|
|
puts
|
|
print_process_list("Nginx processes", @stats.nginx_processes)
|
|
end
|
|
|
|
puts
|
|
print_process_list("Passenger processes", @stats.passenger_processes, :show_ppid => false)
|
|
|
|
if @stats.platform_provides_private_dirty_rss_information? &&
|
|
Process.euid != 0 &&
|
|
@stats.root_privileges_required_for_private_dirty_rss?
|
|
puts "*** WARNING: Please run this tool with #{@colors.bold}#{PlatformInfo.ruby_sudo_command}#{@colors.reset}. Otherwise the " <<
|
|
"private dirty RSS (a reliable metric for real memory usage) of processes cannot be determined."
|
|
end
|
|
end
|
|
|
|
private
|
|
def print_process_list(title, processes, options = {})
|
|
table = Table.new(%w{PID PPID VMSize Private Resident Name}, @colors)
|
|
table.add_rows(processes)
|
|
if options.has_key?(:show_ppid) && !options[:show_ppid]
|
|
table.remove_column('PPID')
|
|
end
|
|
if @stats.platform_provides_private_dirty_rss_information?
|
|
table.remove_column('Resident')
|
|
else
|
|
table.remove_column('Private')
|
|
end
|
|
puts table.to_s(title)
|
|
|
|
if @stats.platform_provides_private_dirty_rss_information?
|
|
total_private_dirty_rss = 0
|
|
some_private_dirty_rss_cannot_be_determined = false
|
|
processes.each do |p|
|
|
if p.private_dirty_rss.is_a?(Numeric)
|
|
total_private_dirty_rss += p.private_dirty_rss
|
|
else
|
|
some_private_dirty_rss_cannot_be_determined = true
|
|
end
|
|
end
|
|
puts "### Processes: #{processes.size}"
|
|
printf "### Total private dirty RSS: %.2f MB", total_private_dirty_rss / 1024.0
|
|
if some_private_dirty_rss_cannot_be_determined
|
|
puts " (?)"
|
|
else
|
|
puts
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
options = CommandLineOptionsParser.new.parse
|
|
App.new.start(options)
|