puma 3.7.1 → 4.1.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 (74) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +229 -1
  3. data/README.md +179 -212
  4. data/docs/architecture.md +37 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +24 -4
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +28 -0
  10. data/docs/restart.md +41 -0
  11. data/docs/signals.md +56 -3
  12. data/docs/systemd.md +130 -37
  13. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  14. data/ext/puma_http11/extconf.rb +8 -0
  15. data/ext/puma_http11/http11_parser.c +84 -84
  16. data/ext/puma_http11/http11_parser.rl +9 -9
  17. data/ext/puma_http11/mini_ssl.c +105 -9
  18. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  19. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +30 -6
  21. data/lib/puma.rb +10 -0
  22. data/lib/puma/accept_nonblock.rb +2 -0
  23. data/lib/puma/app/status.rb +13 -0
  24. data/lib/puma/binder.rb +33 -18
  25. data/lib/puma/cli.rb +48 -33
  26. data/lib/puma/client.rb +94 -22
  27. data/lib/puma/cluster.rb +69 -21
  28. data/lib/puma/commonlogger.rb +2 -0
  29. data/lib/puma/configuration.rb +134 -136
  30. data/lib/puma/const.rb +16 -2
  31. data/lib/puma/control_cli.rb +31 -18
  32. data/lib/puma/convenient.rb +5 -3
  33. data/lib/puma/daemon_ext.rb +2 -0
  34. data/lib/puma/delegation.rb +2 -0
  35. data/lib/puma/detect.rb +2 -0
  36. data/lib/puma/dsl.rb +349 -113
  37. data/lib/puma/events.rb +8 -4
  38. data/lib/puma/io_buffer.rb +3 -6
  39. data/lib/puma/jruby_restart.rb +2 -1
  40. data/lib/puma/launcher.rb +60 -36
  41. data/lib/puma/minissl.rb +85 -28
  42. data/lib/puma/null_io.rb +2 -0
  43. data/lib/puma/plugin.rb +2 -0
  44. data/lib/puma/plugin/tmp_restart.rb +3 -2
  45. data/lib/puma/rack/builder.rb +4 -1
  46. data/lib/puma/rack/urlmap.rb +2 -0
  47. data/lib/puma/rack_default.rb +2 -0
  48. data/lib/puma/reactor.rb +218 -30
  49. data/lib/puma/runner.rb +18 -4
  50. data/lib/puma/server.rb +149 -56
  51. data/lib/puma/single.rb +16 -5
  52. data/lib/puma/state_file.rb +2 -0
  53. data/lib/puma/tcp_logger.rb +2 -0
  54. data/lib/puma/thread_pool.rb +59 -6
  55. data/lib/puma/util.rb +2 -6
  56. data/lib/rack/handler/puma.rb +58 -19
  57. data/tools/jungle/README.md +12 -2
  58. data/tools/jungle/init.d/README.md +2 -0
  59. data/tools/jungle/init.d/puma +8 -8
  60. data/tools/jungle/init.d/run-puma +1 -1
  61. data/tools/jungle/rc.d/README.md +74 -0
  62. data/tools/jungle/rc.d/puma +61 -0
  63. data/tools/jungle/rc.d/puma.conf +10 -0
  64. data/tools/trickletest.rb +1 -1
  65. metadata +25 -85
  66. data/.github/issue_template.md +0 -20
  67. data/Gemfile +0 -12
  68. data/Manifest.txt +0 -77
  69. data/Rakefile +0 -158
  70. data/gemfiles/2.1-Gemfile +0 -12
  71. data/lib/puma/compat.rb +0 -14
  72. data/lib/puma/java_io_buffer.rb +0 -45
  73. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  74. data/puma.gemspec +0 -52
data/lib/puma/events.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/const'
2
4
  require "puma/null_io"
3
5
  require 'stringio'
@@ -27,8 +29,8 @@ module Puma
27
29
  #
28
30
  def initialize(stdout, stderr)
29
31
  @formatter = DefaultFormatter.new
30
- @stdout = stdout
31
- @stderr = stderr
32
+ @stdout = stdout.dup
33
+ @stderr = stderr.dup
32
34
 
33
35
  @stdout.sync = true
34
36
  @stderr.sync = true
@@ -91,8 +93,10 @@ module Puma
91
93
  # parsing exception.
