1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

Extract prune_bundler code into it's own class. (#2797)

Co-authored-by: shields <shields@tablecheck.com>
This commit is contained in:
Johnny Shields 2022-02-02 04:07:38 +09:00 committed by GitHub
parent c92b69fc0f
commit d7cfe19eb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 167 additions and 132 deletions

View file

@ -6,6 +6,7 @@ require 'puma/cluster'
require 'puma/single'
require 'puma/const'
require 'puma/binder'
require 'puma/launcher/bundle_pruner'
module Puma
# Puma::Launcher is the single entry point for starting a Puma server based on user
@ -80,7 +81,7 @@ module Puma
Dir.chdir(@restart_dir)
prune_bundler if prune_bundler?
prune_bundler!
@environment = @options[:environment] if @options[:environment]
set_rack_environment
@ -300,67 +301,10 @@ module Puma
@runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
end
# @!attribute [r] files_to_require_after_prune
def files_to_require_after_prune
puma = spec_for_gem("puma")
require_paths_for_gem(puma) + extra_runtime_deps_directories
end
# @!attribute [r] extra_runtime_deps_directories
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
# @!attribute [r] puma_wild_location
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
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']
bundle_app_config = Bundler.original_env['BUNDLE_APP_CONFIG']
with_unbundled_env do
ENV['GEM_HOME'] = home
ENV['BUNDLE_GEMFILE'] = bundle_gemfile
ENV['PUMA_BUNDLER_PRUNED'] = '1'
ENV["BUNDLE_APP_CONFIG"] = bundle_app_config
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
#
# 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
#
def integrate_with_systemd
return unless ENV["NOTIFY_SOCKET"]
@ -378,14 +322,6 @@ module Puma
systemd.start_watchdog
end
def spec_for_gem(gem_name)
Bundler.rubygems.loaded_specs(gem_name)
end
def require_paths_for_gem(gem_spec)
gem_spec.full_require_paths
end
def log(str)
@events.log str
end
@ -424,6 +360,11 @@ module Puma
@options[:prune_bundler] && clustered? && !@options[:preload_app]
end
def prune_bundler!
return unless prune_bundler?
BundlePruner.new(@original_argv, @options[:extra_runtime_dependencies], @events).prune
end
def generate_restart_data
if dir = @options[:directory]
@restart_dir = dir
@ -534,23 +475,6 @@ module Puma
end
end
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
# @version 5.0.0
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
def log_config
log "Configuration:"

View file

@ -0,0 +1,103 @@
# frozen_string_literal: true
module Puma
class Launcher
# This class is used to pickup Gemfile changes during
# application restarts.
class BundlePruner
def initialize(original_argv, extra_runtime_dependencies, events)
@original_argv = Array(original_argv)
@extra_runtime_dependencies = Array(extra_runtime_dependencies)
@events = events
end
def prune
return if ENV['PUMA_BUNDLER_PRUNED']
return unless defined?(Bundler)
require_rubygems_min_version!
unless puma_wild_path
log "! Unable to prune Bundler environment, continuing"
return
end
dirs = paths_to_require_after_prune
log '* Pruning Bundler environment'
home = ENV['GEM_HOME']
bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
bundle_app_config = Bundler.original_env['BUNDLE_APP_CONFIG']
with_unbundled_env do
ENV['GEM_HOME'] = home
ENV['BUNDLE_GEMFILE'] = bundle_gemfile
ENV['PUMA_BUNDLER_PRUNED'] = '1'
ENV["BUNDLE_APP_CONFIG"] = bundle_app_config
args = [Gem.ruby, puma_wild_path, '-I', dirs.join(':')] + @original_argv
# Ruby 2.0+ defaults to true which breaks socket activation
args += [{:close_others => false}]
Kernel.exec(*args)
end
end
private
def require_rubygems_min_version!
min_version = Gem::Version.new('2.2')
return if min_version <= Gem::Version.new(Gem::VERSION)
raise "prune_bundler is not supported on your version of RubyGems. " \
"You must have RubyGems #{min_version}+ to use this feature."
end
def puma_wild_path
puma_lib_dir = puma_require_paths.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
File.expand_path(File.join(puma_lib_dir, '../bin/puma-wild'))
end
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
def paths_to_require_after_prune
puma_require_paths + extra_runtime_deps_paths
end
def extra_runtime_deps_paths
@extra_runtime_dependencies.map do |dep_name|
if (spec = spec_for_gem(dep_name))
require_paths_for_gem(spec)
else
log "* Could not load extra dependency: #{dep_name}"
nil
end
end.flatten.compact
end
def puma_require_paths
require_paths_for_gem(spec_for_gem('puma'))
end
def spec_for_gem(gem_name)
Bundler.rubygems.loaded_specs(gem_name)
end
def require_paths_for_gem(gem_spec)
gem_spec.full_require_paths
end
def log(str)
@events.log(str)
end
end
end
end

