puma 3.12.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1400 -451
  3. data/LICENSE +23 -20
  4. data/README.md +131 -60
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +24 -19
  7. data/docs/compile_options.md +19 -0
  8. data/docs/deployment.md +38 -13
  9. data/docs/fork_worker.md +33 -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 +1 -1
  19. data/docs/plugins.md +20 -10
  20. data/docs/rails_dev_mode.md +29 -0
  21. data/docs/restart.md +47 -22
  22. data/docs/signals.md +7 -6
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +48 -70
  25. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  26. data/ext/puma_http11/ext_help.h +1 -1
  27. data/ext/puma_http11/extconf.rb +27 -0
  28. data/ext/puma_http11/http11_parser.c +81 -108
  29. data/ext/puma_http11/http11_parser.h +1 -1
  30. data/ext/puma_http11/http11_parser.java.rl +22 -38
  31. data/ext/puma_http11/http11_parser.rl +1 -1
  32. data/ext/puma_http11/http11_parser_common.rl +3 -3
  33. data/ext/puma_http11/mini_ssl.c +254 -91
  34. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  35. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  36. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +89 -106
  37. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +92 -22
  38. data/ext/puma_http11/puma_http11.c +34 -50
  39. data/lib/puma.rb +54 -0
  40. data/lib/puma/app/status.rb +68 -49
  41. data/lib/puma/binder.rb +191 -139
  42. data/lib/puma/cli.rb +15 -15
  43. data/lib/puma/client.rb +247 -226
  44. data/lib/puma/cluster.rb +221 -212
  45. data/lib/puma/cluster/worker.rb +183 -0
  46. data/lib/puma/cluster/worker_handle.rb +90 -0
  47. data/lib/puma/commonlogger.rb +2 -2
  48. data/lib/puma/configuration.rb +58 -51
  49. data/lib/puma/const.rb +32 -20
  50. data/lib/puma/control_cli.rb +109 -67
  51. data/lib/puma/detect.rb +24 -3
  52. data/lib/puma/dsl.rb +519 -121
  53. data/lib/puma/error_logger.rb +104 -0
  54. data/lib/puma/events.rb +55 -31
  55. data/lib/puma/io_buffer.rb +7 -5
  56. data/lib/puma/jruby_restart.rb +0 -58
  57. data/lib/puma/json.rb +96 -0
  58. data/lib/puma/launcher.rb +178 -68
  59. data/lib/puma/minissl.rb +147 -48
  60. data/lib/puma/minissl/context_builder.rb +79 -0
  61. data/lib/puma/null_io.rb +13 -1
  62. data/lib/puma/plugin.rb +6 -12
  63. data/lib/puma/plugin/tmp_restart.rb +2 -0
  64. data/lib/puma/queue_close.rb +26 -0
  65. data/lib/puma/rack/builder.rb +2 -4
  66. data/lib/puma/rack/urlmap.rb +2 -0
  67. data/lib/puma/rack_default.rb +2 -0
  68. data/lib/puma/reactor.rb +85 -316
  69. data/lib/puma/request.rb +467 -0
  70. data/lib/puma/runner.rb +31 -52
  71. data/lib/puma/server.rb +275 -726
  72. data/lib/puma/single.rb +11 -67
  73. data/lib/puma/state_file.rb +8 -3
  74. data/lib/puma/systemd.rb +46 -0
  75. data/lib/puma/thread_pool.rb +129 -81
  76. data/lib/puma/util.rb +13 -6
  77. data/lib/rack/handler/puma.rb +5 -6
  78. data/tools/Dockerfile +16 -0
  79. data/tools/trickletest.rb +0 -1
  80. metadata +45 -28
  81. data/ext/puma_http11/io_buffer.c +0 -155
  82. data/lib/puma/accept_nonblock.rb +0 -23
  83. data/lib/puma/compat.rb +0 -14
  84. data/lib/puma/convenient.rb +0 -25
  85. data/lib/puma/daemon_ext.rb +0 -33
  86. data/lib/puma/delegation.rb +0 -13
  87. data/lib/puma/java_io_buffer.rb +0 -47
  88. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  89. data/lib/puma/tcp_logger.rb +0 -41
  90. data/tools/jungle/README.md +0 -19
  91. data/tools/jungle/init.d/README.md +0 -61
  92. data/tools/jungle/init.d/puma +0 -421
  93. data/tools/jungle/init.d/run-puma +0 -18
  94. data/tools/jungle/upstart/README.md +0 -61
  95. data/tools/jungle/upstart/puma-manager.conf +0 -31
  96. data/tools/jungle/upstart/puma.conf +0 -69
@@ -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,50 +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 (#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}): #{error.inspect}\n---\n"
103
+ def parse_error(error, req)
104
+ @error_logger.info(error: error, req: req, text: 'HTTP parse error, malformed request')
97
105
  end
98
106
 
99
107
  # An SSL error has occurred.
100
- # +server+ is the Server object, +peeraddr+ peer address, +peercert+
101
- # any peer certificate (if present), and +error+ an exception object.
108
+ # @param error <Puma::MiniSSL::SSLError>
109
+ # @param ssl_socket <Puma::MiniSSL::Socket>
102
110
  #
103
- 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
104
114
  subject = peercert ? peercert.subject : nil
105
- @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}")
106
116
  end
107
117
 
108
118
  # An unknown error has occurred.
109
- # +server+ is the Server object, +error+ an exception object,
110
- # +kind+ some additional info, and +env+ the request.
119
+ # +error+ an exception object, +req+ the request,
120
+ # and +text+ additional info
111
121
  #
112
- def unknown_error(server, error, kind="Unknown", env=nil)
113
- if error.respond_to? :render
114
- error.render "#{Time.now}: #{kind} error", @stderr
115
- else
116
- if env
117
- string_block = [ "#{Time.now}: #{kind} error handling request { #{env['REQUEST_METHOD']} #{env['PATH_INFO']} }" ]
118
- string_block << error.inspect
119
- else
120
- string_block = [ "#{Time.now}: #{kind} error: #{error.inspect}" ]
121
- end
122
- string_block << error.backtrace
123
- @stderr.puts string_block.join("\n")
124
- 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)
125
133
  end
126
134
 
127
135
  def on_booted(&block)
128
136
  register(:on_booted, &block)
129
137
  end
130
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
+
131
147
  def fire_on_booted!
132
148
  fire(:on_booted)
133
149
  end
134
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
+
135
159
  DEFAULT = new(STDOUT, STDERR)
136
160
 
137
161
  # Returns an Events object which writes its status to 2 StringIO
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/detect'
3
+ module Puma
4
+ class IOBuffer < String
5
+ def append(*args)
6
+ args.each { |a| concat(a) }
7
+ end
4
8
 
5
- if Puma.jruby?
6
- require 'puma/java_io_buffer'
7
- else
8
- require 'puma/puma_http11'
9
+ alias reset clear
10
+ end
9
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
@@ -2,12 +2,9 @@
2
2
 
3
3
  require 'puma/events'
4
4
  require 'puma/detect'
5
-
6
5
  require 'puma/cluster'
7
6
  require 'puma/single'
8
-
9
7
  require 'puma/const'
10
-
11
8
  require 'puma/binder'
12
9
 
13
10
  module Puma
@@ -50,8 +47,9 @@ module Puma
50
47
  @original_argv = @argv.dup
51
48
  @config = conf
52
49
 
53
- @binder = Binder.new(@events)
54
- @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 }
55
53
 
56
54
  @environment = conf.environment
57
55
 
@@ -60,19 +58,25 @@ module Puma
60
58
 
61
59
  @config.load
62
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
+
63
68
  @options = @config.options
64
69
  @config.clamp
65
70
 
71
+ @events.formatter = Events::PidFormatter.new if clustered?
72
+ @events.formatter = options[:log_formatter] if @options[:log_formatter]
73
+
66
74
  generate_restart_data
67
75
 
68
76
  if clustered? && !Process.respond_to?(:fork)
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?
@@ -81,7 +85,6 @@ module Puma
81
85
  set_rack_environment
82
86
 
83
87
  if clustered?
84
- @events.formatter = Events::PidFormatter.new
85
88
  @options[:logger] = @events
86
89
 
87
90
  @runner = Cluster.new(self, @events)
@@ -91,6 +94,8 @@ module Puma
91
94
  Puma.stats_object = @runner
92
95
 
