mirror of
https://github.com/puma/puma.git
synced 2022-11-09 13:48:40 -05:00
ff6f08c87a
Using APP_ENV to set the environment is supported by Sinatra and Sidekiq, so it makes sense to support the same behavior in Puma. Like Sidekiq, APP_ENV will take precedence over RACK_ENV and RAILS_ENV. APP_ENV defers to any argument passed via the --environment flag. Closes https://github.com/puma/puma/issues/2692
306 lines
8 KiB
Ruby
306 lines
8 KiB
Ruby
# 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
|
|
|
|
# values must be string or nil
|
|
# value of `nil` means command cannot be processed via signal
|
|
# @version 5.0.3
|
|
CMD_PATH_SIG_MAP = {
|
|
'gc' => nil,
|
|
'gc-stats' => nil,
|
|
'halt' => 'SIGQUIT',
|
|
'phased-restart' => 'SIGUSR1',
|
|
'refork' => 'SIGURG',
|
|
'reload-worker-directory' => nil,
|
|
'restart' => 'SIGUSR2',
|
|
'start' => nil,
|
|
'stats' => nil,
|
|
'status' => '',
|
|
'stop' => 'SIGTERM',
|
|
'thread-backtraces' => nil
|
|
}.freeze
|
|
|
|
# @deprecated 6.0.0
|
|
COMMANDS = CMD_PATH_SIG_MAP.keys.freeze
|
|
|
|
# commands that cannot be used in a request
|
|
NO_REQ_COMMANDS = %w{refork}.freeze
|
|
|
|
# @version 5.0.0
|
|
PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}.freeze
|
|
|
|
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['APP_ENV'] || 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) (#{CMD_PATH_SIG_MAP.keys.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
|
|
@stdout.puts Const::PUMA_VERSION
|
|
exit
|
|
end
|
|
end
|
|
|
|
opts.order!(argv) { |a| opts.terminate a }
|
|
opts.parse!
|
|
|
|
@command = argv.shift
|
|
|
|
# check presence of command
|
|
unless @command
|
|
raise "Available commands: #{CMD_PATH_SIG_MAP.keys.join(", ")}"
|
|
end
|
|
|
|
unless CMD_PATH_SIG_MAP.key? @command
|
|
raise "Invalid command: #{@command}"
|
|
end
|
|
|
|
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
|
|
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
|
|
@pid = File.read(@pidfile, mode: 'rb:UTF-8').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 { |ssl| ssl.sync_close = true } # default is false
|
|
.tap(&:connect)
|
|
when 'tcp'
|
|
TCPSocket.new uri.host, uri.port
|
|
when 'unix'
|
|
# check for abstract UNIXSocket
|
|
UNIXSocket.new(@control_url.start_with?('unix://@') ?
|
|
"\0#{uri.host}#{uri.path}" : "#{uri.host}#{uri.path}")
|
|
else
|
|
raise "Invalid scheme: #{uri.scheme}"
|
|
end
|
|
|
|
if @command == 'status'
|
|
message 'Puma is started'
|
|
elsif NO_REQ_COMMANDS.include? @command
|
|
raise "Invalid request command: #{@command}"
|
|
else
|
|
url = "/#{@command}"
|
|
|
|
if @control_auth_token
|
|
url = url + "?token=#{@control_auth_token}"
|
|
end
|
|
|
|
server.syswrite "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
|
|
if server
|
|
if uri.scheme == 'ssl'
|
|
server.sysclose
|
|
else
|
|
server.close unless server.closed?
|
|
end
|
|
end
|
|
end
|
|
|
|
def send_signal
|
|
unless @pid
|
|
raise 'Neither pid nor control url available'
|
|
end
|
|
|
|
begin
|
|
sig = CMD_PATH_SIG_MAP[@command]
|
|
|
|
if sig.nil?
|
|
@stdout.puts "'#{@command}' not available via pid only"
|
|
@stdout.flush unless @stdout.sync
|
|
return
|
|
elsif sig.start_with? 'SIG'
|
|
Process.kill sig, @pid
|
|
elsif @command == 'status'
|
|
begin
|
|
Process.kill 0, @pid
|
|
@stdout.puts 'Puma is started'
|
|
@stdout.flush unless @stdout.sync
|
|
rescue Errno::ESRCH
|
|
raise 'Puma is not running'
|
|
end
|
|
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? || @control_url
|
|
send_request
|
|
else
|
|
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
|