puma 3.11.4 → 4.2.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +130 -1
  3. data/README.md +100 -44
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/restart.md +4 -2
  7. data/docs/systemd.md +27 -9
  8. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  9. data/ext/puma_http11/extconf.rb +8 -0
  10. data/ext/puma_http11/http11_parser.c +37 -62
  11. data/ext/puma_http11/http11_parser_common.rl +3 -3
  12. data/ext/puma_http11/mini_ssl.c +96 -5
  13. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  14. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +21 -4
  15. data/lib/puma/accept_nonblock.rb +7 -1
  16. data/lib/puma/app/status.rb +35 -29
  17. data/lib/puma/binder.rb +47 -11
  18. data/lib/puma/cli.rb +21 -7
  19. data/lib/puma/client.rb +227 -191
  20. data/lib/puma/cluster.rb +70 -31
  21. data/lib/puma/commonlogger.rb +2 -0
  22. data/lib/puma/configuration.rb +6 -3
  23. data/lib/puma/const.rb +24 -18
  24. data/lib/puma/control_cli.rb +33 -14
  25. data/lib/puma/convenient.rb +2 -0
  26. data/lib/puma/delegation.rb +2 -0
  27. data/lib/puma/detect.rb +2 -0
  28. data/lib/puma/dsl.rb +308 -76
  29. data/lib/puma/events.rb +6 -1
  30. data/lib/puma/io_buffer.rb +3 -6
  31. data/lib/puma/jruby_restart.rb +2 -0
  32. data/lib/puma/launcher.rb +102 -55
  33. data/lib/puma/minissl.rb +41 -19
  34. data/lib/puma/null_io.rb +2 -0
  35. data/lib/puma/plugin/tmp_restart.rb +2 -0
  36. data/lib/puma/plugin.rb +7 -2
  37. data/lib/puma/rack/builder.rb +4 -1
  38. data/lib/puma/rack/urlmap.rb +2 -0
  39. data/lib/puma/rack_default.rb +2 -0
  40. data/lib/puma/reactor.rb +220 -34
  41. data/lib/puma/runner.rb +14 -4
  42. data/lib/puma/server.rb +82 -40
  43. data/lib/puma/single.rb +15 -3
  44. data/lib/puma/state_file.rb +2 -0
  45. data/lib/puma/tcp_logger.rb +2 -0
  46. data/lib/puma/thread_pool.rb +59 -36
  47. data/lib/puma/util.rb +2 -6
  48. data/lib/puma.rb +8 -0
  49. data/lib/rack/handler/puma.rb +6 -3
  50. data/tools/docker/Dockerfile +16 -0
  51. data/tools/jungle/init.d/puma +6 -6
  52. data/tools/trickletest.rb +0 -1
  53. metadata +22 -10
  54. data/lib/puma/compat.rb +0 -14
  55. data/lib/puma/daemon_ext.rb +0 -31
  56. data/lib/puma/java_io_buffer.rb +0 -45
  57. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
data/lib/puma/launcher.rb CHANGED
@@ -1,11 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/events'
2
4
  require 'puma/detect'
3
-
4
5
  require 'puma/cluster'
5
6
  require 'puma/single'
6
-
7
7
  require 'puma/const'
8
-
9
8
  require 'puma/binder'
10
9
 
11
10
  module Puma
@@ -61,10 +60,13 @@ module Puma
61
60
  @options = @config.options
62
61
  @config.clamp
63
62
 
63
+ @events.formatter = Events::PidFormatter.new if clustered?
64
+ @events.formatter = options[:log_formatter] if @options[:log_formatter]
65
+
64
66
  generate_restart_data
65
67
 
66
- if clustered? && (Puma.jruby? || Puma.windows?)
67
- unsupported 'worker mode not supported on JRuby or Windows'
68
+ if clustered? && !Process.respond_to?(:fork)
69
+ unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
68
70
  end
69
71
 
70
72
  if @options[:daemon] && Puma.windows?
@@ -79,7 +81,6 @@ module Puma
79
81
  set_rack_environment
80
82
 
81
83
  if clustered?
82
- @events.formatter = Events::PidFormatter.new
83
84
  @options[:logger] = @events
84
85
 
85
86
  @runner = Cluster.new(self, @events)
@@ -122,19 +123,6 @@ module Puma
122
123
  File.unlink(path) if path && File.exist?(path)
