puma 4.3.12 → 6.3.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1729 -521
  3. data/LICENSE +23 -20
  4. data/README.md +169 -45
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +63 -26
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +60 -69
  9. data/docs/fork_worker.md +31 -0
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +9 -0
  14. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  15. data/{tools → docs}/jungle/rc.d/puma +2 -2
  16. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +66 -0
  18. data/docs/nginx.md +2 -2
  19. data/docs/plugins.md +15 -15
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +46 -23
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +84 -128
  25. data/docs/testing_benchmarks_local_files.md +150 -0
  26. data/docs/testing_test_rackup_ci_files.md +36 -0
  27. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  28. data/ext/puma_http11/ext_help.h +1 -1
  29. data/ext/puma_http11/extconf.rb +49 -12
  30. data/ext/puma_http11/http11_parser.c +46 -48
  31. data/ext/puma_http11/http11_parser.h +2 -2
  32. data/ext/puma_http11/http11_parser.java.rl +3 -3
  33. data/ext/puma_http11/http11_parser.rl +3 -3
  34. data/ext/puma_http11/http11_parser_common.rl +2 -2
  35. data/ext/puma_http11/mini_ssl.c +278 -93
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +4 -6
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
  40. data/ext/puma_http11/puma_http11.c +46 -57
  41. data/lib/puma/app/status.rb +53 -39
  42. data/lib/puma/binder.rb +237 -121
  43. data/lib/puma/cli.rb +34 -34
  44. data/lib/puma/client.rb +172 -98
  45. data/lib/puma/cluster/worker.rb +180 -0
  46. data/lib/puma/cluster/worker_handle.rb +97 -0
  47. data/lib/puma/cluster.rb +226 -231
  48. data/lib/puma/commonlogger.rb +21 -14
  49. data/lib/puma/configuration.rb +114 -87
  50. data/lib/puma/const.rb +139 -95
  51. data/lib/puma/control_cli.rb +99 -79
  52. data/lib/puma/detect.rb +33 -2
  53. data/lib/puma/dsl.rb +516 -110
  54. data/lib/puma/error_logger.rb +113 -0
  55. data/lib/puma/events.rb +16 -115
  56. data/lib/puma/io_buffer.rb +44 -2
  57. data/lib/puma/jruby_restart.rb +2 -59
  58. data/lib/puma/json_serialization.rb +96 -0
  59. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  60. data/lib/puma/launcher.rb +164 -155
  61. data/lib/puma/log_writer.rb +147 -0
  62. data/lib/puma/minissl/context_builder.rb +36 -19
  63. data/lib/puma/minissl.rb +230 -55
  64. data/lib/puma/null_io.rb +18 -1
  65. data/lib/puma/plugin/systemd.rb +90 -0
  66. data/lib/puma/plugin/tmp_restart.rb +1 -1
  67. data/lib/puma/plugin.rb +3 -12
  68. data/lib/puma/rack/builder.rb +7 -11
  69. data/lib/puma/rack/urlmap.rb +0 -0
  70. data/lib/puma/rack_default.rb +19 -4
  71. data/lib/puma/reactor.rb +93 -368
  72. data/lib/puma/request.rb +671 -0
  73. data/lib/puma/runner.rb +92 -75
  74. data/lib/puma/sd_notify.rb +149 -0
  75. data/lib/puma/server.rb +321 -794
  76. data/lib/puma/single.rb +20 -74
  77. data/lib/puma/state_file.rb +45 -8
  78. data/lib/puma/thread_pool.rb +140 -68
  79. data/lib/puma/util.rb +21 -4
  80. data/lib/puma.rb +54 -7
  81. data/lib/rack/handler/puma.rb +113 -87
  82. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  83. data/tools/trickletest.rb +0 -0
  84. metadata +33 -24
  85. data/docs/tcp_mode.md +0 -96
  86. data/ext/puma_http11/io_buffer.c +0 -155
  87. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  88. data/lib/puma/accept_nonblock.rb +0 -29
  89. data/lib/puma/tcp_logger.rb +0 -41
  90. data/tools/jungle/README.md +0 -19
  91. data/tools/jungle/init.d/README.md +0 -61
  92. data/tools/jungle/init.d/puma +0 -421
  93. data/tools/jungle/init.d/run-puma +0 -18
  94. data/tools/jungle/upstart/README.md +0 -61
  95. data/tools/jungle/upstart/puma-manager.conf +0 -31
  96. data/tools/jungle/upstart/puma.conf +0 -69
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,17 +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
- KEYS_NOT_TO_PERSIST_IN_STATE = [
19
- :logger, :lowlevel_error_handler,
20
- :before_worker_shutdown, :before_worker_boot, :before_worker_fork,
21
- :after_worker_boot, :before_fork, :on_restart
22
- ]
19
+ autoload :BundlePruner, 'puma/launcher/bundle_pruner'
20
+
23
21
  # Returns an instance of Launcher
