123456789_123456789_123456789_123456789_123456789_

Class: Puma::Launcher

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

Overview

Launcher is the single entry point for starting a ::Puma server based on user configuration. It is responsible for taking user supplied arguments and resolving them with configuration in config/puma.rb or config/puma/<env>.rb.

It is responsible for either launching a cluster of ::Puma workers or a single puma server.

Constant Summary

  • KEYS_NOT_TO_PERSIST_IN_STATE =
    # File 'lib/puma/launcher.rb', line 18
    [
      :logger, :lowlevel_error_handler,
      :before_worker_shutdown, :before_worker_boot, :before_worker_fork,
      :after_worker_boot, :before_fork, :on_restart
    ]

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(conf, launcher_args = {}) ⇒ Launcher

Returns an instance of Launcher

conf A Configuration object indicating how to run the server.

launcher_args A Hash that currently has one required key :events, this is expected to hold an object similar to an Events.stdio, this object will be responsible for broadcasting Puma's internal state to a logging destination. An optional key :argv can be supplied, this should be an array of strings, these arguments are re-used when restarting the puma server.

Examples:

conf = Puma::Configuration.new do |user_config|
  user_config.threads 1, 10
  user_config.app do |env|
    [200, {}, ["hello world"]]
  end
end
Puma::Launcher.new(conf, events: Puma::Events.stdio).run
[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 43

def initialize(conf, launcher_args={})
  @runner        = nil
  @events        = launcher_args[:events] || Events::DEFAULT
  @argv          = launcher_args[:argv] || []
  @original_argv = @argv.dup
  @config        = conf

  @binder        = Binder.new(@events, conf)
  @binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
  @binder.create_activated_fds(ENV).each { |k| ENV.delete k }

  @environment = conf.environment

  # Advertise the Configuration
  Puma.cli_config = @config if defined?(Puma.cli_config)

  @config.load

  if @config.options[:bind_to_activated_sockets]
    @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
      @config.options[:binds],
      @config.options[:bind_to_activated_sockets] == 'only'
    )
  end

  @options = @config.options
  @config.clamp

  @events.formatter = Events::PidFormatter.new if clustered?
  @events.formatter = options[:log_formatter] if @options[:log_formatter]

  generate_restart_data

  if clustered? && !Process.respond_to?(:fork)
    unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
  end

  Dir.chdir(@restart_dir)

  prune_bundler if prune_bundler?

  @environment = @options[:environment] if @options[:environment]
  set_rack_environment

  if clustered?
    @options[:logger] = @events

    @runner = Cluster.new(self, @events)
  else
    @runner = Single.new(self, @events)
  end
  Puma.stats_object = @runner

  @status = :run

  log_config if ENV['PUMA_LOG_CONFIG']
end

Instance Attribute Details

#binder (readonly)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 101

attr_reader :binder, :events, :config, :options, :restart_dir

#clustered?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 367

def clustered?
  (@options[:workers] || 0) > 0
end

#config (readonly)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 101

attr_reader :binder, :events, :config, :options, :restart_dir

#connected_ports (readonly)

Return all tcp ports the launcher may be using, TCP or SSL

Version:

  • 5.0.0

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 204

def connected_ports
  @binder.connected_ports
end

#environment (readonly, private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 400

def environment
  @environment
end

#events (readonly)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 101

attr_reader :binder, :events, :config, :options, :restart_dir

#extra_runtime_deps_directories (readonly, private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 287

def extra_runtime_deps_directories
  Array(@options[:extra_runtime_dependencies]).map do |d_name|
    if (spec = spec_for_gem(d_name))
      require_paths_for_gem(spec)
    else
      log "* Could not load extra dependency: #{d_name}"
      nil
    end
  end.flatten.compact
end

#files_to_require_after_prune (readonly, private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 280

def files_to_require_after_prune
  puma = spec_for_gem("puma")

  require_paths_for_gem(puma) + extra_runtime_deps_directories
end

#options (readonly)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 101

attr_reader :binder, :events, :config, :options, :restart_dir

#prune_bundler (readonly, private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 306

def prune_bundler
  return if ENV['PUMA_BUNDLER_PRUNED']
  return unless defined?(Bundler)
  require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
  unless puma_wild_location
    log "! Unable to prune Bundler environment, continuing"
    return
  end

  dirs = files_to_require_after_prune

  log '* Pruning Bundler environment'
  home = ENV['GEM_HOME']
  bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
  with_unbundled_env do
    ENV['GEM_HOME'] = home
    ENV['BUNDLE_GEMFILE'] = bundle_gemfile
    ENV['PUMA_BUNDLER_PRUNED'] = '1'
    args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':')] + @original_argv
    # Ruby 2.0+ defaults to true which breaks socket activation
    args += [{:close_others => false}]
    Kernel.exec(*args)
  end