92
94
  #
93
95
  def parse_error(server, env, error)
94
- @stderr.puts "#{Time.now}: HTTP parse error, malformed request (#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}): #{error.inspect}"
95
- @stderr.puts "#{Time.now}: ENV: #{env.inspect}\n---\n"
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"
96
100
  end
97
101
 
98
102
  # An SSL error has occurred.
@@ -1,7 +1,4 @@
1
- require 'puma/detect'
1
+ # frozen_string_literal: true
2
2
 
3
- if Puma.jruby?
4
- require 'puma/java_io_buffer'
5
- else
6
- require 'puma/puma_http11'
7
- end
3
+ require 'puma/detect'
4
+ require 'puma/puma_http11'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ffi'
2
4
 
3
5
  module Puma
@@ -80,4 +82,3 @@ module Puma
80
82
  end
81
83
  end
82
84
  end
83
-
data/lib/puma/launcher.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/events'
2
4
  require 'puma/detect'
3
5
 
@@ -34,13 +36,13 @@ module Puma
34
36
  #
35
37
  # Examples:
36
38
  #
37
- # conf = Puma::Configuration.new do |c|
38
- # c.threads 1, 10
39
- # c.app do |env|
39
+ # conf = Puma::Configuration.new do |user_config|
40
+ # user_config.threads 1, 10
41
+ # user_config.app do |env|
40
42
  # [200, {}, ["hello world"]]
41
43
  # end
42
44
  # end
43
- # Puma::Launcher.new(conf, argv: Puma::Events.stdio).run
45
+ # Puma::Launcher.new(conf, events: Puma::Events.stdio).run
44
46
  def initialize(conf, launcher_args={})
45
47
  @runner = nil
46
48
  @events = launcher_args[:events] || Events::DEFAULT
@@ -59,11 +61,15 @@ module Puma
59
61
  @config.load
60
62
 
61
63
  @options = @config.options
64
+ @config.clamp
65
+
66
+ @events.formatter = Events::PidFormatter.new if clustered?
67
+ @events.formatter = options[:log_formatter] if @options[:log_formatter]
62
68
 
63
69
  generate_restart_data
64
70
 
65
- if clustered? && (Puma.jruby? || Puma.windows?)
66
- unsupported 'worker mode not supported on JRuby or Windows'
71
+ if clustered? && !Process.respond_to?(:fork)
72
+ unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
67
73
  end
68
74
 
69
75
  if @options[:daemon] && Puma.windows?
@@ -78,13 +84,13 @@ module Puma
78
84
  set_rack_environment
79
85
 
80
86
  if clustered?
81
- @events.formatter = Events::PidFormatter.new
82
87
  @options[:logger] = @events
83
88
 
84
89
  @runner = Cluster.new(self, @events)
85
90
  else
86
91
  @runner = Single.new(self, @events)
87
92
  end
93
+ Puma.stats_object = @runner
88
94
 
89
95
  @status = :run
90
96
  end
@@ -162,6 +168,17 @@ module Puma
162
168
 
163
169
  # Run the server. This blocks until the server is stopped
164
170
  def run
171
+ previous_env =
172
+ if defined?(Bundler)
173
+ env = Bundler::ORIGINAL_ENV.dup
174
+ # add -rbundler/setup so we load from Gemfile when restarting
175
+ bundle = "-rbundler/setup"
176
+ env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
177
+ env
178
+ else
179
+ ENV.to_h
180
+ end
181
+
165
182
  @config.clamp
166
183
 
167
184
  @config.plugins.fire_starts self
@@ -177,6 +194,7 @@ module Puma
177
194
  graceful_stop
178
195
  when :restart
179
196
  log "* Restarting..."
197
+ ENV.replace(previous_env)
180
198
  @runner.before_restart
181
199
  restart!
182
200
  when :exit
@@ -198,6 +216,15 @@ module Puma
198
216
  end
199
217
  end
200
218
 
219
+ 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}")
225
+ end
226
+ end
227
+
201
228
  private
202
229
 
203
230
  def reload_worker_directory
@@ -221,13 +248,13 @@ module Puma
221
248
  else
222
249
  redirects = {:close_others => true}
223
250
  @binder.listeners.each_with_index do |(l, io), i|
224
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
225
- redirects[io.to_i] = io.to_i
251
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
252
+ redirects[io.to_i] = io.to_i
226
253
  end
