puma 3.12.6 → 6.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1806 -451
  3. data/LICENSE +23 -20
  4. data/README.md +217 -65
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +59 -21
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +69 -58
  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/docs/kubernetes.md +66 -0
  17. data/docs/nginx.md +2 -2
  18. data/docs/plugins.md +22 -12
  19. data/docs/rails_dev_mode.md +28 -0
  20. data/docs/restart.md +47 -22
  21. data/docs/signals.md +13 -11
  22. data/docs/stats.md +142 -0
  23. data/docs/systemd.md +94 -120
  24. data/docs/testing_benchmarks_local_files.md +150 -0
  25. data/docs/testing_test_rackup_ci_files.md +36 -0
  26. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  27. data/ext/puma_http11/ext_help.h +1 -1
  28. data/ext/puma_http11/extconf.rb +61 -3
  29. data/ext/puma_http11/http11_parser.c +103 -117
  30. data/ext/puma_http11/http11_parser.h +2 -2
  31. data/ext/puma_http11/http11_parser.java.rl +22 -38
  32. data/ext/puma_http11/http11_parser.rl +3 -3
  33. data/ext/puma_http11/http11_parser_common.rl +6 -6
  34. data/ext/puma_http11/mini_ssl.c +389 -99
  35. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  36. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  37. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
  38. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +248 -92
  39. data/ext/puma_http11/puma_http11.c +49 -57
  40. data/lib/puma/app/status.rb +71 -49
  41. data/lib/puma/binder.rb +244 -150
  42. data/lib/puma/cli.rb +38 -34
  43. data/lib/puma/client.rb +388 -244
  44. data/lib/puma/cluster/worker.rb +180 -0
  45. data/lib/puma/cluster/worker_handle.rb +97 -0
  46. data/lib/puma/cluster.rb +261 -243
  47. data/lib/puma/commonlogger.rb +21 -14
  48. data/lib/puma/configuration.rb +116 -88
  49. data/lib/puma/const.rb +154 -104
  50. data/lib/puma/control_cli.rb +115 -70
  51. data/lib/puma/detect.rb +33 -2
  52. data/lib/puma/dsl.rb +764 -134
  53. data/lib/puma/error_logger.rb +113 -0
  54. data/lib/puma/events.rb +16 -112
  55. data/lib/puma/io_buffer.rb +42 -5
  56. data/lib/puma/jruby_restart.rb +2 -59
  57. data/lib/puma/json_serialization.rb +96 -0
  58. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  59. data/lib/puma/launcher.rb +184 -133
  60. data/lib/puma/log_writer.rb +147 -0
  61. data/lib/puma/minissl/context_builder.rb +93 -0
  62. data/lib/puma/minissl.rb +263 -70
  63. data/lib/puma/null_io.rb +18 -1
  64. data/lib/puma/plugin/systemd.rb +90 -0
  65. data/lib/puma/plugin/tmp_restart.rb +3 -1
  66. data/lib/puma/plugin.rb +7 -13
  67. data/lib/puma/rack/builder.rb +9 -11
  68. data/lib/puma/rack/urlmap.rb +2 -0
  69. data/lib/puma/rack_default.rb +21 -4
  70. data/lib/puma/reactor.rb +93 -315
  71. data/lib/puma/request.rb +671 -0
  72. data/lib/puma/runner.rb +94 -69
  73. data/lib/puma/sd_notify.rb +149 -0
  74. data/lib/puma/server.rb +327 -772
  75. data/lib/puma/single.rb +20 -74
  76. data/lib/puma/state_file.rb +45 -8
  77. data/lib/puma/thread_pool.rb +146 -92
  78. data/lib/puma/util.rb +22 -10
  79. data/lib/puma.rb +60 -5
  80. data/lib/rack/handler/puma.rb +116 -90
  81. data/tools/Dockerfile +16 -0
  82. data/tools/trickletest.rb +0 -1
  83. metadata +54 -32
  84. data/ext/puma_http11/io_buffer.c +0 -155
  85. data/lib/puma/accept_nonblock.rb +0 -23
  86. data/lib/puma/compat.rb +0 -14
  87. data/lib/puma/convenient.rb +0 -25
  88. data/lib/puma/daemon_ext.rb +0 -33
  89. data/lib/puma/delegation.rb +0 -13
  90. data/lib/puma/java_io_buffer.rb +0 -47
  91. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  92. data/lib/puma/tcp_logger.rb +0 -41
  93. data/tools/jungle/README.md +0 -19
  94. data/tools/jungle/init.d/README.md +0 -61
  95. data/tools/jungle/init.d/puma +0 -421
  96. data/tools/jungle/init.d/run-puma +0 -18
  97. data/tools/jungle/upstart/README.md +0 -61
  98. data/tools/jungle/upstart/puma-manager.conf +0 -31
  99. data/tools/jungle/upstart/puma.conf +0 -69
  100. /data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
data/lib/puma/launcher.rb CHANGED
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/events'
4
- require 'puma/detect'
5
-
6
- require 'puma/cluster'
7
- require 'puma/single'
8
-
9
- require 'puma/const'
10
-
11
- 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'
12
10
 
13
11
  module Puma
14
12
  # Puma::Launcher is the single entry point for starting a Puma server based on user
@@ -18,17 +16,14 @@ module Puma
18
16
  # It is responsible for either launching a cluster of Puma workers or a single
19
17
  # puma server.
20
18
  class Launcher
21
- KEYS_NOT_TO_PERSIST_IN_STATE = [
22
- :logger, :lowlevel_error_handler,
23
- :before_worker_shutdown, :before_worker_boot, :before_worker_fork,
24
- :after_worker_boot, :before_fork, :on_restart
25
- ]
19
+ autoload :BundlePruner, 'puma/launcher/bundle_pruner'
20
+
26
21
  # Returns an instance of Launcher
27
22
  #
28
23
  # +conf+ A Puma::Configuration object indicating how to run the server.
29
24
  #
30
25
  # +launcher_args+ A Hash that currently has one required key `:events`,
31
- # 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`,
32
27
  # this object will be responsible for broadcasting Puma's internal state
33
28
  # to a logging destination. An optional key `:argv` can be supplied,
34
29
  # this should be an array of strings, these arguments are re-used when
@@ -42,58 +37,78 @@ module Puma
42
37
  # [200, {}, ["hello world"]]
43
38
  # end
44
39
  # end
45
- # Puma::Launcher.new(conf, events: Puma::Events.stdio).run
40
+ # Puma::Launcher.new(conf, log_writer: Puma::LogWriter.stdio).run
46
41
  def initialize(conf, launcher_args={})
47
42
  @runner = nil
48
- @events = launcher_args[:events] || Events::DEFAULT
43
+ @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
44
+ @events = launcher_args[:events] || Events.new
49
45
  @argv = launcher_args[:argv] || []
50
46
  @original_argv = @argv.dup
51
47
  @config = conf
52
48
 
53
- @binder = Binder.new(@events)
54
- @binder.import_from_env
55
-
56
- @environment = conf.environment
49
+ @config.options[:log_writer] = @log_writer
57
50
 
58
51
  # Advertise the Configuration
59
52
  Puma.cli_config = @config if defined?(Puma.cli_config)
60
53
 
61
54
  @config.load
62
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
+
63
76
  @options = @config.options
64
77
  @config.clamp
65
78
 
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]
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
- @events.formatter = Events::PidFormatter.new
85
- @options[:logger] = @events
98
+ @options[:logger] = @log_writer
86
99
 
87
- @runner = Cluster.new(self, @events)
100
+ @runner = Cluster.new(self)
88
101
  else
89
- @runner = Single.new(self, @events)
102
+ @runner = Single.new(self)
90
103
  end
91
104
  Puma.stats_object = @runner
92
105
 
93
106
  @status = :run
107
+
108
+ log_config if ENV['PUMA_LOG_CONFIG']
94
109
  end
95
110
 
96
- attr_reader :binder, :events, :config, :options, :restart_dir
111
+ attr_reader :binder, :log_writer, :events, :config, :options, :restart_dir
97
112
 
98
113
  # Return stats about the server
99
114
  def stats
@@ -106,16 +121,18 @@ module Puma
106
121
  write_pid
107
122
 
108
123
  path = @options[:state]
124
+ permission = @options[:state_permission]
109
125
  return unless path
