123456789_123456789_123456789_123456789_123456789_

Class: Puma::ControlCLI

Relationships & Source Files
Inherits: Object
Defined in: lib/puma/control_cli.rb

Constant Summary

  • CMD_PATH_SIG_MAP =

    values must be string or nil value of nil means command cannot be processed via signal

    Version:

    • 5.0.3

    # File 'lib/puma/control_cli.rb', line 15
    {
      'gc'       => nil,
      'gc-stats' => nil,
      'halt'              => 'SIGQUIT',
      'info'              => 'SIGINFO',
      'phased-restart'    => 'SIGUSR1',
      'refork'            => 'SIGURG',
      'reload-worker-directory' => nil,
      'reopen-log'        => 'SIGHUP',
      'restart'           => 'SIGUSR2',
      'start'    => nil,
      'stats'    => nil,
      'status'   => '',
      'stop'              => 'SIGTERM',
      'thread-backtraces' => nil,
      'worker-count-down' => 'SIGTTOU',
      'worker-count-up'   => 'SIGTTIN'
    }.freeze
  • NO_REQ_COMMANDS =

    commands that cannot be used in a request

    # File 'lib/puma/control_cli.rb', line 35
    %w[info reopen-log worker-count-down worker-count-up].freeze
  • PRINTABLE_COMMANDS =

    Version:

    • 5.0.0

    # File 'lib/puma/control_cli.rb', line 38
    %w[gc-stats stats thread-backtraces].freeze

Class Method Summary

Instance Method Summary

Constructor Details

.new(argv, stdout = STDOUT, stderr = STDERR) ⇒ ControlCLI

[ GitHub ]

  
# File 'lib/puma/control_cli.rb', line 40

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
      require_relative 'configuration'
      require_relative 'log_writer'

      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

Instance Method Details

#message(msg)

[ GitHub ]

  
# File 'lib/puma/control_cli.rb', line 143

def message(msg)
  @stdout.puts msg unless @quiet
end

#prepare_configuration

[ GitHub ]

  
# File 'lib/puma/control_cli.rb', line 147

def prepare_configuration
  if @state
    unless File.exist? @state
      raise "State file not found: #{@state}"
    end

    require_relative 'state_file'

    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

#run

[ GitHub ]

  
# File 'lib/puma/control_cli.rb', line 277

def run
  return start if @command == 'start'
  prepare_configuration

  if Puma.windows? || @control_url && !NO_REQ_COMMANDS.include?(@command)
    send_request
  else
    send_signal
  end

rescue => e
  message e.message
  exit 1
end

#send_request

[ GitHub ]

  
# File 'lib/puma/control_cli.rb', line 167

def send_request
  uri = URI.parse @control_url

  host = uri.host

  # create server object by scheme
  server =
    case uri.scheme
    when 'ssl'
      require 'openssl'
      host = host[1..-2] if host&.start_with? '['
      OpenSSL::SSL::SSLSocket.new(
        TCPSocket.new(host, uri.port),
        OpenSSL::SSL::SSLContext.new)
        .tap { |ssl| ssl.sync_close = true }  # default is false
        .tap(&:connect)
    when 'tcp'
      host = host[1..-2] if host&.start_with? '['
      TCPSocket.new host, uri.port
    when 'unix'
      # check for abstract UNIXSocket
      UNIXSocket.new(@control_url.start_with?('unix://@') ?
        "\0#{host}#{uri.path}" : "#{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.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

#send_signal

[ GitHub ]

  
# File 'lib/puma/control_cli.rb', line 238

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'
      if Signal.list.key? sig.sub(/\ASIG/, '')
        Process.kill sig, @pid
      else
        raise "Signal '#{sig}' not available'"
      end
    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

#start (private)

[ GitHub ]

  
# File 'lib/puma/control_cli.rb', line 293

def start
  require_relative '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

  log_writer = Puma::LogWriter.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, log_writer
  cli.run
end