123
124
  end
124
125
 
125
- # If configured, write the pid of the current process out
126
- # to a file.
127
- def write_pid
128
- path = @options[:pidfile]
129
- return unless path
130
-
131
- File.open(path, 'w') { |f| f.puts Process.pid }
132
- cur = Process.pid
133
- at_exit do
134
- delete_pidfile if cur == Process.pid
135
- end
136
- end
137
-
138
126
  # Begin async shutdown of the server
139
127
  def halt
140
128
  @status = :halt
@@ -212,8 +200,29 @@ module Puma
212
200
  end
213
201
  end
214
202
 
203
+ def close_binder_listeners
204
+ @binder.close_listeners
205
+ end
206
+
207
+ def close_binder_unix_paths
208
+ @binder.close_unix_paths
209
+ end
210
+
215
211
  private
216
212
 
213
+ # If configured, write the pid of the current process out
214
+ # to a file.
215
+ def write_pid
216
+ path = @options[:pidfile]
217
+ return unless path
218
+
219
+ File.open(path, 'w') { |f| f.puts Process.pid }
220
+ cur = Process.pid
221
+ at_exit do
222
+ delete_pidfile if cur == Process.pid
223
+ end
224
+ end
225
+
217
226
  def reload_worker_directory
218
227
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
219
228
  end
@@ -233,48 +242,71 @@ module Puma
233
242
  Dir.chdir(@restart_dir)
234
243
  Kernel.exec(*argv)
235
244
  else
236
- redirects = {:close_others => true}
237
- @binder.listeners.each_with_index do |(l, io), i|
238
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
239
- redirects[io.to_i] = io.to_i
240
- end
241
-
242
245
  argv = restart_args
243
246
  Dir.chdir(@restart_dir)
244
- argv += [redirects] if RUBY_VERSION >= '1.9'
247
+ argv += [@binder.redirects_for_restart]
245
248
  Kernel.exec(*argv)
246
249
  end
247
250
  end
248
251
 
249
- def prune_bundler
250
- return unless defined?(Bundler)
251
- puma = Bundler.rubygems.loaded_specs("puma")
252
- dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
252
+ def dependencies_and_files_to_require_after_prune
253
+ puma = spec_for_gem("puma")
254
+
255
+ deps = puma.runtime_dependencies.map do |d|
256
+ "#{d.name}:#{spec_for_gem(d.name).version}"
257
+ end
258
+
259
+ [deps, require_paths_for_gem(puma) + extra_runtime_deps_directories]
260
+ end
261
+
262
+ def extra_runtime_deps_directories
263
+ Array(@options[:extra_runtime_dependencies]).map do |d_name|
264
+ if (spec = spec_for_gem(d_name))
265
+ require_paths_for_gem(spec)
266
+ else
267
+ log "* Could not load extra dependency: #{d_name}"
268
+ nil
269
+ end
270
+ end.flatten.compact
271
+ end
272
+
273
+ def puma_wild_location
274
+ puma = spec_for_gem("puma")
275
+ dirs = require_paths_for_gem(puma)
253
276
  puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
277
+ File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
278
+ end
254
279
 
255
- unless puma_lib_dir
280
+ def prune_bundler
281
+ return unless defined?(Bundler)
282
+ require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
283
+ unless puma_wild_location
256
284
  log "! Unable to prune Bundler environment, continuing"
257
285
  return
258
286
  end
259
287
 
260
- deps = puma.runtime_dependencies.map do |d|
261
- spec = Bundler.rubygems.loaded_specs(d.name)
262
- "#{d.name}:#{spec.version.to_s}"
263
- end
288
+ deps, dirs = dependencies_and_files_to_require_after_prune
264
289
 
265
290
  log '* Pruning Bundler environment'
266
291
  home = ENV['GEM_HOME']
267
292
  Bundler.with_clean_env do
268
293
  ENV['GEM_HOME'] = home
269
294
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
270
- wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
271
- args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
295
+ args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':'), deps.join(',')] + @original_argv
272
296
  # Ruby 2.0+ defaults to true which breaks socket activation
273
- args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
297
+ args += [{:close_others => false}]
274
298
  Kernel.exec(*args)
275
299
  end
276
300
  end
277
301
 
302
+ def spec_for_gem(gem_name)
303
+ Bundler.rubygems.loaded_specs(gem_name)
304
+ end
305
+
306
+ def require_paths_for_gem(gem_spec)
307
+ gem_spec.full_require_paths
308
+ end
309
+
278
310
  def log(str)
279
311
  @events.log str
280
312
  end
@@ -294,6 +326,21 @@ module Puma
294
326
  log "- Goodbye!"
295
327
  end
296
328
 
329
+ def log_thread_status
330
+ Thread.list.each do |thread|
331
+ log "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
332
+ logstr = "Thread: TID-#{thread.object_id.to_s(36)}"
333
+ logstr += " #{thread.name}" if thread.respond_to?(:name)
334
+ log logstr
335
+
336
+ if thread.backtrace
337
+ log thread.backtrace.join("\n")
338
+ else
339
+ log "<no backtrace available>"
340
+ end
341
+ end
342
+ end
343
+
297
344
  def set_process_title
298
345
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
299
346
  end
@@ -317,16 +364,6 @@ module Puma
317
364
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
318
365
  end
319
366
 
320
- def close_binder_listeners
321
- @binder.listeners.each do |l, io|
322
- io.close
323
- uri = URI.parse(l)
324
- next unless uri.scheme == 'unix'
325
- File.unlink("#{uri.host}#{uri.path}")
326
- end
327
- end
328
-
329
-
330
367
  def generate_restart_data
331
368
  if dir = @options[:directory]
332
369
  @restart_dir = dir
@@ -395,7 +432,7 @@ module Puma
395
432
  Signal.trap "SIGTERM" do
396
433
  graceful_stop
397
434
 
398
- raise SignalException, "SIGTERM"
435
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
399
436
  end
400
437
  rescue Exception
401
438
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
@@ -403,12 +440,6 @@ module Puma
403
440
 
404
441
  begin
405
442
  Signal.trap "SIGINT" do
406
- if Puma.jruby?
407
- @status = :exit
408
- graceful_stop
409
- exit
410
- end
411
-
412
443
  stop
413
444
  end
414
445
  rescue Exception
@@ -426,6 +457,22 @@ module Puma
426
457
  rescue Exception
427
458
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
428
459
  end
460
+
461
+ begin
462
+ Signal.trap "SIGINFO" do
463
+ log_thread_status
464
+ end
465
+ rescue Exception
466
+ # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
467
+ # to see this constantly on Linux.
468
+ end
469
+ end
470
+
471
+ def require_rubygems_min_version!(min_version, feature)
472
+ return if min_version <= Gem::Version.new(Gem::VERSION)
473
+
474
+ raise "#{feature} is not supported on your version of RubyGems. " \
475
+ "You must have RubyGems #{min_version}+ to use this feature."
429
476
  end
430
477
  end
431
478
  end
data/lib/puma/minissl.rb CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'io/wait'
3
- rescue LoadError
5
+ rescue LoadError
4
6
  end
5
7
 
6
8
  module Puma
@@ -52,22 +54,21 @@ module Puma
52
54
  output = engine_read_all
53
55
  return output if output
54
56
 
55
- begin
56
- data = @socket.read_nonblock(size, exception: false)
57
- if data == :wait_readable || data == :wait_writable
58
- if @socket.to_io.respond_to?(data)
59
- @socket.to_io.__send__(data)
60
- elsif data == :wait_readable
61
- IO.select([@socket.to_io])
62
- else
63
- IO.select(nil, [@socket.to_io])
64
- end
65
- elsif !data
66
- return nil
67
- else
68
- break
69
- end
70
- end while true
57
+ data = @socket.read_nonblock(size, exception: false)
58
+ if data == :wait_readable || data == :wait_writable
59
+ # It would make more sense to let @socket.read_nonblock raise
60
+ # EAGAIN if necessary but it seems like it'll misbehave on Windows.
61
+ # I don't have a Windows machine to debug this so I can't explain
62
+ # exactly whats happening in that OS. Please let me know if you
63
+ # find out!
64
+ #
65
+ # In the meantime, we can emulate the correct behavior by
66
+ # capturing :wait_readable & :wait_writable and raising EAGAIN
67
+ # ourselves.
68
+ raise IO::EAGAINWaitReadable
69
+ elsif data.nil?
70
+ return nil
71
+ end
71
72
 
72
73
  @engine.inject(data)