227
254
 
228
255
  argv = restart_args
229
256
  Dir.chdir(@restart_dir)
230
- argv += [redirects] if RUBY_VERSION >= '1.9'
257
+ argv += [redirects]
231
258
  Kernel.exec(*argv)
232
259
  end
233
260
  end
@@ -256,7 +283,7 @@ module Puma
256
283
  wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
257
284
  args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
258
285
  # Ruby 2.0+ defaults to true which breaks socket activation
259
- args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
286
+ args += [{:close_others => false}]
260
287
  Kernel.exec(*args)
261
288
  end
262
289
  end
@@ -285,8 +312,8 @@ module Puma
285
312
  end
286
313
 
287
314
  def title
288
- buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
289
- buffer << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
315
+ buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
316
+ buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
290
317
  buffer
291
318
  end
292
319
 
@@ -303,16 +330,6 @@ module Puma
303
330
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
304
331
  end
305
332
 
306
- def close_binder_listeners
307
- @binder.listeners.each do |l, io|
308
- io.close
309
- uri = URI.parse(l)
310
- next unless uri.scheme == 'unix'
311
- File.unlink("#{uri.host}#{uri.path}")
312
- end
313
- end
314
-
315
-
316
333
  def generate_restart_data
317
334
  if dir = @options[:directory]
318
335
  @restart_dir = dir
@@ -336,8 +353,6 @@ module Puma
336
353
 
337
354
  @restart_dir ||= Dir.pwd
338
355
 
339
- require 'rubygems'
340
-
341
356
  # if $0 is a file in the current directory, then restart
342
357
  # it the same, otherwise add -S on there because it was
343
358
  # picked up in PATH.
@@ -348,9 +363,10 @@ module Puma
348
363
  arg0 = [Gem.ruby, "-S", $0]
349
364
  end
350
365
 
351
- # Detect and reinject -Ilib from the command line
366
+ # Detect and reinject -Ilib from the command line, used for testing without bundler
367
+ # cruby has an expanded path, jruby has just "lib"
352
368
  lib = File.expand_path "lib"
353
- arg0[1,0] = ["-I", lib] if $:[0] == lib
369
+ arg0[1,0] = ["-I", lib] if [lib, "lib"].include?($LOAD_PATH[0])
354
370
 
355
371
  if defined? Puma::WILD_ARGS
356
372
  @restart_argv = arg0 + Puma::WILD_ARGS + @original_argv
@@ -380,12 +396,28 @@ module Puma
380
396
 
381
397
  begin
382
398
  Signal.trap "SIGTERM" do
383
- stop
399
+ graceful_stop
400
+
401
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
384
402
  end
385
403
  rescue Exception
386
404
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
387
405
  end
388
406
 
407
+ begin
408
+ Signal.trap "SIGINT" do
409
+ if Puma.jruby?
410
+ @status = :exit
411
+ graceful_stop
412
+ exit
413
+ end
414
+
415
+ stop
416
+ end
417
+ rescue Exception
418
+ log "*** SIGINT not implemented, signal based gracefully stopping unavailable!"
419
+ end
420
+
389
421
  begin
390
422
  Signal.trap "SIGHUP" do
391
423
  if @runner.redirected_io?
@@ -397,14 +429,6 @@ module Puma
397
429
  rescue Exception
398
430
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
399
431
  end
400
-
401
- if Puma.jruby?
402
- Signal.trap("INT") do
403
- @status = :exit
404
- graceful_stop
405
- exit
406
- end
407
- end
408
432
  end
409
433
  end
410
434
  end
data/lib/puma/minissl.rb CHANGED
@@ -1,3 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'io/wait'
5
+ rescue LoadError
6
+ end
7
+
1
8
  module Puma
2
9
  module MiniSSL
3
10
  class Socket
@@ -11,6 +18,10 @@ module Puma
11
18
  @socket
12
19
  end
13
20
 
21
+ def closed?
22
+ @socket.closed?
23
+ end
24
+
14
25
  def readpartial(size)
15
26
  while true
16
27
  output = @engine.read
@@ -36,12 +47,28 @@ module Puma
36
47
  output
37
48
  end
38
49
 