24
22
  #
25
23
  # +conf+ A Puma::Configuration object indicating how to run the server.
26
24
  #
27
25
  # +launcher_args+ A Hash that currently has one required key `:events`,
28
- # 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`,
29
27
  # this object will be responsible for broadcasting Puma's internal state
30
28
  # to a logging destination. An optional key `:argv` can be supplied,
31
29
  # this should be an array of strings, these arguments are re-used when
@@ -39,60 +37,78 @@ module Puma
39
37
  # [200, {}, ["hello world"]]
40
38
  # end
41
39
  # end
42
- # Puma::Launcher.new(conf, events: Puma::Events.stdio).run
40
+ # Puma::Launcher.new(conf, log_writer: Puma::LogWriter.stdio).run
43
41
  def initialize(conf, launcher_args={})
44
42
  @runner = nil
45
- @events = launcher_args[:events] || Events::DEFAULT
43
+ @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
44
+ @events = launcher_args[:events] || Events.new
46
45
  @argv = launcher_args[:argv] || []
47
46
  @original_argv = @argv.dup
48
47
  @config = conf
49
48
 
50
- @binder = Binder.new(@events)
51
- @binder.import_from_env
52
-
53
- @environment = conf.environment
49
+ @config.options[:log_writer] = @log_writer
54
50
 
55
51
  # Advertise the Configuration
56
52
  Puma.cli_config = @config if defined?(Puma.cli_config)
57
53
 
58
54
  @config.load
59
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
+
69
+ if @config.options[:bind_to_activated_sockets]
70
+ @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
71
+ @config.options[:binds],
72
+ @config.options[:bind_to_activated_sockets] == 'only'
73
+ )
74
+ end
75
+
60
76
  @options = @config.options
61
77
  @config.clamp
62
78
 
63
- @events.formatter = Events::PidFormatter.new if clustered?
64
- @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]
81
+
82
+ @log_writer.custom_logger = options[:custom_logger] if @options[:custom_logger]
65
83
 
66
84
  generate_restart_data
67
85
 
68
- if clustered? && !Process.respond_to?(:fork)
86
+ if clustered? && !Puma.forkable?
69
87
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
70
88
  end
71
89
 
72
- if @options[:daemon] && Puma.windows?
73
- unsupported 'daemon mode not supported on Windows'
74
- end
75
-
76
90
  Dir.chdir(@restart_dir)
77
91
 
78
- prune_bundler if prune_bundler?
92
+ prune_bundler!
79
93
 
80
94
  @environment = @options[:environment] if @options[:environment]
81
95
  set_rack_environment
82
96
 
83
97
  if clustered?
84
- @options[:logger] = @events
98
+ @options[:logger] = @log_writer
85
99
 
86
- @runner = Cluster.new(self, @events)
100
+ @runner = Cluster.new(self)
87
101
  else
88
- @runner = Single.new(self, @events)
102
+ @runner = Single.new(self)
89
103
  end
90
104
  Puma.stats_object = @runner
91
105
 
92
106
  @status = :run
107
+
108
+ log_config if ENV['PUMA_LOG_CONFIG']
93
109
  end
94
110
 
95
- attr_reader :binder, :events, :config, :options, :restart_dir
111
+ attr_reader :binder, :log_writer, :events, :config, :options, :restart_dir
96
112
 
