puma 4.1.1 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +149 -10
  3. data/LICENSE +23 -20
  4. data/README.md +30 -46
  5. data/docs/architecture.md +3 -3
  6. data/docs/deployment.md +9 -3
  7. data/docs/fork_worker.md +31 -0
  8. data/docs/jungle/README.md +13 -0
  9. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma +0 -0
  11. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  12. data/{tools → docs}/jungle/upstart/README.md +0 -0
  13. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  14. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  15. data/docs/plugins.md +20 -10
  16. data/docs/signals.md +7 -6
  17. data/docs/systemd.md +1 -63
  18. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  19. data/ext/puma_http11/extconf.rb +6 -0
  20. data/ext/puma_http11/http11_parser.c +40 -63
  21. data/ext/puma_http11/http11_parser.java.rl +21 -37
  22. data/ext/puma_http11/http11_parser.rl +3 -1
  23. data/ext/puma_http11/http11_parser_common.rl +3 -3
  24. data/ext/puma_http11/mini_ssl.c +15 -2
  25. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  28. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  29. data/ext/puma_http11/puma_http11.c +9 -38
  30. data/lib/puma.rb +23 -0
  31. data/lib/puma/app/status.rb +46 -30
  32. data/lib/puma/binder.rb +112 -124
  33. data/lib/puma/cli.rb +11 -15
  34. data/lib/puma/client.rb +250 -209
  35. data/lib/puma/cluster.rb +203 -85
  36. data/lib/puma/commonlogger.rb +2 -2
  37. data/lib/puma/configuration.rb +31 -42
  38. data/lib/puma/const.rb +24 -19
  39. data/lib/puma/control_cli.rb +46 -17
  40. data/lib/puma/detect.rb +17 -0
  41. data/lib/puma/dsl.rb +162 -70
  42. data/lib/puma/error_logger.rb +97 -0
  43. data/lib/puma/events.rb +35 -31
  44. data/lib/puma/io_buffer.rb +9 -2
  45. data/lib/puma/jruby_restart.rb +0 -58
  46. data/lib/puma/launcher.rb +117 -58
  47. data/lib/puma/minissl.rb +60 -18
  48. data/lib/puma/minissl/context_builder.rb +73 -0
  49. data/lib/puma/null_io.rb +1 -1
  50. data/lib/puma/plugin.rb +6 -12
  51. data/lib/puma/rack/builder.rb +0 -4
  52. data/lib/puma/reactor.rb +16 -9
  53. data/lib/puma/runner.rb +11 -32
  54. data/lib/puma/server.rb +173 -193
  55. data/lib/puma/single.rb +7 -64
  56. data/lib/puma/state_file.rb +6 -3
  57. data/lib/puma/thread_pool.rb +104 -81
  58. data/lib/rack/handler/puma.rb +1 -5
  59. data/tools/Dockerfile +16 -0
  60. data/tools/trickletest.rb +0 -1
  61. metadata +23 -24
  62. data/ext/puma_http11/io_buffer.c +0 -155
  63. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  64. data/lib/puma/convenient.rb +0 -25
  65. data/lib/puma/daemon_ext.rb +0 -33
  66. data/lib/puma/delegation.rb +0 -13
  67. data/lib/puma/tcp_logger.rb +0 -41
  68. data/tools/jungle/README.md +0 -19
  69. data/tools/jungle/init.d/README.md +0 -61
  70. data/tools/jungle/init.d/puma +0 -421
  71. data/tools/jungle/init.d/run-puma +0 -18
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puma/const'
4
+
5
+ module Puma
6
+ # The implementation of a detailed error logging.
7
+ # @version 5.0.0
8
+ #
9
+ class ErrorLogger
10
+ include Const
11
+
12
+ attr_reader :ioerr
13
+
14
+ REQUEST_FORMAT = %{"%s %s%s" - (%s)}
15
+
16
+ def initialize(ioerr)
17
+ @ioerr = ioerr
18
+ @ioerr.sync = true
19
+
20
+ @debug = ENV.key? 'PUMA_DEBUG'
21
+ end
22
+
23
+ def self.stdio
24
+ new $stderr
25
+ end
26
+
27
+ # Print occured error details.
28
+ # +options+ hash with additional options:
29
+ # - +error+ is an exception object
30
+ # - +req+ the http request
31
+ # - +text+ (default nil) custom string to print in title
32
+ # and before all remaining info.
33
+ #
34
+ def info(options={})
35
+ ioerr.puts title(options)
36
+ end
37
+
38
+ # Print occured error details only if
39
+ # environment variable PUMA_DEBUG is defined.
40
+ # +options+ hash with additional options:
41
+ # - +error+ is an exception object
42
+ # - +req+ the http request
43
+ # - +text+ (default nil) custom string to print in title
44
+ # and before all remaining info.
45
+ #
46
+ def debug(options={})
47
+ return unless @debug
48
+
49
+ error = options[:error]
50
+ req = options[:req]
51
+
52
+ string_block = []
53
+ string_block << title(options)
54
+ string_block << request_dump(req) if req
55
+ string_block << error_backtrace(options) if error
56
+
57
+ ioerr.puts string_block.join("\n")
58
+ end
59
+
60
+ def title(options={})
61
+ text = options[:text]
62
+ req = options[:req]
63
+ error = options[:error]
64
+
65
+ string_block = ["#{Time.now}"]
66
+ string_block << " #{text}" if text
67
+ string_block << " (#{request_title(req)})" if request_parsed?(req)
68
+ string_block << ": #{error.inspect}" if error
69
+ string_block.join('')
70
+ end
71
+
72
+ def request_dump(req)
73
+ "Headers: #{request_headers(req)}\n" \
74
+ "Body: #{req.body}"
75
+ end
76
+
77
+ def request_title(req)
78
+ env = req.env
79
+
80
+ REQUEST_FORMAT % [
81
+ env[REQUEST_METHOD],
82
+ env[REQUEST_PATH] || env[PATH_INFO],
83
+ env[QUERY_STRING] || "",
84
+ env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR] || "-"
85
+ ]
86
+ end
87
+
88
+ def request_headers(req)
89
+ headers = req.env.select { |key, _| key.start_with?('HTTP_') }
90
+ headers.map { |key, value| [key[5..-1], value] }.to_h.inspect
91
+ end
92
+
93
+ def request_parsed?(req)
94
+ req && req.env[REQUEST_METHOD]
95
+ end
96
+ end
97
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/const'
4
3
  require "puma/null_io"
4
+ require 'puma/error_logger'
5
5
  require 'stringio'
6
6
 
7
7
  module Puma
@@ -23,8 +23,6 @@ module Puma
23
23
  end
24
24
  end
25
25
 
26
- include Const
27
-
28
26
  # Create an Events object that prints to +stdout+ and +stderr+.
29
27
  #
30
28
  def initialize(stdout, stderr)
@@ -36,6 +34,7 @@ module Puma
36
34
  @stderr.sync = true
37
35
 
38
36
  @debug = ENV.key? 'PUMA_DEBUG'
37
+ @error_logger = ErrorLogger.new(@stderr)
39
38
 
40
39
  @hooks = Hash.new { |h,k| h[k] = [] }
41
40
  end
@@ -66,7 +65,8 @@ module Puma
66
65
  # Write +str+ to +@stdout+
67
66
  #
68
67
  def log(str)
69
- @stdout.puts format(str)
68
+ @stdout.puts format(str) if @stdout.respond_to? :puts
69
+ rescue Errno::EPIPE
70
70
  end
71
71
 
72
72
  def write(str)
@@ -80,7 +80,7 @@ module Puma
80
80
  # Write +str+ to +@stderr+
81
81
  #
82
82
  def error(str)
83
- @stderr.puts format("ERROR: #{str}")
83
+ @error_logger.info(text: format("ERROR: #{str}"))
84
84
  exit 1