39
- def read_nonblock(size)
50
+ def read_nonblock(size, *_)
51
+ # *_ is to deal with keyword args that were added
52
+ # at some point (and being used in the wild)
40
53
  while true
41
54
  output = engine_read_all
42
55
  return output if output
43
56
 
44
- data = @socket.read_nonblock(size)
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
45
72
 
46
73
  @engine.inject(data)
47
74
  output = engine_read_all
@@ -55,6 +82,8 @@ module Puma
55
82
  end
56
83
 
57
84
  def write(data)
85
+ return 0 if data.empty?
86
+
58
87
  need = data.bytesize
59
88
 
60
89
  while true
@@ -77,39 +106,46 @@ module Puma
77
106
  alias_method :syswrite, :write
78
107
  alias_method :<<, :write
79
108
 
109
+ # This is a temporary fix to deal with websockets code using
110
+ # write_nonblock. The problem with implementing it properly
111
+ # is that it means we'd have to have the ability to rewind
112
+ # an engine because after we write+extract, the socket
113
+ # write_nonblock call might raise an exception and later
114
+ # code would pass the same data in, but the engine would think
115
+ # it had already written the data in. So for the time being
116
+ # (and since write blocking is quite rare), go ahead and actually
117
+ # block in write_nonblock.
118
+ def write_nonblock(data, *_)
119
+ write data
120
+ end
121
+
80
122
  def flush
81
123
  @socket.flush
82
124
  end
83
125
 
84
- def close
85
- begin
86
- # Try to setup (so that we can then close them) any
87
- # partially initialized sockets.
88
- while @engine.init?
89
- # Don't let this socket hold this loop forever.
90
- # If it can't send more packets within 1s, then
91
- # give up.
92
- return unless IO.select([@socket], nil, nil, 1)
93
- begin
94
- read_nonblock(1024)
95
- rescue Errno::EAGAIN
96
- end
97
- end
98
-
99
- done = @engine.shutdown
100
-
101
- while true
102
- enc = @engine.extract
103
- @socket.write enc
104
-
105
- notify = @socket.sysread(1024)
126
+ def read_and_drop(timeout = 1)
127
+ return :timeout unless IO.select([@socket], nil, nil, timeout)
128
+ return :eof unless read_nonblock(1024)
129
+ :drop
130
+ rescue Errno::EAGAIN
131
+ # do nothing
132
+ :eagain
133
+ end
106
134
 
107
- @engine.inject notify
108
- done = @engine.shutdown
135
+ def should_drop_bytes?
136
+ @engine.init? || !@engine.shutdown
137
+ end
109
138
 
110
- break if done
139
+ def close
140
+ begin
141
+ # Read any drop any partially initialized sockets and any received bytes during shutdown.
142
+ # Don't let this socket hold this loop forever.
143
+ # If it can't send more packets within 1s, then give up.
144
+ while should_drop_bytes?
145
+ return if [:timeout, :eof].include?(read_and_drop(1))
111
146
  end
112
147
  rescue IOError, SystemCallError
148
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
113
149
  # nothing
114
150
  ensure
115
151
  @socket.close
@@ -140,11 +176,18 @@ module Puma
140
176
 
141
177
  class Context
142
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
143
185
 
144
186
  if defined?(JRUBY_VERSION)
145
187
  # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
146
188
  attr_reader :keystore
147
189
  attr_accessor :keystore_pass
190
+ attr_accessor :ssl_cipher_list
148
191
 
149
192
  def keystore=(keystore)
150
193
  raise ArgumentError, "No such keystore file '#{keystore}'" unless File.exist? keystore
@@ -160,6 +203,7 @@ module Puma
160
203
  attr_reader :key
161
204
  attr_reader :cert
162
205
  attr_reader :ca
206
+ attr_accessor :ssl_cipher_filter
163
207
 
164
208
  def key=(key)
165
209
  raise ArgumentError, "No such key file '#{key}'" unless File.exist? key
@@ -181,6 +225,19 @@ module Puma
181
225
  raise "Cert not configured" unless @cert
182
226
  end
183
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
+
184
241
  end
185
242
 
186
243
  VERIFY_NONE = 0
@@ -214,7 +271,7 @@ module Puma
214
271
  end
215
272
 
216
273
  def close
217
- @socket.close
274
+ @socket.close unless @socket.closed? # closed? call is for Windows
218
275
  end
219
276
  end
220
277
  end