puma 4.3.12 → 6.0.2

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1618 -521
  3. data/LICENSE +23 -20
  4. data/README.md +130 -42
  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/jungle/README.md +9 -0
  11. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  12. data/{tools → docs}/jungle/rc.d/puma +2 -2
  13. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  14. data/docs/kubernetes.md +66 -0
  15. data/docs/nginx.md +2 -2
  16. data/docs/plugins.md +15 -15
  17. data/docs/rails_dev_mode.md +28 -0
  18. data/docs/restart.md +46 -23
  19. data/docs/signals.md +13 -11
  20. data/docs/stats.md +142 -0
  21. data/docs/systemd.md +85 -128
  22. data/docs/testing_benchmarks_local_files.md +150 -0
  23. data/docs/testing_test_rackup_ci_files.md +36 -0
  24. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  25. data/ext/puma_http11/ext_help.h +1 -1
  26. data/ext/puma_http11/extconf.rb +49 -12
  27. data/ext/puma_http11/http11_parser.c +46 -48
  28. data/ext/puma_http11/http11_parser.h +2 -2
  29. data/ext/puma_http11/http11_parser.java.rl +3 -3
  30. data/ext/puma_http11/http11_parser.rl +3 -3
  31. data/ext/puma_http11/http11_parser_common.rl +2 -2
  32. data/ext/puma_http11/mini_ssl.c +250 -93
  33. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  34. data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
  35. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +4 -6
  36. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
  37. data/ext/puma_http11/puma_http11.c +46 -57
  38. data/lib/puma/app/status.rb +52 -38
  39. data/lib/puma/binder.rb +232 -119
  40. data/lib/puma/cli.rb +33 -33
  41. data/lib/puma/client.rb +129 -88
  42. data/lib/puma/cluster/worker.rb +175 -0
  43. data/lib/puma/cluster/worker_handle.rb +97 -0
  44. data/lib/puma/cluster.rb +224 -231
  45. data/lib/puma/commonlogger.rb +2 -2
  46. data/lib/puma/configuration.rb +112 -87
  47. data/lib/puma/const.rb +86 -91
  48. data/lib/puma/control_cli.rb +99 -79
  49. data/lib/puma/detect.rb +31 -2
  50. data/lib/puma/dsl.rb +426 -110
  51. data/lib/puma/error_logger.rb +112 -0
  52. data/lib/puma/events.rb +16 -115
  53. data/lib/puma/io_buffer.rb +44 -2
  54. data/lib/puma/jruby_restart.rb +2 -59
  55. data/lib/puma/json_serialization.rb +96 -0
  56. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  57. data/lib/puma/launcher.rb +170 -148
  58. data/lib/puma/log_writer.rb +137 -0
  59. data/lib/puma/minissl/context_builder.rb +35 -19
  60. data/lib/puma/minissl.rb +213 -55
  61. data/lib/puma/null_io.rb +18 -1
  62. data/lib/puma/plugin/tmp_restart.rb +1 -1
  63. data/lib/puma/plugin.rb +3 -12
  64. data/lib/puma/rack/builder.rb +5 -9
  65. data/lib/puma/rack_default.rb +1 -1
  66. data/lib/puma/reactor.rb +85 -369
  67. data/lib/puma/request.rb +644 -0
  68. data/lib/puma/runner.rb +86 -76
  69. data/lib/puma/server.rb +306 -793
  70. data/lib/puma/single.rb +18 -74
  71. data/lib/puma/state_file.rb +45 -8
  72. data/lib/puma/systemd.rb +47 -0
  73. data/lib/puma/thread_pool.rb +136 -68
  74. data/lib/puma/util.rb +21 -4
  75. data/lib/puma.rb +54 -7
  76. data/lib/rack/handler/puma.rb +11 -12
  77. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  78. metadata +31 -23
  79. data/docs/tcp_mode.md +0 -96
  80. data/ext/puma_http11/io_buffer.c +0 -155
  81. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  82. data/lib/puma/accept_nonblock.rb +0 -29
  83. data/lib/puma/tcp_logger.rb +0 -41
  84. data/tools/jungle/README.md +0 -19
  85. data/tools/jungle/init.d/README.md +0 -61
  86. data/tools/jungle/init.d/puma +0 -421
  87. data/tools/jungle/init.d/run-puma +0 -18
  88. data/tools/jungle/upstart/README.md +0 -61
  89. data/tools/jungle/upstart/puma-manager.conf +0 -31
  90. 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,69 @@ 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
