puma 2.16.0 → 3.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/{History.txt → History.md} +489 -70
  3. data/README.md +143 -174
  4. data/docs/architecture.md +36 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +1 -1
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/nginx.md +2 -2
  10. data/docs/plugins.md +28 -0
  11. data/docs/restart.md +39 -0
  12. data/docs/signals.md +56 -3
  13. data/docs/systemd.md +272 -0
  14. data/ext/puma_http11/extconf.rb +2 -0
  15. data/ext/puma_http11/http11_parser.c +291 -447
  16. data/ext/puma_http11/http11_parser.h +1 -0
  17. data/ext/puma_http11/http11_parser.java.rl +5 -5
  18. data/ext/puma_http11/http11_parser.rl +10 -9
  19. data/ext/puma_http11/http11_parser_common.rl +1 -1
  20. data/ext/puma_http11/io_buffer.c +8 -8
  21. data/ext/puma_http11/mini_ssl.c +64 -6
  22. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +113 -131
  23. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +9 -2
  24. data/ext/puma_http11/puma_http11.c +1 -0
  25. data/lib/puma/app/status.rb +9 -1
  26. data/lib/puma/binder.rb +90 -38
  27. data/lib/puma/cli.rb +134 -491
  28. data/lib/puma/client.rb +142 -4
  29. data/lib/puma/cluster.rb +132 -76
  30. data/lib/puma/commonlogger.rb +19 -20
  31. data/lib/puma/compat.rb +3 -7
  32. data/lib/puma/configuration.rb +206 -67
  33. data/lib/puma/const.rb +21 -31
  34. data/lib/puma/control_cli.rb +92 -103
  35. data/lib/puma/convenient.rb +23 -0
  36. data/lib/puma/daemon_ext.rb +6 -0
  37. data/lib/puma/detect.rb +10 -1
  38. data/lib/puma/dsl.rb +203 -45
  39. data/lib/puma/events.rb +22 -13
  40. data/lib/puma/io_buffer.rb +1 -1
  41. data/lib/puma/jruby_restart.rb +1 -2
  42. data/lib/puma/launcher.rb +431 -0
  43. data/lib/puma/minissl.rb +83 -4
  44. data/lib/puma/null_io.rb +19 -11
  45. data/lib/puma/plugin/tmp_restart.rb +34 -0
  46. data/lib/puma/plugin.rb +115 -0
  47. data/lib/puma/rack/backports/uri/common_193.rb +17 -13
  48. data/lib/puma/rack/builder.rb +3 -0
  49. data/lib/puma/rack/urlmap.rb +9 -8
  50. data/lib/puma/reactor.rb +18 -0
  51. data/lib/puma/runner.rb +43 -15
  52. data/lib/puma/server.rb +141 -35
  53. data/lib/puma/single.rb +16 -6
  54. data/lib/puma/state_file.rb +29 -0
  55. data/lib/puma/tcp_logger.rb +8 -1
  56. data/lib/puma/thread_pool.rb +60 -10
  57. data/lib/puma/util.rb +1 -5
  58. data/lib/puma.rb +13 -4
  59. data/lib/rack/handler/puma.rb +76 -29
  60. data/tools/jungle/README.md +12 -2
  61. data/tools/jungle/init.d/README.md +9 -2
  62. data/tools/jungle/init.d/puma +86 -59
  63. data/tools/jungle/init.d/run-puma +16 -1
  64. data/tools/jungle/rc.d/README.md +74 -0
  65. data/tools/jungle/rc.d/puma +61 -0
  66. data/tools/jungle/rc.d/puma.conf +10 -0
  67. data/tools/jungle/upstart/puma.conf +1 -1
  68. data/tools/trickletest.rb +1 -1
  69. metadata +28 -95
  70. data/COPYING +0 -55
  71. data/Gemfile +0 -13
  72. data/Manifest.txt +0 -74
  73. data/Rakefile +0 -158
  74. data/docs/config.md +0 -0
  75. data/lib/puma/capistrano.rb +0 -94
  76. data/lib/puma/rack/backports/uri/common_18.rb +0 -56
  77. data/lib/puma/rack/backports/uri/common_192.rb +0 -52
  78. data/puma.gemspec +0 -52
@@ -0,0 +1,431 @@
1
+ require 'puma/events'
2
+ require 'puma/detect'
3
+
4
+ require 'puma/cluster'
5
+ require 'puma/single'
6
+
7
+ require 'puma/const'
8
+
9
+ require 'puma/binder'
10
+
11
+ module Puma
12
+ # Puma::Launcher is the single entry point for starting a Puma server based on user
13
+ # configuration. It is responsible for taking user supplied arguments and resolving them
14
+ # with configuration in `config/puma.rb` or `config/puma/<env>.rb`.
15
+ #
16
+ # It is responsible for either launching a cluster of Puma workers or a single
17
+ # puma server.
18
+ class Launcher
19
+ KEYS_NOT_TO_PERSIST_IN_STATE = [
20
+ :logger, :lowlevel_error_handler,
21
+ :before_worker_shutdown, :before_worker_boot, :before_worker_fork,
22
+ :after_worker_boot, :before_fork, :on_restart
23
+ ]
24
+ # Returns an instance of Launcher
25
+ #
26
+ # +conf+ A Puma::Configuration object indicating how to run the server.
27
+ #
28
+ # +launcher_args+ A Hash that currently has one required key `:events`,
29
+ # this is expected to hold an object similar to an `Puma::Events.stdio`,
30
+ # this object will be responsible for broadcasting Puma's internal state
31
+ # to a logging destination. An optional key `:argv` can be supplied,
32
+ # this should be an array of strings, these arguments are re-used when
33
+ # restarting the puma server.
34
+ #
35
+ # Examples:
36
+ #
37
+ # conf = Puma::Configuration.new do |user_config|
38
+ # user_config.threads 1, 10
39
+ # user_config.app do |env|
40
+ # [200, {}, ["hello world"]]
41
+ # end
42
+ # end
43
+ # Puma::Launcher.new(conf, events: Puma::Events.stdio).run
44
+ def initialize(conf, launcher_args={})
45
+ @runner = nil
46
+ @events = launcher_args[:events] || Events::DEFAULT
47
+ @argv = launcher_args[:argv] || []
48
+ @original_argv = @argv.dup
49
+ @config = conf
50
+
51
+ @binder = Binder.new(@events)
52
+ @binder.import_from_env
53
+
54
+ @environment = conf.environment
55
+
56
+ # Advertise the Configuration
57
+ Puma.cli_config = @config if defined?(Puma.cli_config)
58
+
59
+ @config.load
60
+
61
+ @options = @config.options
62
+ @config.clamp
63
+
64
+ generate_restart_data
65
+
66
+ if clustered? && (Puma.jruby? || Puma.windows?)
67
+ unsupported 'worker mode not supported on JRuby or Windows'
68
+ end
69
+
70
+ if @options[:daemon] && Puma.windows?
71
+ unsupported 'daemon mode not supported on Windows'
72
+ end
73
+
74
+ Dir.chdir(@restart_dir)
75
+
76
+ prune_bundler if prune_bundler?
77
+
78
+ @environment = @options[:environment] if @options[:environment]
79
+ set_rack_environment
80
+
81
+ if clustered?
82
+ @events.formatter = Events::PidFormatter.new
83
+ @options[:logger] = @events
84
+
85
+ @runner = Cluster.new(self, @events)
86
+ else
87
+ @runner = Single.new(self, @events)
88
+ end
89
+ Puma.stats_object = @runner
90
+
91
+ @status = :run
92
+ end
93
+
94
+ attr_reader :binder, :events, :config, :options, :restart_dir
95
+
96
+ # Return stats about the server
97
+ def stats
98
+ @runner.stats
99
+ end
100
+
101
+ # Write a state file that can be used by pumactl to control
102
+ # the server
103
+ def write_state
104
+ write_pid
105
+
106
+ path = @options[:state]
107
+ return unless path
108
+
109
+ require 'puma/state_file'
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
+ previous_env =
168
+ if defined?(Bundler)
169
+ env = Bundler::ORIGINAL_ENV.dup
170
+ # add -rbundler/setup so we load from Gemfile when restarting
171
+ bundle = "-rbundler/setup"
172
+ env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
173
+ env
174
+ else
175
+ ENV.to_h
176
+ end
177
+
178
+ @config.clamp
179
+
180
+ @config.plugins.fire_starts self
181
+
182
+ setup_signals
183
+ set_process_title
184
+ @runner.run
185
+
186
+ case @status
187
+ when :halt
188
+ log "* Stopping immediately!"
189
+ when :run, :stop
190
+ graceful_stop
191
+ when :restart
192
+ log "* Restarting..."
193
+ ENV.replace(previous_env)
194
+ @runner.before_restart
195
+ restart!
196
+ when :exit
197
+ # nothing
198
+ end
199
+ end
200
+
201
+ # Return which tcp port the launcher is using, if it's using TCP
202
+ def connected_port
203
+ @binder.connected_port
204
+ end
205
+
206
+ def restart_args
207
+ cmd = @options[:restart_cmd]
208
+ if cmd
209
+ cmd.split(' ') + @original_argv
210
+ else
211
+ @restart_argv
212
+ end
213
+ end
214
+
215
+ private
216
+
217
+ def reload_worker_directory
218
+ @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
219
+ end
220
+
221
+ def restart!
222
+ @config.run_hooks :on_restart, self
223
+
224
+ if Puma.jruby?
225
+ close_binder_listeners
226
+
227
+ require 'puma/jruby_restart'
228
+ JRubyRestart.chdir_exec(@restart_dir, restart_args)
229
+ elsif Puma.windows?
230
+ close_binder_listeners
231
+
232
+ argv = restart_args
233
+ Dir.chdir(@restart_dir)
234
+ Kernel.exec(*argv)
235
+ else
236
+ redirects = {:close_others => true}
237
+ @binder.listeners.each_with_index do |(l, io), i|
238
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
239
+ redirects[io.to_i] = io.to_i
240
+ end
241
+
242
+ argv = restart_args
243
+ Dir.chdir(@restart_dir)
244
+ argv += [redirects] if RUBY_VERSION >= '1.9'
245
+ Kernel.exec(*argv)
246
+ end
247
+ end
248
+
249
+ def prune_bundler
250
+ return unless defined?(Bundler)
251
+ puma = Bundler.rubygems.loaded_specs("puma")
252
+ dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
253
+ puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
254
+
255
+ unless puma_lib_dir
256
+ log "! Unable to prune Bundler environment, continuing"
257
+ return
258
+ end
259
+
260
+ deps = puma.runtime_dependencies.map do |d|
261
+ spec = Bundler.rubygems.loaded_specs(d.name)
262
+ "#{d.name}:#{spec.version.to_s}"
263
+ end
264
+
265
+ log '* Pruning Bundler environment'
266
+ home = ENV['GEM_HOME']
267
+ Bundler.with_clean_env do
268
+ ENV['GEM_HOME'] = home
269
+ ENV['PUMA_BUNDLER_PRUNED'] = '1'
270
+ wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
271
+ args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
272
+ # Ruby 2.0+ defaults to true which breaks socket activation
273
+ args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
274
+ Kernel.exec(*args)
275
+ end
276
+ end
277
+
278
+ def log(str)
279
+ @events.log str
280
+ end
281
+
282
+ def clustered?
283
+ (@options[:workers] || 0) > 0
284
+ end
285
+
286
+ def unsupported(str)
287
+ @events.error(str)
288
+ raise UnsupportedOption
289
+ end
290
+
291
+ def graceful_stop
292
+ @runner.stop_blocked
293
+ log "=== puma shutdown: #{Time.now} ==="
294
+ log "- Goodbye!"
295
+ end
296
+
297
+ def set_process_title
298
+ Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
299
+ end
300
+
301
+ def title
302
+ buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
303
+ buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
304
+ buffer
305
+ end
306
+
307
+ def set_rack_environment
308
+ @options[:environment] = environment
309
+ ENV['RACK_ENV'] = environment
310
+ end
311
+
312
+ def environment
313
+ @environment
314
+ end
315
+
316
+ def prune_bundler?
317
+ @options[:prune_bundler] && clustered? && !@options[:preload_app]
318
+ end
319
+
320
+ def close_binder_listeners
321
+ @binder.listeners.each do |l, io|
322
+ io.close
323
+ uri = URI.parse(l)
324
+ next unless uri.scheme == 'unix'
325
+ File.unlink("#{uri.host}#{uri.path}")
326
+ end
327
+ end
328
+
329
+
330
+ def generate_restart_data
331
+ if dir = @options[:directory]
332
+ @restart_dir = dir
333
+
334
+ elsif Puma.windows?
335
+ # I guess the value of PWD is garbage on windows so don't bother
336
+ # using it.
337
+ @restart_dir = Dir.pwd
338
+
339
+ # Use the same trick as unicorn, namely favor PWD because
340
+ # it will contain an unresolved symlink, useful for when
341
+ # the pwd is /data/releases/current.
342
+ elsif dir = ENV['PWD']
343
+ s_env = File.stat(dir)
344
+ s_pwd = File.stat(Dir.pwd)
345
+
346
+ if s_env.ino == s_pwd.ino and (Puma.jruby? or s_env.dev == s_pwd.dev)
347
+ @restart_dir = dir
348
+ end
349
+ end
350
+
351
+ @restart_dir ||= Dir.pwd
352
+
353
+ # if $0 is a file in the current directory, then restart
354
+ # it the same, otherwise add -S on there because it was
355
+ # picked up in PATH.
356
+ #
357
+ if File.exist?($0)
358
+ arg0 = [Gem.ruby, $0]
359
+ else
360
+ arg0 = [Gem.ruby, "-S", $0]
361
+ end
362
+
363
+ # Detect and reinject -Ilib from the command line, used for testing without bundler
364
+ # cruby has an expanded path, jruby has just "lib"
365
+ lib = File.expand_path "lib"
366
+ arg0[1,0] = ["-I", lib] if [lib, "lib"].include?($LOAD_PATH[0])
367
+
368
+ if defined? Puma::WILD_ARGS
369
+ @restart_argv = arg0 + Puma::WILD_ARGS + @original_argv
370
+ else
371
+ @restart_argv = arg0 + @original_argv
372
+ end
373
+ end
374
+
375
+ def setup_signals
376
+ begin
377
+ Signal.trap "SIGUSR2" do
378
+ restart
379
+ end
380
+ rescue Exception
381
+ log "*** SIGUSR2 not implemented, signal based restart unavailable!"
382
+ end
383
+
384
+ unless Puma.jruby?
385
+ begin
386
+ Signal.trap "SIGUSR1" do
387
+ phased_restart
388
+ end
389
+ rescue Exception
390
+ log "*** SIGUSR1 not implemented, signal based restart unavailable!"
391
+ end
392
+ end
393
+
394
+ begin
395
+ Signal.trap "SIGTERM" do
396
+ graceful_stop
397
+
398
+ raise SignalException, "SIGTERM"
399
+ end
400
+ rescue Exception
401
+ log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
402
+ end
403
+
404
+ begin
405
+ Signal.trap "SIGINT" do
406
+ if Puma.jruby?
407
+ @status = :exit
408
+ graceful_stop
409
+ exit
410
+ end
411
+
412
+ stop
413
+ end
414
+ rescue Exception
415
+ log "*** SIGINT not implemented, signal based gracefully stopping unavailable!"
416
+ end
417
+
418
+ begin
419
+ Signal.trap "SIGHUP" do
420
+ if @runner.redirected_io?
421
+ @runner.redirect_io
422
+ else
423
+ stop
424
+ end
425
+ end
426
+ rescue Exception
427
+ log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
428
+ end
429
+ end
430
+ end
431
+ end
data/lib/puma/minissl.rb CHANGED
@@ -1,3 +1,8 @@
1
+ begin
2
+ require 'io/wait'
3
+ rescue LoadError
4
+ end
5
+
1
6
  module Puma