97
113
  # Return stats about the server
98
114
  def stats
@@ -105,16 +121,18 @@ module Puma
105
121
  write_pid
106
122
 
107
123
  path = @options[:state]
124
+ permission = @options[:state_permission]
108
125
  return unless path
109
126
 
110
- require 'puma/state_file'
127
+ require_relative 'state_file'
111
128
 
112
129
  sf = StateFile.new
113
130
  sf.pid = Process.pid
114
131
  sf.control_url = @options[:control_url]
115
132
  sf.control_auth_token = @options[:control_auth_token]
133
+ sf.running_from = File.expand_path('.')
116
134
 
117
- sf.save path
135
+ sf.save path, permission
118
136
  end
119
137
 
120
138
  # Delete the configured pidfile
@@ -150,18 +168,20 @@ module Puma
150
168
  true
151
169
  end
152
170
 
171
+ # Begin a refork if supported
172
+ def refork
173
+ if clustered? && @runner.respond_to?(:fork_worker!) && @options[:fork_worker]
174
+ @runner.fork_worker!
175
+ true
176
+ else
177
+ log "* refork called but not available."
178
+ false
179
+ end
180
+ end
181
+
153
182
  # Run the server. This blocks until the server is stopped
154
183
  def run
155
- previous_env =
156
- if defined?(Bundler)
157
- env = Bundler::ORIGINAL_ENV.dup
158
- # add -rbundler/setup so we load from Gemfile when restarting
159
- bundle = "-rbundler/setup"
160
- env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
161
- env
162
- else
163
- ENV.to_h
164
- end
184
+ previous_env = get_env
165
185
 
166
186
  @config.clamp
167
187
 
@@ -169,29 +189,21 @@ module Puma
169
189
 
170
190
  setup_signals
171
191
  set_process_title
192
+
193
+ # This blocks until the server is stopped
172
194
  @runner.run
173
195
 
174
- case @status
175
- when :halt
176
- log "* Stopping immediately!"
177
- when :run, :stop
178
- graceful_stop
179
- when :restart
180
- log "* Restarting..."
181
- ENV.replace(previous_env)
182
- @runner.before_restart
183
- restart!
184
- when :exit
185
- # nothing
186
- end
187
- @binder.close_unix_paths
196
+ do_run_finished(previous_env)
188
197
  end
189
198
 
190
- # Return which tcp port the launcher is using, if it's using TCP
191
- def connected_port
192
- @binder.connected_port
199
+ # Return all tcp ports the launcher may be using, TCP or SSL
200
+ # @!attribute [r] connected_ports
201
+ # @version 5.0.0
202
+ def connected_ports
203
+ @binder.connected_ports
193
204
  end
194
205
 
206
+ # @!attribute [r] restart_args
195
207
  def restart_args
196
208
  cmd = @options[:restart_cmd]
197
209
  if cmd
@@ -202,35 +214,79 @@ module Puma
202
214
  end
203
215
 
204
216
  def close_binder_listeners
217
+ @runner.close_control_listeners
205
218
  @binder.close_listeners
219
+ unless @status == :restart
220
+ log "=== puma shutdown: #{Time.now} ==="
221
+ log "- Goodbye!"
222
+ end
223
+ end
224
+
225
+ # @!attribute [r] thread_status
226
+ # @version 5.0.0
227
+ def thread_status
228
+ Thread.list.each do |thread|
229
+ name = "Thread: TID-#{thread.object_id.to_s(36)}"
230
+ name += " #{thread['label']}" if thread['label']
231
+ name += " #{thread.name}" if thread.respond_to?(:name) && thread.name
232
+ backtrace = thread.backtrace || ["<no backtrace available>"]
233
+
234
+ yield name, backtrace
235
+ end
206
236
  end
207
237
 
208
238
  private
209
239
 
210
- # If configured, write the pid of the current process out
211
- # to a file.
212
- def write_pid
213
- path = @options[:pidfile]
214
- return unless path
240
+ def get_env
241
+ if defined?(Bundler)
242
+ env = Bundler::ORIGINAL_ENV.dup
243
+ # add -rbundler/setup so we load from Gemfile when restarting
244
+ bundle = "-rbundler/setup"
245
+ env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
246
+ env
247
+ else
248
+ ENV.to_h
249
+ end
250
+ end
215
251
 