+ if @config.options[:bind_to_activated_sockets]
63
+ @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
64
+ @config.options[:binds],
65
+ @config.options[:bind_to_activated_sockets] == 'only'
66
+ )
67
+ end
68
+
60
69
  @options = @config.options
61
70
  @config.clamp
62
71
 
63
- @events.formatter = Events::PidFormatter.new if clustered?
64
- @events.formatter = options[:log_formatter] if @options[:log_formatter]
72
+ @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
73
+ @log_writer.formatter = options[:log_formatter] if @options[:log_formatter]
65
74
 
66
75
  generate_restart_data
67
76
 
68
- if clustered? && !Process.respond_to?(:fork)
77
+ if clustered? && !Puma.forkable?
69
78
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
70
79
  end
71
80
 
72
- if @options[:daemon] && Puma.windows?
73
- unsupported 'daemon mode not supported on Windows'
74
- end
75
-
76
81
  Dir.chdir(@restart_dir)
77
82
 
78
- prune_bundler if prune_bundler?
83
+ prune_bundler!
79
84
 
80
85
  @environment = @options[:environment] if @options[:environment]
81
86
  set_rack_environment
82
87
 
83
88
  if clustered?
84
- @options[:logger] = @events
89
+ @options[:logger] = @log_writer
85
90
 
86
- @runner = Cluster.new(self, @events)
91
+ @runner = Cluster.new(self)
87
92
  else
88
- @runner = Single.new(self, @events)
93
+ @runner = Single.new(self)
89
94
  end
90
95
  Puma.stats_object = @runner
91
96
 
92
97
  @status = :run
98
+
99
+ log_config if ENV['PUMA_LOG_CONFIG']
93
100
  end
94
101
 
95
- attr_reader :binder, :events, :config, :options, :restart_dir
102
+ attr_reader :binder, :log_writer, :events, :config, :options, :restart_dir
96
103
 
97
104
  # Return stats about the server
98
105
  def stats
@@ -105,16 +112,18 @@ module Puma
105
112
  write_pid
106
113
 
107
114
  path = @options[:state]
115
+ permission = @options[:state_permission]
108
116
  return unless path
109
117
 
110
- require 'puma/state_file'
118
+ require_relative 'state_file'
111
119
 
112
120
  sf = StateFile.new
113
121
  sf.pid = Process.pid
114
122
  sf.control_url = @options[:control_url]
115
123
  sf.control_auth_token = @options[:control_auth_token]
124
+ sf.running_from = File.expand_path('.')
116
125
 
117
- sf.save path
126
+ sf.save path, permission
118
127
  end
119
128
 
120
129
  # Delete the configured pidfile
@@ -150,18 +159,20 @@ module Puma
150
159
  true
151
160
  end
152
161
 
162
+ # Begin a refork if supported
163
+ def refork
164
+ if clustered? && @runner.respond_to?(:fork_worker!) && @options[:fork_worker]
165
+ @runner.fork_worker!
166
+ true
167
+ else
168
+ log "* refork called but not available."
169
+ false
170
+ end
171
+ end
172
+
153
173
  # Run the server. This blocks until the server is stopped
154
174
  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
175
+ previous_env = get_env
165
176
 
166
177
  @config.clamp
167
178
 
@@ -169,29 +180,22 @@ module Puma
169
180
 
170
181
  setup_signals
171
182
  set_process_title
183
+ integrate_with_systemd
184
+
185
+ # This blocks until the server is stopped
172
186
  @runner.run
173
187
 
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
188
+ do_run_finished(previous_env)
188
189
  end
189
190
 
190
- # Return which tcp port the launcher is using, if it's using TCP
191
- def connected_port
192
- @binder.connected_port
191
+ # Return all tcp ports the launcher may be using, TCP or SSL
192
+ # @!attribute [r] connected_ports
193
+ # @version 5.0.0
194
+ def connected_ports
195
+ @binder.connected_ports
193
196
  end
194
197
 
198
+ # @!attribute [r] restart_args
195
199
  def restart_args
196
200
  cmd = @options[:restart_cmd]
197
201
  if cmd
@@ -202,35 +206,79 @@ module Puma
202
206
  end
203
207
 
204
208
  def close_binder_listeners
209
+ @runner.close_control_listeners
205
210
  @binder.close_listeners
211
+ unless @status == :restart
212
+ log "=== puma shutdown: #{Time.now} ==="
213
+ log "- Goodbye!"
214
+ end
215
+ end
216
+
217
+ # @!attribute [r] thread_status
218
+ # @version 5.0.0
219
+ def thread_status
220
+ Thread.list.each do |thread|
221
+ name = "Thread: TID-#{thread.object_id.to_s(36)}"
222
+ name += " #{thread['label']}" if thread['label']
223
+ name += " #{thread.name}" if thread.respond_to?(:name) && thread.name
224
+ backtrace = thread.backtrace || ["<no backtrace available>"]
225
+
226
+ yield name, backtrace
227
+ end
206
228
  end
207
229
 
208
230
  private
209
231
 
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
232
+ def get_env
233
+ if defined?(Bundler)
234
+ env = Bundler::ORIGINAL_ENV.dup
235
+ # add -rbundler/setup so we load from Gemfile when restarting
236
+ bundle = "-rbundler/setup"
237
+ env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
238
+ env
239
+ else
240
+ ENV.to_h
241
+ end
242
+ end
215
243
 
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
244
+ def do_run_finished(previous_env)
245
+ case @status
246
+ when :halt
247
+ do_forceful_stop
248
+ when :run, :stop
249
+ do_graceful_stop
250
+ when :restart
251
+ do_restart(previous_env)
220
252
  end
253
+
254
+ close_binder_listeners unless @status == :restart
221
255
  end
222
256
 
223
- def reload_worker_directory
224
- @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
257
+ def do_forceful_stop
258
+ log "* Stopping immediately!"
259
+ @runner.stop_control
260
+ end
261
+
262
+ def do_graceful_stop
263
+ @events.fire_on_stopped!
264
+ @runner.stop_blocked
265
+ end
266
+
267
+ def do_restart(previous_env)
268
+ log "* Restarting..."
269
+ ENV.replace(previous_env)
270
+ @runner.stop_control
271
+ restart!
225
272
  end
226
273
 
227
274
  def restart!
228
- @config.run_hooks :on_restart, self
275
+ @events.fire_on_restart!
276
+ @config.run_hooks :on_restart, self, @log_writer
229
277
 
230
278
  if Puma.jruby?
231
279
  close_binder_listeners
232
280
 
233
- require 'puma/jruby_restart'
281
+ require_relative 'jruby_restart'
234
282
  JRubyRestart.chdir_exec(@restart_dir, restart_args)
235
283
  elsif Puma.windows?
236
284
  close_binder_listeners
@@ -241,71 +289,51 @@ module Puma
241
289
  else
242
290
  argv = restart_args
243
291
  Dir.chdir(@restart_dir)
292
+ ENV.update(@binder.redirects_for_restart_env)
244
293
  argv += [@binder.redirects_for_restart]
245
294
  Kernel.exec(*argv)
246
295
  end
247
296
  end
248
297
 
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}"
298
+ # If configured, write the pid of the current process out
299
+ # to a file.
300
+ def write_pid
301
+ path = @options[:pidfile]
302
+ return unless path
303
+ cur_pid = Process.pid
304
+ File.write path, cur_pid, mode: 'wb:UTF-8'
305
+ at_exit do
306
+ delete_pidfile if cur_pid == Process.pid
254
307
  end
255
-
256
- [deps, require_paths_for_gem(puma) + extra_runtime_deps_directories]
257
308
  end
258
309
 
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
310
+ def reload_worker_directory
311
+ @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
268
312
  end
269
313
 
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
314
+ # Puma's systemd integration allows Puma to inform systemd:
315
+ # 1. when it has successfully started
316
+ # 2. when it is starting shutdown
317
+ # 3. periodically for a liveness check with a watchdog thread
318
+ def integrate_with_systemd
319
+ return unless ENV["NOTIFY_SOCKET"]
276
320
 
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"
321
+ begin
322
+ require_relative 'systemd'
323
+ rescue LoadError
324
+ log "Systemd integration failed. It looks like you're trying to use systemd notify but don't have sd_notify gem installed"
282
325
  return
283
326
  end
284
327
 
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)
296
- end
297
- end
298
-
299
- def spec_for_gem(gem_name)
300
- Bundler.rubygems.loaded_specs(gem_name)
301
- end
328
+ log "* Enabling systemd notification integration"
302
329
 
303
- def require_paths_for_gem(gem_spec)
304
- gem_spec.full_require_paths
330
+ systemd = Systemd.new(@log_writer, @events)
331
+ systemd.hook_events
332
+ systemd.start_watchdog
305
333
  end
306
334
 
307
335
  def log(str)
308
- @events.log str
336
+ @log_writer.log(str)
309
337
  end
310
338
 
311
339
  def clustered?
@@ -313,35 +341,15 @@ module Puma
313
341
  end
314
342
 
315
343
  def unsupported(str)
316
- @events.error(str)
344
+ @log_writer.error(str)
317
345
  raise UnsupportedOption
318
346
  end