2
7
  module MiniSSL
3
8
  class Socket
@@ -11,6 +16,10 @@ module Puma
11
16
  @socket
12
17
  end
13
18
 
19
+ def closed?
20
+ @socket.closed?
21
+ end
22
+
14
23
  def readpartial(size)
15
24
  while true
16
25
  output = @engine.read
@@ -36,12 +45,29 @@ module Puma
36
45
  output
37
46
  end
38
47
 
39
- def read_nonblock(size)
48
+ def read_nonblock(size, *_)
49
+ # *_ is to deal with keyword args that were added
50
+ # at some point (and being used in the wild)
40
51
  while true
41
52
  output = engine_read_all
42
53
  return output if output
43
54
 
44
- data = @socket.read_nonblock(size)
55
+ begin
56
+ data = @socket.read_nonblock(size, exception: false)
57
+ if data == :wait_readable || data == :wait_writable
58
+ if @socket.to_io.respond_to?(data)
59
+ @socket.to_io.__send__(data)
60
+ elsif data == :wait_readable
61
+ IO.select([@socket.to_io])
62
+ else
63
+ IO.select(nil, [@socket.to_io])
64
+ end
65
+ elsif !data
66
+ return nil
67
+ else
68
+ break
69
+ end
70
+ end while true
45
71
 