View file

@ -0,0 +1,57 @@
require_relative 'helper'
require 'puma/events'
require 'puma/launcher/bundle_pruner'
class TestBundlePruner < Minitest::Test
def test_paths_to_require_after_prune_is_correctly_built_for_no_extra_deps
skip_if :no_bundler
dirs = bundle_pruner.send(:paths_to_require_after_prune)
assert_equal(2, dirs.length)
assert_match(%r{puma/lib$}, dirs[0]) # lib dir
assert_match(%r{puma-#{Puma::Const::PUMA_VERSION}$}, dirs[1]) # native extension dir
refute_match(%r{gems/rdoc-[\d.]+/lib$}, dirs[2])
end
def test_paths_to_require_after_prune_is_correctly_built_with_extra_deps
skip_if :no_bundler
dirs = bundle_pruner([], ['rdoc']).send(:paths_to_require_after_prune)
assert_equal(3, dirs.length)
assert_match(%r{puma/lib$}, dirs[0]) # lib dir
assert_match(%r{puma-#{Puma::Const::PUMA_VERSION}$}, dirs[1]) # native extension dir
assert_match(%r{gems/rdoc-[\d.]+/lib$}, dirs[2]) # rdoc dir
end
def test_extra_runtime_deps_paths_is_empty_for_no_config
assert_equal([], bundle_pruner.send(:extra_runtime_deps_paths))
end
def test_extra_runtime_deps_paths_is_correctly_built
skip_if :no_bundler
dep_dirs = bundle_pruner([], ['rdoc']).send(:extra_runtime_deps_paths)
assert_equal(1, dep_dirs.length)
assert_match(%r{gems/rdoc-[\d.]+/lib$}, dep_dirs.first)
end
def test_puma_wild_path_is_an_absolute_path
skip_if :no_bundler
puma_wild_path = bundle_pruner.send(:puma_wild_path)
assert_match(%r{bin/puma-wild$}, puma_wild_path)
# assert no "/../" in path
refute_match(%r{/\.\./}, puma_wild_path)
end
private
def bundle_pruner(original_argv = nil, extra_runtime_dependencies = nil)
@bundle_pruner ||= Puma::Launcher::BundlePruner.new(original_argv, extra_runtime_dependencies, Puma::Events.null)
end
end

View file

@ -7,55 +7,6 @@ require 'puma/events'
class TestLauncher < Minitest::Test
include TmpPath
def test_files_to_require_after_prune_is_correctly_built_for_no_extra_deps
skip_if :no_bundler
dirs = launcher.send(:files_to_require_after_prune)
assert_equal(2, dirs.length)
assert_match(%r{puma/lib$}, dirs[0]) # lib dir
assert_match(%r{puma-#{Puma::Const::PUMA_VERSION}$}, dirs[1]) # native extension dir
refute_match(%r{gems/rdoc-[\d.]+/lib$}, dirs[2])
end
def test_files_to_require_after_prune_is_correctly_built_with_extra_deps
skip_if :no_bundler
conf = Puma::Configuration.new do |c|
c.extra_runtime_dependencies ['rdoc']
end
dirs = launcher(conf).send(:files_to_require_after_prune)
assert_equal(3, dirs.length)
assert_match(%r{puma/lib$}, dirs[0]) # lib dir
assert_match(%r{puma-#{Puma::Const::PUMA_VERSION}$}, dirs[1]) # native extension dir
assert_match(%r{gems/rdoc-[\d.]+/lib$}, dirs[2]) # rdoc dir
end
def test_extra_runtime_deps_directories_is_empty_for_no_config
assert_equal([], launcher.send(:extra_runtime_deps_directories))
end
def test_extra_runtime_deps_directories_is_correctly_built
skip_if :no_bundler
conf = Puma::Configuration.new do |c|
c.extra_runtime_dependencies ['rdoc']
end
dep_dirs = launcher(conf).send(:extra_runtime_deps_directories)
assert_equal(1, dep_dirs.length)
assert_match(%r{gems/rdoc-[\d.]+/lib$}, dep_dirs.first)
end
def test_puma_wild_location_is_an_absolute_path
skip_if :no_bundler
puma_wild_location = launcher.send(:puma_wild_location)
assert_match(%r{bin/puma-wild$}, puma_wild_location)
# assert no "/../" in path
refute_match(%r{/\.\./}, puma_wild_location)
end
def test_prints_thread_traces
launcher.thread_status do |name, _backtrace|
assert_match "Thread: TID", name