319
347
 
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
348
  def set_process_title
342
349
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
343
350
  end
344
351
 
352
+ # @!attribute [r] title
345
353
  def title
346
354
  buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
347
355
  buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
@@ -353,6 +361,7 @@ module Puma
353
361
  ENV['RACK_ENV'] = environment
354
362
  end
355
363
 
364
+ # @!attribute [r] environment
356
365
  def environment
357
366
  @environment
358
367
  end
@@ -361,6 +370,11 @@ module Puma
361
370
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
362
371
  end
363
372
 
373
+ def prune_bundler!
374
+ return unless prune_bundler?
375
+ BundlePruner.new(@original_argv, @options[:extra_runtime_dependencies], @log_writer).prune
376
+ end
377
+
364
378
  def generate_restart_data
365
379
  if dir = @options[:directory]
366
380
  @restart_dir = dir
@@ -427,7 +441,8 @@ module Puma
427
441
 
428
442
  begin
429
443
  Signal.trap "SIGTERM" do
430
- graceful_stop
444
+ # Shortcut the control flow in case raise_exception_on_sigterm is true
445
+ do_graceful_stop
431
446
 
432
447
  raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
433
448
  end
@@ -456,8 +471,13 @@ module Puma
456
471
  end
457
472
 
458
473
  begin
459
- Signal.trap "SIGINFO" do
460
- log_thread_status
474
+ unless Puma.jruby? # INFO in use by JVM already
475
+ Signal.trap "SIGINFO" do
476
+ thread_status do |name, backtrace|
477
+ @log_writer.log(name)
478
+ @log_writer.log(backtrace.map { |bt| " #{bt}" })
479
+ end
480
+ end
461
481
  end
462
482
  rescue Exception
463
483
  # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
@@ -465,11 +485,13 @@ module Puma
465
485
  end
466
486
  end
467
487
 
468
- def require_rubygems_min_version!(min_version, feature)
469
- return if min_version <= Gem::Version.new(Gem::VERSION)
488
+ def log_config
489
+ log "Configuration:"
490
+
491
+ @config.final_options
492
+ .each { |config_key, value| log "- #{config_key}: #{value}" }
470
493
 
471
- raise "#{feature} is not supported on your version of RubyGems. " \
472
- "You must have RubyGems #{min_version}+ to use this feature."
494
+ log "\n"
473
495
  end
474
496
  end
475
497
  end
@@ -0,0 +1,137 @@
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(str)
84
+ log("% #{str}") if @debug
85
+ end
86
+
87
+ # Write +str+ to +@stderr+
88
+ def error(str)
89
+ @error_logger.info(text: @formatter.call("ERROR: #{str}"))
90
+ exit 1
91
+ end
92
+
93
+ def format(str)
94
+ formatter.call(str)
95
+ end
96
+
97
+ # An HTTP connection error has occurred.
98
+ # +error+ a connection exception, +req+ the request,
99
+ # and +text+ additional info
100
+ # @version 5.0.0
101
+ def connection_error(error, req, text="HTTP connection error")
102
+ @error_logger.info(error: error, req: req, text: text)
103
+ end
104
+
105
+ # An HTTP parse error has occurred.
106
+ # +error+ a parsing exception,
107
+ # and +req+ the request.
108
+ def parse_error(error, req)
109
+ @error_logger.info(error: error, req: req, text: 'HTTP parse error, malformed request')
110
+ end
111
+
112
+ # An SSL error has occurred.
113
+ # @param error <Puma::MiniSSL::SSLError>
114
+ # @param ssl_socket <Puma::MiniSSL::Socket>
115
+ def ssl_error(error, ssl_socket)
116
+ peeraddr = ssl_socket.peeraddr.last rescue "<unknown>"
117
+ peercert = ssl_socket.peercert
118
+ subject = peercert ? peercert.subject : nil
119
+ @error_logger.info(error: error, text: "SSL error, peer: #{peeraddr}, peer cert: #{subject}")
120
+ end
121
+
122
+ # An unknown error has occurred.
123
+ # +error+ an exception object, +req+ the request,
124
+ # and +text+ additional info
125
+ def unknown_error(error, req=nil, text="Unknown error")
126
+ @error_logger.info(error: error, req: req, text: text)
127
+ end
128
+
129
+ # Log occurred error debug dump.
130
+ # +error+ an exception object, +req+ the request,
131
+ # and +text+ additional info
132
+ # @version 5.0.0
133
+ def debug_error(error, req=nil, text="")
134
+ @error_logger.debug(error: error, req: req, text: text)
135
+ end
136
+ end
137
+ end