puma 4.3.6 → 5.3.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1346 -518
  3. data/LICENSE +23 -20
  4. data/README.md +74 -31
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +24 -20
  7. data/docs/compile_options.md +19 -0
  8. data/docs/deployment.md +15 -10
  9. data/docs/fork_worker.md +33 -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 +1 -1
  16. data/docs/plugins.md +2 -2
  17. data/docs/rails_dev_mode.md +29 -0
  18. data/docs/restart.md +46 -23
  19. data/docs/signals.md +7 -6
  20. data/docs/stats.md +142 -0
  21. data/docs/systemd.md +27 -67
  22. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  23. data/ext/puma_http11/ext_help.h +1 -1
  24. data/ext/puma_http11/extconf.rb +22 -8
  25. data/ext/puma_http11/http11_parser.c +45 -47
  26. data/ext/puma_http11/http11_parser.h +1 -1
  27. data/ext/puma_http11/http11_parser.java.rl +1 -1
  28. data/ext/puma_http11/http11_parser.rl +1 -1
  29. data/ext/puma_http11/mini_ssl.c +211 -118
  30. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  31. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  32. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +5 -7
  33. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  34. data/ext/puma_http11/puma_http11.c +31 -50
  35. data/lib/puma.rb +46 -0
  36. data/lib/puma/app/status.rb +47 -36
  37. data/lib/puma/binder.rb +177 -103
  38. data/lib/puma/cli.rb +11 -15
  39. data/lib/puma/client.rb +73 -74
  40. data/lib/puma/cluster.rb +184 -198
  41. data/lib/puma/cluster/worker.rb +183 -0
  42. data/lib/puma/cluster/worker_handle.rb +90 -0
  43. data/lib/puma/commonlogger.rb +2 -2
  44. data/lib/puma/configuration.rb +55 -49
  45. data/lib/puma/const.rb +13 -5
  46. data/lib/puma/control_cli.rb +93 -76
  47. data/lib/puma/detect.rb +24 -3
  48. data/lib/puma/dsl.rb +266 -92
  49. data/lib/puma/error_logger.rb +104 -0
  50. data/lib/puma/events.rb +55 -34
  51. data/lib/puma/io_buffer.rb +9 -2
  52. data/lib/puma/jruby_restart.rb +0 -58
  53. data/lib/puma/json.rb +96 -0
  54. data/lib/puma/launcher.rb +113 -45
  55. data/lib/puma/minissl.rb +114 -33
  56. data/lib/puma/minissl/context_builder.rb +6 -3
  57. data/lib/puma/null_io.rb +13 -1
  58. data/lib/puma/plugin.rb +1 -10
  59. data/lib/puma/queue_close.rb +26 -0
  60. data/lib/puma/rack/builder.rb +0 -4
  61. data/lib/puma/reactor.rb +85 -369
  62. data/lib/puma/request.rb +467 -0
  63. data/lib/puma/runner.rb +29 -58
  64. data/lib/puma/server.rb +267 -729
  65. data/lib/puma/single.rb +9 -65
  66. data/lib/puma/state_file.rb +8 -3
  67. data/lib/puma/systemd.rb +46 -0
  68. data/lib/puma/thread_pool.rb +119 -53
  69. data/lib/puma/util.rb +12 -0
  70. data/lib/rack/handler/puma.rb +2 -3
  71. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  72. metadata +25 -21
  73. data/docs/tcp_mode.md +0 -96
  74. data/ext/puma_http11/io_buffer.c +0 -155
  75. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  76. data/lib/puma/accept_nonblock.rb +0 -29
  77. data/lib/puma/tcp_logger.rb +0 -41
  78. data/tools/jungle/README.md +0 -19
  79. data/tools/jungle/init.d/README.md +0 -61
  80. data/tools/jungle/init.d/puma +0 -421
  81. data/tools/jungle/init.d/run-puma +0 -18
  82. data/tools/jungle/upstart/README.md +0 -61
  83. data/tools/jungle/upstart/puma-manager.conf +0 -31
  84. data/tools/jungle/upstart/puma.conf +0 -69
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puma/const'
4
+
5
+ module Puma
6
+ # The implementation of a detailed error logging.
7
+ # @version 5.0.0
8
+ #
9
+ class ErrorLogger
10
+ include Const
11
+
12
+ attr_reader :ioerr
13
+
14
+ REQUEST_FORMAT = %{"%s %s%s" - (%s)}
15
+
16
+ def initialize(ioerr)
17
+ @ioerr = ioerr
18
+
19
+ @debug = ENV.key? 'PUMA_DEBUG'
20
+ end
21
+
22
+ def self.stdio
23
+ new $stderr
24
+ end
25
+
26
+ # Print occurred error details.
27
+ # +options+ hash with additional options:
28
+ # - +error+ is an exception object
29
+ # - +req+ the http request
30
+ # - +text+ (default nil) custom string to print in title
31
+ # and before all remaining info.
32
+ #
33
+ def info(options={})
34
+ log title(options)
35
+ end
36
+
37
+ # Print occurred error details only if
38
+ # environment variable PUMA_DEBUG is defined.
39
+ # +options+ hash with additional options:
40
+ # - +error+ is an exception object
41
+ # - +req+ the http request
42
+ # - +text+ (default nil) custom string to print in title
43
+ # and before all remaining info.
44
+ #
45
+ def debug(options={})
46
+ return unless @debug
47
+
48
+ error = options[:error]
49
+ req = options[:req]
50
+
51
+ string_block = []
52
+ string_block << title(options)
53
+ string_block << request_dump(req) if request_parsed?(req)
54
+ string_block << error.backtrace if error
55
+
56
+ log string_block.join("\n")
57
+ end
58
+
59
+ def title(options={})
60
+ text = options[:text]
61
+ req = options[:req]
62
+ error = options[:error]
63
+
64
+ string_block = ["#{Time.now}"]
65
+ string_block << " #{text}" if text
66
+ string_block << " (#{request_title(req)})" if request_parsed?(req)
67
+ string_block << ": #{error.inspect}" if error
68
+ string_block.join('')
69
+ end
70
+
71
+ def request_dump(req)
72
+ "Headers: #{request_headers(req)}\n" \
73
+ "Body: #{req.body}"
74
+ end
75
+
76
+ def request_title(req)
77
+ env = req.env
78
+
79
+ REQUEST_FORMAT % [
80
+ env[REQUEST_METHOD],
81
+ env[REQUEST_PATH] || env[PATH_INFO],
82
+ env[QUERY_STRING] || "",
83
+ env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR] || "-"
84
+ ]
85
+ end
86
+
87
+ def request_headers(req)
88
+ headers = req.env.select { |key, _| key.start_with?('HTTP_') }
89
+ headers.map { |key, value| [key[5..-1], value] }.to_h.inspect
90
+ end
91
+
92
+ def request_parsed?(req)
93
+ req && req.env[REQUEST_METHOD]
94
+ end
95
+
96
+ private
97
+
98
+ def log(str)
99
+ ioerr.puts str
100
+
101
+ ioerr.flush unless ioerr.sync
102
+ end
103
+ end
104
+ end
data/lib/puma/events.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/const'
4
3
  require "puma/null_io"