85
85
  end
86
86
 
@@ -88,43 +88,47 @@ module Puma
88
88
  formatter.call(str)
89
89
  end
90
90
 
91
+ # An HTTP connection error has occurred.
92
+ # +error+ a connection exception, +req+ the request,
93
+ # and +text+ additional info
94
+ # @version 5.0.0
95
+ #
96
+ def connection_error(error, req, text="HTTP connection error")
97
+ @error_logger.info(error: error, req: req, text: text)
98
+ end
99
+
91
100
  # An HTTP parse error has occurred.
92
- # +server+ is the Server object, +env+ the request, and +error+ a
93
- # parsing exception.
101
+ # +error+ a parsing exception,
102
+ # and +req+ the request.
94
103
  #
95
- def parse_error(server, env, error)
96
- @stderr.puts "#{Time.now}: HTTP parse error, malformed request " \
97
- "(#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}#{env[REQUEST_PATH]}): " \
98
- "#{error.inspect}" \
99
- "\n---\n"
104
+ def parse_error(error, req)
105
+ @error_logger.info(error: error, req: req, text: 'HTTP parse error, malformed request')
100
106
  end
101
107
 
102
108
  # An SSL error has occurred.
103
- # +server+ is the Server object, +peeraddr+ peer address, +peercert+
104
- # any peer certificate (if present), and +error+ an exception object.
109
+ # +error+ an exception object, +peeraddr+ peer address,
110
+ # and +peercert+ any peer certificate (if present).
105
111
  #
106
- def ssl_error(server, peeraddr, peercert, error)
112
+ def ssl_error(error, peeraddr, peercert)
107
113
  subject = peercert ? peercert.subject : nil
108
- @stderr.puts "#{Time.now}: SSL error, peer: #{peeraddr}, peer cert: #{subject}, #{error.inspect}"
114
+ @error_logger.info(error: error, text: "SSL error, peer: #{peeraddr}, peer cert: #{subject}")
109
115
  end
110
116
 
111
117
  # An unknown error has occurred.
112
- # +server+ is the Server object, +error+ an exception object,
113
- # +kind+ some additional info, and +env+ the request.
118
+ # +error+ an exception object, +req+ the request,
119
+ # and +text+ additional info
114
120
  #
115
- def unknown_error(server, error, kind="Unknown", env=nil)
116
- if error.respond_to? :render
117
- error.render "#{Time.now}: #{kind} error", @stderr
118
- else
119
- if env
120
- string_block = [ "#{Time.now}: #{kind} error handling request { #{env['REQUEST_METHOD']} #{env['PATH_INFO']} }" ]
121
- string_block << error.inspect
122
- else
123
- string_block = [ "#{Time.now}: #{kind} error: #{error.inspect}" ]
124
- end
125
- string_block << error.backtrace
126
- @stderr.puts string_block.join("\n")
127
- end
121
+ def unknown_error(error, req=nil, text="Unknown error")
122
+ @error_logger.info(error: error, req: req, text: text)
123
+ end
124
+
125
+ # Log occurred error debug dump.
126
+ # +error+ an exception object, +req+ the request,
127
+ # and +text+ additional info
128
+ # @version 5.0.0
129
+ #
130
+ def debug_error(error, req=nil, text="")
131
+ @error_logger.debug(error: error, req: req, text: text)
128
132
  end
129
133
 
130
134
  def on_booted(&block)
@@ -1,4 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/detect'
4
- require 'puma/puma_http11'
3
+ module Puma
4
+ class IOBuffer < String
5
+ def append(*args)
6
+ args.each { |a| concat(a) }
7
+ end
8
+
9
+ alias reset clear
10
+ end
11
+ end
@@ -22,63 +22,5 @@ module Puma
22
22
  execlp(cmd, *argv)
23
23
  raise SystemCallError.new(FFI.errno)
24
24
  end