46
72
  @engine.inject(data)
47
73
  output = engine_read_all
@@ -55,6 +81,8 @@ module Puma
55
81
  end
56
82
 
57
83
  def write(data)
84
+ return 0 if data.empty?
85
+
58
86
  need = data.bytesize
59
87
 
60
88
  while true
@@ -75,13 +103,52 @@ module Puma
75
103
  end
76
104
 
77
105
  alias_method :syswrite, :write
106
+ alias_method :<<, :write
107
+
108
+ # This is a temporary fix to deal with websockets code using
109
+ # write_nonblock. The problem with implementing it properly
110
+ # is that it means we'd have to have the ability to rewind
111
+ # an engine because after we write+extract, the socket
112
+ # write_nonblock call might raise an exception and later
113
+ # code would pass the same data in, but the engine would think
114
+ # it had already written the data in. So for the time being
115
+ # (and since write blocking is quite rare), go ahead and actually
116
+ # block in write_nonblock.
117
+ def write_nonblock(data, *_)
118
+ write data
119
+ end
78
120
 
79
121
  def flush
80
122
  @socket.flush
81
123
  end
82
124
 
125
+ def read_and_drop(timeout = 1)
126
+ return :timeout unless IO.select([@socket], nil, nil, timeout)
127
+ read_nonblock(1024)
128
+ :drop
129
+ rescue Errno::EAGAIN
130
+ # do nothing
131
+ :eagain
132
+ end
133
+
134
+ def should_drop_bytes?
135
+ @engine.init? || !@engine.shutdown
136
+ end
137
+
83
138
  def close