216
- File.open(path, 'w') { |f| f.puts Process.pid }
217
- cur = Process.pid
218
- at_exit do
219
- delete_pidfile if cur == Process.pid
252
+ def do_run_finished(previous_env)
253
+ case @status
254
+ when :halt
255
+ do_forceful_stop
256
+ when :run, :stop
257
+ do_graceful_stop
258
+ when :restart
259
+ do_restart(previous_env)
220
260
  end
261
+
262
+ close_binder_listeners unless @status == :restart
221
263
  end
222
264
 
223
- def reload_worker_directory
224
- @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
265
+ def do_forceful_stop
266
+ log "* Stopping immediately!"
267
+ @runner.stop_control
268
+ end
269
+
270
+ def do_graceful_stop
271
+ @events.fire_on_stopped!
272
+ @runner.stop_blocked
273
+ end
274
+
275
+ def do_restart(previous_env)
276
+ log "* Restarting..."
277
+ ENV.replace(previous_env)
278
+ @runner.stop_control
279
+ restart!
225
280
  end
226
281
 
227
282
  def restart!
228
- @config.run_hooks :on_restart, self
283
+ @events.fire_on_restart!
284
+ @config.run_hooks :on_restart, self, @log_writer
229
285
 
230
286
  if Puma.jruby?
231
287
  close_binder_listeners
232
288
 
233
- require 'puma/jruby_restart'
289
+ require_relative 'jruby_restart'
234
290
  JRubyRestart.chdir_exec(@restart_dir, restart_args)
235
291
  elsif Puma.windows?
236
292
  close_binder_listeners
@@ -241,71 +297,30 @@ module Puma
241
297
  else
242
298
  argv = restart_args
243
299
  Dir.chdir(@restart_dir)
300
+ ENV.update(@binder.redirects_for_restart_env)
244
301
  argv += [@binder.redirects_for_restart]
245
302
  Kernel.exec(*argv)
246
303
  end
247
304
  end
248
305
 
249
- def dependencies_and_files_to_require_after_prune
250
- puma = spec_for_gem("puma")
251
-
252
- deps = puma.runtime_dependencies.map do |d|
253
- "#{d.name}:#{spec_for_gem(d.name).version}"
254
- end
255
-
256
- [deps, require_paths_for_gem(puma) + extra_runtime_deps_directories]
257
- end
258
-
259
- def extra_runtime_deps_directories
260
- Array(@options[:extra_runtime_dependencies]).map do |d_name|
261
- if (spec = spec_for_gem(d_name))
262
- require_paths_for_gem(spec)
263
- else
264
- log "* Could not load extra dependency: #{d_name}"
265
- nil
266
- end
267
- end.flatten.compact
268
- end
269
-
270
- def puma_wild_location
271
- puma = spec_for_gem("puma")
272
- dirs = require_paths_for_gem(puma)
273
- puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
274
- File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
275
- end
276
-
277
- def prune_bundler
278
- return unless defined?(Bundler)
279
- require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
280
- unless puma_wild_location
281
- log "! Unable to prune Bundler environment, continuing"
282
- return
283
- end
284
-
285
- deps, dirs = dependencies_and_files_to_require_after_prune
286
-
287
- log '* Pruning Bundler environment'
288
- home = ENV['GEM_HOME']
289
- Bundler.with_clean_env do
290
- ENV['GEM_HOME'] = home
291
- ENV['PUMA_BUNDLER_PRUNED'] = '1'
292
- args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':'), deps.join(',')] + @original_argv
293
- # Ruby 2.0+ defaults to true which breaks socket activation
294
- args += [{:close_others => false}]
295
- Kernel.exec(*args)
306
+ # If configured, write the pid of the current process out
307
+ # to a file.
308
+ def write_pid
309
+ path = @options[:pidfile]
310
+ return unless path
311
+ cur_pid = Process.pid
312
+ File.write path, cur_pid, mode: 'wb:UTF-8'
313
+ at_exit do
314
+ delete_pidfile if cur_pid == Process.pid
296
315
  end
