Skip to content

Instantly share code, notes, and snippets.

@andrewle
Forked from ahoward/a.rb
Created May 30, 2014 17:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andrewle/d41bd27b82410c225c4a to your computer and use it in GitHub Desktop.
Save andrewle/d41bd27b82410c225c4a to your computer and use it in GitHub Desktop.
#! /usr/bin/env ruby
# this script can create a static build from virtually any rails' project. it
# has two simple requirements.
#
# 1) you have
#
# gem 'passenger'
#
# in your Gemfile
#
# 2) you have
#
# Rails.configuration.before_initialize do
#
# ActionController::Base.module_eval do
# before_filter :enforce_trailing_slash
#
# protected
# def enforce_trailing_slash
# ext = request.fullpath.split('.', 2)[1]
#
# if ext.nil? and request.format.to_s == 'text/html'
# url = request.original_url
#
# if request.get? && !url.ends_with?('/')
# redirect_to(url + '/')
# return
# end
# end
# end
# end
#
# end
#
# in config/initializers/trailing_slashes.rb
#
# after that all you need to do is run
#
# ~> ./script/build
#
# and you'll have a preview-able static cache of all reachable pages in
#
# public/builds/$UUID
#
# check
#
# public/builds/$UUID/wget.oe
#
# if the build fails for any reasons. examine the detailed output for '404' or '500', etc.
#
# the build will be suitable for deployment to s3, bitballoon, etc. and will
# include all images, assets, etc.
#
# make a build
@build = Build.new
@build.build!
puts @build.directory
BEGIN {
#
require 'fileutils'
require 'thread'
require 'socket'
require 'timeout'
require 'uri'
require 'open-uri'
require 'securerandom'
require 'rubygems'
require 'logger'
# awesome sauce static build support
#
class Build
#
attr_accessor :script_dir
attr_accessor :rails_root
attr_accessor :lib_dir
attr_accessor :directory
attr_accessor :uuid
attr_accessor :env
attr_accessor :passenger
attr_accessor :url
#
def initialize(*args, &block)
@options = args.last.is_a?(Hash) ? args.pop : {}
@logger = @options[:logger] || Logger.new(STDERR)
@script_dir = File.expand_path(File.dirname(__FILE__))
@rails_root = File.expand_path(File.dirname(@script_dir))
@lib_dir = File.join(@rails_root, 'lib')
@url = ENV['RAILS_URL']
@uuid = ENV['RAILS_BUILD'] || SecureRandom.uuid
@env = ENV['RAILS_BUILD_ENV'] || ENV['RAILS_ENV'] || 'development'
@passenger = 'bundle exec passenger'
Dir.chdir(@rails_root)
if File.exists?('./Gemfile')
require 'bundler/setup'
Bundler.setup(:require => false)
end
$LOAD_PATH.unshift(@lib_dir)
@directory = File.join(@rails_root, 'public', 'system', 'builds', @uuid)
ENV['RAILS_BUILD'] ||= @uuid
ENV['RAILS_BUILD_ENV'] ||= @env
end
#
def build!
log(:debug, "building #{ @directory }...")
log(:debug, "locking...")
lock!
log(:debug, "locked.")
start_passenger! unless @url
wget_mirror!
end
#
def lock!(max = 3600)
started_at = Time.now.to_f
loop do
if DATA.flock(File::LOCK_EX | File::LOCK_NB)
break
else
abort "could not obtain lock for #{ max } seconds" if((Time.now.to_f - started_at) > max)
sleep(rand)
end
end
end
#
def start_passenger!
validate_passenger_version!
@url =
nil
passenger_port =
nil
ports =
(3001 .. 4001).to_a
ports.each do |port|
next unless port_open?(port)
start_passenger =
"#{ @passenger } start --daemonize --environment #{ @env } --port #{ port } --max-pool-size 16 --min-instances 16"
passenger_output =
`#{ start_passenger } 2>&1`.strip
t = Time.now.to_f
timeout = 10
i = 0
loop do
i += 1
begin
url = "http://0.0.0.0:#{ port }"
open(url){|socket| socket.read}
@url = url
passenger_port = port
break
rescue Object => e
if i > 2
log :error, "#{ e.message }(#{ e.class })\n"
log :error, "#{ passenger_output }\n\n"
end
if((Time.now.to_f - t) > timeout)
abort("could not start passenger inside of #{ timeout } ;-/")
else
sleep(rand(0.42))
end
end
end
break if @url
end
# barf if passenger could not be started
#
unless @url
abort("could not start passenger on any of ports #{ ports.first } .. #{ ports.last }")
end
log(:info, "started passenger on #{ @url }")
# set assassins to ensure the passenger daemon never outlives the build script
# no matter how it is killed (even -9)
#
stop_passenger =
"#{ @passenger } stop --port #{ passenger_port }"
at_exit{
`#{ stop_passenger } >/dev/null 2>&1`
log(:info, "stopped passenger on #{ @url }")
}
pppid = Process.pid
unless fork
at_exit{ exit! }
Process.setsid
if fork
exit
else
loop do
begin
Process.kill(0, pppid)
rescue Object => e
if e.is_a?(Errno::ESRCH)
`#{ stop_passenger } >/dev/null 2>&1`
end
exit
end
sleep(1 + rand)
end
end
end
end
#
def validate_passenger_version!
abort("could not find passenger") unless `#{ @passenger }`.to_s =~ /phusion\s+passenger/i
passenger_version = `#{ passenger } --version 2>/dev/null`.to_s.match(/version\s+([0-9.]+)/).to_a.last
abort("could not find passenger") unless passenger_version
major = passenger_version.to_s.split('.').first.to_i
abort("could not find passenger >= 4") unless(major && major >= 4)
end
#
def port_open?(port, options = {})
seconds = options[:timeout] || 1
ip = options[:ip] || '0.0.0.0'
Timeout::timeout(seconds) do
begin
TCPSocket.new(ip, port).close
false
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
true
rescue Object
false
end
end
rescue Timeout::Error
false
end
#
def wget_mirror!
FileUtils.rm_rf(@directory)
FileUtils.mkdir_p(@directory)
Dir.chdir(@directory) do
mirrored = false
a = Time.now
wget = "wget -m -r -E -nH -l 42 --trust-server-names -P . #{ @url } > wget.oe 2>&1"
mirrored = system(wget)
b = Time.now
if mirrored
log(:info, "built site in #{ (b.to_f - a.to_f) }s")
else
log(:error, "failed to build site!")
end
end
end
#
def to_s
@directory.to_s
end
#
def log(level, *args, &block)
@logger.send(level, *args, &block)
end
end
}
__END__
__LOCK__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment