puma 5.6.4 → 6.1.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +199 -3
  3. data/README.md +22 -17
  4. data/bin/puma-wild +1 -1
  5. data/docs/compile_options.md +34 -0
  6. data/docs/fork_worker.md +1 -3
  7. data/docs/nginx.md +1 -1
  8. data/docs/systemd.md +1 -2
  9. data/docs/testing_benchmarks_local_files.md +150 -0
  10. data/docs/testing_test_rackup_ci_files.md +36 -0
  11. data/ext/puma_http11/extconf.rb +18 -10
  12. data/ext/puma_http11/http11_parser.c +1 -1
  13. data/ext/puma_http11/http11_parser.h +1 -1
  14. data/ext/puma_http11/http11_parser.java.rl +2 -2
  15. data/ext/puma_http11/http11_parser.rl +2 -2
  16. data/ext/puma_http11/http11_parser_common.rl +2 -2
  17. data/ext/puma_http11/mini_ssl.c +63 -24
  18. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  19. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +166 -65
  21. data/ext/puma_http11/puma_http11.c +17 -9
  22. data/lib/puma/app/status.rb +6 -3
  23. data/lib/puma/binder.rb +41 -46
  24. data/lib/puma/cli.rb +11 -17
  25. data/lib/puma/client.rb +54 -16
  26. data/lib/puma/cluster/worker.rb +18 -11
  27. data/lib/puma/cluster/worker_handle.rb +4 -1
  28. data/lib/puma/cluster.rb +33 -30
  29. data/lib/puma/configuration.rb +75 -58
  30. data/lib/puma/const.rb +76 -88
  31. data/lib/puma/control_cli.rb +21 -18
  32. data/lib/puma/detect.rb +4 -0
  33. data/lib/puma/dsl.rb +111 -49
  34. data/lib/puma/error_logger.rb +17 -9
  35. data/lib/puma/events.rb +6 -126
  36. data/lib/puma/io_buffer.rb +39 -4
  37. data/lib/puma/jruby_restart.rb +2 -1
  38. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  39. data/lib/puma/launcher.rb +111 -175
  40. data/lib/puma/log_writer.rb +141 -0
  41. data/lib/puma/minissl/context_builder.rb +23 -12
  42. data/lib/puma/minissl.rb +91 -15
  43. data/lib/puma/null_io.rb +5 -0
  44. data/lib/puma/plugin/systemd.rb +90 -0
  45. data/lib/puma/plugin/tmp_restart.rb +1 -1
  46. data/lib/puma/rack/builder.rb +4 -4
  47. data/lib/puma/rack_default.rb +19 -4
  48. data/lib/puma/reactor.rb +4 -4
  49. data/lib/puma/request.rb +344 -161
  50. data/lib/puma/runner.rb +52 -20
  51. data/lib/puma/sd_notify.rb +149 -0
  52. data/lib/puma/server.rb +57 -69
  53. data/lib/puma/single.rb +13 -11
  54. data/lib/puma/state_file.rb +2 -4
  55. data/lib/puma/thread_pool.rb +16 -16
  56. data/lib/puma/util.rb +12 -14
  57. data/lib/puma.rb +12 -11
  58. data/lib/rack/handler/puma.rb +115 -94
  59. metadata +10 -5
  60. data/lib/puma/queue_close.rb +0 -26
  61. data/lib/puma/systemd.rb +0 -46
@@ -16,7 +16,8 @@ module Puma
16
16
  def self.chdir_exec(dir, argv)
17
17
  chdir(dir)
18
18
  cmd = argv.first
19
- argv = ([:string] * argv.size).zip(argv).flatten
19
+ argv = ([:string] * argv.size).zip(argv)
20
+ argv.flatten!
20
21
  argv << :string
21
22
  argv << nil
22
23
  execlp(cmd, *argv)
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class Launcher
5
+
6
+ # This class is used to pickup Gemfile changes during
7
+ # application restarts.
8
+ class BundlePruner
9
+
10
+ def initialize(original_argv, extra_runtime_dependencies, log_writer)
11
+ @original_argv = Array(original_argv)
12
+ @extra_runtime_dependencies = Array(extra_runtime_dependencies)
13
+ @log_writer = log_writer
14
+ end
15
+
16
+ def prune
17
+ return if ENV['PUMA_BUNDLER_PRUNED']
18
+ return unless defined?(Bundler)
19
+
20
+ require_rubygems_min_version!
21
+
22
+ unless puma_wild_path
23
+ log "! Unable to prune Bundler environment, continuing"
24
+ return
25
+ end
26
+
27
+ dirs = paths_to_require_after_prune
28
+
29
+ log '* Pruning Bundler environment'
30
+ home = ENV['GEM_HOME']
31
+ bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
32
+ bundle_app_config = Bundler.original_env['BUNDLE_APP_CONFIG']
33
+
34
+ with_unbundled_env do
35
+ ENV['GEM_HOME'] = home
36
+ ENV['BUNDLE_GEMFILE'] = bundle_gemfile
37
+ ENV['PUMA_BUNDLER_PRUNED'] = '1'
38
+ ENV["BUNDLE_APP_CONFIG"] = bundle_app_config
39
+ args = [Gem.ruby, puma_wild_path, '-I', dirs.join(':')] + @original_argv
40
+ # Ruby 2.0+ defaults to true which breaks socket activation
41
+ args += [{:close_others => false}]
42
+ Kernel.exec(*args)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def require_rubygems_min_version!
49
+ min_version = Gem::Version.new('2.2')
50
+
51
+ return if min_version <= Gem::Version.new(Gem::VERSION)
52
+
53
+ raise "prune_bundler is not supported on your version of RubyGems. " \
54
+ "You must have RubyGems #{min_version}+ to use this feature."
55
+ end
56
+
57
+ def puma_wild_path
58
+ puma_lib_dir = puma_require_paths.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
59
+ File.expand_path(File.join(puma_lib_dir, '../bin/puma-wild'))
60
+ end
61
+
62
+ def with_unbundled_env
63
+ bundler_ver = Gem::Version.new(Bundler::VERSION)
64
+ if bundler_ver < Gem::Version.new('2.1.0')
65
+ Bundler.with_clean_env { yield }
66
+ else
67
+ Bundler.with_unbundled_env { yield }
68
+ end
69
+ end
70
+
71
+ def paths_to_require_after_prune
72
+ puma_require_paths + extra_runtime_deps_paths
73
+ end
74
+
75
+ def extra_runtime_deps_paths
76
+ t = @extra_runtime_dependencies.map do |dep_name|
77
+ if (spec = spec_for_gem(dep_name))
78
+ require_paths_for_gem(spec)
79
+ else
80
+ log "* Could not load extra dependency: #{dep_name}"
81
+ nil
82
+ end
83
+ end
84
+ t.flatten!; t.compact!; t
85
+ end
86
+
87
+ def puma_require_paths
88
+ require_paths_for_gem(spec_for_gem('puma'))
89
+ end
90
+
91
+ def spec_for_gem(gem_name)
92
+ Bundler.rubygems.loaded_specs(gem_name)
93
+ end
94
+
95
+ def require_paths_for_gem(gem_spec)
96
+ gem_spec.full_require_paths
97
+ end
98
+
99
+ def log(str)
100
+ @log_writer.log(str)
101
+ end
102
+ end
103
+ end
104
+ end
data/lib/puma/launcher.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/events'
4
- require 'puma/detect'
5
- require 'puma/cluster'
6
- require 'puma/single'
7
- require 'puma/const'
8
- require 'puma/binder'
3
+ require_relative 'log_writer'
4
+ require_relative 'events'
5
+ require_relative 'detect'
6
+ require_relative 'cluster'
7
+ require_relative 'single'
8
+ require_relative 'const'
9
+ require_relative 'binder'
9
10
 
10
11
  module Puma
11
12
  # Puma::Launcher is the single entry point for starting a Puma server based on user
@@ -15,18 +16,14 @@ module Puma
15
16
  # It is responsible for either launching a cluster of Puma workers or a single
16
17
  # puma server.
17
18
  class Launcher
18
- # @deprecated 6.0.0
19
- KEYS_NOT_TO_PERSIST_IN_STATE = [
20
- :logger, :lowlevel_error_handler,
21
- :before_worker_shutdown, :before_worker_boot, :before_worker_fork,
22
- :after_worker_boot, :before_fork, :on_restart
23
- ]
19
+ autoload :BundlePruner, 'puma/launcher/bundle_pruner'
20
+
24
21
  # Returns an instance of Launcher
25
22
  #
26
23
  # +conf+ A Puma::Configuration object indicating how to run the server.
27
24
  #
28
25
  # +launcher_args+ A Hash that currently has one required key `:events`,
29
- # this is expected to hold an object similar to an `Puma::Events.stdio`,
26
+ # this is expected to hold an object similar to an `Puma::LogWriter.stdio`,
30
27
  # this object will be responsible for broadcasting Puma's internal state
31
28
  # to a logging destination. An optional key `:argv` can be supplied,
32
29
  # this should be an array of strings, these arguments are re-used when
@@ -40,25 +37,35 @@ module Puma
40
37
  # [200, {}, ["hello world"]]
41
38
  # end
42
39
  # end
43
- # Puma::Launcher.new(conf, events: Puma::Events.stdio).run
40
+ # Puma::Launcher.new(conf, log_writer: Puma::LogWriter.stdio).run
44
41
  def initialize(conf, launcher_args={})
45
42
  @runner = nil
46
- @events = launcher_args[:events] || Events::DEFAULT
43
+ @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
44
+ @events = launcher_args[:events] || Events.new
47
45
  @argv = launcher_args[:argv] || []
48
46
  @original_argv = @argv.dup
49
47
  @config = conf
50
48
 
51
- @binder = Binder.new(@events, conf)
52
- @binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
53
- @binder.create_activated_fds(ENV).each { |k| ENV.delete k }
54
-
55
- @environment = conf.environment
49
+ @config.options[:log_writer] = @log_writer
56
50
 
57
51
  # Advertise the Configuration
58
52
  Puma.cli_config = @config if defined?(Puma.cli_config)
59
53
 
60
54
  @config.load
61
55
 
56
+ @binder = Binder.new(@log_writer, conf)
57
+ @binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
58
+ @binder.create_activated_fds(ENV).each { |k| ENV.delete k }
59
+
60
+ @environment = conf.environment
61
+
62
+ # Load the systemd integration if we detect systemd's NOTIFY_SOCKET.
63
+ # Skip this on JRuby though, because it is incompatible with the systemd
64
+ # integration due to https://github.com/jruby/jruby/issues/6504
65
+ if ENV["NOTIFY_SOCKET"] && !Puma.jruby?
66
+ @config.plugins.create('systemd')
67
+ end
68
+
62
69
  if @config.options[:bind_to_activated_sockets]
63
70
  @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
64
71
  @config.options[:binds],
@@ -69,8 +76,8 @@ module Puma
69
76
  @options = @config.options
70
77
  @config.clamp
71
78
 
72
- @events.formatter = Events::PidFormatter.new if clustered?
73
- @events.formatter = options[:log_formatter] if @options[:log_formatter]
79
+ @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
80
+ @log_writer.formatter = options[:log_formatter] if @options[:log_formatter]
74
81
 
75
82
  generate_restart_data
76
83
 
@@ -80,17 +87,17 @@ module Puma
80
87
 
81
88
  Dir.chdir(@restart_dir)
82
89
 
83
- prune_bundler if prune_bundler?
90
+ prune_bundler!
84
91
 
85
92
  @environment = @options[:environment] if @options[:environment]
86
93
  set_rack_environment
87
94
 
88
95
  if clustered?
89
- @options[:logger] = @events
96
+ @options[:logger] = @log_writer
90
97
 
91
- @runner = Cluster.new(self, @events)
98
+ @runner = Cluster.new(self)
92
99
  else
93
- @runner = Single.new(self, @events)
100
+ @runner = Single.new(self)
94
101
  end
95
102
  Puma.stats_object = @runner
96
103
 
@@ -99,7 +106,7 @@ module Puma
99
106
  log_config if ENV['PUMA_LOG_CONFIG']
100
107
  end
101
108
 
102
- attr_reader :binder, :events, :config, :options, :restart_dir
109
+ attr_reader :binder, :log_writer, :events, :config, :options, :restart_dir
103
110
 
104
111
  # Return stats about the server
105
112
  def stats
@@ -115,7 +122,7 @@ module Puma
115
122
  permission = @options[:state_permission]
116
123
  return unless path
117
124
 
118
- require 'puma/state_file'
125
+ require_relative 'state_file'
119
126
 
120
127
  sf = StateFile.new
121
128
  sf.pid = Process.pid
@@ -159,18 +166,20 @@ module Puma
159
166
  true
160
167
  end
161
168
 
169
+ # Begin a refork if supported
170
+ def refork
171
+ if clustered? && @runner.respond_to?(:fork_worker!) && @options[:fork_worker]
172
+ @runner.fork_worker!
173
+ true
174
+ else
175
+ log "* refork called but not available."
176
+ false
177
+ end
178
+ end
179
+
162
180
  # Run the server. This blocks until the server is stopped
163
181
  def run
164
- previous_env =
165
- if defined?(Bundler)
166
- env = Bundler::ORIGINAL_ENV.dup
167
- # add -rbundler/setup so we load from Gemfile when restarting
168
- bundle = "-rbundler/setup"
169
- env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
170
- env
171
- else
172
- ENV.to_h
173
- end
182
+ previous_env = get_env
174
183
 
175
184
  @config.clamp
176
185
 
@@ -178,24 +187,11 @@ module Puma
178
187
 
179
188
  setup_signals
180
189
  set_process_title
181
- integrate_with_systemd
190
+
191
+ # This blocks until the server is stopped
182
192
  @runner.run
183
193
 
184
- case @status
185
- when :halt
186
- log "* Stopping immediately!"
187
- @runner.stop_control
188
- when :run, :stop
189
- graceful_stop
190
- when :restart
191
- log "* Restarting..."
192
- ENV.replace(previous_env)
193
- @runner.stop_control
194
- restart!
195
- when :exit
196
- # nothing
197
- end
198
- close_binder_listeners unless @status == :restart
194
+ do_run_finished(previous_env)
199
195
  end
200
196
 
201
197
  # Return all tcp ports the launcher may be using, TCP or SSL
@@ -239,30 +235,56 @@ module Puma
239
235
 
240
236
  private
241
237
 
242
- # If configured, write the pid of the current process out
243
- # to a file.
244
- def write_pid
245
- path = @options[:pidfile]
246
- return unless path
247
- cur_pid = Process.pid
248
- File.write path, cur_pid, mode: 'wb:UTF-8'
249
- at_exit do
250
- delete_pidfile if cur_pid == Process.pid
238
+ def get_env
239
+ if defined?(Bundler)
240
+ env = Bundler::ORIGINAL_ENV.dup
241
+ # add -rbundler/setup so we load from Gemfile when restarting
242
+ bundle = "-rbundler/setup"
243
+ env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
244
+ env
245
+ else
246
+ ENV.to_h
251
247
  end
252
248
  end
253
249
 
