Merge pull request #436 from middleman/rack_reloader
Rely on Webrick directly for preview, makes quick app reloading simple.
This commit is contained in:
commit
c117415913
7 changed files with 211 additions and 228 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
161
middleman-core/lib/middleman-core/preview_server.rb
Normal file
161
middleman-core/lib/middleman-core/preview_server.rb
Normal 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
|
|
@ -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
|
Loading…
Reference in a new issue