diff --git a/middleman-core/lib/middleman-core/application.rb b/middleman-core/lib/middleman-core/application.rb index fe025677..0fe53343 100644 --- a/middleman-core/lib/middleman-core/application.rb +++ b/middleman-core/lib/middleman-core/application.rb @@ -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 @@ -262,4 +262,4 @@ module Middleman end end -end +end \ No newline at end of file diff --git a/middleman-core/lib/middleman-core/cli/server.rb b/middleman-core/lib/middleman-core/cli/server.rb index 7c8fc375..c43a507b 100644 --- a/middleman-core/lib/middleman-core/cli/server.rb +++ b/middleman-core/lib/middleman-core/cli/server.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/core_extensions/extensions.rb b/middleman-core/lib/middleman-core/core_extensions/extensions.rb index 8dc87bbc..646d4fcb 100644 --- a/middleman-core/lib/middleman-core/core_extensions/extensions.rb +++ b/middleman-core/lib/middleman-core/core_extensions/extensions.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb b/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb index 190c35f7..0e33af37 100644 --- a/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb +++ b/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb @@ -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] 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] 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 diff --git a/middleman-core/lib/middleman-core/core_extensions/request.rb b/middleman-core/lib/middleman-core/core_extensions/request.rb index 901cdf96..f16a408d 100644 --- a/middleman-core/lib/middleman-core/core_extensions/request.rb +++ b/middleman-core/lib/middleman-core/core_extensions/request.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server.rb b/middleman-core/lib/middleman-core/preview_server.rb new file mode 100644 index 00000000..1ecb1783 --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server.rb @@ -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] 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 \ No newline at end of file diff --git a/middleman-core/lib/middleman-core/watcher.rb b/middleman-core/lib/middleman-core/watcher.rb deleted file mode 100644 index 18ae6346..00000000 --- a/middleman-core/lib/middleman-core/watcher.rb +++ /dev/null @@ -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] 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] 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] 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