84
- @socket.close
139
+ begin
140
+ # Read any drop any partially initialized sockets and any received bytes during shutdown.
141
+ # Don't let this socket hold this loop forever.
142
+ # If it can't send more packets within 1s, then give up.
143
+ while should_drop_bytes?
144
+ return if read_and_drop(1) == :timeout
145
+ end
146
+ rescue IOError, SystemCallError
147
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
148
+ # nothing
149
+ ensure
150
+ @socket.close
151
+ end
85
152
  end
86
153
 
87
154
  def peeraddr
@@ -118,6 +185,11 @@ module Puma
118
185
  raise ArgumentError, "No such keystore file '#{keystore}'" unless File.exist? keystore
119
186
  @keystore = keystore
120
187
  end
188
+
189
+ def check
190
+ raise "Keystore not configured" unless @keystore
191
+ end
192
+
121
193
  else
122
194
  # non-jruby Context properties
123
195
  attr_reader :key
@@ -138,6 +210,11 @@ module Puma
138
210
  raise ArgumentError, "No such ca file '#{ca}'" unless File.exist? ca
139
211
  @ca = ca
140
212
  end
213
+
214
+ def check
215
+ raise "Key not configured" unless @key
216
+ raise "Cert not configured" unless @cert
217
+ end
141
218
  end