25
-
26
- PermKey = 'PUMA_DAEMON_PERM'
27
- RestartKey = 'PUMA_DAEMON_RESTART'
28
-
29
- # Called to tell things "Your now always in daemon mode,
30
- # don't try to reenter it."
31
- #
32
- def self.perm_daemonize
33
- ENV[PermKey] = "1"
34
- end
35
-
36
- def self.daemon?
37
- ENV.key?(PermKey) || ENV.key?(RestartKey)
38
- end
39
-
40
- def self.daemon_init
41
- return true if ENV.key?(PermKey)
42
-
43
- return false unless ENV.key? RestartKey
44
-
45
- master = ENV[RestartKey]
46
-
47
- # In case the master disappears early
48
- begin
49
- Process.kill "SIGUSR2", master.to_i
50
- rescue SystemCallError => e
51
- end
52
-
53
- ENV[RestartKey] = ""
54
-
55
- setsid
56
-
57
- null = File.open "/dev/null", "w+"
58
- STDIN.reopen null
59
- STDOUT.reopen null
60
- STDERR.reopen null
61
-
62
- true
63
- end
64
-
65
- def self.daemon_start(dir, argv)
66
- ENV[RestartKey] = Process.pid.to_s
67
-
68
- if k = ENV['PUMA_JRUBY_DAEMON_OPTS']
69
- ENV['JRUBY_OPTS'] = k
70
- end
71
-
72
- cmd = argv.first
73
- argv = ([:string] * argv.size).zip(argv).flatten
74
- argv << :string
75
- argv << nil
76
-
77
- chdir(dir)
78
- ret = fork
79
- return ret if ret != 0
80
- execlp(cmd, *argv)
81
- raise SystemCallError.new(FFI.errno)
82
- end
83
25
  end
84
26
  end
@@ -2,12 +2,9 @@
2
2
 
3
3
  require 'puma/events'
4
4
  require 'puma/detect'
5
-
6
5
  require 'puma/cluster'
7
6
  require 'puma/single'
8
-
9
7
  require 'puma/const'
10
-
11
8
  require 'puma/binder'
12
9
 
13
10
  module Puma
@@ -50,8 +47,9 @@ module Puma
50
47
  @original_argv = @argv.dup
51
48
  @config = conf
52
49
 
53
- @binder = Binder.new(@events)
54
- @binder.import_from_env
50
+ @binder = Binder.new(@events, conf)
51
+ @binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
52
+ @binder.create_activated_fds(ENV).each { |k| ENV.delete k }
55
53
 
56
54
  @environment = conf.environment
57
55
 
@@ -72,10 +70,6 @@ module Puma
72
70
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
73
71
  end
74
72
 
75
- if @options[:daemon] && Puma.windows?
76
- unsupported 'daemon mode not supported on Windows'
77
- end
78
-
79
73
  Dir.chdir(@restart_dir)
80
74
 
81
75
  prune_bundler if prune_bundler?
@@ -108,6 +102,7 @@ module Puma
108
102
  write_pid
109
103
 
110
104
  path = @options[:state]
105
+ permission = @options[:state_permission]
111
106
  return unless path
112
107
 
113
108
  require 'puma/state_file'
@@ -116,8 +111,9 @@ module Puma
116
111
  sf.pid = Process.pid
117
112
  sf.control_url = @options[:control_url]
118
113
  sf.control_auth_token = @options[:control_auth_token]
114
+ sf.running_from = File.expand_path('.')
119
115
 
120
- sf.save path
116
+ sf.save path, permission
121
117
  end
122
118
 
123
119
  # Delete the configured pidfile
@@ -126,19 +122,6 @@ module Puma
126
122
  File.unlink(path) if path && File.exist?(path)
127
123
  end
128
124
 
129
- # If configured, write the pid of the current process out
130
- # to a file.
131
- def write_pid
132
- path = @options[:pidfile]
133
- return unless path
134
-
135
- File.open(path, 'w') { |f| f.puts Process.pid }
136
- cur = Process.pid
137
- at_exit do
138
- delete_pidfile if cur == Process.pid
139
- end
140
- end
141
-
142
125
  # Begin async shutdown of the server