end

#prune_bundler?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 404

def prune_bundler?
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
end

#puma_wild_location (readonly, private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 299

def puma_wild_location
  puma = spec_for_gem("puma")
  dirs = require_paths_for_gem(puma)
  puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
  File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
end

#restart_args (readonly)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 209

def restart_args
  cmd = @options[:restart_cmd]
  if cmd
    cmd.split(' ') + @original_argv
  else
    @restart_argv
  end
end

#restart_dir (readonly)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 101

attr_reader :binder, :events, :config, :options, :restart_dir

#thread_status (readonly)

Version:

  • 5.0.0

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 225

def thread_status
  Thread.list.each do |thread|
    name = "Thread: TID-#{thread.object_id.to_s(36)}"
    name += " #{thread['label']}" if thread['label']
    name += " #{thread.name}" if thread.respond_to?(:name) && thread.name
    backtrace = thread.backtrace || ["<no backtrace available>"]

    yield name, backtrace
  end
end

#title (readonly, private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 388

def title
  buffer  = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
  buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
  buffer
end

Instance Method Details

#close_binder_listeners

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 218

def close_binder_listeners
  @runner.close_control_listeners
  @binder.close_listeners
end

#delete_pidfile

Delete the configured pidfile

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 129

def delete_pidfile
  path = @options[:pidfile]
  File.unlink(path) if path && File.exist?(path)
end

#generate_restart_data (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 408

def generate_restart_data
  if dir = @options[:directory]
    @restart_dir = dir

  elsif Puma.windows?
    # I guess the value of PWD is garbage on windows so don't bother
    # using it.
    @restart_dir = Dir.pwd

    # Use the same trick as unicorn, namely favor PWD because
    # it will contain an unresolved symlink, useful for when
    # the pwd is /data/releases/current.
  elsif dir = ENV['PWD']
    s_env = File.stat(dir)
    s_pwd = File.stat(Dir.pwd)

    if s_env.ino == s_pwd.ino and (Puma.jruby? or s_env.dev == s_pwd.dev)
      @restart_dir = dir
    end
  end

  @restart_dir ||= Dir.pwd

  # if $0 is a file in the current directory, then restart
  # it the same, otherwise add -S on there because it was
  # picked up in PATH.
  #
  if File.exist?($0)
    arg0 = [Gem.ruby, $0]
  else
    arg0 = [Gem.ruby, "-S", $0]
  end

  # Detect and reinject -Ilib from the command line, used for testing without bundler
  # cruby has an expanded path, jruby has just "lib"
  lib = File.expand_path "lib"
  arg0[1,0] = ["-I", lib] if [lib, "lib"].include?($LOAD_PATH[0])

  if defined? Puma::WILD_ARGS
    @restart_argv = arg0 + Puma::WILD_ARGS + @original_argv
  else
    @restart_argv = arg0 + @original_argv
  end
end

#graceful_stop (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 376

def graceful_stop
  @events.fire_on_stopped!
  @runner.stop_blocked
  log "=== puma shutdown: #{Time.now} ==="
  log "- Goodbye!"
end

#halt

Begin async shutdown of the server

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 135

def halt
  @status = :halt
  @runner.halt
end

#integrate_with_systemd (private)

Puma's systemd integration allows ::Puma to inform systemd:

1. when it has successfully started
2. when it is starting shutdown
3. periodically for a liveness check with a watchdog thread
[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 338

def integrate_with_systemd
  return unless ENV["NOTIFY_SOCKET"]

  begin
    require 'puma/systemd'
  rescue LoadError
    log "Systemd integration failed. It looks like you're trying to use systemd notify but don't have sd_notify gem installed"
    return
  end

  log "* Enabling systemd notification integration"

  systemd = Systemd.new(@events)
  systemd.hook_events
  systemd.start_watchdog
end

#log(str) (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 363

def log(str)
  @events.log str
end

#log_config (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 534

def log_config
  log "Configuration:"

  @config.final_options
    .each { |config_key, value| log "- #{config_key}: #{value}" }

  log "\n"
end

#phased_restart

Begin a phased restart if supported

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 154

def phased_restart
  unless @runner.respond_to?(:phased_restart) and @runner.phased_restart
    log "* phased-restart called but not available, restarting normally."
    return restart
  end
  true
end

#reload_worker_directory (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 251

def reload_worker_directory
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
end

#require_paths_for_gem(gem_spec) (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 359

def require_paths_for_gem(gem_spec)
  gem_spec.full_require_paths
end

#require_rubygems_min_version!(min_version, feature) (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 517

def require_rubygems_min_version!(min_version, feature)
  return if min_version <= Gem::Version.new(Gem::VERSION)

  raise "#{feature} is not supported on your version of RubyGems. " \
          "You must have RubyGems #{min_version}+ to use this feature."
end

#restart

Begin async restart of the server

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 148

def restart
  @status = :restart
  @runner.restart
end

#restart! (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 255

def restart!
  @events.fire_on_restart!
  @config.run_hooks :on_restart, self, @events

  if Puma.jruby?
    close_binder_listeners

    require 'puma/jruby_restart'
    JRubyRestart.chdir_exec(@restart_dir, restart_args)
  elsif Puma.windows?
    close_binder_listeners

    argv = restart_args
    Dir.chdir(@restart_dir)
    Kernel.exec(*argv)
  else
    argv = restart_args
    Dir.chdir(@restart_dir)
    ENV.update(@binder.redirects_for_restart_env)
    argv += [@binder.redirects_for_restart]
    Kernel.exec(*argv)
  end
end

#run

Run the server. This blocks until the server is stopped

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 163

def run
  previous_env =
    if defined?(Bundler)
      env = Bundler::ORIGINAL_ENV.dup
      # add -rbundler/setup so we load from Gemfile when restarting
      bundle = "-rbundler/setup"
      env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
      env
    else
      ENV.to_h
    end

  @config.clamp

  @config.plugins.fire_starts self

  setup_signals
  set_process_title
  integrate_with_systemd
  @runner.run

  case @status
  when :halt
    log "* Stopping immediately!"
    @runner.stop_control
  when :run, :stop
    graceful_stop
  when :restart
    log "* Restarting..."
    ENV.replace(previous_env)
    @runner.stop_control
    restart!
  when :exit
    # nothing
  end
  close_binder_listeners unless @status == :restart
end

#set_process_title (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 383

def set_process_title
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
end

#set_rack_environment (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 394

def set_rack_environment
  @options[:environment] = environment
  ENV['RACK_ENV'] = environment
end

#setup_signals (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 453

def setup_signals
  begin
    Signal.trap "SIGUSR2" do
      restart
    end
  rescue Exception
    log "*** SIGUSR2 not implemented, signal based restart unavailable!"
  end

  unless Puma.jruby?
    begin
      Signal.trap "SIGUSR1" do
        phased_restart
      end
    rescue Exception
      log "*** SIGUSR1 not implemented, signal based restart unavailable!"
    end
  end

  begin
    Signal.trap "SIGTERM" do
      graceful_stop

      raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
    end
  rescue Exception
    log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
  end

  begin
    Signal.trap "SIGINT" do
      stop
    end
  rescue Exception
    log "*** SIGINT not implemented, signal based gracefully stopping unavailable!"
  end

  begin
    Signal.trap "SIGHUP" do
      if @runner.redirected_io?
        @runner.redirect_io
      else
        stop
      end
    end
  rescue Exception
    log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
  end

  begin
    unless Puma.jruby? # INFO in use by JVM already
      Signal.trap "SIGINFO" do
        thread_status do |name, backtrace|
          @events.log name
          @events.log backtrace.map { |bt| "  #{bt}" }
        end
      end
    end
  rescue Exception
    # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
    # to see this constantly on Linux.
  end
end

#spec_for_gem(gem_name) (private)

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 355

def spec_for_gem(gem_name)
  Bundler.rubygems.loaded_specs(gem_name)
end

#stats

Return stats about the server

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 104

def stats
  @runner.stats
end

#stop

Begin async shutdown of the server gracefully

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 141

def stop
  @events.fire_on_stopped!
  @status = :stop
  @runner.stop
end

#unsupported(str) (private)

Raises:

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 371

def unsupported(str)
  @events.error(str)
  raise UnsupportedOption
end

#with_unbundled_env (private)

Version:

  • 5.0.0

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 525

def with_unbundled_env
  bundler_ver = Gem::Version.new(Bundler::VERSION)
  if bundler_ver < Gem::Version.new('2.1.0')
    Bundler.with_clean_env { yield }
  else
    Bundler.with_unbundled_env { yield }
  end
end

#write_pid (private)

If configured, write the pid of the current process out to a file.

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 240

def write_pid
  path = @options[:pidfile]
  return unless path

  File.open(path, 'w') { |f| f.puts Process.pid }
  cur = Process.pid
  at_exit do
    delete_pidfile if cur == Process.pid
  end
end

#write_state

Write a state file that can be used by pumactl to control the server

[ GitHub ]

  
# File 'lib/puma/launcher.rb', line 110

def write_state
  write_pid

  path = @options[:state]
  permission = @options[:state_permission]
  return unless path

  require 'puma/state_file'

  sf = StateFile.new
  sf.pid = Process.pid
  sf.control_url = @options[:control_url]
  sf.control_auth_token = @options[:control_auth_token]
  sf.running_from = File.expand_path('.')

  sf.save path, permission
end