110
126
 
111
- require 'puma/state_file'
127
+ require_relative 'state_file'
112
128
 
113
129
  sf = StateFile.new
114
130
  sf.pid = Process.pid
115
131
  sf.control_url = @options[:control_url]
116
132
  sf.control_auth_token = @options[:control_auth_token]
133
+ sf.running_from = File.expand_path('.')
117
134
 
118
- sf.save path
135
+ sf.save path, permission
119
136
  end
120
137
 
121
138
  # Delete the configured pidfile
@@ -124,19 +141,6 @@ module Puma
124
141
  File.unlink(path) if path && File.exist?(path)
125
142
  end
126
143
 
127
- # If configured, write the pid of the current process out
128
- # to a file.
129
- def write_pid
130
- path = @options[:pidfile]
131
- return unless path
132
-
133
- File.open(path, 'w') { |f| f.puts Process.pid }
134
- cur = Process.pid
135
- at_exit do
136
- delete_pidfile if cur == Process.pid
137
- end
138
- end
139
-
140
144
  # Begin async shutdown of the server
141
145
  def halt
142
146
  @status = :halt
@@ -164,18 +168,20 @@ module Puma
164
168
  true
165
169
  end
166
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
+
167
182
  # Run the server. This blocks until the server is stopped
168
183
  def run
169
- previous_env =
170
- if defined?(Bundler)
171
- env = Bundler::ORIGINAL_ENV.dup
172
- # add -rbundler/setup so we load from Gemfile when restarting
173
- bundle = "-rbundler/setup"
174
- env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
175
- env
176
- else
177
- ENV.to_h
178
- end
184
+ previous_env = get_env
179
185
 
180
186
  @config.clamp
181
187
 
@@ -183,28 +189,21 @@ module Puma
183
189
 
184
190
  setup_signals
185
191
  set_process_title
192
+
193
+ # This blocks until the server is stopped
186
194
  @runner.run
187
195
 
188
- case @status
189
- when :halt
190
- log "* Stopping immediately!"
191
- when :run, :stop
192
- graceful_stop
193
- when :restart
194
- log "* Restarting..."
195
- ENV.replace(previous_env)
196
- @runner.before_restart
197
- restart!
198
- when :exit
199
- # nothing
200
- end
196
+ do_run_finished(previous_env)
201
197
  end
202
198
 
203
- # Return which tcp port the launcher is using, if it's using TCP
204
- def connected_port
205
- @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
206
204
  end
207
205
 
206
+ # @!attribute [r] restart_args
208
207
  def restart_args
209
208
  cmd = @options[:restart_cmd]
210
209
  if cmd
@@ -214,19 +213,80 @@ module Puma
214
213
  end
215
214
  end
216
215
 
216
+ def close_binder_listeners
217
+ @runner.close_control_listeners
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
236
+ end
237
+
217
238
  private
218
239
 
219
- def reload_worker_directory
220
- @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
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
251
+
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)
260
+ end
261
+
262
+ close_binder_listeners unless @status == :restart
263
+ end
264
+
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!
221
280
  end
222
281
 
223
282
  def restart!
224
- @config.run_hooks :on_restart, self
283
+ @events.fire_on_restart!
284
+ @config.run_hooks :on_restart, self, @log_writer
225
285
 
226
286
  if Puma.jruby?
227
287
  close_binder_listeners
228
288
 
229
- require 'puma/jruby_restart'
289
+ require_relative 'jruby_restart'
230
290
  JRubyRestart.chdir_exec(@restart_dir, restart_args)
231
291
  elsif Puma.windows?
232
292
  close_binder_listeners
@@ -235,50 +295,32 @@ module Puma
235
295
  Dir.chdir(@restart_dir)
236
296
  Kernel.exec(*argv)
237
297
  else
238
- redirects = {:close_others => true}
239
- @binder.listeners.each_with_index do |(l, io), i|
240
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
241
- redirects[io.to_i] = io.to_i
242
- end
243
-
244
298
  argv = restart_args
245
299
  Dir.chdir(@restart_dir)
246
- argv += [redirects] if RUBY_VERSION >= '1.9'
300
+ ENV.update(@binder.redirects_for_restart_env)
301
+ argv += [@binder.redirects_for_restart]
247
302
  Kernel.exec(*argv)