143
126
  def halt
144
127
  @status = :halt
@@ -190,21 +173,24 @@ module Puma
190
173
  case @status
191
174
  when :halt
192
175
  log "* Stopping immediately!"
176
+ @runner.stop_control
193
177
  when :run, :stop
194
178
  graceful_stop
195
179
  when :restart
196
180
  log "* Restarting..."
197
181
  ENV.replace(previous_env)
198
- @runner.before_restart
182
+ @runner.stop_control
199
183
  restart!
200
184
  when :exit
201
185
  # nothing
202
186
  end
187
+ close_binder_listeners unless @status == :restart
203
188
  end
204
189
 
205
- # Return which tcp port the launcher is using, if it's using TCP
206
- def connected_port
207
- @binder.connected_port
190
+ # Return all tcp ports the launcher may be using, TCP or SSL
191
+ # @version 5.0.0
192
+ def connected_ports
193
+ @binder.connected_ports
208
194
  end
209
195
 
210
196
  def restart_args
@@ -217,22 +203,43 @@ module Puma
217
203
  end
218
204
 
219
205
  def close_binder_listeners
220
- @binder.listeners.each do |l, io|
221
- io.close
222
- uri = URI.parse(l)
223
- next unless uri.scheme == 'unix'
224
- File.unlink("#{uri.host}#{uri.path}")
206
+ @runner.close_control_listeners
207
+ @binder.close_listeners
208
+ end
209
+
210
+ # @version 5.0.0
211
+ def thread_status
212
+ Thread.list.each do |thread|
213
+ name = "Thread: TID-#{thread.object_id.to_s(36)}"
214
+ name += " #{thread['label']}" if thread['label']
215
+ name += " #{thread.name}" if thread.respond_to?(:name) && thread.name
216
+ backtrace = thread.backtrace || ["<no backtrace available>"]
217
+
218
+ yield name, backtrace
225
219
  end
226
220
  end
227
221
 
228
222
  private
229
223
 
224
+ # If configured, write the pid of the current process out
225
+ # to a file.
226
+ def write_pid
227
+ path = @options[:pidfile]
228
+ return unless path
229
+
230
+ File.open(path, 'w') { |f| f.puts Process.pid }
231
+ cur = Process.pid
232
+ at_exit do
233
+ delete_pidfile if cur == Process.pid
234
+ end
235
+ end
236
+
230
237
  def reload_worker_directory
231
238
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
232
239
  end
233
240
 
234
241
  def restart!
235
- @config.run_hooks :on_restart, self
242
+ @config.run_hooks :on_restart, self, @events
236
243
 
237
244
  if Puma.jruby?
238
245
  close_binder_listeners
@@ -246,48 +253,75 @@ module Puma
246
253
  Dir.chdir(@restart_dir)
247
254
  Kernel.exec(*argv)
248
255
  else
249
- redirects = {:close_others => true}
250
- @binder.listeners.each_with_index do |(l, io), i|
251
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
252
- redirects[io.to_i] = io.to_i
253
- end
254
-
255
256
  argv = restart_args
256
257
  Dir.chdir(@restart_dir)
257
- argv += [redirects]
258
+ ENV.update(@binder.redirects_for_restart_env)
259
+ argv += [@binder.redirects_for_restart]
258
260
  Kernel.exec(*argv)
259
261
  end
260
262
  end
261
263
 
