#!/usr/bin/env ruby
#
# Trivial Passenger memory monitor and recycler. See the configuration file
# /etc/passenger-recycler.yaml for options. Execute via SCL.
#
require 'yaml'

CONFIG = {}.freeze
CONFIG_FILE = '/etc/passenger-recycler.yaml'.freeze
CONFIG = YAML.load_file(CONFIG_FILE) if File.readable?(CONFIG_FILE)
exit 0 unless CONFIG[:ENABLED]

def running?(pid)
  Process.getpgid(pid) != -1
rescue Errno::ESRCH
  false
end

def debug(msg)
  puts(msg) if CONFIG[:DEBUG]
end

def verbose(msg)
  puts(msg) if CONFIG[:VERBOSE]
end

def kill(pid)
  return unless running?(pid) && CONFIG[:KILL_BUSY]
  verbose "Process #{pid} is still running, sending SIGKILL"
  Process.kill 'KILL', pid
  sleep 5
end

def process_status?(pid)
  if running?(pid)
    verbose "Process #{pid} still terminating, moving on..."
  else
    verbose "Process successfully #{pid} terminated"
  end
end

def show_passenger_status(status_messages)
  status_messages.each { |msg| verbose msg }
end

require 'phusion_passenger'
PhusionPassenger.locate_directories
require 'phusion_passenger/platform_info'
require 'phusion_passenger/platform_info/ruby'
require 'phusion_passenger/admin_tools/memory_stats'
stats = PhusionPassenger::AdminTools::MemoryStats.new
killed = 0

def get_process_start(pid)
  `ps -p#{pid} -o start=`.strip
rescue StandardError => e
  verbose "Error: #{e.message} \nReturning '?'"
  '?'
end

def get_rss_info(process)
  get_pid_mem_kb = process.private_dirty_rss || process.rss
  get_pid_mem_mb = format('%.0f', get_pid_mem_kb / 1024)
  [get_pid_mem_kb, get_pid_mem_mb]
end

stats.passenger_processes.each do |p|
  pid = p.pid.to_i
  started = get_process_start(pid)
  get_pid_mem_kb, get_pid_mem_mb = get_rss_info(p)
  debug "Checking #{pid} with RSS of #{get_pid_mem_kb}"
  next unless get_pid_mem_kb > CONFIG[:MAX_PRIV_RSS_MEMORY]
  status_ps = `ps -p#{pid} -u`
  status_all = `passenger-status 2> /dev/null`
  status_backtraces = `passenger-status --show=backtraces 2>/dev/null`
  verbose("Terminating #{pid} (started #{started}) with private RSS size of #{get_pid_mem_mb} MB")
  begin
    Process.kill 'SIGUSR1', pid
    sleep CONFIG[:GRACEFUL_SHUTDOWN_SLEEP]
    kill(pid)
    process_status?(pid)
    show_passenger_status([status_ps, status_all, status_backtraces]) if CONFIG[:SEND_STATUS]
    killed += 1
    exit(1) if killed >= CONFIG[:MAX_TERMINATION]
  rescue Errno::ESRCH
    puts "#{Process.pid}: #{pid} is NOT running"
  end
end
exit 0