73
74
  output = engine_read_all
@@ -124,7 +125,7 @@ module Puma
124
125
 
125
126
  def read_and_drop(timeout = 1)
126
127
  return :timeout unless IO.select([@socket], nil, nil, timeout)
127
- read_nonblock(1024)
128
+ return :eof unless read_nonblock(1024)
128
129
  :drop
129
130
  rescue Errno::EAGAIN
130
131
  # do nothing
@@ -141,7 +142,7 @@ module Puma
141
142
  # Don't let this socket hold this loop forever.
142
143
  # If it can't send more packets within 1s, then give up.
143
144
  while should_drop_bytes?
144
- return if read_and_drop(1) == :timeout
145
+ return if [:timeout, :eof].include?(read_and_drop(1))
145
146
  end
146
147
  rescue IOError, SystemCallError
147
148
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
@@ -175,11 +176,18 @@ module Puma
175
176
 
176
177
  class Context
177
178
  attr_accessor :verify_mode
179
+ attr_reader :no_tlsv1, :no_tlsv1_1
180
+
181
+ def initialize
182
+ @no_tlsv1 = false
183
+ @no_tlsv1_1 = false
184
+ end
178
185
 
179
186
  if defined?(JRUBY_VERSION)
180
187
  # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
181
188
  attr_reader :keystore
182
189
  attr_accessor :keystore_pass
190
+ attr_accessor :ssl_cipher_list
183
191
 
184
192
  def keystore=(keystore)
185
193
  raise ArgumentError, "No such keystore file '#{keystore}'" unless File.exist? keystore
@@ -195,6 +203,7 @@ module Puma
195
203
  attr_reader :key
196
204
  attr_reader :cert
197
205
  attr_reader :ca
206
+ attr_accessor :ssl_cipher_filter
198
207
 
199
208
  def key=(key)
200
209
  raise ArgumentError, "No such key file '#{key}'" unless File.exist? key
@@ -216,6 +225,19 @@ module Puma
216
225
  raise "Cert not configured" unless @cert
217
226
  end
218
227
  end
228
+
229
+ # disables TLSv1
230
+ def no_tlsv1=(tlsv1)
231
+ raise ArgumentError, "Invalid value of no_tlsv1" unless ['true', 'false', true, false].include?(tlsv1)
232
+ @no_tlsv1 = tlsv1
233
+ end
234
+
235
+ # disables TLSv1 and TLSv1.1. Overrides `#no_tlsv1=`
236
+ def no_tlsv1_1=(tlsv1_1)
237
+ raise ArgumentError, "Invalid value of no_tlsv1" unless ['true', 'false', true, false].include?(tlsv1_1)
238
+ @no_tlsv1_1 = tlsv1_1
239
+ end
240
+
219
241
  end
220
242
 
221
243
  VERIFY_NONE = 0
data/lib/puma/null_io.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  # Provides an IO-like object that always appears to contain no data.
3
5
  # Used as the value for rack.input when the request has no body.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/plugin'
2
4
 
3
5
  Puma::Plugin.create do
data/lib/puma/plugin.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  class UnknownPlugin < RuntimeError; end
3
5
 
@@ -60,8 +62,11 @@ module Puma
60
62
  end
61
63
 
62
64
  def fire_background
63
- @background.each do |b|
64
- Thread.new(&b)
65
+ @background.each_with_index do |b, i|
66
+ Thread.new do
67
+ Puma.set_thread_name "plugin background #{i}"
68
+ b.call
69
+ end
65
70
  end
66
71
  end
67
72
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  end
3
5
 
@@ -110,7 +112,8 @@ module Puma::Rack
110
112
 
111
113
  has_options = false
112
114
  server.valid_options.each do |name, description|
113
- next if name.to_s.match(/^(Host|Port)[^a-zA-Z]/) # ignore handler's host and port options, we do our own.
115
+ next if name.to_s =~ /^(Host|Port)[^a-zA-Z]/ # ignore handler's host and port options, we do our own.
116
+
114
117
  info << " -O %-21s %s" % [name, description]
115
118
  has_options = true
116
119
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma::Rack
2
4
  # Rack::URLMap takes a hash mapping urls or paths to apps, and
3
5
  # dispatches accordingly. Support for HTTP/1.1 host names exists if
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/handler/puma'
2
4
 
3
5
  module Rack::Handler