Skip to content

Instantly share code, notes, and snippets.

@mwpastore
Last active November 13, 2015 17:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mwpastore/f9fd4d4274a6fa72e8b5 to your computer and use it in GitHub Desktop.
Save mwpastore/f9fd4d4274a6fa72e8b5 to your computer and use it in GitHub Desktop.
Rakefile task for Puma with Rerun
require 'bundler/setup'
namespace :server do
desc 'Start the Puma webserver in development mode (rerun, debug).'
task :debug do
# Delay requiring these to avoid Rake failures in production.
require 'file-tail'
require 'rerun'
require 'tempfile'
class Rerun::PumaRunner < Rerun::Runner
alias_method :stop_without_signal, :stop
def stop_with_signal(signal = nil)
_signal = @options[:signal]
@options[:signal] = signal
stop_without_signal
@options[:signal] = _signal
end
alias_method :stop, :stop_with_signal
end
rerun_options = %w'--restart --signal USR2' # or USR1 for phased restarts
rerun_command = 'puma --debug --quiet' \
' --redirect-stdout %<stdout>s' \
' --redirect-stderr %<stderr>s' \
' --redirect-append'
fail 'Must run task from top-level directory' \
unless Dir.pwd == __dir__
# Redirect Puma output to prevent errors writing to non-existent TTY.
tmpfile = Tempfile.new %w[rerun-puma- .log]
last_line = -1
at_exit do
# Make sure we show all lines of Puma output if the TTY is still there.
File.open(tmpfile) do |logfile|
logfile.extend File::Tail
logfile.return_if_eof = true
logfile.forward last_line + 1
logfile.tail { |line| puts line }
end
# Close and delete the temporary file.
tmpfile.close!
end
runner = Rerun::PumaRunner.new \
rerun_command % {
:stdout => tmpfile.path,
:stderr => tmpfile.path
},
Rerun::Options.parse(rerun_options)
trap 'SIGHUP' do
unless $stdin.tty? and not $stdin.closed?
# Prevent Ruby from trying to write to the now non-existent TTY.
[$stdout, $stderr].each { |fd| fd.reopen(File::NULL, 'w') }
end
runner.stop
exit false
end
runner.start
runner.join # this does not block
File.open(tmpfile) do |logfile|
logfile.extend File::Tail
logfile.interval = 10
logfile.tail do |line|
last_line += 1
puts line
end
end
end
end
@mwpastore
Copy link
Author

This is a lot of code for something ostensibly simple:

  1. Start Puma with rerun.
  2. Have rerun use SIGUSR2 to tell Puma when to restart the workers to pull in changes.
  3. Gracefully (and quickly) shutdown Puma when the user enters 'x' or 'q' or Ctrl-C.
  4. Gracefully shutdown Puma if the terminal suddenly disconnects.
  5. Show the user all of the expected output (and error messages) from Puma and rerun.

You'd think, "Just rerun -- puma, right?" Unfortunately, a lot of these things are mutually exclusive due to the way rerun works with a custom signal, quirks in the way Puma forks processes, spawns threads, and dups file descriptors, and Ruby's tendency to raise exceptions when stdout and/or stderr go away (exceptions that are raised from deep within rerun and Puma and therefore difficult for the humble user to catch and handle while still terminating things gracefully).

Again, the above is a lot of code, but it meets all of my requirements. Suggestions welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment