puma 5.3.2 → 5.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +211 -11
  3. data/README.md +47 -6
  4. data/docs/architecture.md +49 -16
  5. data/docs/compile_options.md +4 -2
  6. data/docs/deployment.md +53 -67
  7. data/docs/plugins.md +15 -15
  8. data/docs/rails_dev_mode.md +2 -3
  9. data/docs/restart.md +6 -6
  10. data/docs/signals.md +11 -10
  11. data/docs/stats.md +8 -8
  12. data/docs/systemd.md +64 -67
  13. data/ext/puma_http11/extconf.rb +34 -6
  14. data/ext/puma_http11/http11_parser.c +23 -10
  15. data/ext/puma_http11/http11_parser_common.rl +1 -1
  16. data/ext/puma_http11/mini_ssl.c +90 -12
  17. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +49 -47
  18. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +38 -55
  19. data/ext/puma_http11/puma_http11.c +1 -1
  20. data/lib/puma/app/status.rb +7 -4
  21. data/lib/puma/binder.rb +51 -6
  22. data/lib/puma/cli.rb +14 -4
  23. data/lib/puma/client.rb +143 -25
  24. data/lib/puma/cluster/worker.rb +8 -18
  25. data/lib/puma/cluster/worker_handle.rb +4 -0
  26. data/lib/puma/cluster.rb +30 -24
  27. data/lib/puma/configuration.rb +4 -1
  28. data/lib/puma/const.rb +9 -8
  29. data/lib/puma/control_cli.rb +19 -13
  30. data/lib/puma/detect.rb +8 -2
  31. data/lib/puma/dsl.rb +111 -13
  32. data/lib/puma/{json.rb → json_serialization.rb} +1 -1
  33. data/lib/puma/launcher.rb +15 -1
  34. data/lib/puma/minissl/context_builder.rb +8 -6
  35. data/lib/puma/minissl.rb +33 -27
  36. data/lib/puma/null_io.rb +5 -0
  37. data/lib/puma/plugin.rb +2 -2
  38. data/lib/puma/rack/builder.rb +1 -1
  39. data/lib/puma/request.rb +19 -10
  40. data/lib/puma/runner.rb +22 -8
  41. data/lib/puma/server.rb +37 -29
  42. data/lib/puma/state_file.rb +42 -7
  43. data/lib/puma/thread_pool.rb +7 -5
  44. data/lib/puma/util.rb +20 -4
  45. data/lib/puma.rb +6 -4
  46. data/lib/rack/version_restriction.rb +15 -0
  47. data/tools/Dockerfile +1 -1
  48. metadata +8 -7
data/lib/puma/client.rb CHANGED
@@ -23,6 +23,8 @@ module Puma
23
23
 
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
+ class HttpParserError501 < IOError; end
27
+
26
28
  # An instance of this class represents a unique request from a client.
27
29
  # For example, this could be a web request from a browser or from CURL.
28
30
  #
@@ -35,7 +37,30 @@ module Puma
35
37
  # Instances of this class are responsible for knowing if
36
38
  # the header and body are fully buffered via the `try_to_finish` method.
37
39
  # They can be used to "time out" a response via the `timeout_at` reader.
40
+ #
38
41
  class Client
42
+
43
+ # this tests all values but the last, which must be chunked
44
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
+
46
+ # chunked body validation
47
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
+ CHUNK_VALID_ENDING = Const::LINE_END
49
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
50
+
51
+ # The maximum number of bytes we'll buffer looking for a valid
52
+ # chunk header.
53
+ MAX_CHUNK_HEADER_SIZE = 4096
54
+
55
+ # The maximum amount of excess data the client sends
56
+ # using chunk size extensions before we abort the connection.
57
+ MAX_CHUNK_EXCESS = 16 * 1024
58
+
59
+ # Content-Length header value validation
60
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
61
+
62
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
63
+
39
64
  # The object used for a request with no body. All requests with
40
65
  # no body share this one object since it has no state.
41
66
  EmptyBody = NullIO.new
@@ -56,6 +81,7 @@ module Puma
56
81
  @parser = HttpParser.new
57
82
  @parsed_bytes = 0
58
83
  @read_header = true
84
+ @read_proxy = false
59
85
  @ready = false
60
86
 
61
87
  @body = nil
@@ -71,6 +97,7 @@ module Puma
71
97
  @peerip = nil
72
98
  @listener = nil
73
99
  @remote_addr_header = nil
100
+ @expect_proxy_proto = false
74
101
 
75
102
  @body_remain = 0
76
103
 
@@ -106,7 +133,7 @@ module Puma
106
133
 
107
134
  # @!attribute [r] in_data_phase
108
135
  def in_data_phase
109
- !@read_header
136
+ !(@read_header || @read_proxy)
110
137
  end
111
138
 
112
139
  def set_timeout(val)
@@ -121,6 +148,7 @@ module Puma
121
148
  def reset(fast_check=true)
122
149
  @parser.reset
123
150
  @read_header = true
151
+ @read_proxy = !!@expect_proxy_proto
124
152
  @env = @proto_env.dup
125
153
  @body = nil
126
154
  @tempfile = nil
@@ -131,6 +159,8 @@ module Puma
131
159
  @in_last_chunk = false
132
160
 
133
161
  if @buffer
162
+ return false unless try_to_parse_proxy_protocol
163
+
134
164
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
135
165
 
136
166
  if @parser.finished?
@@ -143,8 +173,7 @@ module Puma
143
173
  return false
144
174
  else
145
175
  begin
146
- if fast_check &&
147
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
176
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
148
177
  return try_to_finish
149
178
  end
150
179
  rescue IOError
@@ -157,13 +186,37 @@ module Puma
157
186
  def close
158
187
  begin
159
188
  @io.close
160
- rescue IOError
161
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
189
+ rescue IOError, Errno::EBADF
190
+ Puma::Util.purge_interrupt_queue
162
191
  end
163
192
  end
164
193
 
194
+ # If necessary, read the PROXY protocol from the buffer. Returns
195
+ # false if more data is needed.
196
+ def try_to_parse_proxy_protocol
197
+ if @read_proxy
198
+ if @expect_proxy_proto == :v1
199
+ if @buffer.include? "\r\n"
200
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
201
+ if md[1]
202
+ @peerip = md[1].split(" ")[0]
203
+ end
204
+ @buffer = md.post_match
205
+ end
206
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
207
+ # request, this is just HTTP from a non-PROXY client; move on
208
+ @read_proxy = false
209
+ return @buffer.size > 0
210
+ else
211
+ return false
212
+ end
213
+ end
214
+ end
215
+ true
216
+ end
217
+
165
218
  def try_to_finish
166
- return read_body unless @read_header
219
+ return read_body if in_data_phase
167
220
 
168
221
  begin
169
222
  data = @io.read_nonblock(CHUNK_SIZE)
@@ -188,6 +241,8 @@ module Puma
188
241
  @buffer = data
189
242
  end
190
243
 
244
+ return false unless try_to_parse_proxy_protocol
245
+
191
246
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
192
247
 
193
248
  if @parser.finished?
@@ -202,13 +257,13 @@ module Puma
202
257
 
203
258
  def eagerly_finish
204
259
  return true if @ready
205
- return false unless IO.select([@to_io], nil, nil, 0)
260
+ return false unless @to_io.wait_readable(0)
206
261
  try_to_finish
207
262
  end
208
263
 
209
264
  def finish(timeout)
210
265
  return if @ready
211
- IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
266
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
212
267
  end
213
268
 
214
269
  def timeout!
@@ -244,6 +299,17 @@ module Puma
244
299
  @parsed_bytes == 0
245
300
  end
246
301
 
302
+ def expect_proxy_proto=(val)
303
+ if val
304
+ if @read_header
305
+ @read_proxy = true
306
+ end
307
+ else
308
+ @read_proxy = false
309
+ end
310
+ @expect_proxy_proto = val
311
+ end
312
+
247
313
  private
248
314
 
249
315
  def setup_body
@@ -261,16 +327,27 @@ module Puma
261
327
  body = @parser.body
262
328
 
263
329
  te = @env[TRANSFER_ENCODING2]
264
-
265
330
  if te
266
- if te.include?(",")
267
- te.split(",").each do |part|
268
- if CHUNKED.casecmp(part.strip) == 0
269
- return setup_chunked_body(body)
270
- end
331
+ te_lwr = te.downcase
332
+ if te.include? ','
333
+ te_ary = te_lwr.split ','
334
+ te_count = te_ary.count CHUNKED
335
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
336
+ if te_ary.last == CHUNKED && te_count == 1 && te_valid
337
+ @env.delete TRANSFER_ENCODING2
338
+ return setup_chunked_body body
339
+ elsif te_count >= 1
340
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
341
+ elsif !te_valid
342
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
271
343
  end
272
- elsif CHUNKED.casecmp(te) == 0
273
- return setup_chunked_body(body)
344
+ elsif te_lwr == CHUNKED
345
+ @env.delete TRANSFER_ENCODING2
346
+ return setup_chunked_body body
347
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
348
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
349
+ else
350
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
274
351
  end
275
352
  end
276
353
 
@@ -278,7 +355,12 @@ module Puma
278
355
 
279
356
  cl = @env[CONTENT_LENGTH]
280
357
 
281
- unless cl
358
+ if cl
359
+ # cannot contain characters that are not \d, or be empty
360
+ if cl =~ CONTENT_LENGTH_VALUE_INVALID || cl.empty?
361
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
362
+ end
363
+ else
282
364
  @buffer = body.empty? ? nil : body
283
365
  @body = EmptyBody
284
366
  set_ready
@@ -309,7 +391,7 @@ module Puma
309
391
 
310
392
  @body_remain = remain
311
393
 
312
- return false
394
+ false
313
395
  end
314
396
 
315
397
  def read_body
@@ -386,6 +468,7 @@ module Puma
386
468
  @chunked_body = true
387
469
  @partial_part_left = 0
388
470
  @prev_chunk = ""
471
+ @excess_cr = 0
389
472
 
390
473
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
391
474
  @body.unlink
@@ -436,25 +519,51 @@ module Puma
436
519
 
437
520
  while !io.eof?
438
521
  line = io.gets
439
- if line.end_with?("\r\n")
440
- len = line.strip.to_i(16)
522
+ if line.end_with?(CHUNK_VALID_ENDING)
523
+ # Puma doesn't process chunk extensions, but should parse if they're
524
+ # present, which is the reason for the semicolon regex
525
+ chunk_hex = line.strip[/\A[^;]+/]
526
+ if chunk_hex =~ CHUNK_SIZE_INVALID
527
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
528
+ end
529
+ len = chunk_hex.to_i(16)
441
530
  if len == 0
442
531
  @in_last_chunk = true
443
532
  @body.rewind
444
533
  rest = io.read
445
- last_crlf_size = "\r\n".bytesize
446
- if rest.bytesize < last_crlf_size
534
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
447
535
  @buffer = nil
448
- @partial_part_left = last_crlf_size - rest.bytesize
536
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
449
537
  return false
450
538
  else
451
- @buffer = rest[last_crlf_size..-1]
539
+ # if the next character is a CRLF, set buffer to everything after that CRLF
540
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
541
+ CHUNK_VALID_ENDING_SIZE
542
+ else # we have started a trailer section, which we do not support. skip it!
543
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
544
+ end
545
+
546
+ @buffer = rest[start_of_rest..-1]
452
547
  @buffer = nil if @buffer.empty?
453
548
  set_ready
454
549
  return true
455
550
  end
456
551
  end
457
552
 