297
316
  end
298
317
 
299
- def spec_for_gem(gem_name)
300
- Bundler.rubygems.loaded_specs(gem_name)
301
- end
302
-
303
- def require_paths_for_gem(gem_spec)
304
- gem_spec.full_require_paths
318
+ def reload_worker_directory
319
+ @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
305
320
  end
306
321
 
307
322
  def log(str)
308
- @events.log str
323
+ @log_writer.log(str)
309
324
  end
310
325
 
311
326
  def clustered?
@@ -313,35 +328,15 @@ module Puma
313
328
  end
314
329
 
315
330
  def unsupported(str)
316
- @events.error(str)
331
+ @log_writer.error(str)
317
332
  raise UnsupportedOption
318
333
  end
319
334
 
320
- def graceful_stop
321
- @runner.stop_blocked
322
- log "=== puma shutdown: #{Time.now} ==="
323
- log "- Goodbye!"
324
- end
325
-
326
- def log_thread_status
327
- Thread.list.each do |thread|
328
- log "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
329
- logstr = "Thread: TID-#{thread.object_id.to_s(36)}"
330
- logstr += " #{thread.name}" if thread.respond_to?(:name)
331
- log logstr
332
-
333
- if thread.backtrace
334
- log thread.backtrace.join("\n")
335
- else
336
- log "<no backtrace available>"
337
- end
338
- end
339
- end
340
-
341
335
  def set_process_title
342
336
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
343
337
  end
344
338
 
339
+ # @!attribute [r] title
345
340
  def title
346
341
  buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
347
342
  buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
@@ -353,6 +348,7 @@ module Puma
353
348
  ENV['RACK_ENV'] = environment
354
349
  end
355
350
 
351
+ # @!attribute [r] environment
356
352
  def environment
357
353
  @environment
358
354
  end
@@ -361,6 +357,11 @@ module Puma
361
357
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
362
358
  end
363
359
 
360
+ def prune_bundler!
361
+ return unless prune_bundler?
362
+ BundlePruner.new(@original_argv, @options[:extra_runtime_dependencies], @log_writer).prune
363
+ end
364
+
364
365
  def generate_restart_data
365
366
  if dir = @options[:directory]
366
367
  @restart_dir = dir
@@ -427,7 +428,8 @@ module Puma
427
428
 
428
429
  begin
429
430
  Signal.trap "SIGTERM" do
430
- graceful_stop
431
+ # Shortcut the control flow in case raise_exception_on_sigterm is true
432
+ do_graceful_stop
431
433
 
432
434
  raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
433
435
  end
@@ -456,8 +458,13 @@ module Puma
456
458
  end
457
459
 
458
460
  begin
459
- Signal.trap "SIGINFO" do
460
- log_thread_status
461
+ unless Puma.jruby? # INFO in use by JVM already
462
+ Signal.trap "SIGINFO" do
463
+ thread_status do |name, backtrace|
464
+ @log_writer.log(name)
465
+ @log_writer.log(backtrace.map { |bt| " #{bt}" })
466
+ end
467
+ end
461
468
  end
462
469
  rescue Exception
463
470
  # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
@@ -465,11 +472,13 @@ module Puma
465
472
  end
466
473
  end
467
474
 
468
- def require_rubygems_min_version!(min_version, feature)
469
- return if min_version <= Gem::Version.new(Gem::VERSION)
475
+ def log_config
476
+ log "Configuration:"
477
+
478
+ @config.final_options
479
+ .each { |config_key, value| log "- #{config_key}: #{value}" }
470
480
 
471
- raise "#{feature} is not supported on your version of RubyGems. " \
472
- "You must have RubyGems #{min_version}+ to use this feature."
481
+ log "\n"
473
482
  end
474
483
  end
475
484
  end