262
- def prune_bundler
263
- return unless defined?(Bundler)
264
- puma = Bundler.rubygems.loaded_specs("puma")
265
- dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
264
+ def dependencies_and_files_to_require_after_prune
265
+ puma = spec_for_gem("puma")
266
+
267
+ deps = puma.runtime_dependencies.map do |d|
268
+ "#{d.name}:#{spec_for_gem(d.name).version}"
269
+ end
270
+
271
+ [deps, require_paths_for_gem(puma) + extra_runtime_deps_directories]
272
+ end
273
+
274
+ def extra_runtime_deps_directories
275
+ Array(@options[:extra_runtime_dependencies]).map do |d_name|
276
+ if (spec = spec_for_gem(d_name))
277
+ require_paths_for_gem(spec)
278
+ else
279
+ log "* Could not load extra dependency: #{d_name}"
280
+ nil
281
+ end
282
+ end.flatten.compact
283
+ end
284
+
285
+ def puma_wild_location
286
+ puma = spec_for_gem("puma")
287
+ dirs = require_paths_for_gem(puma)
266
288
  puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
289
+ File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
290
+ end
267
291
 
268
- unless puma_lib_dir
292
+ def prune_bundler
293
+ return if ENV['PUMA_BUNDLER_PRUNED']
294
+ return unless defined?(Bundler)
295
+ require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
296
+ unless puma_wild_location
269
297
  log "! Unable to prune Bundler environment, continuing"
270
298
  return
271
299
  end
272
300
 
273
- deps = puma.runtime_dependencies.map do |d|
274
- spec = Bundler.rubygems.loaded_specs(d.name)
275
- "#{d.name}:#{spec.version.to_s}"
276
- end
301
+ deps, dirs = dependencies_and_files_to_require_after_prune
277
302
 
278
303
  log '* Pruning Bundler environment'
279
304
  home = ENV['GEM_HOME']
280
- Bundler.with_clean_env do
305
+ bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
306
+ with_unbundled_env do
281
307
  ENV['GEM_HOME'] = home
308
+ ENV['BUNDLE_GEMFILE'] = bundle_gemfile
282
309
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
283
- wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
284
- args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
310
+ args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':'), deps.join(',')] + @original_argv
285
311
  # Ruby 2.0+ defaults to true which breaks socket activation
286
312
  args += [{:close_others => false}]
287
313
  Kernel.exec(*args)
288
314
  end
289
315
  end
290
316
 
317
+ def spec_for_gem(gem_name)
318
+ Bundler.rubygems.loaded_specs(gem_name)
319
+ end
320
+
321
+ def require_paths_for_gem(gem_spec)
322
+ gem_spec.full_require_paths
323
+ end
324
+
291
325
  def log(str)
292
326
  @events.log str
293
327
  end
@@ -406,12 +440,6 @@ module Puma
406
440
 
407
441
  begin
408
442
  Signal.trap "SIGINT" do
409
- if Puma.jruby?
410
- @status = :exit
411
- graceful_stop
412
- exit
413
- end
414
-
415
443
  stop
416
444
  end
417
445
  rescue Exception
@@ -429,6 +457,37 @@ module Puma
429
457
  rescue Exception
430
458
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
431
459
  end
460
+
461
+ begin
462
+ unless Puma.jruby? # INFO in use by JVM already
463
+ Signal.trap "SIGINFO" do
464
+ thread_status do |name, backtrace|
465
+ @events.log name
466
+ @events.log backtrace.map { |bt| " #{bt}" }
467
+ end
468
+ end
469
+ end
470
+ rescue Exception
471
+ # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
472
+ # to see this constantly on Linux.
473
+ end
474
+ end
475
+
476
+ def require_rubygems_min_version!(min_version, feature)
477
+ return if min_version <= Gem::Version.new(Gem::VERSION)
478
+
479
+ raise "#{feature} is not supported on your version of RubyGems. " \
480
+ "You must have RubyGems #{min_version}+ to use this feature."
481
+ end
482
+
483
+ # @version 5.0.0
484
+ def with_unbundled_env
485
+ bundler_ver = Gem::Version.new(Bundler::VERSION)
486
+ if bundler_ver < Gem::Version.new('2.1.0')
487
+ Bundler.with_clean_env { yield }
488
+ else
489
+ Bundler.with_unbundled_env { yield }
490
+ end
432
491
  end
433
492
  end
434
493
  end