Merge pull request #436 from middleman/rack_reloader

Rely on Webrick directly for preview, makes quick app reloading simple.
This commit is contained in:
Thomas Reynolds 2012-05-19 22:01:39 -07:00
commit c117415913
7 changed files with 211 additions and 228 deletions

View file

@ -123,7 +123,7 @@ module Middleman
include Middleman::CoreExtensions::Extensions
# Basic Rack Request Handling
register Middleman::CoreExtensions::Request
include Middleman::CoreExtensions::Request
# Handle exceptions
register Middleman::CoreExtensions::ShowExceptions

View file

@ -1,4 +1,4 @@
require "middleman-core/watcher"
require "middleman-core/preview_server"
# CLI Module
module Middleman::Cli
@ -50,7 +50,7 @@ module Middleman::Cli
}
puts "== The Middleman is loading"
Middleman::Watcher.start(params)
Middleman::PreviewServer.start(params)
end
end

View file

@ -36,7 +36,7 @@ module Middleman
# Register extension
class << self
# @private
def included(app)
def registered(app)
# Using for version parsing
require "rubygems"
@ -50,6 +50,7 @@ module Middleman
app.send :include, InstanceMethods
app.delegate :configure, :to => :"self.class"
end
alias :included :registered
end
# Class methods

View file