4
+ require 'puma/error_logger'
5
5
  require 'stringio'
6
6
 
7
7
  module Puma
@@ -23,8 +23,6 @@ module Puma
23
23
  end
24
24
  end
25
25
 
26
- include Const
27
-
28
26
  # Create an Events object that prints to +stdout+ and +stderr+.
29
27
  #
30
28
  def initialize(stdout, stderr)
@@ -32,10 +30,8 @@ module Puma
32
30
  @stdout = stdout
33
31
  @stderr = stderr
34
32
 
35
- @stdout.sync = true
36
- @stderr.sync = true
37
-
38
33
  @debug = ENV.key? 'PUMA_DEBUG'
34
+ @error_logger = ErrorLogger.new(@stderr)
39
35
 
40
36
  @hooks = Hash.new { |h,k| h[k] = [] }
41
37
  end
@@ -66,7 +62,10 @@ module Puma
66
62
  # Write +str+ to +@stdout+
67
63
  #
68
64
  def log(str)
69
- @stdout.puts format(str)
65
+ @stdout.puts format(str) if @stdout.respond_to? :puts
66
+
67
+ @stdout.flush unless @stdout.sync
68
+ rescue Errno::EPIPE
70
69
  end
71
70
 
72
71
  def write(str)
@@ -80,7 +79,7 @@ module Puma
80
79
  # Write +str+ to +@stderr+