553
+ # Track the excess as a function of the size of the
554
+ # header vs the size of the actual data. Excess can
555
+ # go negative (and is expected to) when the body is
556
+ # significant.
557
+ # The additional of chunk_hex.size and 2 compensates
558
+ # for a client sending 1 byte in a chunked body over
559
+ # a long period of time, making sure that that client
560
+ # isn't accidentally eventually punished.
561
+ @excess_cr += (line.size - len - chunk_hex.size - 2)
562
+
563
+ if @excess_cr >= MAX_CHUNK_EXCESS
564
+ raise HttpParserError, "Maximum chunk excess detected"
565
+ end
566
+
458
567
  len += 2
459
568
 
460
569
  part = io.read(len)
@@ -468,7 +577,12 @@ module Puma
468
577
 
469
578
  case
470
579
  when got == len
471
- write_chunk(part[0..-3]) # to skip the ending \r\n
580
+ # proper chunked segment must end with "\r\n"
581
+ if part.end_with? CHUNK_VALID_ENDING
582
+ write_chunk(part[0..-3]) # to skip the ending \r\n
583
+ else
584
+ raise HttpParserError, "Chunk size mismatch"
585
+ end
472
586
  when got <= len - 2
473
587
  write_chunk(part)
474
588
  @partial_part_left = len - part.size
@@ -477,6 +591,10 @@ module Puma
477
591
  @partial_part_left = len - part.size
478
592
  end
479
593
  else
594
+ if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
595
+ raise HttpParserError, "maximum size of chunk header exceeded"
596
+ end
597
+
480
598
  @prev_chunk = line
481
599
  return false
482
600
  end
@@ -33,9 +33,9 @@ module Puma
33
33
  Signal.trap "SIGINT", "IGNORE"
34
34
  Signal.trap "SIGCHLD", "DEFAULT"
35
35
 
36
- Thread.new do
37
- Puma.set_thread_name "worker check pipe"
38
- IO.select [@check_pipe]
36
+ Thread.new do
37
+ Puma.set_thread_name "wrkr check"
38
+ @check_pipe.wait_readable
39
39
  log "! Detected parent died, dying"
40
40
  exit! 1
41
41
  end
@@ -76,7 +76,7 @@ module Puma
76
76
  end
77
77
 
78
78
  Thread.new do
79
- Puma.set_thread_name "worker fork pipe"
79
+ Puma.set_thread_name "wrkr fork"
80
80
  while (idx = @fork_pipe.gets)
81
81
  idx = idx.to_i
82
82
  if idx == -1 # stop server
@@ -106,7 +106,7 @@ module Puma
106
106
  begin
107
107
  @worker_write << "b#{Process.pid}:#{index}\n"
108
108
  rescue SystemCallError, IOError
109
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
109
+ Puma::Util.purge_interrupt_queue
110
110
  STDERR.puts "Master seems to have exited, exiting."
111
111
  return
112
112
  end
@@ -114,7 +114,7 @@ module Puma
114
114
  while restart_server.pop
115
115
  server_thread = server.run
116
116
  stat_thread ||= Thread.new(@worker_write) do |io|
117
- Puma.set_thread_name "stat payload"
117
+ Puma.set_thread_name "stat pld"
118
118
  base_payload = "p#{Process.pid}"
119
119
 
120
120
  while true
@@ -127,10 +127,10 @@ module Puma
127
127
  payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
128
128
  io << payload
129
129
  rescue IOError
130
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
130
+ Puma::Util.purge_interrupt_queue
131
131
  break
132
132
  end
133
- sleep Const::WORKER_CHECK_INTERVAL
133
+ sleep @options[:worker_check_interval]
134
134
  end
135
135
  end
136
136
  server_thread.join
@@ -168,16 +168,6 @@ module Puma
168
168
  @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
169
169
  pid
170
170
  end
171
-
172
- def wakeup!
173
- return unless @wakeup
174
-
175
- begin
176
- @wakeup.write "!" unless @wakeup.closed?
177
- rescue SystemCallError, IOError
178
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
179
- end
180
- end
181
171
  end
182
172
  end
183
173
  end
@@ -40,6 +40,10 @@ module Puma
40
40
  @stage = :booted
41
41
  end
42
42
 
43
+ def term!
44
+ @term = true
45
+ end
46
+
43
47
  def term?
44
48
  @term
45
49
  end
data/lib/puma/cluster.rb CHANGED
@@ -108,24 +108,42 @@ module Puma
108
108
  def cull_workers
109
109
  diff = @workers.size - @options[:workers]
110
110
  return if diff < 1
111
+ debug "Culling #{diff} workers"
111
112
 
112
- debug "Culling #{diff.inspect} workers"
113
+ workers = workers_to_cull(diff)
114
+ debug "Workers to cull: #{workers.inspect}"
113
115
 
114
- workers_to_cull = @workers[-diff,diff]
115
- debug "Workers to cull: #{workers_to_cull.inspect}"
116
-
117
- workers_to_cull.each do |worker|
116
+ workers.each do |worker|
118
117
  log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
119
118
  worker.term
120
119
  end
121
120
  end
122
121
 
122
+ def workers_to_cull(diff)
123
+ workers = @workers.sort_by(&:started_at)
124
+
125
+ # In fork_worker mode, worker 0 acts as our master process.
126
+ # We should avoid culling it to preserve copy-on-write memory gains.
127
+ workers.reject! { |w| w.index == 0 } if @options[:fork_worker]
128
+
129
+ workers[cull_start_index(diff), diff]
130
+ end
131
+
132
+ def cull_start_index(diff)
133
+ case @options[:worker_culling_strategy]
134
+ when :oldest
135
+ 0
136
+ else # :youngest
137
+ -diff
138
+ end
139
+ end
140
+
123
141
  # @!attribute [r] next_worker_index
124
142
  def next_worker_index
125
- all_positions = 0...@options[:workers]
126
- occupied_positions = @workers.map { |w| w.index }
127
- available_positions = all_positions.to_a - occupied_positions
128
- available_positions.first
143
+ occupied_positions = @workers.map(&:index)
144
+ idx = 0
145
+ idx += 1 until !occupied_positions.include?(idx)
146
+ idx
129
147
  end
130
148
 
131
149
  def all_workers_booted?
@@ -135,7 +153,7 @@ module Puma
135
153
  def check_workers
136
154
  return if @next_check >= Time.now
137
155
 
138
- @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
156
+ @next_check = Time.now + @options[:worker_check_interval]
139
157
 
140
158
  timeout_workers
141
159
  wait_workers
@@ -164,16 +182,6 @@ module Puma
164
182
  ].compact.min
165
183
  end
166
184
 
167
- def wakeup!
168
- return unless @wakeup
169
-
170
- begin
171
- @wakeup.write "!" unless @wakeup.closed?
172
- rescue SystemCallError, IOError
173
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
174
- end
175
- end
176
-
177
185
  def worker(index, master)
178
186
  @workers = []
179
187
 
@@ -426,9 +434,7 @@ module Puma
426
434
 
427
435
  check_workers
428
436
 
429
- res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
430
-
431
- if res
437
+ if read.wait_readable([0, @next_check - Time.now].max)
432
438
  req = read.read_nonblock(1)
433
439
 
434
440
  @next_check = Time.now if req == "!"
@@ -452,7 +458,7 @@ module Puma
452
458
  workers_not_booted -= 1
453
459
  when "e"
454
460
  # external term, see worker method, Signal.trap "SIGTERM"
455
- w.instance_variable_set :@term, true
461
+ w.term!
456
462
  when "t"
457
463
  w.term unless w.term?
458
464
  when "p"
@@ -11,6 +11,7 @@ module Puma
11
11
 
12
12
  DefaultTCPHost = "0.0.0.0"
13
13
  DefaultTCPPort = 9292
14
+ DefaultWorkerCheckInterval = 5
14
15
  DefaultWorkerTimeout = 60
