puma 3.11.4 → 6.0.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1717 -432
  3. data/LICENSE +23 -20
  4. data/README.md +190 -64
  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/{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 +22 -12
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +47 -22
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +95 -120
  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 -2
  28. data/ext/puma_http11/ext_help.h +1 -1
  29. data/ext/puma_http11/extconf.rb +61 -3
  30. data/ext/puma_http11/http11_parser.c +106 -118
  31. data/ext/puma_http11/http11_parser.h +2 -2
  32. data/ext/puma_http11/http11_parser.java.rl +22 -38
  33. data/ext/puma_http11/http11_parser.rl +6 -4
  34. data/ext/puma_http11/http11_parser_common.rl +6 -6
  35. data/ext/puma_http11/mini_ssl.c +376 -93
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +250 -88
  40. data/ext/puma_http11/puma_http11.c +49 -57
  41. data/lib/puma/app/status.rb +71 -49
  42. data/lib/puma/binder.rb +243 -148
  43. data/lib/puma/cli.rb +50 -36
  44. data/lib/puma/client.rb +373 -233
  45. data/lib/puma/cluster/worker.rb +175 -0
  46. data/lib/puma/cluster/worker_handle.rb +97 -0
  47. data/lib/puma/cluster.rb +268 -235
  48. data/lib/puma/commonlogger.rb +4 -2
  49. data/lib/puma/configuration.rb +116 -88
  50. data/lib/puma/const.rb +49 -30
  51. data/lib/puma/control_cli.rb +123 -76
  52. data/lib/puma/detect.rb +33 -2
  53. data/lib/puma/dsl.rb +685 -135
  54. data/lib/puma/error_logger.rb +112 -0
  55. data/lib/puma/events.rb +17 -111
  56. data/lib/puma/io_buffer.rb +44 -5
  57. data/lib/puma/jruby_restart.rb +4 -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 +196 -130
  61. data/lib/puma/log_writer.rb +137 -0
  62. data/lib/puma/minissl/context_builder.rb +92 -0
  63. data/lib/puma/minissl.rb +249 -69
  64. data/lib/puma/null_io.rb +20 -1
  65. data/lib/puma/plugin/tmp_restart.rb +3 -1
  66. data/lib/puma/plugin.rb +9 -13
  67. data/lib/puma/rack/builder.rb +8 -9
  68. data/lib/puma/rack/urlmap.rb +2 -0
  69. data/lib/puma/rack_default.rb +3 -1
  70. data/lib/puma/reactor.rb +90 -187
  71. data/lib/puma/request.rb +644 -0
  72. data/lib/puma/runner.rb +94 -71
  73. data/lib/puma/server.rb +337 -715
  74. data/lib/puma/single.rb +27 -72
  75. data/lib/puma/state_file.rb +46 -7
  76. data/lib/puma/systemd.rb +47 -0
  77. data/lib/puma/thread_pool.rb +184 -93
  78. data/lib/puma/util.rb +23 -10
  79. data/lib/puma.rb +60 -3
  80. data/lib/rack/handler/puma.rb +17 -15
  81. data/tools/Dockerfile +16 -0
  82. data/tools/trickletest.rb +0 -1
  83. metadata +53 -33
  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 -23
  88. data/lib/puma/daemon_ext.rb +0 -31
  89. data/lib/puma/delegation.rb +0 -11
  90. data/lib/puma/java_io_buffer.rb +0 -45
  91. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  92. data/lib/puma/tcp_logger.rb +0 -39
  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
data/lib/puma/launcher.rb CHANGED
@@ -1,12 +1,12 @@
1
- require 'puma/events'
2
- require 'puma/detect'
1
+ # frozen_string_literal: true
3
2
 
4
- require 'puma/cluster'
5
- require 'puma/single'
6
-
7
- require 'puma/const'
8
-
9
- 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'
10
10
 
11
11
  module Puma
12
12
  # Puma::Launcher is the single entry point for starting a Puma server based on user
@@ -16,17 +16,14 @@ module Puma
16
16
  # It is responsible for either launching a cluster of Puma workers or a single
17
17
  # puma server.
18
18
  class Launcher
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,58 +37,69 @@ 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)
52
- @binder.import_from_env
53
-
54
- @environment = conf.environment
49
+ @config.options[:log_writer] = @log_writer
55
50
 
56
51
  # Advertise the Configuration
57
52
  Puma.cli_config = @config if defined?(Puma.cli_config)
58
53
 
59
54
  @config.load
60
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
+
61
69
  @options = @config.options
62
70
  @config.clamp
63
71
 
64
- generate_restart_data
72
+ @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
73
+ @log_writer.formatter = options[:log_formatter] if @options[:log_formatter]
65
74
 
66
- if clustered? && (Puma.jruby? || Puma.windows?)
67
- unsupported 'worker mode not supported on JRuby or Windows'
68
- end
75
+ generate_restart_data
69
76
 
70
- if @options[:daemon] && Puma.windows?
71
- unsupported 'daemon mode not supported on Windows'
77
+ if clustered? && !Puma.forkable?
78
+ unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
72
79
  end
73
80
 
74
81
  Dir.chdir(@restart_dir)
75
82
 
76
- prune_bundler if prune_bundler?
83
+ prune_bundler!
77
84
 
78
85
  @environment = @options[:environment] if @options[:environment]
79
86
  set_rack_environment
80
87
 
81
88
  if clustered?
82
- @events.formatter = Events::PidFormatter.new
83
- @options[:logger] = @events
89
+ @options[:logger] = @log_writer
84
90
 
85
- @runner = Cluster.new(self, @events)
91
+ @runner = Cluster.new(self)
86
92
  else
87
- @runner = Single.new(self, @events)
93
+ @runner = Single.new(self)
88
94
  end
89
95
  Puma.stats_object = @runner
90
96
 
91
97
  @status = :run
98
+
99
+ log_config if ENV['PUMA_LOG_CONFIG']
92
100
  end
93
101
 
94
- attr_reader :binder, :events, :config, :options, :restart_dir
102
+ attr_reader :binder, :log_writer, :events, :config, :options, :restart_dir
95
103
 
96
104
  # Return stats about the server
97
105
  def stats
@@ -104,16 +112,18 @@ module Puma
104
112
  write_pid
105
113
 
106
114
  path = @options[:state]
115
+ permission = @options[:state_permission]
107
116
  return unless path
108
117
 
109
- require 'puma/state_file'
118
+ require_relative 'state_file'
110
119
 
111
120
  sf = StateFile.new
112
121
  sf.pid = Process.pid
113
122
  sf.control_url = @options[:control_url]
114
123
  sf.control_auth_token = @options[:control_auth_token]
124
+ sf.running_from = File.expand_path('.')
115
125
 
116
- sf.save path
126
+ sf.save path, permission
117
127
  end
118
128
 
119
129
  # Delete the configured pidfile
@@ -122,19 +132,6 @@ module Puma
122
132
  File.unlink(path) if path && File.exist?(path)
123
133
  end
124
134
 
125
- # If configured, write the pid of the current process out
126
- # to a file.
127
- def write_pid
128
- path = @options[:pidfile]
129
- return unless path
130
-
131
- File.open(path, 'w') { |f| f.puts Process.pid }
132
- cur = Process.pid
133
- at_exit do
134
- delete_pidfile if cur == Process.pid
135
- end
136
- end
137
-
138
135
  # Begin async shutdown of the server
139
136
  def halt
140
137
  @status = :halt
@@ -162,18 +159,20 @@ module Puma
162
159
  true
163
160
  end
164
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
+
165
173
  # Run the server. This blocks until the server is stopped
166
174
  def run
167
- previous_env =
168
- if defined?(Bundler)
169
- env = Bundler::ORIGINAL_ENV.dup
170
- # add -rbundler/setup so we load from Gemfile when restarting
171
- bundle = "-rbundler/setup"
172
- env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
173
- env
174
- else
175
- ENV.to_h
176
- end
175
+ previous_env = get_env
177
176
 
178
177
  @config.clamp
179
178
 
@@ -181,28 +180,22 @@ module Puma
181
180
 
182
181
  setup_signals
183
182
  set_process_title
183
+ integrate_with_systemd
184
+
185
+ # This blocks until the server is stopped
184
186
  @runner.run
185
187
 
186
- case @status
187
- when :halt
188
- log "* Stopping immediately!"
189
- when :run, :stop
190
- graceful_stop
191
- when :restart
192
- log "* Restarting..."
193
- ENV.replace(previous_env)
194
- @runner.before_restart
195
- restart!
196
- when :exit
197
- # nothing
198
- end
188
+ do_run_finished(previous_env)
199
189
  end
200
190
 
201
- # Return which tcp port the launcher is using, if it's using TCP
202
- def connected_port
203
- @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
204
196
  end
205
197
 
198
+ # @!attribute [r] restart_args
206
199
  def restart_args
207
200
  cmd = @options[:restart_cmd]
208
201
  if cmd
@@ -212,19 +205,80 @@ module Puma
212
205
  end
213
206
  end
214
207
 
208
+ def close_binder_listeners
209
+ @runner.close_control_listeners
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
228
+ end
229
+
215
230
  private
216
231
 
217
- def reload_worker_directory
218
- @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
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
243
+
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)
252
+ end
253
+
254
+ close_binder_listeners unless @status == :restart
255
+ end
256
+
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!
219
272
  end
220
273
 
221
274
  def restart!
222
- @config.run_hooks :on_restart, self
275
+ @events.fire_on_restart!
276
+ @config.run_hooks :on_restart, self, @log_writer
223
277
 
224
278
  if Puma.jruby?
225
279
  close_binder_listeners
226
280
 
227
- require 'puma/jruby_restart'
281
+ require_relative 'jruby_restart'
228
282
  JRubyRestart.chdir_exec(@restart_dir, restart_args)
229
283
  elsif Puma.windows?
230
284
  close_binder_listeners