81
80
  #
82
81
  def error(str)
83
- @stderr.puts format("ERROR: #{str}")
82
+ @error_logger.info(text: format("ERROR: #{str}"))
84
83
  exit 1
85
84
  end
86
85
 
@@ -88,53 +87,75 @@ module Puma
88
87
  formatter.call(str)
89
88
  end
90
89
 
90
+ # An HTTP connection error has occurred.
91
+ # +error+ a connection exception, +req+ the request,
92
+ # and +text+ additional info
93
+ # @version 5.0.0
94
+ #
95
+ def connection_error(error, req, text="HTTP connection error")
96
+ @error_logger.info(error: error, req: req, text: text)
97
+ end
98
+
91
99
  # An HTTP parse error has occurred.
92
- # +server+ is the Server object, +env+ the request, and +error+ a
93
- # parsing exception.
100
+ # +error+ a parsing exception,
101
+ # and +req+ the request.
94
102
  #
95
- def parse_error(server, env, error)
96
- @stderr.puts "#{Time.now}: HTTP parse error, malformed request " \
97
- "(#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}#{env[REQUEST_PATH]}): " \
98
- "#{error.inspect}" \
99
- "\n---\n"
103
+ def parse_error(error, req)
104
+ @error_logger.info(error: error, req: req, text: 'HTTP parse error, malformed request')
100
105
  end
101
106
 
102
107
  # An SSL error has occurred.
103
- # +server+ is the Server object, +peeraddr+ peer address, +peercert+
104
- # any peer certificate (if present), and +error+ an exception object.
108
+ # @param error <Puma::MiniSSL::SSLError>
109
+ # @param ssl_socket <Puma::MiniSSL::Socket>
105
110
  #
106
- def ssl_error(server, peeraddr, peercert, error)
111
+ def ssl_error(error, ssl_socket)
112
+ peeraddr = ssl_socket.peeraddr.last rescue "<unknown>"
113
+ peercert = ssl_socket.peercert
107
114
  subject = peercert ? peercert.subject : nil
108
- @stderr.puts "#{Time.now}: SSL error, peer: #{peeraddr}, peer cert: #{subject}, #{error.inspect}"
115
+ @error_logger.info(error: error, text: "SSL error, peer: #{peeraddr}, peer cert: #{subject}")
109
116
  end
110
117
 
111
118
  # An unknown error has occurred.
112
- # +server+ is the Server object, +error+ an exception object,
113
- # +kind+ some additional info, and +env+ the request.
119
+ # +error+ an exception object, +req+ the request,
120
+ # and +text+ additional info
114
121
  #
115
- def unknown_error(server, error, kind="Unknown", env=nil)
116
- if error.respond_to? :render
117
- error.render "#{Time.now}: #{kind} error", @stderr
118
- else
119
- if env
120
- string_block = [ "#{Time.now}: #{kind} error handling request { #{env['REQUEST_METHOD']} #{env['PATH_INFO']} }" ]
121
- string_block << error.inspect
122
- else
123
- string_block = [ "#{Time.now}: #{kind} error: #{error.inspect}" ]
124
- end
125
- string_block << error.backtrace
126
- @stderr.puts string_block.join("\n")
127
- end
122
+ def unknown_error(error, req=nil, text="Unknown error")
123
+ @error_logger.info(error: error, req: req, text: text)
124
+ end
125
+
126
+ # Log occurred error debug dump.
127
+ # +error+ an exception object, +req+ the request,
128
+ # and +text+ additional info
129
+ # @version 5.0.0
130
+ #
131
+ def debug_error(error, req=nil, text="")
132
+ @error_logger.debug(error: error, req: req, text: text)
128
133
  end
129
134
 
130
135
  def on_booted(&block)
131
136
  register(:on_booted, &block)
132
137
  end
133
138
 