254
- def reload_worker_directory
255
- @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
250
+ def do_run_finished(previous_env)
251
+ case @status
252
+ when :halt
253
+ do_forceful_stop
254
+ when :run, :stop
255
+ do_graceful_stop
256
+ when :restart
257
+ do_restart(previous_env)
258
+ end
259
+
260
+ close_binder_listeners unless @status == :restart
261
+ end
262
+
263
+ def do_forceful_stop
264
+ log "* Stopping immediately!"
265
+ @runner.stop_control
266
+ end
267
+
268
+ def do_graceful_stop
269
+ @events.fire_on_stopped!
270
+ @runner.stop_blocked
271
+ end
272
+
273
+ def do_restart(previous_env)
274
+ log "* Restarting..."
275
+ ENV.replace(previous_env)
276
+ @runner.stop_control
277
+ restart!
256
278
  end
257
279
 
258
280
  def restart!
259
281
  @events.fire_on_restart!
260
- @config.run_hooks :on_restart, self, @events
282
+ @config.run_hooks :on_restart, self, @log_writer
261
283
 
262
284
  if Puma.jruby?
263
285
  close_binder_listeners
264
286
 
265
- require 'puma/jruby_restart'
287
+ require_relative 'jruby_restart'
266
288
  JRubyRestart.chdir_exec(@restart_dir, restart_args)
267
289
  elsif Puma.windows?
268
290
  close_binder_listeners
@@ -279,94 +301,24 @@ module Puma
279
301
  end
280
302
  end
281
303
 
282
- # @!attribute [r] files_to_require_after_prune
283
- def files_to_require_after_prune
284
- puma = spec_for_gem("puma")
285
-
286
- require_paths_for_gem(puma) + extra_runtime_deps_directories
287
- end
288
-
289
- # @!attribute [r] extra_runtime_deps_directories
290
- def extra_runtime_deps_directories
291
- Array(@options[:extra_runtime_dependencies]).map do |d_name|
292
- if (spec = spec_for_gem(d_name))
293
- require_paths_for_gem(spec)
294
- else
295
- log "* Could not load extra dependency: #{d_name}"
296
- nil
297
- end
298
- end.flatten.compact
299
- end
300
-
301
- # @!attribute [r] puma_wild_location
302
- def puma_wild_location
303
- puma = spec_for_gem("puma")
304
- dirs = require_paths_for_gem(puma)
305
- puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
306
- File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
307
- end
308
-
309
- def prune_bundler
310
- return if ENV['PUMA_BUNDLER_PRUNED']
311
- return unless defined?(Bundler)
312
- require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
313
- unless puma_wild_location
314
- log "! Unable to prune Bundler environment, continuing"
315
- return
316
- end
317
-
318
- dirs = files_to_require_after_prune
319
-
320
- log '* Pruning Bundler environment'
321
- home = ENV['GEM_HOME']
322
- bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
323
- bundle_app_config = Bundler.original_env['BUNDLE_APP_CONFIG']
324
- with_unbundled_env do
325
- ENV['GEM_HOME'] = home
326
- ENV['BUNDLE_GEMFILE'] = bundle_gemfile
327
- ENV['PUMA_BUNDLER_PRUNED'] = '1'
328
- ENV["BUNDLE_APP_CONFIG"] = bundle_app_config
329
- args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':')] + @original_argv
330
- # Ruby 2.0+ defaults to true which breaks socket activation
331
- args += [{:close_others => false}]
332
- Kernel.exec(*args)
333
- end
334
- end
335
-
336
- #
337
- # Puma's systemd integration allows Puma to inform systemd:
338
- # 1. when it has successfully started
339
- # 2. when it is starting shutdown
340
- # 3. periodically for a liveness check with a watchdog thread
341
- #
342
-
343
- def integrate_with_systemd
344
- return unless ENV["NOTIFY_SOCKET"]
345
-
346
- begin
347
- require 'puma/systemd'
348
- rescue LoadError
349
- log "Systemd integration failed. It looks like you're trying to use systemd notify but don't have sd_notify gem installed"
350
- return
304
+ # If configured, write the pid of the current process out
305
+ # to a file.
306
+ def write_pid
307
+ path = @options[:pidfile]
308
+ return unless path
309
+ cur_pid = Process.pid
310
+ File.write path, cur_pid, mode: 'wb:UTF-8'
311
+ at_exit do
312
+ delete_pidfile if cur_pid == Process.pid
351
313
  end
352
-
353
- log "* Enabling systemd notification integration"
354
-
355
- systemd = Systemd.new(@events)
356
- systemd.hook_events
357
- systemd.start_watchdog
358
314
  end
359
315
 
360
- def spec_for_gem(gem_name)
361
- Bundler.rubygems.loaded_specs(gem_name)
362
- end
363
-
364
- def require_paths_for_gem(gem_spec)
365
- gem_spec.full_require_paths
316
+ def reload_worker_directory
317
+ @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
366
318
  end
367
319
 
368
320
  def log(str)
369
- @events.log str
321
+ @log_writer.log(str)
370
322
  end
371
323
 
372
324
  def clustered?
@@ -374,15 +326,10 @@ module Puma
374
326
  end
375
327
 
376
328
  def unsupported(str)
377
- @events.error(str)
329
+ @log_writer.error(str)
378
330
  raise UnsupportedOption
379
331
  end
380
332
 
381
- def graceful_stop
382
- @events.fire_on_stopped!
383
- @runner.stop_blocked
384
- end
385
-
386
333
  def set_process_title
387
334
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
388
335
  end
@@ -408,6 +355,11 @@ module Puma
408
355
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
409
356
  end
410
357
 
358
+ def prune_bundler!
359
+ return unless prune_bundler?
360
+ BundlePruner.new(@original_argv, @options[:extra_runtime_dependencies], @log_writer).prune
361
+ end
362
+
411
363
  def generate_restart_data
412
364
  if dir = @options[:directory]
413
365
  @restart_dir = dir
@@ -474,7 +426,8 @@ module Puma
474
426
 
475
427
  begin
476
428
  Signal.trap "SIGTERM" do
477
- graceful_stop
429
+ # Shortcut the control flow in case raise_exception_on_sigterm is true
430
+ do_graceful_stop
478
431
 
479
432
  raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
480
433
  end
@@ -506,8 +459,8 @@ module Puma
506
459
  unless Puma.jruby? # INFO in use by JVM already
507
460
  Signal.trap "SIGINFO" do
508
461
  thread_status do |name, backtrace|
509
- @events.log name
510
- @events.log backtrace.map { |bt| " #{bt}" }
462
+ @log_writer.log(name)
463
+ @log_writer.log(backtrace.map { |bt| " #{bt}" })
511
464
  end
512
465
  end
513
466
  end
@@ -517,23 +470,6 @@ module Puma
517
470
  end
518
471
  end
519
472
 
520
- def require_rubygems_min_version!(min_version, feature)
521
- return if min_version <= Gem::Version.new(Gem::VERSION)
522
-
523
- raise "#{feature} is not supported on your version of RubyGems. " \
524
- "You must have RubyGems #{min_version}+ to use this feature."
525
- end
526
-
527
- # @version 5.0.0
528
- def with_unbundled_env
529
- bundler_ver = Gem::Version.new(Bundler::VERSION)
530
- if bundler_ver < Gem::Version.new('2.1.0')
531
- Bundler.with_clean_env { yield }
532
- else
533
- Bundler.with_unbundled_env { yield }
534
- end
535
- end
536
-
537
473
  def log_config
538
474
  log "Configuration:"