@@ -233,50 +287,53 @@ module Puma
233
287
  Dir.chdir(@restart_dir)
234
288
  Kernel.exec(*argv)
235
289
  else
236
- redirects = {:close_others => true}
237
- @binder.listeners.each_with_index do |(l, io), i|
238
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
239
- redirects[io.to_i] = io.to_i
240
- end
241
-
242
290
  argv = restart_args
243
291
  Dir.chdir(@restart_dir)
244
- argv += [redirects] if RUBY_VERSION >= '1.9'
292
+ ENV.update(@binder.redirects_for_restart_env)
293
+ argv += [@binder.redirects_for_restart]
245
294
  Kernel.exec(*argv)
246
295
  end
247
296
  end
248
297
 
249
- def prune_bundler
250
- return unless defined?(Bundler)
251
- puma = Bundler.rubygems.loaded_specs("puma")
252
- dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
253
- puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
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
307
+ end
308
+ end
254
309
 
255
- unless puma_lib_dir
256
- log "! Unable to prune Bundler environment, continuing"
310
+ def reload_worker_directory
311
+ @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
312
+ end
313
+
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"]
320
+
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"
257
325
  return
258
326
  end
259
327
 
260
- deps = puma.runtime_dependencies.map do |d|
261
- spec = Bundler.rubygems.loaded_specs(d.name)
262
- "#{d.name}:#{spec.version.to_s}"
263
- end
328
+ log "* Enabling systemd notification integration"
264
329
 
265
- log '* Pruning Bundler environment'
266
- home = ENV['GEM_HOME']
267
- Bundler.with_clean_env do
268
- ENV['GEM_HOME'] = home
269
- ENV['PUMA_BUNDLER_PRUNED'] = '1'
270
- wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
271
- args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
272
- # Ruby 2.0+ defaults to true which breaks socket activation
273
- args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
274
- Kernel.exec(*args)
275
- end
330
+ systemd = Systemd.new(@log_writer, @events)
331
+ systemd.hook_events
332
+ systemd.start_watchdog
276
333
  end
277
334
 
278
335
  def log(str)
279
- @events.log str
336
+ @log_writer.log(str)
280
337
  end
281
338
 
282
339
  def clustered?
@@ -284,20 +341,15 @@ module Puma
284
341
  end
285
342
 
286
343
  def unsupported(str)
287
- @events.error(str)
344
+ @log_writer.error(str)
288
345
  raise UnsupportedOption
289
346
  end
290
347
 
291
- def graceful_stop
292
- @runner.stop_blocked
293
- log "=== puma shutdown: #{Time.now} ==="
294
- log "- Goodbye!"
295
- end
296
-
297
348
  def set_process_title
298
349
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
299
350
  end
300
351
 
352
+ # @!attribute [r] title
301
353
  def title
302
354
  buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
303
355
  buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
@@ -309,6 +361,7 @@ module Puma
309
361
  ENV['RACK_ENV'] = environment
310
362
  end
311
363
 
364
+ # @!attribute [r] environment
312
365
  def environment
313
366
  @environment
314
367
  end
@@ -317,16 +370,11 @@ module Puma
317
370
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
318
371
  end
319
372
 
320
- def close_binder_listeners
321
- @binder.listeners.each do |l, io|
322
- io.close
323
- uri = URI.parse(l)
324
- next unless uri.scheme == 'unix'
325
- File.unlink("#{uri.host}#{uri.path}")
326
- end
373
+ def prune_bundler!
374
+ return unless prune_bundler?
375
+ BundlePruner.new(@original_argv, @options[:extra_runtime_dependencies], @log_writer).prune
327
376
  end
328
377
 
329
-
330
378
  def generate_restart_data
331
379
  if dir = @options[:directory]
332
380
  @restart_dir = dir
@@ -393,9 +441,10 @@ module Puma
393
441
 
394
442
  begin
395
443
  Signal.trap "SIGTERM" do
396
- graceful_stop
444
+ # Shortcut the control flow in case raise_exception_on_sigterm is true
445
+ do_graceful_stop
397
446
 
398
- raise SignalException, "SIGTERM"
447
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
399
448
  end
400
449
  rescue Exception
401
450
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
@@ -403,12 +452,6 @@ module Puma
403
452
 
404
453
  begin
405
454
  Signal.trap "SIGINT" do
406
- if Puma.jruby?
407
- @status = :exit
408
- graceful_stop
409
- exit
410
- end
411
-
412
455
  stop
413
456
  end
414
457
  rescue Exception
@@ -426,6 +469,29 @@ module Puma
426
469
  rescue Exception
427
470
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
428
471
  end
472
+
473
+ begin
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
481
+ end
482
+ rescue Exception
483
+ # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
484
+ # to see this constantly on Linux.
485
+ end
486
+ end
487
+
488
+ def log_config
489
+ log "Configuration:"
490
+
491
+ @config.final_options
492
+ .each { |config_key, value| log "- #{config_key}: #{value}" }
493
+
494
+ log "\n"
429
495
  end
430
496
  end
431
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