139
+ def on_restart(&block)
140
+ register(:on_restart, &block)
141
+ end
142
+
143
+ def on_stopped(&block)
144
+ register(:on_stopped, &block)
145
+ end
146
+
134
147
  def fire_on_booted!
135
148
  fire(:on_booted)
136
149
  end
137
150
 
151
+ def fire_on_restart!
152
+ fire(:on_restart)
153
+ end
154
+
155
+ def fire_on_stopped!
156
+ fire(:on_stopped)
157
+ end
158
+
138
159
  DEFAULT = new(STDOUT, STDERR)
139
160
 
140
161
  # Returns an Events object which writes its status to 2 StringIO
@@ -1,4 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/detect'
4
- require 'puma/puma_http11'
3
+ module Puma
4
+ class IOBuffer < String
5
+ def append(*args)
6
+ args.each { |a| concat(a) }
7
+ end
8
+
9
+ alias reset clear
10
+ end
11
+ end
@@ -22,63 +22,5 @@ module Puma
22
22
  execlp(cmd, *argv)
23
23
  raise SystemCallError.new(FFI.errno)
24
24
  end
25
-
26
- PermKey = 'PUMA_DAEMON_PERM'
27
- RestartKey = 'PUMA_DAEMON_RESTART'
28
-
29
- # Called to tell things "Your now always in daemon mode,
30
- # don't try to reenter it."
31
- #
32
- def self.perm_daemonize
33
- ENV[PermKey] = "1"
34
- end
35
-
36
- def self.daemon?
37
- ENV.key?(PermKey) || ENV.key?(RestartKey)
38
- end
39
-
40
- def self.daemon_init
41
- return true if ENV.key?(PermKey)
42
-
43
- return false unless ENV.key? RestartKey
44
-
45
- master = ENV[RestartKey]
46
-
47
- # In case the master disappears early
48
- begin
49
- Process.kill "SIGUSR2", master.to_i
50
- rescue SystemCallError => e
51
- end
52
-
53
- ENV[RestartKey] = ""
54
-
55
- setsid
56
-
57
- null = File.open "/dev/null", "w+"
58
- STDIN.reopen null
59
- STDOUT.reopen null
60
- STDERR.reopen null
61
-
62
- true
63
- end
64
-
65
- def self.daemon_start(dir, argv)
66
- ENV[RestartKey] = Process.pid.to_s
67
-
68
- if k = ENV['PUMA_JRUBY_DAEMON_OPTS']
69
- ENV['JRUBY_OPTS'] = k
70
- end
71
-
72
- cmd = argv.first
73
- argv = ([:string] * argv.size).zip(argv).flatten
74
- argv << :string
75
- argv << nil
76
-
77
- chdir(dir)
78
- ret = fork
79
- return ret if ret != 0
80
- execlp(cmd, *argv)
81
- raise SystemCallError.new(FFI.errno)
82
- end
83
25
  end
84
26
  end
