puma 2.16.0 → 3.0.0.rc1

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.

@@ -138,5 +138,10 @@ module Puma
138
138
  def self.stdio
139
139
  Events.new $stdout, $stderr
140
140
  end
141
+
142
+ def self.null
143
+ n = NullIO.new
144
+ Events.new n, n
145
+ end
141
146
  end
142
147
  end
@@ -0,0 +1,397 @@
1
+ require 'puma/server'
2
+ require 'puma/const'
3
+ require 'puma/configuration'
4
+ require 'puma/binder'
5
+ require 'puma/detect'
6
+ require 'puma/daemon_ext'
7
+ require 'puma/util'
8
+ require 'puma/single'
9
+ require 'puma/cluster'
10
+ require 'puma/state_file'
11
+
12
+ require 'puma/commonlogger'
13
+
14
+ module Puma
15
+ # Puam::Launcher is the single entry point for starting a Puma server based on user
16
+ # configuration. It is responsible for taking user supplied arguments and resolving them
17
+ # with configuration in `config/puma.rb` or `config/puma/<env>.rb`.
18
+ #
19
+ # It is responsible for either launching a cluster of Puma workers or a single
20
+ # puma server.
21
+ class Launcher
22
+ KEYS_NOT_TO_PERSIST_IN_STATE = [
23
+ :logger, :lowlevel_error_handler,
24
+ :before_worker_shutdown, :before_worker_boot, :before_worker_fork,
25
+ :after_worker_boot, :before_fork, :on_restart
26
+ ]
27
+ # Returns an instance of Launcher
28
+ #
29
+ # +conf+ A Puma::Configuration object indicating how to run the server.
30
+ #
31
+ # +launcher_args+ A Hash that currently has one required key `:events`,
32
+ # this is expected to hold an object similar to an `Puma::Events.stdio`,
33
+ # this object will be responsible for broadcasting Puma's internal state
34
+ # to a logging destination. An optional key `:argv` can be supplied,
35
+ # this should be an array of strings, these arguments are re-used when
36
+ # restarting the puma server.
37
+ #
38
+ # Examples:
39
+ #
40
+ # conf = Puma::Configuration.new do |c|
41
+ # c.threads 1, 10
42
+ # c.app do |env|
43
+ # [200, {}, ["hello world"]]
44
+ # end
45
+ # end
46
+ # Puma::Launcher.new(conf, argv: Puma::Events.stdio).run
47
+ def initialize(conf, launcher_args={})
48
+ @runner = nil
49
+ @events = launcher_args[:events] || Events::DEFAULT
50
+ @argv = launcher_args[:argv] || []
51
+ @original_argv = @argv.dup
52
+ @config = conf
53
+
54
+ @binder = Binder.new(@events)
55
+ @binder.import_from_env
56
+
57
+ generate_restart_data
58
+
59
+ @environment = conf.environment
60
+
61
+ # Advertise the Configuration
62
+ Puma.cli_config = @config if defined?(Puma.cli_config)
63
+
64
+ @config.load
65
+
66
+ @options = @config.options
67
+
68
+ if clustered? && (Puma.jruby? || Puma.windows?)
69
+ unsupported 'worker mode not supported on JRuby or Windows'
70
+ end
71
+
72
+ if @options[:daemon] && Puma.windows?
73
+ unsupported 'daemon mode not supported on Windows'
74
+ end
75
+
76
+ dir = @options[:directory]
77
+ Dir.chdir(dir) if dir
78
+
79
+ prune_bundler if prune_bundler?
80
+
81
+ @environment = @options[:environment] if @options[:environment]
82
+ set_rack_environment
83
+
84
+ if clustered?
85
+ @events.formatter = Events::PidFormatter.new
86
+ @options[:logger] = @events
87
+
88
+ @runner = Cluster.new(self, @events)
89
+ else
90
+ @runner = Single.new(self, @events)
91
+ end
92
+
93
+ @status = :run
94
+ end
95
+
96
+ attr_reader :binder, :events, :config, :options
97
+
98
+ # Return stats about the server
99
+ def stats
100
+ @runner.stats
101
+ end
102
+
103
+ # Write a state file that can be used by pumactl to control
104
+ # the server
105
+ def write_state
106
+ write_pid
107
+
108
+ path = @options[:state]
109
+ return unless path
110
+
111
+ sf = StateFile.new
112
+ sf.pid = Process.pid
113
+ sf.control_url = @options[:control_url]
114
+ sf.control_auth_token = @options[:control_auth_token]
115
+
116
+ sf.save path
117
+ end
118
+
119
+ # Delete the configured pidfile
120
+ def delete_pidfile
121
+ path = @options[:pidfile]
122
+ File.unlink(path) if path && File.exist?(path)
123
+ end
124
+
125
+ # If configured, write the pid of the current process out
126
+ # to a file.
127
+ def write_pid
128
+ path = @options[:pidfile]
129
+ return unless path
130
+
131
+ File.open(path, 'w') { |f| f.puts Process.pid }
132
+ cur = Process.pid
133
+ at_exit do
134
+ delete_pidfile if cur == Process.pid
135
+ end
136
+ end
137
+
138
+ # Begin async shutdown of the server
139
+ def halt
140
+ @status = :halt
141
+ @runner.halt
142
+ end
143
+
144
+ # Begin async shutdown of the server gracefully
145
+ def stop
146
+ @status = :stop
147
+ @runner.stop
148
+ end
149
+
150
+ # Begin async restart of the server
151
+ def restart
152
+ @status = :restart
153
+ @runner.restart
154
+ end
155
+
156
+ # Begin a phased restart if supported
157
+ def phased_restart
158
+ unless @runner.respond_to?(:phased_restart) and @runner.phased_restart
159
+ log "* phased-restart called but not available, restarting normally."
160
+ return restart
161
+ end
162
+ true
163
+ end
164
+
165
+ # Run the server. This blocks until the server is stopped
166
+ def run
167
+ @config.clamp
168
+
169
+ @config.plugins.fire_starts self
170
+
171
+ setup_signals
172
+ set_process_title
173
+ @runner.run
174
+
175
+ case @status
176
+ when :halt
177
+ log "* Stopping immediately!"
178
+ when :run, :stop
179
+ graceful_stop
180
+ when :restart
181
+ log "* Restarting..."
182
+ @runner.before_restart
183
+ restart!
184
+ when :exit
185
+ # nothing
186
+ end
187
+ end
188
+
189
+ # Return which tcp port the launcher is using, if it's using TCP
190
+ def connected_port
191
+ @binder.connected_port
192
+ end
193
+
194
+ private
195
+
196
+ def reload_worker_directory
197
+ @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
198
+ end
199
+
200
+ def restart!
201
+ @config.run_hooks :on_restart, self
202
+
203
+ if Puma.jruby?
204
+ close_binder_listeners
205
+
206
+ require 'puma/jruby_restart'
207
+ JRubyRestart.chdir_exec(@restart_dir, restart_args)
208
+ elsif Puma.windows?
209
+ close_binder_listeners
210
+
211
+ argv = restart_args
212
+ Dir.chdir(@restart_dir)
213
+ Kernel.exec(*argv)
214
+ else
215
+ redirects = {:close_others => true}
216
+ @binder.listeners.each_with_index do |(l, io), i|
217
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
218
+ redirects[io.to_i] = io.to_i
219
+ end
220
+
221
+ argv = restart_args
222
+ Dir.chdir(@restart_dir)
223
+ argv += [redirects] if RUBY_VERSION >= '1.9'
224
+ Kernel.exec(*argv)
225
+ end
226
+ end
227
+
228
+ def prune_bundler
229
+ return unless defined?(Bundler)
230
+ puma = Bundler.rubygems.loaded_specs("puma")
231
+ dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
232
+ puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
233
+
234
+ unless puma_lib_dir
235
+ log "! Unable to prune Bundler environment, continuing"
236
+ return
237
+ end
238
+
239
+ deps = puma.runtime_dependencies.map do |d|
240
+ spec = Bundler.rubygems.loaded_specs(d.name)
241
+ "#{d.name}:#{spec.version.to_s}"
242
+ end
243
+
244
+ log '* Pruning Bundler environment'
245
+ home = ENV['GEM_HOME']
246
+ Bundler.with_clean_env do
247
+ ENV['GEM_HOME'] = home
248
+ ENV['PUMA_BUNDLER_PRUNED'] = '1'
249
+ wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
250
+ args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
251
+ Kernel.exec(*args)
252
+ end
253
+ end
254
+
255
+ def log(str)
256
+ @events.log str
257
+ end
258
+
259
+ def clustered?
260
+ (@options[:workers] || 0) > 0
261
+ end
262
+
263
+ def restart_args
264
+ cmd = @options[:restart_cmd]
265
+ if cmd
266
+ cmd.split(' ') + @original_argv
267
+ else
268
+ @restart_argv
269
+ end
270
+ end
271
+
272
+ def unsupported(str)
273
+ @events.error(str)
274
+ raise UnsupportedOption
275
+ end
276
+
277
+ def graceful_stop
278
+ @runner.stop_blocked
279
+ log "=== puma shutdown: #{Time.now} ==="
280
+ log "- Goodbye!"
281
+ end
282
+
283
+ def set_process_title
284
+ Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
285
+ end
286
+
287
+ def title
288
+ buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
289
+ buffer << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
290
+ buffer
291
+ end
292
+
293
+ def set_rack_environment
294
+ @options[:environment] = environment
295
+ ENV['RACK_ENV'] = environment
296
+ end
297
+
298
+ def environment
299
+ @environment
300
+ end
301
+
302
+ def prune_bundler?
303
+ @options[:prune_bundler] && clustered? && !@options[:preload_app]
304
+ end
305
+
306
+ def close_binder_listeners
307
+ @binder.listeners.each do |l, io|
308
+ io.close
309
+ uri = URI.parse(l)
310
+ next unless uri.scheme == 'unix'
311
+ File.unlink("#{uri.host}#{uri.path}")
312
+ end
313
+ end
314
+
315
+
316
+ def generate_restart_data
317
+ # Use the same trick as unicorn, namely favor PWD because
318
+ # it will contain an unresolved symlink, useful for when
319
+ # the pwd is /data/releases/current.
320
+ if dir = ENV['PWD']
321
+ s_env = File.stat(dir)
322
+ s_pwd = File.stat(Dir.pwd)
323
+
324
+ if s_env.ino == s_pwd.ino and (Puma.jruby? or s_env.dev == s_pwd.dev)
325
+ @restart_dir = dir
326
+ @config.configure { |c| c.worker_directory dir }
327
+ end
328
+ end
329
+
330
+ @restart_dir ||= Dir.pwd
331
+
332
+ require 'rubygems'
333
+
334
+ # if $0 is a file in the current directory, then restart
335
+ # it the same, otherwise add -S on there because it was
336
+ # picked up in PATH.
337
+ #
338
+ if File.exist?($0)
339
+ arg0 = [Gem.ruby, $0]
340
+ else
341
+ arg0 = [Gem.ruby, "-S", $0]
342
+ end
343
+
344
+ # Detect and reinject -Ilib from the command line
345
+ lib = File.expand_path "lib"
346
+ arg0[1,0] = ["-I", lib] if $:[0] == lib
347
+
348
+ if defined? Puma::WILD_ARGS
349
+ @restart_argv = arg0 + Puma::WILD_ARGS + @original_argv
350
+ else
351
+ @restart_argv = arg0 + @original_argv
352
+ end
353
+ end
354
+
355
+ def setup_signals
356
+ begin
357
+ Signal.trap "SIGUSR2" do
358
+ restart
359
+ end
360
+ rescue Exception
361
+ log "*** SIGUSR2 not implemented, signal based restart unavailable!"
362
+ end
363
+
364
+ begin
365
+ Signal.trap "SIGUSR1" do
366
+ phased_restart
367
+ end
368
+ rescue Exception
369
+ log "*** SIGUSR1 not implemented, signal based restart unavailable!"
370
+ end
371
+
372
+ begin
373
+ Signal.trap "SIGTERM" do
374
+ stop
375
+ end
376
+ rescue Exception
377
+ log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
378
+ end
379
+
380
+ begin
381
+ Signal.trap "SIGHUP" do
382
+ @runner.redirect_io
383
+ end
384
+ rescue Exception
385
+ log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
386
+ end
387
+
388
+ if Puma.jruby?
389
+ Signal.trap("INT") do
390
+ @status = :exit
391
+ graceful_stop
392
+ exit
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end
@@ -30,5 +30,20 @@ module Puma
30
30
  #
