puma 5.0.4 → 5.5.1

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 +250 -48
  3. data/README.md +90 -24
  4. data/docs/architecture.md +57 -20
  5. data/docs/compile_options.md +21 -0
  6. data/docs/deployment.md +53 -67
  7. data/docs/fork_worker.md +2 -0
  8. data/docs/jungle/rc.d/README.md +1 -1
  9. data/docs/kubernetes.md +66 -0
  10. data/docs/plugins.md +15 -15
  11. data/docs/rails_dev_mode.md +28 -0
  12. data/docs/restart.md +7 -7
  13. data/docs/signals.md +10 -10
  14. data/docs/stats.md +142 -0
  15. data/docs/systemd.md +85 -66
  16. data/ext/puma_http11/extconf.rb +36 -6
  17. data/ext/puma_http11/http11_parser.c +64 -59
  18. data/ext/puma_http11/http11_parser.h +1 -1
  19. data/ext/puma_http11/http11_parser.java.rl +1 -1
  20. data/ext/puma_http11/http11_parser.rl +1 -1
  21. data/ext/puma_http11/http11_parser_common.rl +1 -1
  22. data/ext/puma_http11/mini_ssl.c +177 -84
  23. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +39 -41
  24. data/ext/puma_http11/puma_http11.c +8 -2
  25. data/lib/puma/app/status.rb +4 -7
  26. data/lib/puma/binder.rb +121 -46
  27. data/lib/puma/cli.rb +9 -0
  28. data/lib/puma/client.rb +58 -19
  29. data/lib/puma/cluster/worker.rb +19 -16
  30. data/lib/puma/cluster/worker_handle.rb +9 -2
  31. data/lib/puma/cluster.rb +46 -22
  32. data/lib/puma/configuration.rb +18 -2
  33. data/lib/puma/const.rb +14 -4
  34. data/lib/puma/control_cli.rb +76 -71
  35. data/lib/puma/detect.rb +14 -10
  36. data/lib/puma/dsl.rb +143 -26
  37. data/lib/puma/error_logger.rb +12 -5
  38. data/lib/puma/events.rb +18 -3
  39. data/lib/puma/json_serialization.rb +96 -0
  40. data/lib/puma/launcher.rb +54 -6
  41. data/lib/puma/minissl/context_builder.rb +6 -0
  42. data/lib/puma/minissl.rb +54 -38
  43. data/lib/puma/null_io.rb +12 -0
  44. data/lib/puma/plugin.rb +1 -1
  45. data/lib/puma/queue_close.rb +7 -7
  46. data/lib/puma/rack/builder.rb +1 -1
  47. data/lib/puma/reactor.rb +19 -12
  48. data/lib/puma/request.rb +45 -16
  49. data/lib/puma/runner.rb +38 -13
  50. data/lib/puma/server.rb +62 -123
  51. data/lib/puma/state_file.rb +5 -3
  52. data/lib/puma/systemd.rb +46 -0
  53. data/lib/puma/thread_pool.rb +10 -7
  54. data/lib/puma/util.rb +8 -1
  55. data/lib/puma.rb +36 -10
  56. data/lib/rack/handler/puma.rb +1 -0
  57. metadata +15 -9
data/lib/puma/minissl.rb CHANGED
@@ -73,7 +73,6 @@ module Puma
73
73
 
74
74
  def engine_read_all
75
75
  output = @engine.read
76
- raise SSLError.exception "HTTP connection?" if bad_tlsv1_3?
77
76
  while output and additional_output = @engine.read
78
77
  output << additional_output
79
78
  end
@@ -100,6 +99,7 @@ module Puma
100
99
  # ourselves.
101
100
  raise IO::EAGAINWaitReadable
102
101
  elsif data.nil?
102
+ raise SSLError.exception "HTTP connection?" if bad_tlsv1_3?
103
103
  return nil
104
104
  end
105
105
 
@@ -117,22 +117,23 @@ module Puma
117
117
  def write(data)
118
118
  return 0 if data.empty?
119
119
 
120
- need = data.bytesize
120
+ data_size = data.bytesize
121
+ need = data_size
121
122
 
122
123
  while true
123
124
  wrote = @engine.write data
124
- enc = @engine.extract
125
125
 
126
- while enc
127
- @socket.write enc
128
- enc = @engine.extract
126
+ enc_wr = ''.dup
127
+ while (enc = @engine.extract)
128
+ enc_wr << enc
129
129
  end
130
+ @socket.write enc_wr unless enc_wr.empty?
130
131
 
131
132
  need -= wrote
132
133
 
133
- return data.bytesize if need == 0
134
+ return data_size if need == 0
134
135
 
135
- data = data[wrote..-1]
136
+ data = data.byteslice(wrote..-1)
136
137
  end
137
138
  end
138
139
 
@@ -160,30 +161,15 @@ module Puma
160
161
  @socket.flush
161
162
  end
162
163
 
163
- def read_and_drop(timeout = 1)
164
- return :timeout unless IO.select([@socket], nil, nil, timeout)
165
- case @socket.read_nonblock(1024, exception: false)
166
- when nil
167
- :eof
168
- when :wait_readable
169
- :eagain
170
- else
171
- :drop
172
- end
173
- end
174
-
175
- def should_drop_bytes?
176
- @engine.init? || !@engine.shutdown
177
- end
178
-
179
164
  def close
180
165
  begin
181
- # Read any drop any partially initialized sockets and any received bytes during shutdown.
182
- # Don't let this socket hold this loop forever.
183
- # If it can't send more packets within 1s, then give up.
184
- return if [:timeout, :eof].include?(read_and_drop(1)) while should_drop_bytes?
166
+ unless @engine.shutdown
167
+ while alert_data = @engine.extract
168
+ @socket.write alert_data
169
+ end
170
+ end
185
171
  rescue IOError, SystemCallError
186
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
172
+ Puma::Util.purge_interrupt_queue
187
173
  # nothing
188
174
  ensure
189
175
  @socket.close
@@ -245,6 +231,7 @@ module Puma
245
231
  attr_reader :cert
246
232
  attr_reader :ca
247
233
  attr_accessor :ssl_cipher_filter
234
+ attr_accessor :verification_flags
248
235
 
249
236
  def key=(key)
250
237
  raise ArgumentError, "No such key file '#{key}'" unless File.exist? key
@@ -287,33 +274,58 @@ module Puma
287
274
  VERIFY_PEER = 1
288
275
  VERIFY_FAIL_IF_NO_PEER_CERT = 2
289
276
 
277
+ # https://github.com/openssl/openssl/blob/master/include/openssl/x509_vfy.h.in
278
+ # /* Certificate verify flags */
279
+ VERIFICATION_FLAGS = {
280
+ "USE_CHECK_TIME" => 0x2,
281
+ "CRL_CHECK" => 0x4,
282
+ "CRL_CHECK_ALL" => 0x8,
283
+ "IGNORE_CRITICAL" => 0x10,
284
+ "X509_STRICT" => 0x20,
285
+ "ALLOW_PROXY_CERTS" => 0x40,
286
+ "POLICY_CHECK" => 0x80,
287
+ "EXPLICIT_POLICY" => 0x100,
288
+ "INHIBIT_ANY" => 0x200,
289
+ "INHIBIT_MAP" => 0x400,
290
+ "NOTIFY_POLICY" => 0x800,
291
+ "EXTENDED_CRL_SUPPORT" => 0x1000,
292
+ "USE_DELTAS" => 0x2000,
293
+ "CHECK_SS_SIGNATURE" => 0x4000,
294
+ "TRUSTED_FIRST" => 0x8000,
295
+ "SUITEB_128_LOS_ONLY" => 0x10000,
296
+ "SUITEB_192_LOS" => 0x20000,
297
+ "SUITEB_128_LOS" => 0x30000,
298
+ "PARTIAL_CHAIN" => 0x80000,
299
+ "NO_ALT_CHAINS" => 0x100000,
300
+ "NO_CHECK_TIME" => 0x200000
301
+ }.freeze
302
+
290
303
  class Server
291
304
  def initialize(socket, ctx)
292
305
  @socket = socket
293
306
  @ctx = ctx
294
- end
295
-
296
- # @!attribute [r] to_io
297
- def to_io
298
- @socket
307
+ @eng_ctx = IS_JRUBY ? @ctx : SSLContext.new(ctx)
299
308
  end
300
309
 
301
310
  def accept
302
311
  @ctx.check
303
312
  io = @socket.accept
304
- engine = Engine.server @ctx
305
-
313
+ engine = Engine.server @eng_ctx
306
314
  Socket.new io, engine
307
315
  end
308
316
 
309
317
  def accept_nonblock
310
318
  @ctx.check
311
319
  io = @socket.accept_nonblock
312
- engine = Engine.server @ctx
313
-
320
+ engine = Engine.server @eng_ctx
314
321
  Socket.new io, engine
315
322
  end
316
323
 
324
+ # @!attribute [r] to_io
325
+ def to_io
326
+ @socket
327
+ end
328
+
317
329
  # @!attribute [r] addr
318
330
  # @version 5.0.0
319
331
  def addr
@@ -323,6 +335,10 @@ module Puma
323
335
  def close
324
336
  @socket.close unless @socket.closed? # closed? call is for Windows
325
337
  end
338
+
339
+ def closed?
340
+ @socket.closed?
341
+ end
326
342
  end
327
343
  end
328
344
  end
data/lib/puma/null_io.rb CHANGED
@@ -9,6 +9,10 @@ module Puma
9
9
  nil
10
10
  end
11
11
 
12
+ def string
13
+ ""
14
+ end
15
+
12
16
  def each
13
17
  end
14
18
 
@@ -32,6 +36,10 @@ module Puma
32
36
  true
33
37
  end
34
38
 
39
+ def sync
40
+ true
41
+ end
42
+
35
43
  def sync=(v)
36
44
  end
37
45
 
@@ -40,5 +48,9 @@ module Puma
40
48
 
41
49
  def write(*ary)
42
50
  end
51
+
52
+ def flush
53
+ self
54
+ end
43
55
  end
44
56
  end
data/lib/puma/plugin.rb CHANGED
@@ -91,7 +91,7 @@ module Puma
91
91
  path = ary.first[CALLER_FILE]
92
92
 
93
93
  m = %r!puma/plugin/([^/]*)\.rb$!.match(path)
94
- return m[1]
94
+ m[1]
95
95
  end
96
96
 
97
97
  def self.create(&blk)
@@ -5,22 +5,22 @@ module Puma
5
5
  # Add a simple implementation for earlier Ruby versions.
6
6
  #
7
7
  module QueueClose
8
- def initialize
9
- @closed = false
10
- super
11
- end
12
8
  def close
9
+ num_waiting.times {push nil}
13
10
  @closed = true
14
11
  end
15
12
  def closed?
16
- @closed
13
+ @closed ||= false
17
14
  end
18
15
  def push(object)
19
- @closed ||= false
20
- raise ClosedQueueError if @closed
16
+ raise ClosedQueueError if closed?
21
17
  super
22
18
  end
23
19
  alias << push
20
+ def pop(non_block=false)
21
+ return nil if !non_block && closed? && empty?
22
+ super
23
+ end
24
24
  end
25
25
  ::Queue.prepend QueueClose
26
26
  end
@@ -165,7 +165,7 @@ module Puma::Rack
165
165
  require config
166
166
  app = Object.const_get(::File.basename(config, '.rb').capitalize)
167
167
  end
168
- return app, options
168
+ [app, options]
169
169
  end
170
170
 
171
171
  def self.new_from_string(builder_script, file="(rackup)")
data/lib/puma/reactor.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'puma/queue_close' unless ::Queue.instance_methods.include? :close
4
4
 
5
5
  module Puma
6
+ class UnsupportedBackend < StandardError; end
7
+
6
8
  # Monitors a collection of IO objects, calling a block whenever
7
9
  # any monitored object either receives data or times out, or when the Reactor shuts down.
8
10
  #
@@ -18,9 +20,12 @@ module Puma
18
20
  # Create a new Reactor to monitor IO objects added by #add.
19
21
  # The provided block will be invoked when an IO has data available to read,
20
22
  # its timeout elapses, or when the Reactor shuts down.
21
- def initialize(&block)
23
+ def initialize(backend, &block)
22
24
  require 'nio'
23
- @selector = NIO::Selector.new
25
+ unless backend == :auto || NIO::Selector.backends.include?(backend)
26
+ raise "unsupported IO selector backend: #{backend} (available backends: #{NIO::Selector.backends.join(', ')})"
27
+ end
28
+ @selector = backend == :auto ? NIO::Selector.new : NIO::Selector.new(backend)
24
29
  @input = Queue.new
25
30
  @timeouts = []
26
31
  @block = block
@@ -38,11 +43,11 @@ module Puma
38
43
  end
39
44
  end
40
45
 
41
- # Add a new IO object to monitor.
46
+ # Add a new client to monitor.
42
47
  # The object must respond to #timeout and #timeout_at.
43
48
  # Returns false if the reactor is already shut down.
44
- def add(io)
45
- @input << io
49
+ def add(client)
50
+ @input << client
46
51
  @selector.wakeup
47
52
  true
48
53
  rescue ClosedQueueError
@@ -92,17 +97,19 @@ module Puma
92
97
  end
93
98
 
94
99
  # Start monitoring the object.
95
- def register(io)
96
- @selector.register(io, :r).value = io
97
- @timeouts << io
100
+ def register(client)
101
+ @selector.register(client.to_io, :r).value = client
102
+ @timeouts << client
103
+ rescue ArgumentError
104
+ # unreadable clients raise error when processed by NIO
98
105
  end
99
106
 
100
107
  # 'Wake up' a monitored object by calling the provided block.
101
108
  # Stop monitoring the object if the block returns `true`.
102
- def wakeup!(io)
103
- if @block.call(io)
104
- @selector.deregister(io)
105
- @timeouts.delete(io)
109
+ def wakeup!(client)
110
+ if @block.call client
111
+ @selector.deregister client.to_io
112
+ @timeouts.delete client
106
113
  end
107
114
  end
108
115
  end
data/lib/puma/request.rb CHANGED
@@ -26,11 +26,12 @@ module Puma
26
26
  # Finally, it'll return +true+ on keep-alive connections.
27
27
  # @param client [Puma::Client]
28
28
  # @param lines [Puma::IOBuffer]
29
+ # @param requests [Integer]
29
30
  # @return [Boolean,:async]
30
31
  #
31
- def handle_request(client, lines)
32
+ def handle_request(client, lines, requests)
32
33
  env = client.env
33
- io = client.io
34
+ io = client.io # io may be a MiniSSL::Socket
34
35
 
35
36
  return false if closed_socket?(io)
36
37
 
@@ -50,7 +51,7 @@ module Puma
50
51
  head = env[REQUEST_METHOD] == HEAD
51
52
 
52
53
  env[RACK_INPUT] = body
53
- env[RACK_URL_SCHEME] = default_server_port(env) == PORT_443 ? HTTPS : HTTP
54
+ env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
54
55
 
55
56
  if @early_hints
56
57
  env[EARLY_HINTS] = lambda { |headers|
@@ -110,7 +111,7 @@ module Puma
110
111
 
111
112
  cork_socket io
112
113
 
113
- str_headers(env, status, headers, res_info, lines)
114
+ str_headers(env, status, headers, res_info, lines, requests, client)
114
115
 
115
116
  line_ending = LINE_END
116
117
 
@@ -148,8 +149,9 @@ module Puma
148
149
  res_body.each do |part|
149
150
  next if part.bytesize.zero?
150
151
  if chunked
151
- str = part.bytesize.to_s(16) << line_ending << part << line_ending
152
- fast_write io, str
152
+ fast_write io, (part.bytesize.to_s(16) << line_ending)
153
+ fast_write io, part # part may have different encoding
154
+ fast_write io, line_ending
153
155
  else
154
156
  fast_write io, part
155
157
  end
@@ -174,7 +176,7 @@ module Puma
174
176
  after_reply.each { |o| o.call }
175
177
  end
176
178
 
177
- return res_info[:keep_alive]
179
+ res_info[:keep_alive]
178
180
  end
179
181
 
180
182
  # @param env [Hash] see Puma::Client#env, from request
@@ -199,7 +201,7 @@ module Puma
199
201
  begin
200
202
  n = io.syswrite str
201
203
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
202
- if !IO.select(nil, [io], nil, WRITE_TIMEOUT)
204
+ unless io.wait_writable WRITE_TIMEOUT
203
205
  raise ConnectionError, "Socket timeout writing data"
204
206
  end
205
207
 
@@ -230,7 +232,11 @@ module Puma
230
232
  #
231
233
  def normalize_env(env, client)
232
234
  if host = env[HTTP_HOST]
233
- if colon = host.index(":")
235
+ # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
236
+ if colon = host.rindex("]:") # IPV6 with port
237
+ env[SERVER_NAME] = host[0, colon+1]
238
+ env[SERVER_PORT] = host[colon+2, host.bytesize]
239
+ elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
234
240
  env[SERVER_NAME] = host[0, colon]
235
241
  env[SERVER_PORT] = host[colon+1, host.bytesize]
236
242
  else
@@ -282,13 +288,20 @@ module Puma
282
288
  end
283
289
  # private :normalize_env
284
290
 
291
+ # @param header_key [#to_s]
292
+ # @return [Boolean]
293
+ #
294
+ def illegal_header_key?(header_key)
295
+ !!(ILLEGAL_HEADER_KEY_REGEX =~ header_key.to_s)
296
+ end
297
+
285
298
  # @param header_value [#to_s]
286
299
  # @return [Boolean]
287
300
  #
288
- def possible_header_injection?(header_value)
289
- !!(HTTP_INJECTION_REGEX =~ header_value.to_s)
301
+ def illegal_header_value?(header_value)
302
+ !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
290
303
  end
291
- private :possible_header_injection?
304
+ private :illegal_header_key?, :illegal_header_value?
292
305
 
293
306
  # Fixup any headers with `,` in the name to have `_` now. We emit
294
307
  # headers with `,` in them during the parse phase to avoid ambiguity
@@ -334,9 +347,11 @@ module Puma
334
347
  def str_early_hints(headers)
335
348
  eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
336
349
  headers.each_pair do |k, vs|
350
+ next if illegal_header_key?(k)
351
+
337
352
  if vs.respond_to?(:to_s) && !vs.to_s.empty?
338
353
  vs.to_s.split(NEWLINE).each do |v|
339
- next if possible_header_injection?(v)
354
+ next if illegal_header_value?(v)
340
355
  eh_str << "#{k}: #{v}\r\n"
341
356
  end
342
357
  else
@@ -353,9 +368,11 @@ module Puma
353
368
  # @param headers [Hash] the headers returned by the Rack application
354
369
  # @param res_info [Hash] used to pass info between this method and #handle_request
355
370
  # @param lines [Puma::IOBuffer] modified inn place
371
+ # @param requests [Integer] number of inline requests handled
372
+ # @param client [Puma::Client]
356
373
  # @version 5.0.3
357
374
  #
358
- def str_headers(env, status, headers, res_info, lines)
375
+ def str_headers(env, status, headers, res_info, lines, requests, client)
359
376
  line_ending = LINE_END
360
377
  colon = COLON
361
378
 
@@ -396,12 +413,22 @@ module Puma
396
413
  # if running without request queueing
397
414
  res_info[:keep_alive] &&= @queue_requests
398
415
 
416
+ # Close the connection after a reasonable number of inline requests
417
+ # if the server is at capacity and the listener has a new connection ready.
418
+ # This allows Puma to service connections fairly when the number
419
+ # of concurrent connections exceeds the size of the threadpool.
420
+ res_info[:keep_alive] &&= requests < @max_fast_inline ||
421
+ @thread_pool.busy_threads < @max_threads ||
422
+ !client.listener.to_io.wait_readable(0)
423
+
399
424
  res_info[:response_hijack] = nil
400
425
 
401
426
  headers.each do |k, vs|
427
+ next if illegal_header_key?(k)
428
+
402
429
  case k.downcase
403
430
  when CONTENT_LENGTH2
404
- next if possible_header_injection?(vs)
431
+ next if illegal_header_value?(vs)
405
432
  res_info[:content_length] = vs
406
433
  next
407
434
  when TRANSFER_ENCODING
@@ -410,11 +437,13 @@ module Puma
410
437
  when HIJACK
411
438
  res_info[:response_hijack] = vs
412
439
  next
440
+ when BANNED_HEADER_KEY
441
+ next
413
442
  end
414
443
 
415
444
  if vs.respond_to?(:to_s) && !vs.to_s.empty?
416
445
  vs.to_s.split(NEWLINE).each do |v|
417
- next if possible_header_injection?(v)
446
+ next if illegal_header_value?(v)
418
447
  lines.append k, colon, v, line_ending
419
448
  end
420
449
  else
data/lib/puma/runner.rb CHANGED
@@ -15,6 +15,16 @@ module Puma
15
15
  @app = nil
16
16
  @control = nil
17
17
  @started_at = Time.now
18
+ @wakeup = nil
19
+ end
20
+
21
+ def wakeup!
22
+ return unless @wakeup
23
+
24
+ @wakeup.write "!" unless @wakeup.closed?
25
+
26
+ rescue SystemCallError, IOError
27
+ Puma::Util.purge_interrupt_queue
18
28
  end
19
29
 
20
30
  def development?
@@ -55,11 +65,11 @@ module Puma
55
65
  app = Puma::App::Status.new @launcher, token
56
66
 
57
67
  control = Puma::Server.new app, @launcher.events,
58
- { min_threads: 0, max_threads: 1 }
68
+ { min_threads: 0, max_threads: 1, queue_requests: false }
59
69
 
60
70
  control.binder.parse [str], self, 'Starting control server'
61
71
 
62
- control.run
72
+ control.run thread_name: 'control'
63
73
  @control = control
64
74
  end
65
75
 
@@ -86,9 +96,16 @@ module Puma
86
96
  max_t = @options[:max_threads]
87
97
 
88
98
  log "Puma starting in #{mode} mode..."
89
- log "* Version #{Puma::Const::PUMA_VERSION} (#{ruby_engine}), codename: #{Puma::Const::CODE_NAME}"
90
- log "* Min threads: #{min_t}, max threads: #{max_t}"
91
- log "* Environment: #{ENV['RACK_ENV']}"
99
+ log "* Puma version: #{Puma::Const::PUMA_VERSION} (#{ruby_engine}) (\"#{Puma::Const::CODE_NAME}\")"
100
+ log "* Min threads: #{min_t}"
101
+ log "* Max threads: #{max_t}"
102
+ log "* Environment: #{ENV['RACK_ENV']}"
103
+
104
+ if mode == "cluster"
105
+ log "* Master PID: #{Process.pid}"
106
+ else
107
+ log "* PID: #{Process.pid}"
108
+ end
92
109
  end
93
110
 
94
111
  def redirected_io?
@@ -101,23 +118,24 @@ module Puma
101
118
  append = @options[:redirect_append]
102
119
 
103
120
  if stdout
104
- unless Dir.exist?(File.dirname(stdout))
105
- raise "Cannot redirect STDOUT to #{stdout}"
106
- end
121
+ ensure_output_directory_exists(stdout, 'STDOUT')
107
122
 
108
123
  STDOUT.reopen stdout, (append ? "a" : "w")
109
- STDOUT.sync = true
110
124
  STDOUT.puts "=== puma startup: #{Time.now} ==="
125
+ STDOUT.flush unless STDOUT.sync
111
126
  end
112
127
 
113
128
  if stderr
114
- unless Dir.exist?(File.dirname(stderr))
115
- raise "Cannot redirect STDERR to #{stderr}"
116
- end
129
+ ensure_output_directory_exists(stderr, 'STDERR')
117
130
 
118
131
  STDERR.reopen stderr, (append ? "a" : "w")
119
- STDERR.sync = true
120
132
  STDERR.puts "=== puma startup: #{Time.now} ==="
133
+ STDERR.flush unless STDERR.sync
134
+ end
135
+
136
+ if @options[:mutate_stdout_and_stderr_to_sync_on_write]
137
+ STDOUT.sync = true
138
+ STDERR.sync = true
121
139
  end
122
140
  end
123
141
 
@@ -147,5 +165,12 @@ module Puma
147
165
  server.inherit_binder @launcher.binder
148
166
  server
149
167
  end
168
+
169
+ private
170
+ def ensure_output_directory_exists(path, io_name)
171
+ unless Dir.exist?(File.dirname(path))
172
+ raise "Cannot redirect #{io_name} to #{path}"
173
+ end
174
+ end
150
175
  end
151
176
  end