data/lib/puma/json.rb ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+ require 'stringio'
3
+
4
+ module Puma
5
+
6
+ # Puma deliberately avoids the use of the json gem and instead performs JSON
7
+ # serialization without any external dependencies. In a puma cluster, loading
8
+ # any gem into the puma master process means that operators cannot use a
9
+ # phased restart to upgrade their application if the new version of that
10
+ # application uses a different version of that gem. The json gem in
11
+ # particular is additionally problematic because it leverages native
12
+ # extensions. If the puma master process relies on a gem with native
13
+ # extensions and operators remove gems from disk related to old releases,
14
+ # subsequent phased restarts can fail.
15
+ #
16
+ # The implementation of JSON serialization in this module is not designed to
17
+ # be particularly full-featured or fast. It just has to handle the few places
18
+ # where Puma relies on JSON serialization internally.
19
+
20
+ module JSON
21
+ QUOTE = /"/
22
+ BACKSLASH = /\\/
23
+ CONTROL_CHAR_TO_ESCAPE = /[\x00-\x1F]/ # As required by ECMA-404
24
+ CHAR_TO_ESCAPE = Regexp.union QUOTE, BACKSLASH, CONTROL_CHAR_TO_ESCAPE
25
+
26
+ class SerializationError < StandardError; end
27
+
28
+ class << self
29
+ def generate(value)
30
+ StringIO.open do |io|
31
+ serialize_value io, value
32
+ io.string
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def serialize_value(output, value)
39
+ case value
40
+ when Hash
41
+ output << '{'
42
+ value.each_with_index do |(k, v), index|
43
+ output << ',' if index != 0
44
+ serialize_object_key output, k
45
+ output << ':'
46
+ serialize_value output, v
47
+ end
48
+ output << '}'
49
+ when Array
50
+ output << '['
51
+ value.each_with_index do |member, index|
52
+ output << ',' if index != 0
53
+ serialize_value output, member
54
+ end
55
+ output << ']'
56
+ when Integer, Float
57
+ output << value.to_s
58
+ when String
59
+ serialize_string output, value
60
+ when true
61
+ output << 'true'
62
+ when false
63
+ output << 'false'
64
+ when nil
65
+ output << 'null'
66
+ else
67
+ raise SerializationError, "Unexpected value of type #{value.class}"
68
+ end
69
+ end
70
+
71
+ def serialize_string(output, value)
72
+ output << '"'
73
+ output << value.gsub(CHAR_TO_ESCAPE) do |character|
74
+ case character
75
+ when BACKSLASH
76
+ '\\\\'
77
+ when QUOTE
78
+ '\\"'
79
+ when CONTROL_CHAR_TO_ESCAPE
80
+ '\u%.4X' % character.ord
81
+ end
82
+ end
83
+ output << '"'
84
+ end
85
+
86
+ def serialize_object_key(output, value)
87
+ case value
88
+ when Symbol, String
89
+ serialize_string output, value.to_s
90
+ else
91
+ raise SerializationError, "Could not serialize object of type #{value.class} as object key"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
data/lib/puma/launcher.rb CHANGED
@@ -47,8 +47,9 @@ module Puma
47
47
  @original_argv = @argv.dup
48
48
  @config = conf
49
49
 
50
- @binder = Binder.new(@events)
51
- @binder.import_from_env
50
+ @binder = Binder.new(@events, conf)
51
+ @binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
52
+ @binder.create_activated_fds(ENV).each { |k| ENV.delete k }
52
53
 
53
54
  @environment = conf.environment
54
55
 
@@ -57,6 +58,13 @@ module Puma
57
58
 
58
59
  @config.load
59
60
 
61
+ if @config.options[:bind_to_activated_sockets]
62
+ @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
63
+ @config.options[:binds],
64
+ @config.options[:bind_to_activated_sockets] == 'only'
65
+ )
66
+ end
67
+
60
68
  @options = @config.options
61
69
  @config.clamp
62
70
 
@@ -69,10 +77,6 @@ module Puma
69
77
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
70
78
  end
71
79
 
72
- if @options[:daemon] && Puma.windows?
73
- unsupported 'daemon mode not supported on Windows'
74
- end
75
-
76
80
  Dir.chdir(@restart_dir)
77
81
 
78
82
  prune_bundler if prune_bundler?
@@ -90,6 +94,8 @@ module Puma
90
94
  Puma.stats_object = @runner
91
95
 
92
96
  @status = :run
97
+
98
+ log_config if ENV['PUMA_LOG_CONFIG']
93
99
  end
94
100
 
95
101
  attr_reader :binder, :events, :config, :options, :restart_dir
@@ -105,6 +111,7 @@ module Puma
105
111
  write_pid
106
112
 
107
113
  path = @options[:state]
114
+ permission = @options[:state_permission]
108
115
  return unless path
109
116
 
110
117
  require 'puma/state_file'
@@ -113,8 +120,9 @@ module Puma
113
120
  sf.pid = Process.pid
114
121
  sf.control_url = @options[:control_url]
115
122
  sf.control_auth_token = @options[:control_auth_token]
123
+ sf.running_from = File.expand_path('.')
116
124
 
117
- sf.save path
125
+ sf.save path, permission
118
126
  end
119
127
 
120
128
  # Delete the configured pidfile
@@ -169,29 +177,34 @@ module Puma
169
177
 
170
178
  setup_signals
171
179
  set_process_title
180
+ integrate_with_systemd
172
181
  @runner.run
173
182
 
174
183
  case @status
175
184
  when :halt
