-
-
Save andrewle/d41bd27b82410c225c4a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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