15
16
  DefaultWorkerShutdownTimeout = 30
16
17
  end
@@ -195,12 +196,14 @@ module Puma
195
196
  :workers => Integer(ENV['WEB_CONCURRENCY'] || 0),
196
197
  :silence_single_worker_warning => false,
197
198
  :mode => :http,
199
+ :worker_check_interval => DefaultWorkerCheckInterval,
198
200
  :worker_timeout => DefaultWorkerTimeout,
199
201
  :worker_boot_timeout => DefaultWorkerTimeout,
200
202
  :worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
203
+ :worker_culling_strategy => :youngest,
201
204
  :remote_address => :socket,
202
205
  :tag => method(:infer_tag),
203
- :environment => -> { ENV['RACK_ENV'] || ENV['RAILS_ENV'] || "development" },
206
+ :environment => -> { ENV['APP_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development' },
204
207
  :rackup => DefaultRackup,
205
208
  :logger => STDOUT,
206
209
  :persistent_timeout => Const::PERSISTENT_TIMEOUT,
data/lib/puma/const.rb CHANGED
@@ -76,7 +76,7 @@ module Puma
76
76
  508 => 'Loop Detected',
77
77
  510 => 'Not Extended',
78
78
  511 => 'Network Authentication Required'
79
- }
79
+ }.freeze
80
80
 
81
81
  # For some HTTP status codes the client only expects headers.
82
82
  #
@@ -85,7 +85,7 @@ module Puma
85
85
  204 => true,
86
86
  205 => true,
87
87
  304 => true
88
- }
88
+ }.freeze
89
89
 
90
90
  # Frequently used constants when constructing requests or responses. Many times
91
91
  # the constant just refers to a string with the same contents. Using these constants
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "5.3.2".freeze
104
- CODE_NAME = "Sweetnighter".freeze
103
+ PUMA_VERSION = VERSION = "5.6.8".freeze
104
+ CODE_NAME = "Birdie's Version".freeze
105
105
 
106
106
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
107
107
 
@@ -145,9 +145,11 @@ module Puma
145
145
  408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze,
146
146
  # Indicate that there was an internal error, obviously.
147
147
  500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze,
148
+ # Incorrect or invalid header value
149
+ 501 => "HTTP/1.1 501 Not Implemented\r\n\r\n".freeze,
148
150
  # A common header for indicating the server is too busy. Not used yet.
149
151
  503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
150
- }
152
+ }.freeze
151
153
 
152
154
  # The basic max request size we'll try to read.
153
155
  CHUNK_SIZE = 16 * 1024
@@ -235,9 +237,6 @@ module Puma
235
237
 
236
238
  EARLY_HINTS = "rack.early_hints".freeze
237
239
 
238
- # Minimum interval to checks worker health
239
- WORKER_CHECK_INTERVAL = 5
240
-
241
240
  # Illegal character in the key or value of response header
242
241
  DQUOTE = "\"".freeze
243
242
  HTTP_HEADER_DELIMITER = Regexp.escape("(),/:;<=>?@[]{}\\").freeze
@@ -247,5 +246,7 @@ module Puma
247
246
 
248
247
  # Banned keys of response header
249
248
  BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
249
+
250
+ PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
250
251
  end
251
252
  end
@@ -17,26 +17,30 @@ module Puma
17
17
  CMD_PATH_SIG_MAP = {
18
18
  'gc' => nil,
19
19
  'gc-stats' => nil,
20
- 'halt' => 'SIGQUIT',
21
- 'phased-restart' => 'SIGUSR1',
22
- 'refork' => 'SIGURG',
20
+ 'halt' => 'SIGQUIT',
21
+ 'info' => 'SIGINFO',
22
+ 'phased-restart' => 'SIGUSR1',
23
+ 'refork' => 'SIGURG',
23
24
  'reload-worker-directory' => nil,
24
- 'restart' => 'SIGUSR2',
25
+ 'reopen-log' => 'SIGHUP',
26
+ 'restart' => 'SIGUSR2',
25
27
  'start' => nil,
26
28
  'stats' => nil,
27
29
  'status' => '',
28
- 'stop' => 'SIGTERM',
29
- 'thread-backtraces' => nil
30
+ 'stop' => 'SIGTERM',
31
+ 'thread-backtraces' => nil,
32
+ 'worker-count-down' => 'SIGTTOU',
33
+ 'worker-count-up' => 'SIGTTIN'
30
34
  }.freeze
