# frozen_string_literal: true require 'optparse' require_relative 'state_file' require_relative 'const' require_relative 'detect' require_relative 'configuration' require 'uri' require 'socket' module Puma class ControlCLI COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats thread-backtraces} PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces} def initialize(argv, stdout=STDOUT, stderr=STDERR) @state = nil @quiet = false @pidfile = nil @pid = nil @control_url = nil @control_auth_token = nil @config_file = nil @command = nil @environment = ENV['RACK_ENV'] || ENV['RAILS_ENV'] @argv = argv.dup @stdout = stdout @stderr = stderr @cli_options = {} opts = OptionParser.new do |o| o.banner = "Usage: pumactl (-p PID | -P pidfile | -S status_file | -C url -T token | -F config.rb) (#{COMMANDS.join("|")})" o.on "-S", "--state PATH", "Where the state file to use is" do |arg| @state = arg end o.on "-Q", "--quiet", "Not display messages" do |arg| @quiet = true end o.on "-P", "--pidfile PATH", "Pid file" do |arg| @pidfile = arg end o.on "-p", "--pid PID", "Pid" do |arg| @pid = arg.to_i end o.on "-C", "--control-url URL", "The bind url to use for the control server" do |arg| @control_url = arg end o.on "-T", "--control-token TOKEN", "The token to use as authentication for the control server" do |arg| @control_auth_token = arg end o.on "-F", "--config-file PATH", "Puma config script" do |arg| @config_file = arg end o.on "-e", "--environment ENVIRONMENT", "The environment to run the Rack app on (default development)" do |arg| @environment = arg end o.on_tail("-H", "--help", "Show this message") do @stdout.puts o exit end o.on_tail("-V", "--version", "Show version") do puts Const::PUMA_VERSION exit end end opts.order!(argv) { |a| opts.terminate a } opts.parse! @command = argv.shift unless @config_file == '-' environment = @environment || 'development' if @config_file.nil? @config_file = %W(config/puma/#{environment}.rb config/puma.rb).find do |f| File.exist?(f) end end if @config_file config = Puma::Configuration.new({ config_files: [@config_file] }, {}) config.load @state ||= config.options[:state] @control_url ||= config.options[:control_url] @control_auth_token ||= config.options[:control_auth_token] @pidfile ||= config.options[:pidfile] end end # check present of command unless @command raise "Available commands: #{COMMANDS.join(", ")}" end unless COMMANDS.include? @command raise "Invalid command: #{@command}" end rescue => e @stdout.puts e.message exit 1 end def message(msg) @stdout.puts msg unless @quiet end def prepare_configuration if @state unless File.exist? @state raise "State file not found: #{@state}" end sf = Puma::StateFile.new sf.load @state @control_url = sf.control_url @control_auth_token = sf.control_auth_token @pid = sf.pid elsif @pidfile # get pid from pid_file File.open(@pidfile) { |f| @pid = f.read.to_i } end end def send_request uri = URI.parse @control_url # create server object by scheme server = case uri.scheme when "ssl" require 'openssl' OpenSSL::SSL::SSLSocket.new( TCPSocket.new(uri.host, uri.port), OpenSSL::SSL::SSLContext.new ).tap(&:connect) when "tcp" TCPSocket.new uri.host, uri.port when "unix" UNIXSocket.new "#{uri.host}#{uri.path}" else raise "Invalid scheme: #{uri.scheme}" end if @command == "status" message "Puma is started" else url = "/#{@command}" if @control_auth_token url = url + "?token=#{@control_auth_token}" end server << "GET #{url} HTTP/1.0\r\n\r\n" unless data = server.read raise "Server closed connection before responding" end response = data.split("\r\n") if response.empty? raise "Server sent empty response" end (@http,@code,@message) = response.first.split(" ",3) if @code == "403" raise "Unauthorized access to server (wrong auth token)" elsif @code == "404" raise "Command error: #{response.last}" elsif @code != "200" raise "Bad response from server: #{@code}" end message "Command #{@command} sent success" message response.last if PRINTABLE_COMMANDS.include?(@command) end ensure server.close if server && !server.closed? end def send_signal unless @pid raise "Neither pid nor control url available" end begin case @command when "restart" Process.kill "SIGUSR2", @pid when "halt" Process.kill "QUIT", @pid when "stop" Process.kill "SIGTERM", @pid when "stats" puts "Stats not available via pid only" return when "reload-worker-directory" puts "reload-worker-directory not available via pid only" return when "phased-restart" Process.kill "SIGUSR1", @pid when "status" begin Process.kill 0, @pid puts "Puma is started" rescue Errno::ESRCH raise "Puma is not running" end return else return end rescue SystemCallError if @command == "restart" start else raise "No pid '#{@pid}' found" end end message "Command #{@command} sent success" end def run return start if @command == "start" prepare_configuration if Puma.windows? send_request else @control_url ? send_request : send_signal end rescue => e message e.message exit 1 end private def start require 'puma/cli' run_args = [] run_args += ["-S", @state] if @state run_args += ["-q"] if @quiet run_args += ["--pidfile", @pidfile] if @pidfile run_args += ["--control-url", @control_url] if @control_url run_args += ["--control-token", @control_auth_token] if @control_auth_token run_args += ["-C", @config_file] if @config_file run_args += ["-e", @environment] if @environment events = Puma::Events.new @stdout, @stderr # replace $0 because puma use it to generate restart command puma_cmd = $0.gsub(/pumactl$/, 'puma') $0 = puma_cmd if File.exist?(puma_cmd) cli = Puma::CLI.new run_args, events cli.run end end end