puma 2.0.0.b5 → 5.0.0.beta1

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 (106) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +1598 -0
  3. data/LICENSE +23 -20
  4. data/README.md +222 -62
  5. data/bin/puma-wild +31 -0
  6. data/bin/pumactl +1 -1
  7. data/docs/architecture.md +37 -0
  8. data/docs/deployment.md +113 -0
  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 +13 -0
  14. data/docs/jungle/rc.d/README.md +74 -0
  15. data/docs/jungle/rc.d/puma +61 -0
  16. data/docs/jungle/rc.d/puma.conf +10 -0
  17. data/docs/jungle/upstart/README.md +61 -0
  18. data/docs/jungle/upstart/puma-manager.conf +31 -0
  19. data/docs/jungle/upstart/puma.conf +69 -0
  20. data/docs/nginx.md +5 -10
  21. data/docs/plugins.md +38 -0
  22. data/docs/restart.md +41 -0
  23. data/docs/signals.md +97 -0
  24. data/docs/systemd.md +228 -0
  25. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  26. data/ext/puma_http11/extconf.rb +23 -2
  27. data/ext/puma_http11/http11_parser.c +301 -482
  28. data/ext/puma_http11/http11_parser.h +13 -11
  29. data/ext/puma_http11/http11_parser.java.rl +26 -42
  30. data/ext/puma_http11/http11_parser.rl +22 -21
  31. data/ext/puma_http11/http11_parser_common.rl +5 -5
  32. data/ext/puma_http11/mini_ssl.c +377 -18
  33. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -107
  34. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +137 -170
  35. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +265 -191
  36. data/ext/puma_http11/puma_http11.c +57 -81
  37. data/lib/puma.rb +25 -4
  38. data/lib/puma/accept_nonblock.rb +7 -1
  39. data/lib/puma/app/status.rb +61 -24
  40. data/lib/puma/binder.rb +212 -78
  41. data/lib/puma/cli.rb +149 -644
  42. data/lib/puma/client.rb +316 -65
  43. data/lib/puma/cluster.rb +659 -0
  44. data/lib/puma/commonlogger.rb +108 -0
  45. data/lib/puma/configuration.rb +279 -180
  46. data/lib/puma/const.rb +126 -39
  47. data/lib/puma/control_cli.rb +183 -96
  48. data/lib/puma/detect.rb +20 -1
  49. data/lib/puma/dsl.rb +776 -0
  50. data/lib/puma/events.rb +91 -23
  51. data/lib/puma/io_buffer.rb +9 -5
  52. data/lib/puma/jruby_restart.rb +9 -5
  53. data/lib/puma/launcher.rb +487 -0
  54. data/lib/puma/minissl.rb +239 -93
  55. data/lib/puma/minissl/context_builder.rb +76 -0
  56. data/lib/puma/null_io.rb +22 -12
  57. data/lib/puma/plugin.rb +111 -0
  58. data/lib/puma/plugin/tmp_restart.rb +36 -0
  59. data/lib/puma/rack/builder.rb +297 -0
  60. data/lib/puma/rack/urlmap.rb +93 -0
  61. data/lib/puma/rack_default.rb +9 -0
  62. data/lib/puma/reactor.rb +290 -43
  63. data/lib/puma/runner.rb +163 -0
  64. data/lib/puma/server.rb +493 -126
  65. data/lib/puma/single.rb +66 -0
  66. data/lib/puma/state_file.rb +34 -0
  67. data/lib/puma/thread_pool.rb +228 -47
  68. data/lib/puma/util.rb +115 -0
  69. data/lib/rack/handler/puma.rb +78 -31
  70. data/tools/Dockerfile +16 -0
  71. data/tools/trickletest.rb +44 -0
  72. metadata +60 -155
  73. data/COPYING +0 -55
  74. data/Gemfile +0 -8
  75. data/History.txt +0 -196
  76. data/Manifest.txt +0 -56
  77. data/Rakefile +0 -121
  78. data/TODO +0 -5
  79. data/docs/config.md +0 -0
  80. data/ext/puma_http11/io_buffer.c +0 -154
  81. data/lib/puma/capistrano.rb +0 -26
  82. data/lib/puma/compat.rb +0 -11
  83. data/lib/puma/daemon_ext.rb +0 -20
  84. data/lib/puma/delegation.rb +0 -11
  85. data/lib/puma/java_io_buffer.rb +0 -45
  86. data/lib/puma/rack_patch.rb +0 -25
  87. data/puma.gemspec +0 -45
  88. data/test/test_app_status.rb +0 -88
  89. data/test/test_cli.rb +0 -171
  90. data/test/test_config.rb +0 -16
  91. data/test/test_http10.rb +0 -27
  92. data/test/test_http11.rb +0 -126
  93. data/test/test_integration.rb +0 -150
  94. data/test/test_iobuffer.rb +0 -38
  95. data/test/test_minissl.rb +0 -22
  96. data/test/test_null_io.rb +0 -31
  97. data/test/test_persistent.rb +0 -238
  98. data/test/test_puma_server.rb +0 -128
  99. data/test/test_rack_handler.rb +0 -10
  100. data/test/test_rack_server.rb +0 -141
  101. data/test/test_thread_pool.rb +0 -146
  102. data/test/test_unix_socket.rb +0 -39
  103. data/test/test_ws.rb +0 -89
  104. data/tools/jungle/README.md +0 -54
  105. data/tools/jungle/puma +0 -332
  106. data/tools/jungle/run-puma +0 -3
@@ -1,31 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'socket'
5
+
1
6
  require 'puma/const'
7
+ require 'puma/util'
8
+ require 'puma/minissl/context_builder'
2
9
 
3
10
  module Puma
4
11
  class Binder
5
12
  include Puma::Const
6
13
 
14
+ RACK_VERSION = [1,6].freeze
15
+
7
16
  def initialize(events)
8
17
  @events = events
9
18
  @listeners = []
10
19
  @inherited_fds = {}
20
+ @activated_sockets = {}
11
21
  @unix_paths = []
12
22
 
13
23
  @proto_env = {
14
- "rack.version".freeze => Rack::VERSION,
24
+ "rack.version".freeze => RACK_VERSION,
15
25
  "rack.errors".freeze => events.stderr,
16
26
  "rack.multithread".freeze => true,
17
27
  "rack.multiprocess".freeze => false,
18
- "rack.run_once".freeze => true,
28
+ "rack.run_once".freeze => false,
19
29
  "SCRIPT_NAME".freeze => ENV['SCRIPT_NAME'] || "",
20
30
 
21
- # Rack blows up if this is an empty string, and Rack::Lint
22
- # blows up if it's nil. So 'text/plain' seems like the most
23
- # sensible default value.
24
- "CONTENT_TYPE".freeze => "text/plain",
31
+ # I'd like to set a default CONTENT_TYPE here but some things
32
+ # depend on their not being a default set and inferring
33
+ # it from the content. And so if i set it here, it won't
34
+ # infer properly.
25
35
 
26
36
  "QUERY_STRING".freeze => "",
27
37
  SERVER_PROTOCOL => HTTP_11,
28
- SERVER_SOFTWARE => PUMA_VERSION,
38
+ SERVER_SOFTWARE => PUMA_SERVER_STRING,
29
39
  GATEWAY_INTERFACE => CGI_VER
30
40
  }
31
41
 
@@ -33,7 +43,8 @@ module Puma
33
43
  @ios = []
34
44
  end
35
45
 
36
- attr_reader :listeners, :ios
46
+ attr_reader :ios, :listeners, :unix_paths, :proto_env, :envs, :activated_sockets, :inherited_fds
47
+ attr_writer :ios, :listeners
37
48
 
38
49
  def env(sock)
39
50
  @envs.fetch(sock, @proto_env)
@@ -41,95 +52,123 @@ module Puma
41
52
 
42
53
  def close
43
54
  @ios.each { |i| i.close }
44
- @unix_paths.each { |i| File.unlink i }
45
55
  end
46
56
 
47
- def import_from_env
48
- remove = []
57
+ def connected_ports
58
+ ios.map { |io| io.addr[1] }.uniq
59
+ end
49
60
 
50
- ENV.each do |k,v|
51
- if k =~ /PUMA_INHERIT_\d+/
52
- fd, url = v.split(":", 2)
53
- @inherited_fds[url] = fd.to_i
54
- remove << k
55
- end
56
- end
61
+ def create_inherited_fds(env_hash)
62
+ env_hash.select {|k,v| k =~ /PUMA_INHERIT_\d+/}.each do |_k, v|
63
+ fd, url = v.split(":", 2)
64
+ @inherited_fds[url] = fd.to_i
65
+ end.keys # pass keys back for removal
66
+ end
57
67
 
58
- remove.each do |k|
59
- ENV.delete k
68
+ # systemd socket activation.
69
+ # LISTEN_FDS = number of listening sockets. e.g. 2 means accept on 2 sockets w/descriptors 3 and 4.
70
+ # LISTEN_PID = PID of the service process, aka us
71
+ # see https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html
72
+ def create_activated_fds(env_hash)
73
+ return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$
74
+ env_hash['LISTEN_FDS'].to_i.times do |index|
75
+ sock = TCPServer.for_fd(socket_activation_fd(index))
76
+ key = begin # Try to parse as a path
77
+ [:unix, Socket.unpack_sockaddr_un(sock.getsockname)]
78
+ rescue ArgumentError # Try to parse as a port/ip
79
+ port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
80
+ addr = "[#{addr}]" if addr =~ /\:/
81
+ [:tcp, addr, port]
82
+ end
83
+ @activated_sockets[key] = sock
84
+ @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
60
85
  end