@ -1,29 +1,40 @@
require "find"
require "set"
# API for watching file change events
module Middleman
module CoreExtensions
module FileWatcher
IGNORE_LIST = [
/^\.sass-cache\//,
/^\.git\//,
/^\.gitignore$/,
/^\.DS_Store$/,
/^build\//,
/^\.rbenv-.*$/,
/^Gemfile$/,
/^Gemfile\.lock$/,
/~$/
]
# Setup extension
class << self
# Once registered
def registered(app)
require "find"
require "middleman-core/watcher"
require "set"
app.extend ClassMethods
app.send :include, InstanceMethods
# Before parsing config, load the data/ directory
app.before_configuration do
data_path = File.join(self.root, self.data_dir)
self.files.reload_path(data_path) if File.exists?(data_path)
data_path = File.join(root, data_dir)
files.reload_path(data_path) if File.exists?(data_path)
end
# After config, load everything else
app.ready do
self.files.reload_path(self.root)
files.reload_path(root)
end
end
alias :included :registered
@ -45,19 +56,20 @@ module Middleman
# Access the file api
# @return [Middleman::CoreExtensions::FileWatcher::API]
def files
api = self.class.files
api.instance ||= self
api
@_files_api ||= API.new(self)
end
end
# Core File Change API class
class API
attr_accessor :instance, :known_paths
# Initialize api and internal path cache
def initialize
self.known_paths = Set.new
def initialize(app)
@app = app
@known_paths = Set.new
@_changed = []
@_deleted = []
end
# Add callback to be run on file change
@ -65,7 +77,6 @@ module Middleman
# @param [nil,Regexp] matcher A Regexp to match the change path against
# @return [Array<Proc>]
def changed(matcher=nil, &block)
@_changed ||= []
@_changed << [block, matcher] if block_given?
@_changed
end
@ -75,7 +86,6 @@ module Middleman
# @param [nil,Regexp] matcher A Regexp to match the deleted path against
# @return [Array<Proc>]
def deleted(matcher=nil, &block)
@_deleted ||= []
@_deleted << [block, matcher] if block_given?
@_deleted
end
@ -85,8 +95,9 @@ module Middleman
# @param [String] path The file that changed
# @return [void]
def did_change(path)
puts "== File Change: #{path}" if instance.logging? && !::Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
self.known_paths << path
return if IGNORE_LIST.any? { |r| path.match(r) }
puts "== File Change: #{path}" if @app.logging?
@known_paths << path
self.run_callbacks(path, :changed)
end
@ -95,8 +106,9 @@ module Middleman
# @param [String] path The file that was deleted
# @return [void]
def did_delete(path)
puts "== File Deletion: #{path}" if instance.logging? && !::Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
self.known_paths.delete(path)
return if IGNORE_LIST.any? { |r| path.match(r) }
puts "== File Deletion: #{path}" if @app.logging?
@known_paths.delete(path)
self.run_callbacks(path, :deleted)
end
@ -105,13 +117,12 @@ module Middleman
# @param [String] path The path to reload
# @return [void]
def reload_path(path)
relative_path = path.sub("#{self.instance.root}/", "")
subset = self.known_paths.select { |p| p.match(%r{^#{relative_path}}) }
relative_path = path.sub("#{@app.root}/", "")
subset = @known_paths.select { |p| p.match(%r{^#{relative_path}}) }
Find.find(path) do |path|
next if File.directory?(path)
next if Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
relative_path = path.sub("#{self.instance.root}/", "")
relative_path = path.sub("#{@app.root}/", "")
subset.delete(relative_path)
self.did_change(relative_path)
end if File.exists?(path)
@ -126,13 +137,12 @@ module Middleman
# @param [String] path The path to reload
# @return [void]
def find_new_files(path)
relative_path = path.sub("#{self.instance.root}/", "")
subset = self.known_paths.select { |p| p.match(%r{^#{relative_path}}) }
relative_path = path.sub("#{@app.root}/", "")
subset = @known_paths.select { |p| p.match(%r{^#{relative_path}}) }
Find.find(path) do |file|
next if File.directory?(file)
next if Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
relative_path = file.sub("#{self.instance.root}/", "")
relative_path = file.sub("#{@app.root}/", "")
self.did_change(relative_path) unless subset.include?(relative_path)
end if File.exists?(path)
end
@ -144,12 +154,10 @@ module Middleman
# @param [Symbol] callbacks_name The name of the callbacks method
# @return [void]
def run_callbacks(path, callbacks_name)
return if ::Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
self.send(callbacks_name).each do |callback, matcher|
next if path.match(%r{^#{self.instance.build_dir}/})
next if path.match(%r{^#{@app.build_dir}/})
next unless matcher.nil? || path.match(matcher)
self.instance.instance_exec(path, &callback)
@app.instance_exec(path, &callback)
end
end
end

View file

@ -1,3 +1,7 @@
# Built on Rack
require "rack"
require "rack/file"
module Middleman
module CoreExtensions
@ -8,9 +12,6 @@ module Middleman
class << self
# @private
def registered(app)
# Built on Rack
require "rack"
require "rack/file"
# CSSPIE HTC File
::Rack::Mime::MIME_TYPES['.html'] = 'text/x-component'
@ -18,7 +19,6 @@ module Middleman
# Let's serve all HTML as UTF-8
::Rack::Mime::MIME_TYPES['.html'] = 'text/html;charset=utf8'
::Rack::Mime::MIME_TYPES['.htm'] = 'text/html;charset=utf8'
app.extend ClassMethods
app.extend ServerMethods
@ -133,30 +133,8 @@ module Middleman
@@servercounter += 1
const_set("MiddlemanApplication#{@@servercounter}", Class.new(Middleman::Application))
end
# Creates a new Rack::Server
#
# @param [Hash] options to pass to Rack::Server.new
# @return [Rack::Server]
def start_server(options={})
opts = {
:Port => options[:port] || 4567,
:Host => options[:host] || "0.0.0.0",
:AccessLog => []
}
app_class = options[:app] ||= ::Middleman.server.inst
opts[:app] = app_class
require "webrick"
opts[:Logger] = WEBrick::Log::new(nil, 0) if !options[:logging]
opts[:server] = 'webrick'
server = ::Rack::Server.new(opts)
server.start
server
end
end
# Methods to be mixed-in to Middleman::Application
module InstanceMethods
# Backwards-compatibility with old request.path signature
@ -212,22 +190,6 @@ module Middleman
end
def call(env)
# Keep `__middleman__` messaging to this thread
if env["PATH_INFO"] == "/__middleman__"
if env["REQUEST_METHOD"] == "POST"
req = ::Rack::Request.new(env)
if req.params.has_key?("change")
self.files.did_change(req.params["change"])
elsif req.params.has_key?("delete")
self.files.did_delete(req.params["delete"])
end
end
res = ::Rack::Response.new
res.status = 200
return res.finish
end
dup.call!(env)
end

View file

@ -0,0 +1,161 @@
module Middleman
WINDOWS = !!(RUBY_PLATFORM =~ /(mingw|bccwin|wince|mswin32)/i) unless const_defined?(:WINDOWS)
module PreviewServer
DEFAULT_PORT = 4567
class << self
# Start an instance of Middleman::Application
# @return [void]
def start(options={})
require "webrick"
@first_run ||= true
app = ::Middleman::Application.server.inst do
if options[:environment]
set :environment, options[:environment]
end
if options[:debug]
set :logging, true
end
end
puts "== The Middleman is standing watch on port #{options[:port]||4567}"
@webrick_is_running ||= false
@webrick ||= setup_webrick(
options[:host] || "0.0.0.0",
options[:port] || DEFAULT_PORT,
options[:debug] || false
)
mount_instance(app)
if @first_run
@first_run = false
register_signal_handlers unless ::Middleman::WINDOWS
start_file_watcher unless options[:"disable-watcher"]
@webrick.start
end
end
# Detach the current Middleman::Application instance
# @return [void]
def stop
puts "== The Middleman is shutting down"
unmount_instance
end
# Simply stop, then start the server
# @return [void]
def reload
stop
start
end
# Stop the current instance, exit Webrick
# @return [void]
def shutdown
stop
@webrick.shutdown
end
private
def start_file_watcher
preview_server = self
# Watcher Library
require "listen"
listener = Listen.to(Dir.pwd, :relative_paths => true)
listener.change do |modified, added, removed|
added_and_modified = (modified + added)
if added_and_modified.length > 0
# See if the changed file is config.rb or lib/*.rb
return reload if needs_to_reload?(added_and_modified)
# Otherwise forward to Middleman
paths.each do |path|
@app.files.did_change(path)
end
end
if removed.length > 0
# See if the changed file is config.rb or lib/*.rb
return reload if needs_to_reload?(removed)
# Otherwise forward to Middleman
removed.each do |path|
@app.files.did_delete(path)
end
end
end
# Don't block this thread
listener.start(false)
end
# Trap the interupt signal and shut down smoothly
# @return [void]
def register_signal_handlers
trap("INT") { shutdown }
trap("TERM") { shutdown }
trap("QUIT") { shutdown }
end
# Initialize webrick
# @return [void]
def setup_webrick(host, port, is_logging)
@host = host
@port = port
http_opts = {
:BindAddress => @host,
:Port => @port,
:AccessLog => []
}
unless is_logging
http_opts[:Logger] = ::WEBrick::Log::new(nil, 0)
end
::WEBrick::HTTPServer.new(http_opts)
end
# Attach a new Middleman::Application instance
# @param [Middleman::Application] app
# @return [void]
def mount_instance(app)
@app = app
@webrick.mount "/", ::Rack::Handler::WEBrick, @app.class.to_rack_app
end
# Detach the current Middleman::Application instance
# @return [void]
def unmount_instance
@webrick.unmount "/"
@app = nil
end
# Whether the passed files are config.rb, lib/*.rb or helpers
# @param [Array<String>] paths Array of paths to check
# @return [Boolean] Whether the server needs to reload
def needs_to_reload?(paths)
paths.any? do |path|
path.match(%{^config\.rb}) || path.match(%r{^lib/^[^\.](.*)\.rb$}) || path.match(%r{^helpers/^[^\.](.*)_helper\.rb$})
end
end
end
end
end

View file

@ -1,149 +0,0 @@
# File changes are forwarded to the currently running app via HTTP
require "net/http"
require "fileutils"
module Middleman
WINDOWS = !!(RUBY_PLATFORM =~ /(mingw|bccwin|wince|mswin32)/i) unless const_defined?(:WINDOWS)
end
module Middleman
class Watcher
class << self
attr_accessor :singleton
def start(options)
self.singleton = new(options)
self.singleton.watch! unless options[:"disable-watcher"]
self.singleton.start
end
def ignore_list
[
/^\.sass-cache\//,
/^\.git\//,
/^\.gitignore$/,
/^\.DS_Store$/,
/^build\//,
/^\.rbenv-.*$/,
/^Gemfile$/,
/^Gemfile\.lock$/,
/~$/
]
end
end
def initialize(options)
@options = options
register_signal_handlers unless ::Middleman::WINDOWS
end
def watch!
local = self
# Watcher Library
require "listen"
listener = Listen.to(Dir.pwd, :relative_paths => true)
listener.change do |modified, added, removed|
added_and_modified = modified + added
if added_and_modified.length > 0
local.run_on_change(added_and_modified)
end
if removed.length > 0
local.run_on_deletion(removed)
end
end
# Don't block this thread
listener.start(false)
end
# Start an instance of Middleman::Application
# @return [void]
def start
env = (@options[:environment] || "development").to_sym
is_logging = @options.has_key?(:debug) && @options[:debug]
app = ::Middleman::Application.server.inst do
set :environment, env
set :logging, is_logging
end
app_rack = app.class.to_rack_app
opts = @options.dup
opts[:app] = app_rack
opts[:logging] = is_logging
puts "== The Middleman is standing watch on port #{opts[:port]||4567}"
::Middleman::Application.start_server(opts)
end
# Stop the forked Middleman
# @return [void]
def stop
puts "== The Middleman is shutting down"
# TODO: Figure out some way to actually unload the whole thing
# or maybe just re-exec this same thing
end
# Simply stop, then start
# @return [void]
def reload
stop
start
end
# What to do on file change
# @param [Array<String>] paths Array of paths that changed
# @return [void]
def run_on_change(paths)
# See if the changed file is config.rb or lib/*.rb
return reload if needs_to_reload?(paths)
# Otherwise forward to Middleman
paths.each do |path|
tell_server(:change => path) unless self.class.ignore_list.any? { |r| path.match(r) }
end
end
# What to do on file deletion
# @param [Array<String>] paths Array of paths that were removed
# @return [void]
def run_on_deletion(paths)
# See if the changed file is config.rb or lib/*.rb
return reload if needs_to_reload?(paths)
# Otherwise forward to Middleman
paths.each do |path|
tell_server(:delete => path) unless self.class.ignore_list.any? { |r| path.match(r) }
end
end
private
# Trap the interupt signal and shut down FSSM (and thus the server) smoothly
def register_signal_handlers
trap("INT") { stop; exit(0) }
trap("TERM") { stop }
trap("QUIT") { stop; exit(0) }
end
# Whether the passed files are config.rb, lib/*.rb or helpers
# @param [Array<String>] paths Array of paths to check
# @return [Boolean] Whether the server needs to reload
def needs_to_reload?(paths)
return false # disable reloading for now
paths.any? do |path|
path.match(%{^config\.rb}) || path.match(%r{^lib/^[^\.](.*)\.rb$}) || path.match(%r{^helpers/^[^\.](.*)_helper\.rb$})
end
end
# Send a message to the running server
# @param [Hash] params Keys to be hashed and sent to server
# @return [void]
def tell_server(params={})
uri = URI.parse("http://#{@options[:host]}:#{@options[:port]}/__middleman__")
Net::HTTP.post_form(uri, {}.merge(params))
end
end
end