142
219
  end
143
220
 
@@ -156,6 +233,7 @@ module Puma
156
233
  end
157
234
 
158
235
  def accept
236
+ @ctx.check
159
237
  io = @socket.accept
160
238
  engine = Engine.server @ctx
161
239
 
@@ -163,6 +241,7 @@ module Puma
163
241
  end
164
242
 
165
243
  def accept_nonblock
244
+ @ctx.check
166
245
  io = @socket.accept_nonblock
167
246
  engine = Engine.server @ctx
168
247
 
@@ -170,7 +249,7 @@ module Puma
170
249
  end
171
250
 
172
251
  def close
173
- @socket.close
252
+ @socket.close unless @socket.closed? # closed? call is for Windows
174
253
  end
175
254
  end
176
255
  end
data/lib/puma/null_io.rb CHANGED
@@ -1,34 +1,42 @@
1
1
  module Puma
2
-
3
2
  # Provides an IO-like object that always appears to contain no data.
4
3
  # Used as the value for rack.input when the request has no body.
5
4
  #
6
5
  class NullIO
7
- # Always returns nil
8
- #
9
6
  def gets
10
7
  nil
11
8
  end
12
9
 
13
- # Never yields
14
- #
15
10
  def each
16
11
  end
17
12
 
18
- # Mimics IO#read with no data
13
+ # Mimics IO#read with no data.
19
14
  #
20
- def read(count=nil,buffer=nil)
15
+ def read(count = nil, _buffer = nil)
21
16
  (count && count > 0) ? nil : ""
22
17
  end
23
18
 
24
- # Does nothing
25
- #
26
19
  def rewind
27
20
  end
28
21
 
29
- # Does nothing
30
- #
31
22
  def close
32
23
  end
24
+
25
+ def size
26
+ 0
27
+ end
28
+
29
+ def eof?
30
+ true
31
+ end
32
+
33
+ def sync=(v)
34
+ end
35
+
36
+ def puts(*ary)
37
+ end
38
+
39
+ def write(*ary)
40
+ end
33
41
  end
34
42
  end
@@ -0,0 +1,34 @@
1
+ require 'puma/plugin'
2
+
3
+ Puma::Plugin.create do
4
+ def start(launcher)
5
+ path = File.join("tmp", "restart.txt")
6
+
7
+ orig = nil
8
+
9
+ # If we can't write to the path, then just don't bother with this plugin
10
+ begin
11
+ File.write(path, "") unless File.exist?(path)
12
+ orig = File.stat(path).mtime
13
+ rescue SystemCallError
14
+ return
15
+ end
16
+
17
+ in_background do
18
+ while true
19
+ sleep 2
20
+
21
+ begin
22
+ mtime = File.stat(path).mtime
23
+ rescue SystemCallError
24
+ # If the file has disappeared, assume that means don't restart
25
+ else
26
+ if mtime > orig
27
+ launcher.restart
28
+ break
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end