31
31
  def close
32
32
  end
33
+
34
+ # Always zero
35
+ #
36
+ def size
37
+ 0
38
+ end
39
+
40
+ def sync=(v)
41
+ end
42
+
43
+ def puts(*ary)
44
+ end
45
+
46
+ def write(*ary)
47
+ end
33
48
  end
34
49
  end
@@ -0,0 +1,91 @@
1
+ module Puma
2
+ class UnknownPlugin < RuntimeError; end
3
+
4
+ class PluginLoader
5
+ def initialize
6
+ @instances = []
7
+ end
8
+
9
+ def create(name)
10
+ if cls = Plugins.find(name)
11
+ plugin = cls.new(Plugin)
12
+ @instances << plugin
13
+ return plugin
14
+ end
15
+
16
+ raise UnknownPlugin, "File failed to register properly named plugin"
17
+ end
18
+
19
+ def fire_starts(launcher)
20
+ @instances.each do |i|
21
+ if i.respond_to? :start
22
+ i.start(launcher)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class PluginRegistry
29
+ def initialize
30
+ @plugins = {}
31
+ end
32
+
33
+ def register(name, cls)
34
+ @plugins[name] = cls
35
+ end
36
+
37
+ def find(name)
38
+ name = name.to_s
39
+
40
+ if cls = @plugins[name]
41
+ return cls
42
+ end
43
+
44
+ begin
45
+ require "puma/plugin/#{name}"
46
+ rescue LoadError
47
+ raise UnknownPlugin, "Unable to find plugin: #{name}"
48
+ end
49
+
50
+ if cls = @plugins[name]
51
+ return cls
52
+ end
53
+
54
+ raise UnknownPlugin, "file failed to register a plugin"
55
+ end
56
+ end
57
+
58
+ Plugins = PluginRegistry.new
59
+
60
+ class Plugin
61
+ def self.extract_name(ary)
62
+ path = ary.first.split(":").first
63
+
64
+ m = %r!puma/plugin/([^/]*)\.rb$!.match(path)
65
+ return m[1]
66
+ end
67
+
68
+ def self.create(&blk)
69
+ name = extract_name(caller)
70
+
71
+ cls = Class.new(self)
72
+
73
+ cls.class_eval(&blk)
74
+
75
+ Plugins.register name, cls
76
+ end
77
+
78
+ def initialize(loader)
79
+ @loader = loader
80
+ end
81
+
82
+ def in_background(&blk)
83
+ Thread.new(&blk)
84
+ end
85
+
86
+ def workers_supported?
87
+ return false if Puma.jruby? || Puma.windows?
88
+ true
89
+ end
90
+ end
91
+ end