93
96
  @status = :run
97
+
98
+ log_config if ENV['PUMA_LOG_CONFIG']
94
99
  end
95
100
 
96
101
  attr_reader :binder, :events, :config, :options, :restart_dir
@@ -106,6 +111,7 @@ module Puma
106
111
  write_pid
107
112
 
108
113
  path = @options[:state]
114
+ permission = @options[:state_permission]
109
115
  return unless path
110
116
 
111
117
  require 'puma/state_file'
@@ -114,8 +120,9 @@ module Puma
114
120
  sf.pid = Process.pid
115
121
  sf.control_url = @options[:control_url]
116
122
  sf.control_auth_token = @options[:control_auth_token]
123
+ sf.running_from = File.expand_path('.')
117
124
 
118
- sf.save path
125
+ sf.save path, permission
119
126
  end
120
127
 
121
128
  # Delete the configured pidfile
@@ -124,19 +131,6 @@ module Puma
124
131
  File.unlink(path) if path && File.exist?(path)
125
132
  end
126
133
 
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
134
  # Begin async shutdown of the server
141
135
  def halt
142
136
  @status = :halt
@@ -183,28 +177,34 @@ module Puma
183
177
 
184
178
  setup_signals
185
179
  set_process_title
180
+ integrate_with_systemd
186
181
  @runner.run
187
182
 
188
183
  case @status
189
184
  when :halt
190
185
  log "* Stopping immediately!"
186
+ @runner.stop_control
191
187
  when :run, :stop
192
188
  graceful_stop
193
189
  when :restart
194
190
  log "* Restarting..."
195
191
  ENV.replace(previous_env)
196
- @runner.before_restart
192
+ @runner.stop_control
197
193
  restart!
198
194
  when :exit
199
195
  # nothing
200
196
  end
197
+ close_binder_listeners unless @status == :restart
201
198
  end
202
199
 
203
- # Return which tcp port the launcher is using, if it's using TCP
204
- def connected_port
205
- @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
206
205
  end
207
206
 
207
+ # @!attribute [r] restart_args
208
208
  def restart_args
209
209
  cmd = @options[:restart_cmd]
210
210
  if cmd
@@ -214,14 +214,49 @@ module Puma
214
214
  end
215
215
  end
216
216
 
217
+ def close_binder_listeners
218
+ @runner.close_control_listeners
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
237
+ end
238
+
217
239
  private
218
240
 
241
+ # If configured, write the pid of the current process out
242
+ # to a file.
243
+ def write_pid
244
+ path = @options[:pidfile]
245
+ return unless path
246
+ cur_pid = Process.pid
247
+ File.write path, cur_pid, mode: 'wb:UTF-8'
248
+ at_exit do
249
+ delete_pidfile if cur_pid == Process.pid
250
+ end
251
+ end
252
+
219
253
  def reload_worker_directory
220
254
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
221
255
  end
222
256
 
223
257
  def restart!
224
- @config.run_hooks :on_restart, self
258
+ @events.fire_on_restart!
259
+ @config.run_hooks :on_restart, self, @events
225
260
 
226
261
  if Puma.jruby?
227
262
  close_binder_listeners
@@ -235,48 +270,98 @@ module Puma
235
270
  Dir.chdir(@restart_dir)
236
271
  Kernel.exec(*argv)
237
272
  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
273
  argv = restart_args
245
274
  Dir.chdir(@restart_dir)
246
- argv += [redirects] if RUBY_VERSION >= '1.9'
275
+ ENV.update(@binder.redirects_for_restart_env)
276
+ argv += [@binder.redirects_for_restart]
247
277
  Kernel.exec(*argv)
248
278
  end
249
279
  end
250
280
 
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) }
281
+ # @!attribute [r] files_to_require_after_prune
282
+ def files_to_require_after_prune
283
+ puma = spec_for_gem("puma")
284
+
285
+ require_paths_for_gem(puma) + extra_runtime_deps_directories
286
+ end
287
+
288
+ # @!attribute [r] extra_runtime_deps_directories
289
+ def extra_runtime_deps_directories
290
+ Array(@options[:extra_runtime_dependencies]).map do |d_name|
291
+ if (spec = spec_for_gem(d_name))
292
+ require_paths_for_gem(spec)
293
+ else
294
+ log "* Could not load extra dependency: #{d_name}"
295
+ nil
296
+ end
297
+ end.flatten.compact
298
+ end
299
+
300
+ # @!attribute [r] puma_wild_location
301
+ def puma_wild_location
302
+ puma = spec_for_gem("puma")
303
+ dirs = require_paths_for_gem(puma)
255
304
  puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