@@ -0,0 +1,147 @@
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, :custom_logger
32
+
33
+ # Create a LogWriter that prints to +stdout+ and +stderr+.
34
+ def initialize(stdout, stderr)
35
+ @formatter = DefaultFormatter.new
36
+ @custom_logger = nil
37
+ @stdout = stdout
38
+ @stderr = stderr
39
+
40
+ @debug = ENV.key?('PUMA_DEBUG')
41
+ @error_logger = ErrorLogger.new(@stderr)
42
+ end
43
+
44
+ DEFAULT = new(STDOUT, STDERR)
45
+
46
+ # Returns an LogWriter object which writes its status to
47
+ # two StringIO objects.
48
+ def self.strings
49
+ LogWriter.new(StringIO.new, StringIO.new)
50
+ end
51
+
52
+ def self.stdio
53
+ LogWriter.new($stdout, $stderr)
54
+ end
55
+
56
+ def self.null
57
+ n = NullIO.new
58
+ LogWriter.new(n, n)
59
+ end
60
+
61
+ # Write +str+ to +@stdout+
62
+ def log(str)
63
+ if @custom_logger&.respond_to?(:write)
64
+ @custom_logger.write(format(str))
65
+ else
66
+ internal_write "#{@formatter.call str}\n"
67
+ end
68
+ end
69
+
70
+ def write(str)
71
+ internal_write @formatter.call(str)
72
+ end
73
+
74
+ def internal_write(str)
75
+ LOG_QUEUE << str
76
+ while (w_str = LOG_QUEUE.pop(true)) do
77
+ begin
78
+ @stdout.is_a?(IO) and @stdout.wait_writable(1)
79
+ @stdout.write w_str
80
+ @stdout.flush unless @stdout.sync
81
+ rescue Errno::EPIPE, Errno::EBADF, IOError, Errno::EINVAL
82
+ # 'Invalid argument' (Errno::EINVAL) may be raised by flush
83
+ end
84
+ end
85
+ rescue ThreadError
86
+ end
87
+ private :internal_write
88
+
89
+ def debug?
90
+ @debug
91
+ end
92
+
93
+ def debug(str)
94
+ log("% #{str}") if @debug
95
+ end
96
+
97
+ # Write +str+ to +@stderr+
98
+ def error(str)
99
+ @error_logger.info(text: @formatter.call("ERROR: #{str}"))
100
+ exit 1
101
+ end
102
+
103
+ def format(str)
104
+ formatter.call(str)
105
+ end
106
+
107
+ # An HTTP connection error has occurred.
108
+ # +error+ a connection exception, +req+ the request,
109
+ # and +text+ additional info
110
+ # @version 5.0.0
111
+ def connection_error(error, req, text="HTTP connection error")
112
+ @error_logger.info(error: error, req: req, text: text)
113
+ end
114
+
115
+ # An HTTP parse error has occurred.
116
+ # +error+ a parsing exception,
117
+ # and +req+ the request.
118
+ def parse_error(error, req)
119
+ @error_logger.info(error: error, req: req, text: 'HTTP parse error, malformed request')
120
+ end
121
+
122
+ # An SSL error has occurred.
123
+ # @param error <Puma::MiniSSL::SSLError>
124
+ # @param ssl_socket <Puma::MiniSSL::Socket>
125
+ def ssl_error(error, ssl_socket)
126
+ peeraddr = ssl_socket.peeraddr.last rescue "<unknown>"
127
+ peercert = ssl_socket.peercert
128
+ subject = peercert&.subject
129
+ @error_logger.info(error: error, text: "SSL error, peer: #{peeraddr}, peer cert: #{subject}")
130
+ end
131
+
132
+ # An unknown error has occurred.
133
+ # +error+ an exception object, +req+ the request,
134
+ # and +text+ additional info
135
+ def unknown_error(error, req=nil, text="Unknown error")
136
+ @error_logger.info(error: error, req: req, text: text)
137
+ end
138
+
139
+ # Log occurred error debug dump.
140
+ # +error+ an exception object, +req+ the request,
141
+ # and +text+ additional info
142
+ # @version 5.0.0
143
+ def debug_error(error, req=nil, text="")
144
+ @error_logger.debug(error: error, req: req, text: text)
145
+ end
146
+ end
147
+ end