176
185
  log "* Stopping immediately!"
186
+ @runner.stop_control
177
187
  when :run, :stop
178
188
  graceful_stop
179
189
  when :restart
180
190
  log "* Restarting..."
181
191
  ENV.replace(previous_env)
182
- @runner.before_restart
192
+ @runner.stop_control
183
193
  restart!
184
194
  when :exit
185
195
  # nothing
186
196
  end
187
- @binder.close_unix_paths
197
+ close_binder_listeners unless @status == :restart
188
198
  end
189
199
 
190
- # Return which tcp port the launcher is using, if it's using TCP
191
- def connected_port
192
- @binder.connected_port
200
+ # Return all tcp ports the launcher may be using, TCP or SSL
201
+ # @!attribute [r] connected_ports
202
+ # @version 5.0.0
203
+ def connected_ports
204
+ @binder.connected_ports
193
205
  end
194
206
 
207
+ # @!attribute [r] restart_args
195
208
  def restart_args
196
209
  cmd = @options[:restart_cmd]
197
210
  if cmd
@@ -202,7 +215,25 @@ module Puma
202
215
  end
203
216
 
204
217
  def close_binder_listeners
218
+ @runner.close_control_listeners
205
219
  @binder.close_listeners
220
+ unless @status == :restart
221
+ log "=== puma shutdown: #{Time.now} ==="
222
+ log "- Goodbye!"
223
+ end
224
+ end
225
+
226
+ # @!attribute [r] thread_status
227
+ # @version 5.0.0
228
+ def thread_status
229
+ Thread.list.each do |thread|
230
+ name = "Thread: TID-#{thread.object_id.to_s(36)}"
231
+ name += " #{thread['label']}" if thread['label']
232
+ name += " #{thread.name}" if thread.respond_to?(:name) && thread.name
233
+ backtrace = thread.backtrace || ["<no backtrace available>"]
234
+
235
+ yield name, backtrace
236
+ end
206
237
  end
207
238
 
208
239
  private
@@ -212,11 +243,10 @@ module Puma
212
243
  def write_pid
213
244
  path = @options[:pidfile]
214
245
  return unless path
215
-
216
- File.open(path, 'w') { |f| f.puts Process.pid }
217
- cur = Process.pid
246
+ cur_pid = Process.pid
247
+ File.write path, cur_pid, mode: 'wb:UTF-8'
218
248
  at_exit do
219
- delete_pidfile if cur == Process.pid
249
+ delete_pidfile if cur_pid == Process.pid
220
250
  end
221
251
  end
222
252
 
@@ -225,7 +255,8 @@ module Puma
225
255
  end
226
256
 
227
257
  def restart!
228
- @config.run_hooks :on_restart, self
258
+ @events.fire_on_restart!
259
+ @config.run_hooks :on_restart, self, @events
229
260
 
230
261
  if Puma.jruby?
231
262
  close_binder_listeners
@@ -241,21 +272,20 @@ module Puma
241
272
  else
242
273
  argv = restart_args
243
274
  Dir.chdir(@restart_dir)
275
+ ENV.update(@binder.redirects_for_restart_env)
244
276
  argv += [@binder.redirects_for_restart]
245
277
  Kernel.exec(*argv)
246
278
  end
247
279
  end
248
280
 
249
- def dependencies_and_files_to_require_after_prune
281
+ # @!attribute [r] files_to_require_after_prune
282
+ def files_to_require_after_prune
250
283
  puma = spec_for_gem("puma")
251
284
 
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]
285
+ require_paths_for_gem(puma) + extra_runtime_deps_directories
257
286
  end
258
287
 
288
+ # @!attribute [r] extra_runtime_deps_directories
259
289
  def extra_runtime_deps_directories
260
290
  Array(@options[:extra_runtime_dependencies]).map do |d_name|
261
291
  if (spec = spec_for_gem(d_name))
@@ -267,6 +297,7 @@ module Puma
267
297
  end.flatten.compact
268
298
  end
269
299
 
300
+ # @!attribute [r] puma_wild_location
270
301
  def puma_wild_location
271
302
  puma = spec_for_gem("puma")
272
303
  dirs = require_paths_for_gem(puma)