305
+ File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
306
+ end
256
307
 
257
- unless puma_lib_dir
308
+ def prune_bundler
309
+ return if ENV['PUMA_BUNDLER_PRUNED']
310
+ return unless defined?(Bundler)
311
+ require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
312
+ unless puma_wild_location
258
313
  log "! Unable to prune Bundler environment, continuing"
259
314
  return
260
315
  end
261
316
 
262
- deps = puma.runtime_dependencies.map do |d|
263
- spec = Bundler.rubygems.loaded_specs(d.name)
264
- "#{d.name}:#{spec.version.to_s}"
265
- end
317
+ dirs = files_to_require_after_prune
266
318
 
267
319
  log '* Pruning Bundler environment'
268
320
  home = ENV['GEM_HOME']
269
- Bundler.with_clean_env do
321
+ bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
322
+ with_unbundled_env do
270
323
  ENV['GEM_HOME'] = home
324
+ ENV['BUNDLE_GEMFILE'] = bundle_gemfile
271
325
  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
326
+ args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':')] + @original_argv
274
327
  # Ruby 2.0+ defaults to true which breaks socket activation
275
- args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
328
+ args += [{:close_others => false}]
276
329
  Kernel.exec(*args)
277
330
  end
278
331
  end
279
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
+
357
+ def spec_for_gem(gem_name)
358
+ Bundler.rubygems.loaded_specs(gem_name)
359
+ end
360
+
361
+ def require_paths_for_gem(gem_spec)
362
+ gem_spec.full_require_paths
363
+ end
364
+
280
365
  def log(str)
281
366
  @events.log str
282
367
  end
@@ -291,15 +376,15 @@ module Puma
291
376
  end
292
377
 
293
378
  def graceful_stop
379
+ @events.fire_on_stopped!
294
380
  @runner.stop_blocked
295
- log "=== puma shutdown: #{Time.now} ==="
296
- log "- Goodbye!"
297
381
  end
298
382
 
299
383
  def set_process_title
300
384
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
301
385
  end
302
386
 
387
+ # @!attribute [r] title
303
388
  def title
304
389
  buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
305
390
  buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
@@ -311,6 +396,7 @@ module Puma
311
396
  ENV['RACK_ENV'] = environment
312
397
  end
313
398
 
399
+ # @!attribute [r] environment
314
400
  def environment
315
401
  @environment
316
402
  end
@@ -319,16 +405,6 @@ module Puma
319
405
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
320
406
  end
321
407
 
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
329
- end
330
-
331
-
332
408
  def generate_restart_data
333
409
  if dir = @options[:directory]
334
410
  @restart_dir = dir
@@ -397,7 +473,7 @@ module Puma
397
473
  Signal.trap "SIGTERM" do
398
474
  graceful_stop
399
475
 
400
- raise SignalException, "SIGTERM"
476
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
401
477
  end
402
478
  rescue Exception
403
479
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
@@ -405,12 +481,6 @@ module Puma
405
481
 
406
482
  begin
407
483
  Signal.trap "SIGINT" do
408
- if Puma.jruby?
409
- @status = :exit
410
- graceful_stop
411
- exit
412
- end
413
-
414
484
  stop
415
485
  end
416
486
  rescue Exception
@@ -428,6 +498,46 @@ module Puma
428
498
  rescue Exception
429
499
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
430
500
  end
501
+
502
+ begin
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
510
+ end
511
+ rescue Exception
512
+ # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
513
+ # to see this constantly on Linux.
514
+ end
515
+ end
516
+
517
+ def require_rubygems_min_version!(min_version, feature)
518
+ return if min_version <= Gem::Version.new(Gem::VERSION)
519
+
520
+ raise "#{feature} is not supported on your version of RubyGems. " \
521
+ "You must have RubyGems #{min_version}+ to use this feature."
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"
431
541
  end
432
542
  end
433
543
  end