248
303
  end
249
304
  end
250
305
 
251
- def prune_bundler
252
- return unless defined?(Bundler)
253
- puma = Bundler.rubygems.loaded_specs("puma")
254
- dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
255
- puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
256
-
257
- unless puma_lib_dir
258
- log "! Unable to prune Bundler environment, continuing"
259
- return
260
- end
261
-
262
- deps = puma.runtime_dependencies.map do |d|
263
- spec = Bundler.rubygems.loaded_specs(d.name)
264
- "#{d.name}:#{spec.version.to_s}"
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
265
315
  end
316
+ end
266
317
 
267
- log '* Pruning Bundler environment'
268
- home = ENV['GEM_HOME']
269
- Bundler.with_clean_env do
270
- ENV['GEM_HOME'] = home
271
- ENV['PUMA_BUNDLER_PRUNED'] = '1'
272
- wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
273
- args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
274
- # Ruby 2.0+ defaults to true which breaks socket activation
275
- args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
276
- Kernel.exec(*args)
277
- end
318
+ def reload_worker_directory
319
+ @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
278
320
  end
279
321
 
280
322
  def log(str)
281
- @events.log str
323
+ @log_writer.log(str)
282
324
  end
283
325
 
284
326
  def clustered?
@@ -286,20 +328,15 @@ module Puma
286
328
  end
287
329
 
288
330
  def unsupported(str)
289
- @events.error(str)
331
+ @log_writer.error(str)
290
332
  raise UnsupportedOption
291
333
  end
292
334
 
293
- def graceful_stop
294
- @runner.stop_blocked
295
- log "=== puma shutdown: #{Time.now} ==="
296
- log "- Goodbye!"
297
- end
298
-
299
335
  def set_process_title
300
336
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
301
337
  end
302
338
 
339
+ # @!attribute [r] title
303
340
  def title
304
341
  buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
305
342
  buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
@@ -311,6 +348,7 @@ module Puma
311
348
  ENV['RACK_ENV'] = environment
312
349
  end
313
350
 
351
+ # @!attribute [r] environment
314
352
  def environment
315
353
  @environment
316
354
  end
@@ -319,16 +357,11 @@ module Puma
319
357
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
320
358
  end
321
359
 
322
- def close_binder_listeners
323
- @binder.listeners.each do |l, io|
324
- io.close
325
- uri = URI.parse(l)
326
- next unless uri.scheme == 'unix'
327
- File.unlink("#{uri.host}#{uri.path}")
328
- end
360
+ def prune_bundler!
361
+ return unless prune_bundler?
362
+ BundlePruner.new(@original_argv, @options[:extra_runtime_dependencies], @log_writer).prune
329
363
  end
330
364
 
331
-
332
365
  def generate_restart_data
333
366
  if dir = @options[:directory]
334
367
  @restart_dir = dir
@@ -395,9 +428,10 @@ module Puma
395
428
 
396
429
  begin
397
430
  Signal.trap "SIGTERM" do
398
- graceful_stop
431
+ # Shortcut the control flow in case raise_exception_on_sigterm is true
432
+ do_graceful_stop
399
433
 
400
- raise SignalException, "SIGTERM"
434
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
401
435
  end
402
436
  rescue Exception
403
437
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
@@ -405,12 +439,6 @@ module Puma
405
439
 
406
440
  begin
407
441
  Signal.trap "SIGINT" do
408
- if Puma.jruby?
409
- @status = :exit
410
- graceful_stop
411
- exit
412
- end
413
-
414
442
  stop
415
443
  end
416
444
  rescue Exception
@@ -428,6 +456,29 @@ module Puma
428
456
  rescue Exception
429
457
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
430
458
  end
459
+
460
+ begin
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
468
+ end
469
+ rescue Exception
470
+ # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
471
+ # to see this constantly on Linux.
472
+ end
473
+ end
474
+
475
+ def log_config
476
+ log "Configuration:"
477
+
478
+ @config.final_options
479
+ .each { |config_key, value| log "- #{config_key}: #{value}" }
480
+
481
+ log "\n"
431
482
  end
432
483
  end
433
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