@@ -275,6 +306,7 @@ module Puma
275
306
  end
276
307
 
277
308
  def prune_bundler
309
+ return if ENV['PUMA_BUNDLER_PRUNED']
278
310
  return unless defined?(Bundler)
279
311
  require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
280
312
  unless puma_wild_location
@@ -282,20 +314,46 @@ module Puma
282
314
  return
283
315
  end
284
316
 
285
- deps, dirs = dependencies_and_files_to_require_after_prune
317
+ dirs = files_to_require_after_prune
286
318
 
287
319
  log '* Pruning Bundler environment'
288
320
  home = ENV['GEM_HOME']
289
- Bundler.with_clean_env do
321
+ bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
322
+ with_unbundled_env do
290
323
  ENV['GEM_HOME'] = home
324
+ ENV['BUNDLE_GEMFILE'] = bundle_gemfile
291
325
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
292
- args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':'), deps.join(',')] + @original_argv
326
+ args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':')] + @original_argv
293
327
  # Ruby 2.0+ defaults to true which breaks socket activation
294
328
  args += [{:close_others => false}]
295
329
  Kernel.exec(*args)
296
330
  end
297
331
  end
298
332
 
333
+ #
334
+ # Puma's systemd integration allows Puma to inform systemd:
335
+ # 1. when it has successfully started
336
+ # 2. when it is starting shutdown
337
+ # 3. periodically for a liveness check with a watchdog thread
338
+ #
339
+
340
+ def integrate_with_systemd
341
+ return unless ENV["NOTIFY_SOCKET"]
342
+
343
+ begin
344
+ require 'puma/systemd'
345
+ rescue LoadError
346
+ log "Systemd integration failed. It looks like you're trying to use systemd notify but don't have sd_notify gem installed"
347
+ return
348
+ end
349
+
350
+ log "* Enabling systemd notification integration"
351
+
352
+ systemd = Systemd.new(@events)
353
+ systemd.hook_events
354
+ systemd.start_watchdog
355
+ end
356
+
299
357
  def spec_for_gem(gem_name)
300
358
  Bundler.rubygems.loaded_specs(gem_name)
301
359
  end
@@ -318,30 +376,15 @@ module Puma
318
376
  end
319
377
 
320
378
  def graceful_stop
379
+ @events.fire_on_stopped!
321
380
  @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
381
  end
340
382
 
341
383
  def set_process_title
342
384
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
343
385
  end
344
386
 
387
+ # @!attribute [r] title
345
388
  def title
346
389
  buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
347
390
  buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
@@ -353,6 +396,7 @@ module Puma
353
396
  ENV['RACK_ENV'] = environment
354
397
  end
355
398
 
399
+ # @!attribute [r] environment
356
400
  def environment
357
401
  @environment
358
402
  end
@@ -456,8 +500,13 @@ module Puma
456
500
  end
457
501
 
458
502
  begin
459
- Signal.trap "SIGINFO" do
460
- log_thread_status
503
+ unless Puma.jruby? # INFO in use by JVM already
504
+ Signal.trap "SIGINFO" do
505
+ thread_status do |name, backtrace|
506
+ @events.log name
507
+ @events.log backtrace.map { |bt| " #{bt}" }
508
+ end
509
+ end
461
510
  end
462
511
  rescue Exception
463
512
  # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
@@ -471,5 +520,24 @@ module Puma
471
520
  raise "#{feature} is not supported on your version of RubyGems. " \
472
521
  "You must have RubyGems #{min_version}+ to use this feature."
473
522
  end
523
+
524
+ # @version 5.0.0
525
+ def with_unbundled_env
526
+ bundler_ver = Gem::Version.new(Bundler::VERSION)
527
+ if bundler_ver < Gem::Version.new('2.1.0')
528
+ Bundler.with_clean_env { yield }
529
+ else
530
+ Bundler.with_unbundled_env { yield }
531
+ end
532
+ end
533
+
534
+ def log_config
535
+ log "Configuration:"
536
+
537
+ @config.final_options
538
+ .each { |config_key, value| log "- #{config_key}: #{value}" }
539
+
540
+ log "\n"
541
+ end
474
542
  end
475
543
  end