31
35
 
32
36
  # @deprecated 6.0.0
33
37
  COMMANDS = CMD_PATH_SIG_MAP.keys.freeze
34
38
 
35
39
  # commands that cannot be used in a request
36
- NO_REQ_COMMANDS = %w{refork}.freeze
40
+ NO_REQ_COMMANDS = %w[info reopen-log worker-count-down worker-count-up].freeze
37
41
 
38
42
  # @version 5.0.0
39
- PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}.freeze
43
+ PRINTABLE_COMMANDS = %w[gc-stats stats thread-backtraces].freeze
40
44
 
41
45
  def initialize(argv, stdout=STDOUT, stderr=STDERR)
42
46
  @state = nil
@@ -47,7 +51,7 @@ module Puma
47
51
  @control_auth_token = nil
48
52
  @config_file = nil
49
53
  @command = nil
50
- @environment = ENV['RACK_ENV'] || ENV['RAILS_ENV']
54
+ @environment = ENV['APP_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV']
51
55
 
52
56
  @argv = argv.dup
53
57
  @stdout = stdout
@@ -185,8 +189,6 @@ module Puma
185
189
 
186
190
  if @command == 'status'
187
191
  message 'Puma is started'
188
- elsif NO_REQ_COMMANDS.include? @command
189
- raise "Invalid request command: #{@command}"
190
192
  else
191
193
  url = "/#{@command}"
192
194
 
@@ -242,7 +244,11 @@ module Puma
242
244
  @stdout.flush unless @stdout.sync
243
245
  return
244
246
  elsif sig.start_with? 'SIG'
245
- Process.kill sig, @pid
247
+ if Signal.list.key? sig.sub(/\ASIG/, '')
248
+ Process.kill sig, @pid
249
+ else
250
+ raise "Signal '#{sig}' not available'"
251
+ end
246
252
  elsif @command == 'status'
247
253
  begin
248
254
  Process.kill 0, @pid
@@ -268,7 +274,7 @@ module Puma
268
274
  return start if @command == 'start'
269
275
  prepare_configuration
270
276
 
271
- if Puma.windows? || @control_url
277
+ if Puma.windows? || @control_url && !NO_REQ_COMMANDS.include?(@command)
272
278
  send_request
273
279
  else
274
280
  send_signal
data/lib/puma/detect.rb CHANGED
@@ -10,8 +10,10 @@ module Puma
10
10
 
11
11
  IS_JRUBY = Object.const_defined? :JRUBY_VERSION
12
12
 
13
- IS_WINDOWS = !!(RUBY_PLATFORM =~ /mswin|ming|cygwin/ ||
14
- IS_JRUBY && RUBY_DESCRIPTION =~ /mswin/)
13
+ IS_OSX = RUBY_PLATFORM.include? 'darwin'
14
+
15
+ IS_WINDOWS = !!(RUBY_PLATFORM =~ /mswin|ming|cygwin/) ||
16
+ IS_JRUBY && RUBY_DESCRIPTION.include?('mswin')
15
17
 
16
18
  # @version 5.2.0
17
19
  IS_MRI = (RUBY_ENGINE == 'ruby' || RUBY_ENGINE.nil?)
@@ -20,6 +22,10 @@ module Puma
20
22
  IS_JRUBY
21
23
  end
22
24
 
25
+ def self.osx?
26
+ IS_OSX
27
+ end
28
+
23
29
  def self.windows?
24
30
  IS_WINDOWS
25
31
  end