86
+ ["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
61
87
  end
62
88
 
63
- def parse(binds, logger)
89
+ def parse(binds, logger, log_msg = 'Listening')
64
90
  binds.each do |str|
65
91
  uri = URI.parse str
66
92
  case uri.scheme
67
93
  when "tcp"
68
94
  if fd = @inherited_fds.delete(str)
69
- logger.log "* Inherited #{str}"
70
95
  io = inherit_tcp_listener uri.host, uri.port, fd
96
+ logger.log "* Inherited #{str}"
97
+ elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
98
+ io = inherit_tcp_listener uri.host, uri.port, sock
99
+ logger.log "* Activated #{str}"
71
100
  else
72
- logger.log "* Listening on #{str}"
73
- io = add_tcp_listener uri.host, uri.port
101
+ params = Util.parse_query uri.query
102
+
103
+ opt = params.key?('low_latency')
104
+ bak = params.fetch('backlog', 1024).to_i
105
+
106
+ io = add_tcp_listener uri.host, uri.port, opt, bak
107
+
108
+ @ios.each do |i|
109
+ next unless TCPServer === i
110
+ addr = if i.local_address.ipv6?
111
+ "[#{i.local_address.ip_unpack[0]}]:#{i.local_address.ip_unpack[1]}"
112
+ else
113
+ i.local_address.ip_unpack.join(':')
114
+ end
115
+
116
+ logger.log "* #{log_msg} on tcp://#{addr}"
117
+ end
74
118
  end
75
119
 
76
- @listeners << [str, io]
120
+ @listeners << [str, io] if io
77
121
  when "unix"
78
- path = "#{uri.host}#{uri.path}"
122
+ path = "#{uri.host}#{uri.path}".gsub("%20", " ")
79
123
 
80
124
  if fd = @inherited_fds.delete(str)
81
- logger.log "* Inherited #{str}"
82
125
  io = inherit_unix_listener path, fd
126
+ logger.log "* Inherited #{str}"
127
+ elsif sock = @activated_sockets.delete([ :unix, path ])
128
+ io = inherit_unix_listener path, sock
129
+ logger.log "* Activated #{str}"
83
130
  else
84
- logger.log "* Listening on #{str}"
85
-
86
131
  umask = nil
132
+ mode = nil
133
+ backlog = 1024
87
134
 
88
135
  if uri.query
89
- params = Rack::Utils.parse_query uri.query
136
+ params = Util.parse_query uri.query
90
137
  if u = params['umask']
91
138
  # Use Integer() to respect the 0 prefix as octal
92
139
  umask = Integer(u)
93
140
  end
141
+
142
+ if u = params['mode']
143
+ mode = Integer('0'+u)
144
+ end
145
+
146
+ if u = params['backlog']
147
+ backlog = Integer(u)
148
+ end
94
149
  end
95
150
 
96
- io = add_unix_listener path, umask
151
+ io = add_unix_listener path, umask, mode, backlog
152
+ logger.log "* #{log_msg} on #{str}"
97
153
  end
98
154
 
99
155
  @listeners << [str, io]
100
156
  when "ssl"
101
- if IS_JRUBY
102
- @events.error "SSL not supported on JRuby"
103
- raise UnsupportedOption
104
- end
105
-
106
- params = Rack::Utils.parse_query uri.query
107
- require 'puma/minissl'
108
-
109
- ctx = MiniSSL::Context.new
110
- unless params['key']
111
- @events.error "Please specify the SSL key via 'key='"
112
- end
113
-
114
- ctx.key = params['key']
115
-
116
- unless params['cert']
117
- @events.error "Please specify the SSL cert via 'cert='"
118
- end
119
-
120
- ctx.cert = params['cert']
121
-
122
- ctx.verify_mode = MiniSSL::VERIFY_NONE
157
+ params = Util.parse_query uri.query
158
+ ctx = MiniSSL::ContextBuilder.new(params, @events).context
123
159
 
124
160
  if fd = @inherited_fds.delete(str)
125
161
  logger.log "* Inherited #{str}"
126
- io = inherited_ssl_listener fd, ctx
162
+ io = inherit_ssl_listener fd, ctx
163
+ elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
164
+ io = inherit_ssl_listener sock, ctx
165
+ logger.log "* Activated #{str}"
127
166
  else
128
- logger.log "* Listening on #{str}"
129
167
  io = add_ssl_listener uri.host, uri.port, ctx
168
+ logger.log "* Listening on #{str}"
130
169
  end
131
170
 
132
- @listeners << [str, io]
171
+ @listeners << [str, io] if io
133
172
  else
134
173
  logger.error "Invalid URI: #{str}"
135
174
  end
@@ -153,6 +192,16 @@ module Puma
153
192
  end
154
193
  end
155
194
 
195
+ # Also close any unused activated sockets
196
+ @activated_sockets.each do |key, sock|
197
+ logger.log "* Closing unused activated socket: #{key.join ':'}"
198
+ begin
199
+ sock.close
200
+ rescue SystemCallError
201
+ end
202
+ # We have to unlink a unix socket path that's not being used
203
+ File.unlink key[1] if key[0] == :unix
204
+ end
156
205
  end
157
206
 
158
207
  # Tell the server to listen on host +host+, port +port+.
@@ -163,14 +212,23 @@ module Puma
163
212
  # allow to accumulate before returning connection refused.
164
213
  #
165
214
  def add_tcp_listener(host, port, optimize_for_latency=true, backlog=1024)
166
- s = TCPServer.new(host, port)
215
+ if host == "localhost"
216
+ loopback_addresses.each do |addr|
217
+ add_tcp_listener addr, port, optimize_for_latency, backlog
218
+ end
219
+ return
220
+ end
221
+
222
+ host = host[1..-2] if host and host[0..0] == '['
223
+ tcp_server = TCPServer.new(host, port)
167
224
  if optimize_for_latency
168
- s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
225
+ tcp_server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
169
226
  end
170
- s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
171
- s.listen backlog
172
- @ios << s
173
- s
227
+ tcp_server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
228
+ tcp_server.listen backlog
229
+
230
+ @ios << tcp_server
231
+ tcp_server
174
232
  end
175
233
 
176
234
  def inherit_tcp_listener(host, port, fd)
@@ -186,13 +244,18 @@ module Puma
186
244
 
187
245
  def add_ssl_listener(host, port, ctx,
188
246
  optimize_for_latency=true, backlog=1024)
189
- if IS_JRUBY
190
- @events.error "SSL not supported on JRuby"
191
- raise UnsupportedOption
192
- end
193
-
194
247
  require 'puma/minissl'
195
248
 
249
+ MiniSSL.check
250
+
251
+ if host == "localhost"
252
+ loopback_addresses.each do |addr|
253
+ add_ssl_listener addr, port, ctx, optimize_for_latency, backlog
254
+ end
255
+ return
256
+ end
257
+
258
+ host = host[1..-2] if host[0..0] == '['
196
259
  s = TCPServer.new(host, port)
197
260
  if optimize_for_latency
198
261
  s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
@@ -200,6 +263,7 @@ module Puma
200
263
  s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
201
264
  s.listen backlog
202
265
 
266
+
203
267
  ssl = MiniSSL::Server.new s, ctx
204
268
  env = @proto_env.dup
205
269
  env[HTTPS_KEY] = HTTPS
@@ -209,45 +273,115 @@ module Puma
209
273
  s
210
274
  end
211
275
 
212
- def inherited_ssl_listener(fd, ctx)
213
- if IS_JRUBY
214
- @events.error "SSL not supported on JRuby"
215
- raise UnsupportedOption
276
+ def inherit_ssl_listener(fd, ctx)
277
+ require 'puma/minissl'
278
+ MiniSSL.check
279
+
280
+ if fd.kind_of? TCPServer
281
+ s = fd
282
+ else
283
+ s = TCPServer.for_fd(fd)
216
284
  end
285
+ ssl = MiniSSL::Server.new(s, ctx)
286
+
287
+ env = @proto_env.dup
288
+ env[HTTPS_KEY] = HTTPS
289
+ @envs[ssl] = env
290
+
291
+ @ios << ssl
217
292
 
218
- require 'puma/minissl'
219
- s = TCPServer.for_fd(fd)
220
- @ios << MiniSSL::Server.new(s, ctx)
221
293
  s
222
294
  end
223
295
 
224
296
  # Tell the server to listen on +path+ as a UNIX domain socket.
225
297
  #
226
- def add_unix_listener(path, umask=nil)
227
- @unix_paths << path
298
+ def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
299
+ @unix_paths << path unless File.exist? path
228
300
 
229
301
  # Let anyone connect by default
230
302
  umask ||= 0
231
303
 
232
304
  begin
233
305
  old_mask = File.umask(umask)
306
+
307
+ if File.exist? path
308
+ begin
309
+ old = UNIXSocket.new path
310
+ rescue SystemCallError, IOError
311
+ File.unlink path
312
+ else
313
+ old.close
314
+ raise "There is already a server bound to: #{path}"
315
+ end
316
+ end
317
+
234
318
  s = UNIXServer.new(path)
319
+ s.listen backlog
235
320
  @ios << s
236
321
  ensure
237
322
  File.umask old_mask
238
323
  end
239
324
 
325
+ if mode
326
+ File.chmod mode, path
327
+ end
328
+
329
+ env = @proto_env.dup
330
+ env[REMOTE_ADDR] = "127.0.0.1"
331
+ @envs[s] = env
332
+
240
333
  s
241
334
  end
242
335
 
243
336
  def inherit_unix_listener(path, fd)
244
- @unix_paths << path
337
+ @unix_paths << path unless File.exist? path
245
338
 
246
- s = UNIXServer.for_fd fd
339
+ if fd.kind_of? TCPServer
340
+ s = fd
341
+ else
342
+ s = UNIXServer.for_fd fd
343
+ end
247
344
  @ios << s
248
345
 
346
+ env = @proto_env.dup
347
+ env[REMOTE_ADDR] = "127.0.0.1"
348
+ @envs[s] = env
349
+
249
350
  s
250
351
  end
251
352
 
353
+ def close_listeners
354
+ listeners.each do |l, io|
355
+ io.close unless io.closed? # Ruby 2.2 issue
356
+ uri = URI.parse(l)
357
+ next unless uri.scheme == 'unix'
358
+ unix_path = "#{uri.host}#{uri.path}"
359
+ File.unlink unix_path if unix_paths.include? unix_path
360
+ end
361
+ end
362
+
363
+ def redirects_for_restart
364
+ redirects = listeners.map { |a| [a[1].to_i, a[1].to_i] }.to_h
365
+ redirects[:close_others] = true
366
+ redirects
367
+ end
368
+
369
+ def redirects_for_restart_env
370
+ listeners.each_with_object({}).with_index do |(listen, memo), i|
371
+ memo["PUMA_INHERIT_#{i}"] = "#{listen[1].to_i}:#{listen[0]}"
372
+ end
373
+ end
374
+
375
+ private
376
+
377
+ def loopback_addresses
378
+ Socket.ip_address_list.select do |addrinfo|
379
+ addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
380
+ end.map { |addrinfo| addrinfo.ip_address }.uniq
381
+ end
382
+
383
+ def socket_activation_fd(int)
384
+ int + 3 # 3 is the magic number you add to follow the SA protocol
385
+ end
252
386
  end
253
387
  end
@@ -1,726 +1,231 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require 'uri'
3
5
 
4
- require 'puma/server'
5
- require 'puma/const'
6
+ require 'puma'
6
7
  require 'puma/configuration'
7
- require 'puma/binder'
8
- require 'puma/detect'
9
- require 'puma/daemon_ext'
10
- require 'puma/util'
11
-
12
- require 'rack/commonlogger'
13
- require 'rack/utils'
8
+ require 'puma/launcher'
9
+ require 'puma/const'
10
+ require 'puma/events'
14
11
 
15
12
  module Puma
13
+ class << self
14
+ # The CLI exports its Puma::Configuration object here to allow
15
+ # apps to pick it up. An app needs to use it conditionally though
16
+ # since it is not set if the app is launched via another
17
+ # mechanism than the CLI class.
18
+ attr_accessor :cli_config
19
+ end
20
+
16
21
  # Handles invoke a Puma::Server in a command line style.
17
22
  #
18
23
  class CLI
24
+ KEYS_NOT_TO_PERSIST_IN_STATE = Launcher::KEYS_NOT_TO_PERSIST_IN_STATE
25
+
19
26
  # Create a new CLI object using +argv+ as the command line
20
27
  # arguments.
21
28
  #
22
29
  # +stdout+ and +stderr+ can be set to IO-like objects which
23
30
  # this object will report status on.
24
31
  #
25
- def initialize(argv, stdout=STDOUT, stderr=STDERR)
32
+ def initialize(argv, events=Events.stdio)
26
33
  @debug = false
27
- @argv = argv
28
- @stdout = stdout
29
- @stderr = stderr
30
-
31
- @phase = 0
32
- @workers = []
34
+ @argv = argv.dup
33
35
 
34
- @events = Events.new @stdout, @stderr
36
+ @events = events
35
37
 
36
- @server = nil
37
- @status = nil
38
+ @conf = nil
38
39
 
39
- @restart = false
40
- @phased_state = :idle
40
+ @stdout = nil
41
+ @stderr = nil
42
+ @append = false
41
43
 
42
- ENV['NEWRELIC_DISPATCHER'] ||= "puma"
44
+ @control_url = nil
45
+ @control_options = {}
43
46
 
44
47
  setup_options
45
48
 
46
- generate_restart_data
47
-
48
- @binder = Binder.new(@events)
49
- @binder.import_from_env
50
- end
51
-
52
- def restart_on_stop!
53
- @restart = true
54
- end
55
-
56
- def generate_restart_data
57
- # Use the same trick as unicorn, namely favor PWD because
58
- # it will contain an unresolved symlink, useful for when
59
- # the pwd is /data/releases/current.
60
- if dir = ENV['PWD']
61
- s_env = File.stat(dir)
62
- s_pwd = File.stat(Dir.pwd)
63
-
64
- if s_env.ino == s_pwd.ino and s_env.dev == s_pwd.dev
65
- @restart_dir = dir
66
- end
67
- end
68
-
69
- @restart_dir ||= Dir.pwd
70
-
71
- @original_argv = ARGV.dup
72
-
73
- if defined? Rubinius::OS_ARGV
74
- @restart_argv = Rubinius::OS_ARGV
75
- else
76
- require 'rubygems'
77
-
78
- # if $0 is a file in the current directory, then restart
79
- # it the same, otherwise add -S on there because it was
80
- # picked up in PATH.
81
- #
82
- if File.exists?($0)
83
- arg0 = [Gem.ruby, $0]
84
- else
85
- arg0 = [Gem.ruby, "-S", $0]
86
- end
87
-
88
- # Detect and reinject -Ilib from the command line
89
- lib = File.expand_path "lib"
90
- arg0[1,0] = ["-I", lib] if $:[0] == lib
91
-
92
- @restart_argv = arg0 + ARGV
93
- end
94
- end
95
-
96
- def restart!
97
- @options[:on_restart].each do |blk|
98
- blk.call self
99
- end
100
-
101
- if jruby?
102
- @binder.listeners.each_with_index do |(str,io),i|
103
- io.close
104
-
105
- # We have to unlink a unix socket path that's not being used
106
- uri = URI.parse str
107
- if uri.scheme == "unix"
108
- path = "#{uri.host}#{uri.path}"
109
- File.unlink path
110
- end
111
- end
112
-
113
- require 'puma/jruby_restart'
114
- JRubyRestart.chdir_exec(@restart_dir, Gem.ruby, *@restart_argv)
115
- else
116
- @binder.listeners.each_with_index do |(l,io),i|
117
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
118
- end
119
-
120
- if cmd = @options[:restart_cmd]
121
- argv = cmd.split(' ') + @original_argv
122
- else
123
- argv = @restart_argv
124
- end
125
-
126
- Dir.chdir @restart_dir
127
- Kernel.exec(*argv)
128
- end
129
- end
130
-
131
- # Delegate +log+ to +@events+
132
- #
133
- def log(str)
134
- @events.log str
135
- end
136
-
137
- # Delegate +error+ to +@events+
138
- #
139
- def error(str)
140
- @events.error str
141
- end
142
-
143
- def debug(str)
144
- if @options[:debug]
145
- @events.log "- #{str}"
146
- end
147
- end
148
-
149
- def jruby?
150
- IS_JRUBY
151
- end
152
-
153
- def windows?
154
- RUBY_PLATFORM =~ /mswin32|ming32/
155
- end
156
-
157
- def unsupported(str, cond=true)
158
- return unless cond
159
- @events.error str
160
- raise UnsupportedOption
161
- end
162
-
163
- # Build the OptionParser object to handle the available options.
164
- #
165
- def setup_options
166
- @options = {
167
- :min_threads => 0,
168
- :max_threads => 16,
169
- :quiet => false,
170
- :debug => false,
171
- :binds => [],
172
- :workers => 0,
173
- :daemon => false,
174
- :environment => "development"
175
- }
176
-
177
- @parser = OptionParser.new do |o|
178
- o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
179
- @options[:binds] << arg
180
- end
181
-
182
- o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
183
- @options[:config_file] = arg
184
- end
185
-
186
- o.on "--control URL", "The bind url to use for the control server",
187
- "Use 'auto' to use temp unix server" do |arg|
188
- if arg
189
- @options[:control_url] = arg
190
- elsif jruby?
191
- unsupported "No default url available on JRuby"
192
- end
193
- end
194
-
195
- o.on "--control-token TOKEN",
196
- "The token to use as authentication for the control server" do |arg|
197
- @options[:control_auth_token] = arg
198
- end
199
-
200
- o.on "-d", "--daemon", "Daemonize the server into the background" do
201
- @options[:daemon] = true
202
- @options[:quiet] = true
203
- end
204
-
205
- o.on "--debug", "Log lowlevel debugging information" do
206
- @options[:debug] = true
207
- end
208
-
209
- o.on "--dir DIR", "Change to DIR before starting" do |d|
210
- @options[:directory] = d.to_s
211
- end
212
-
213
- o.on "-e", "--environment ENVIRONMENT",
214
- "The environment to run the Rack app on (default development)" do |arg|
215
- @options[:environment] = arg
216
- end
217
-
218
- o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
219
- $LOAD_PATH.unshift(*arg.split(':'))
220
- end
221
-
222
- o.on "-p", "--port PORT", "Define what port TCP port to bind to",
223
- "Use -b for more advanced options" do |arg|
224
- @options[:binds] << "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
225
- end
226
-
227
- o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
228
- @options[:pidfile] = arg
229
- end
230
-
231
- o.on "-q", "--quiet", "Quiet down the output" do
232
- @options[:quiet] = true
233
- end
234
-
235
- o.on "-R", "--restart-cmd CMD",
236
- "The puma command to run during a hot restart",
237
- "Default: inferred" do |cmd|
238
- @options[:restart_cmd] = cmd
239
- end
240
-
241
- o.on "-S", "--state PATH", "Where to store the state details" do |arg|
242
- @options[:state] = arg
243
- end
49
+ begin
50
+ @parser.parse! @argv
244
51
 
245
- o.on '-t', '--threads INT', "min:max threads to use (default 0:16)" do |arg|
246
- min, max = arg.split(":")
247
- if max
248
- @options[:min_threads] = min.to_i
249
- @options[:max_threads] = max.to_i
250
- else
251
- @options[:min_threads] = 0
252
- @options[:max_threads] = arg.to_i
52
+ if file = @argv.shift
53
+ @conf.configure do |user_config, file_config|
54
+ file_config.rackup file
253
55
  end
254
56
  end
255
-
256
- o.on "-w", "--workers COUNT",
257
- "Activate cluster mode: How many worker processes to create" do |arg|
258
- unsupported "-w not supported on JRuby and Windows",
259
- jruby? || windows?
260
-
261
- @options[:workers] = arg.to_i
262
- end
263
-
264
- end
265
-
266
- @parser.banner = "puma <options> <rackup file>"
267
-
268
- @parser.on_tail "-h", "--help", "Show help" do
269
- log @parser
57
+ rescue UnsupportedOption
270
58
  exit 1
271
59
  end
272
- end
273
60
 
274
- # If configured, write the pid of the current process out
275
- # to a file.
276
- #
277
- def write_pid
278
- if path = @options[:pidfile]
279
- File.open(path, "w") do |f|
280
- f.puts Process.pid
61
+ @conf.configure do |user_config, file_config|
62
+ if @stdout || @stderr
63
+ user_config.stdout_redirect @stdout, @stderr, @append
281
64
  end
282
- end
283
- end
284
-
285
- def set_rack_environment
286
- # Try the user option first, then the environment variable,
287
- # finally default to development
288
-
289
- ENV['RACK_ENV'] = @options[:environment] ||
290
- ENV['RACK_ENV'] ||
291
- 'development'
292
- end
293
-
294
- def delete_pidfile
295
- if path = @options[:pidfile]
296
- File.unlink path
297
- end
298
- end
299
-
300
- def write_state
301
- write_pid
302
-
303
- require 'yaml'
304
-
305
- if path = @options[:state]
306
- state = { "pid" => Process.pid }
307
65
 
308
- cfg = @config.dup
309
- cfg.options.delete :on_restart
310
-
311
- state["config"] = cfg
312
-
313
- File.open(path, "w") do |f|
314
- f.write state.to_yaml
66
+ if @control_url
67
+ user_config.activate_control_app @control_url, @control_options
315
68
  end
316
69
  end
317
- end
318
-
319
- # :nodoc:
320
- def parse_options
321
- @parser.parse! @argv
322
70
 
323
- if @argv.last
324
- @options[:rackup] = @argv.shift
325
- end
326
-
327
- @config = Puma::Configuration.new @options
328
- @config.load
329
- end
330
-
331
- def graceful_stop(server)
332
- log " - Gracefully stopping, waiting for requests to finish"
333
- @status.stop(true) if @status
334
- server.stop(true)
335
- delete_pidfile
336
- log " - Goodbye!"
71
+ @launcher = Puma::Launcher.new(@conf, :events => @events, :argv => argv)
337
72
  end
338
73
 
339
- def redirect_io
340
- stdout = @options[:redirect_stdout]
341
- stderr = @options[:redirect_stderr]
342
- append = @options[:redirect_append]
343
-
344
- if stdout
345
- STDOUT.reopen stdout, (append ? "a" : "w")
346
- STDOUT.puts "=== puma startup: #{Time.now} ==="
347
- end
348
-
349
- if stderr
350
- STDERR.reopen stderr, (append ? "a" : "w")
351
- STDERR.puts "=== puma startup: #{Time.now} ==="
352
- end
353
- end
74
+ attr_reader :launcher
354
75
 
355
76
  # Parse the options, load the rackup, start the server and wait
356
77
  # for it to finish.
357
78
  #
358
79
  def run
359
- begin
360
- parse_options
361
- rescue UnsupportedOption
362
- exit 1
363
- end
364
-
365
- if dir = @options[:directory]
366
- Dir.chdir dir
367
- end
368
-
369
- clustered = @options[:workers] > 0
370
-
371
- if clustered
372
- @events = PidEvents.new STDOUT, STDERR
373
- @options[:logger] = @events
374
- end
375
-
376
- set_rack_environment
377
-
378
- if clustered
379
- run_cluster
380
- else
381
- run_single
382
- end
80
+ @launcher.run
383
81
  end
384
82
 
385
- def run_single
386
- write_state
387
-
388
- min_t = @options[:min_threads]
389
- max_t = @options[:max_threads]
390
-
391
- log "Puma #{Puma::Const::PUMA_VERSION} starting..."
392
- log "* Min threads: #{min_t}, max threads: #{max_t}"
393
- log "* Environment: #{ENV['RACK_ENV']}"
394
-
395
- @binder.parse @options[:binds], self
396
-
397
- if @options[:daemon]
398
- Process.daemon(true, true)
399
- end
400
-
401
- server = Puma::Server.new @config.app, @events
402
- server.binder = @binder
403
- server.min_threads = min_t
404
- server.max_threads = max_t
405
-
406
- @server = server
407
-
408
- if str = @options[:control_url]
409
- require 'puma/app/status'
410
-
411
- uri = URI.parse str
412
-
413
- app = Puma::App::Status.new server, self
414
-
415
- if token = @options[:control_auth_token]
416
- app.auth_token = token unless token.empty? or token == :none
417
- end
418
-
419
- status = Puma::Server.new app, @events
420
- status.min_threads = 0
421
- status.max_threads = 1
422
-
423
- case uri.scheme
424
- when "tcp"
425
- log "* Starting status server on #{str}"
426
- status.add_tcp_listener uri.host, uri.port
427
- when "unix"
428
- log "* Starting status server on #{str}"
429
- path = "#{uri.host}#{uri.path}"
430
-
431
- status.add_unix_listener path
432
- else
433
- error "Invalid status URI: #{str}"
434
- end
435
-
436
- status.run
437
- @status = status
438
- end
439
-
440
- begin
441
- Signal.trap "SIGUSR2" do
442
- @restart = true
443
- server.begin_restart
444
- end
445
- rescue Exception
446
- log "*** Sorry signal SIGUSR2 not implemented, restart feature disabled!"
447
- end
448
-
449
- begin
450
- Signal.trap "SIGTERM" do
451
- log " - Gracefully stopping, waiting for requests to finish"
452
- server.stop false
453
- end
454
- rescue Exception
455
- log "*** Sorry signal SIGTERM not implemented, gracefully stopping feature disabled!"
456
- end
457
-
458
- unless @options[:daemon]
459
- log "Use Ctrl-C to stop"
460
- end
461
-
462
- redirect_io
463
-
464
- if jruby?
465
- Signal.trap("INT") do
466
- graceful_stop server
467
- exit
468
- end
469
- end
470
-
471
- begin
472
- server.run.join
473
- rescue Interrupt
474
- graceful_stop server
475
- end
476
-
477
- if @restart
478
- log "* Restarting..."
479
- @status.stop true if @status
480
- restart!
481
- end
482
- end
483
-
484
- def worker
485
- $0 = "puma: cluster worker: #{@master_pid}"
486
- Signal.trap "SIGINT", "IGNORE"
487
-
488
- @master_read.close
489
- @suicide_pipe.close
490
-
491
- Thread.new do
492
- IO.select [@check_pipe]
493
- log "! Detected parent died, dieing"
494
- exit! 1
495
- end
496
-
497
- min_t = @options[:min_threads]
498
- max_t = @options[:max_threads]
499
-
500
- server = Puma::Server.new @config.app, @events
501
- server.min_threads = min_t
502
- server.max_threads = max_t
503
- server.binder = @binder
504
-
505
- Signal.trap "SIGTERM" do
506
- server.stop
507
- end
508
-
509
- @worker_write << "b#{Process.pid}\n"
510
-
511
- server.run.join
512
-
513
- ensure
514
- @worker_write.close
83
+ private
84
+ def unsupported(str)
85
+ @events.error(str)
86
+ raise UnsupportedOption
515
87
  end
516
88
 
517
- def stop_workers
518
- log "- Gracefully shutting down workers..."
519
- @workers.each { |x| x.term }
520
-
521
- begin
522
- Process.waitall
523
- rescue Interrupt
524
- log "! Cancelled waiting for workers"
525
- else
526
- log "- Goodbye!"
89
+ def configure_control_url(command_line_arg)
90
+ if command_line_arg
91
+ @control_url = command_line_arg
92
+ elsif Puma.jruby?
93
+ unsupported "No default url available on JRuby"
527
94
  end
528
95
  end
529
96
 
530
- def start_phased_restart
531
- @phase += 1
532
- log "- Starting phased worker restart, phase: #{@phase}"
533
- end
534
-
535
- class Worker
536
- def initialize(pid, phase)
537
- @pid = pid
538
- @phase = phase
539
- @stage = :started
540
- end
541
-
542
- attr_reader :pid, :phase
543
-
544
- def booted?
545
- @stage == :booted
546
- end
547
-
548
- def boot!
549
- @stage = :booted
550
- end
551
-
552
- def term
553
- begin
554
- Process.kill "TERM", @pid
555
- rescue Errno::ESRCH
556
- end
557
- end
558
- end
97
+ # Build the OptionParser object to handle the available options.
98
+ #
559
99
 
560
- def spawn_workers
561
- diff = @options[:workers] - @workers.size
100
+ def setup_options
101
+ @conf = Configuration.new do |user_config, file_config|
102
+ @parser = OptionParser.new do |o|
103
+ o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
104
+ user_config.bind arg
105
+ end
562
106
 
563
- diff.times do
564
- pid = fork { worker }
565
- debug "Spawned worker: #{pid}"
566
- @workers << Worker.new(pid, @phase)
567
- end
107
+ o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
108
+ file_config.load arg
109
+ end
568
110
 
569
- if diff > 0
570
- @phased_state = :idle
571
- end
572
- end
111
+ o.on "--control-url URL", "The bind url to use for the control server. Use 'auto' to use temp unix server" do |arg|
112
+ configure_control_url(arg)
113
+ end
573
114
 
574
- def all_workers_booted?
575
- @workers.count { |w| !w.booted? } == 0
576
- end
115
+ o.on "--control-token TOKEN",
116
+ "The token to use as authentication for the control server" do |arg|
117
+ @control_options[:auth_token] = arg
118
+ end
577
119
 
578
- def check_workers
579
- while true
580
- pid = Process.waitpid(-1, Process::WNOHANG)
581
- break unless pid
120
+ o.on "--debug", "Log lowlevel debugging information" do
121
+ user_config.debug
122
+ end
582
123
 
583
- @workers.delete_if { |w| w.pid == pid }
584
- end
124
+ o.on "--dir DIR", "Change to DIR before starting" do |d|
125
+ user_config.directory d
126
+ end
585
127
 
586
- spawn_workers
128
+ o.on "-e", "--environment ENVIRONMENT",
129
+ "The environment to run the Rack app on (default development)" do |arg|
130
+ user_config.environment arg
131
+ end
587
132
 
588
- if @phased_state == :idle && all_workers_booted?
589
- # If we're running at proper capacity, check to see if
590
- # we need to phase any workers out (which will restart
591
- # in the right phase).
592
- #
593
- w = @workers.find { |x| x.phase != @phase }
133
+ o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
134
+ "Fork new workers from existing worker. Cluster mode only",
135
+ "Auto-refork after REQUESTS (default 1000)" do |*args|
136
+ user_config.fork_worker(*args.compact)
137
+ end
594
138
 
595
- if w
596
- @phased_state = :waiting
597
- log "- Stopping #{w.pid} for phased upgrade..."
598
- w.term
599
- end
600
- end
601
- end
139
+ o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
140
+ $LOAD_PATH.unshift(*arg.split(':'))
141
+ end
602
142
 
603
- def run_cluster
604
- log "Puma #{Puma::Const::PUMA_VERSION} starting in cluster mode..."
605
- log "* Process workers: #{@options[:workers]}"
606
- log "* Min threads: #{@options[:min_threads]}, max threads: #{@options[:max_threads]}"
607
- log "* Environment: #{ENV['RACK_ENV']}"
143
+ o.on "-p", "--port PORT", "Define the TCP port to bind to",
144
+ "Use -b for more advanced options" do |arg|
145
+ user_config.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
146
+ end
608
147
 
609
- @binder.parse @options[:binds], self
148
+ o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
149
+ user_config.pidfile arg
150
+ end
610
151
 
611
- @master_pid = Process.pid
152
+ o.on "--preload", "Preload the app. Cluster mode only" do
153
+ user_config.preload_app!
154
+ end
612
155
 
613
- read, write = Puma::Util.pipe
156
+ o.on "--prune-bundler", "Prune out the bundler env if possible" do
157
+ user_config.prune_bundler
158
+ end
614
159
 
615
- Signal.trap "SIGCHLD" do
616
- write.write "!"
617
- end
160
+ o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
161
+ user_config.extra_runtime_dependencies arg.split(',')
162
+ end
618
163
 
619
- stop = false
164
+ o.on "-q", "--quiet", "Do not log requests internally (default true)" do
165
+ user_config.quiet
166
+ end
620
167
 
621
- begin
622
- Signal.trap "SIGUSR2" do
623
- @restart = true
624
- stop = true
625
- write.write "!"
626
- end
627
- rescue Exception
628
- end
168
+ o.on "-v", "--log-requests", "Log requests as they occur" do
169
+ user_config.log_requests
170
+ end
629
171
 
630
- begin
631
- Signal.trap "SIGTERM" do
632
- stop = true
633
- write.write "!"
634
- end
635
- rescue Exception
636
- end
172
+ o.on "-R", "--restart-cmd CMD",
173
+ "The puma command to run during a hot restart",
174
+ "Default: inferred" do |cmd|
175
+ user_config.restart_command cmd
176
+ end
637
177
 
638
- phased_restart = false
178
+ o.on "-S", "--state PATH", "Where to store the state details" do |arg|
179
+ user_config.state_path arg
180
+ end
639
181
 
640
- begin
641
- Signal.trap "SIGUSR1" do
642
- phased_restart = true
643
- write.write "!"
644
- end
645
- rescue Exception
646
- end
182
+ o.on '-t', '--threads INT', "min:max threads to use (default 0:16)" do |arg|
183
+ min, max = arg.split(":")
184
+ if max
185
+ user_config.threads min, max
186
+ else
187
+ user_config.threads min, min
188
+ end
189
+ end
647
190
 
648
- # Used by the workers to detect if the master process dies.
649
- # If select says that @check_pipe is ready, it's because the
650
- # master has exited and @suicide_pipe has been automatically
651
- # closed.
652
- #
653
- @check_pipe, @suicide_pipe = Puma::Util.pipe
654
-
655
- if @options[:daemon]
656
- Process.daemon(true, true)
657
- else
658
- log "Use Ctrl-C to stop"
659
- end
191
+ o.on "--early-hints", "Enable early hints support" do
192
+ user_config.early_hints
193
+ end
660
194
 
661
- redirect_io
195
+ o.on "-V", "--version", "Print the version information" do
196
+ puts "puma version #{Puma::Const::VERSION}"
197
+ exit 0
198
+ end
662
199
 
663
- write_state
200
+ o.on "-w", "--workers COUNT",
201
+ "Activate cluster mode: How many worker processes to create" do |arg|
202
+ user_config.workers arg
203
+ end
664
204
 
665
- @master_read, @worker_write = read, write
666
- spawn_workers
205
+ o.on "--tag NAME", "Additional text to display in process listing" do |arg|
206
+ user_config.tag arg
207
+ end
667
208
 
668
- Signal.trap "SIGINT" do
669
- stop = true
670
- write.write "!"
671
- end
209
+ o.on "--redirect-stdout FILE", "Redirect STDOUT to a specific file" do |arg|
210
+ @stdout = arg.to_s
211
+ end
672
212
 
673
- begin
674
- while !stop
675
- begin
676
- res = IO.select([read], nil, nil, 5)
677
-
678
- if res
679
- req = read.read_nonblock(1)
680
-
681
- if req == "b"
682
- pid = read.gets.to_i
683
- w = @workers.find { |w| w.pid == pid }
684
- if w
685
- w.boot!
686
- log "- Worker #{pid} booted, phase: #{w.phase}"
687
- else
688
- log "! Out-of-sync worker list, no #{pid} worker"
689
- end
690
- end
691
- end
213
+ o.on "--redirect-stderr FILE", "Redirect STDERR to a specific file" do |arg|
214
+ @stderr = arg.to_s
215
+ end
692
216
 
693
- check_workers
217
+ o.on "--[no-]redirect-append", "Append to redirected files" do |val|
218
+ @append = val
219
+ end
694
220
 
695
- if phased_restart
696
- start_phased_restart
697
- phased_restart = false
698
- end
221
+ o.banner = "puma <options> <rackup file>"
699
222
 
700
- rescue Interrupt
701
- stop = true
223
+ o.on_tail "-h", "--help", "Show help" do
224
+ $stdout.puts o
225
+ exit 0
702
226
  end
703
227
  end
704
-
705
- stop_workers
706
- ensure
707
- delete_pidfile
708
- @check_pipe.close
709
- @suicide_pipe.close
710
- read.close
711
- write.close
712
228
  end
713
-
714
- if @restart
715
- log "* Restarting..."
716
- restart!
717
- end
718
- end
719
-
720
- def stop
721
- @status.stop(true) if @status
722
- @server.stop(true) if @server
723
- delete_pidfile
724
229
  end
725
230
  end
726
231
  end