539
475
 
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'null_io'
4
+ require_relative 'error_logger'
5
+ require 'stringio'
6
+ require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
7
+
8
+ module Puma
9
+
10
+ # Handles logging concerns for both standard messages
11
+ # (+stdout+) and errors (+stderr+).
12
+ class LogWriter
13
+
14
+ class DefaultFormatter
15
+ def call(str)
16
+ str
17
+ end
18
+ end
19
+
20
+ class PidFormatter
21
+ def call(str)
22
+ "[#{$$}] #{str}"
23
+ end
24
+ end
25
+
26
+ LOG_QUEUE = Queue.new
27
+
28
+ attr_reader :stdout,
29
+ :stderr
30
+
31
+ attr_accessor :formatter
32
+
33
+ # Create a LogWriter that prints to +stdout+ and +stderr+.
34
+ def initialize(stdout, stderr)
35
+ @formatter = DefaultFormatter.new
36
+ @stdout = stdout
37
+ @stderr = stderr
38
+
39
+ @debug = ENV.key?('PUMA_DEBUG')
40
+ @error_logger = ErrorLogger.new(@stderr)
41
+ end
42
+
43
+ DEFAULT = new(STDOUT, STDERR)
44
+
45
+ # Returns an LogWriter object which writes its status to
46
+ # two StringIO objects.
47
+ def self.strings
48
+ LogWriter.new(StringIO.new, StringIO.new)
49
+ end
50
+
51
+ def self.stdio
52
+ LogWriter.new($stdout, $stderr)
53
+ end
54
+
55
+ def self.null
56
+ n = NullIO.new
57
+ LogWriter.new(n, n)
58
+ end
59
+
60
+ # Write +str+ to +@stdout+
61
+ def log(str)
62
+ internal_write "#{@formatter.call str}\n"
63
+ end
64
+
65
+ def write(str)
66
+ internal_write @formatter.call(str)
67
+ end
68
+
69
+ def internal_write(str)
70
+ LOG_QUEUE << str
71
+ while (w_str = LOG_QUEUE.pop(true)) do
72
+ begin
73
+ @stdout.is_a?(IO) and @stdout.wait_writable(1)
74
+ @stdout.write w_str
75
+ @stdout.flush unless @stdout.sync
76
+ rescue Errno::EPIPE, Errno::EBADF, IOError
77
+ end
78
+ end
79
+ rescue ThreadError
80
+ end
81
+ private :internal_write
82
+
83
+ def debug?
84
+ @debug
85
+ end
86
+
87
+ def debug(str)
88
+ log("% #{str}") if @debug
89
+ end
90
+
91
+ # Write +str+ to +@stderr+
92
+ def error(str)
93
+ @error_logger.info(text: @formatter.call("ERROR: #{str}"))
94
+ exit 1
95
+ end
96
+
97
+ def format(str)
98
+ formatter.call(str)
99
+ end
100
+
101
+ # An HTTP connection error has occurred.
102
+ # +error+ a connection exception, +req+ the request,
103
+ # and +text+ additional info
104
+ # @version 5.0.0
105
+ def connection_error(error, req, text="HTTP connection error")
106
+ @error_logger.info(error: error, req: req, text: text)
107
+ end
108
+
109
+ # An HTTP parse error has occurred.
110
+ # +error+ a parsing exception,
111
+ # and +req+ the request.
112
+ def parse_error(error, req)
113
+ @error_logger.info(error: error, req: req, text: 'HTTP parse error, malformed request')
114
+ end
115
+
116
+ # An SSL error has occurred.
117
+ # @param error <Puma::MiniSSL::SSLError>
118
+ # @param ssl_socket <Puma::MiniSSL::Socket>
119
+ def ssl_error(error, ssl_socket)
120
+ peeraddr = ssl_socket.peeraddr.last rescue "<unknown>"
121
+ peercert = ssl_socket.peercert
122
+ subject = peercert ? peercert.subject : nil
123
+ @error_logger.info(error: error, text: "SSL error, peer: #{peeraddr}, peer cert: #{subject}")
124
+ end
125
+
126
+ # An unknown error has occurred.
127
+ # +error+ an exception object, +req+ the request,
128
+ # and +text+ additional info
129
+ def unknown_error(error, req=nil, text="Unknown error")
130
+ @error_logger.info(error: error, req: req, text: text)
131
+ end
132
+
133
+ # Log occurred error debug dump.
134
+ # +error+ an exception object, +req+ the request,
135
+ # and +text+ additional info
136
+ # @version 5.0.0
137
+ def debug_error(error, req=nil, text="")
138
+ @error_logger.debug(error: error, req: req, text: text)
139
+ end
140
+ end
141
+ end