#!/usr/bin/env ruby require "find" require "webrick" require "open3" require "optparse" class ServeIt VERSION = [0, 0, 2] def self.main serve_dir, ignored_paths, command = parse_opts serve_dir = File.expand_path(serve_dir) Server.new(serve_dir, ignored_paths, command).serve end def self.parse_opts options = {:serve_dir => ".", :ignored_paths => []} parser = OptionParser.new do |opts| opts.banner = "Usage: #{$PROGRAM_NAME} [options] command" opts.on_tail("-s", "--serve-dir DIR", "Root directory for server") do |dir| options[:serve_dir] = dir end opts.on_tail("-i", "--ignore PATH", "Ignore changes to file or directory") do |path| options[:ignored_paths] << path end opts.on_tail("--version", "Show version") do |dir| puts ServeIt::VERSION.join('.') exit end end begin parser.parse!(ARGV) rescue OptionParser::InvalidOption => e $stderr.puts e $stderr.puts parser exit 1 end if ARGV.count == 0 command = nil elsif ARGV.count == 1 command = ARGV.fetch(0) else $stderr.write parser.to_s exit 1 end [options.fetch(:serve_dir), options.fetch(:ignored_paths), command] end class Server def initialize(serve_dir, ignored_paths, command) @mutex = Mutex.new @serve_dir = serve_dir @command = command @rebuilder = Rebuilder.new(@command, ignored_paths) if @command end def serve port = 8000 puts "Starting server at http://localhost:#{port}" server = WEBrick::HTTPServer.new(:Port => port) server.mount_proc '/' do |req, res| relative_path = req.path.sub(/^\//, '') local_abs_path = File.absolute_path(relative_path, @serve_dir) if relative_path == "favicon.ico" respond_to_favicon(res) else respond_to_path(res, relative_path, local_abs_path) end end trap 'INT' do server.shutdown end server.start end def respond_to_favicon(res) res.status = 404 end def respond_to_path(res, relative_path, local_abs_path) begin rebuild_if_needed rescue Rebuilder::RebuildFailed => e return respond_to_error(res, e.to_s) end if File.directory?(local_abs_path) respond_to_dir(res, relative_path, local_abs_path) else # We're building a file respond_to_file(res, local_abs_path) end end def respond_to_error(res, message) res.content_type = "text/html" res.body = "
" + message + "
" end def respond_to_dir(res, rel_path, local_abs_path) res.content_type = "text/html" res.body = ( "

Listing for /#{rel_path}

\n" + Dir.entries(local_abs_path).select do |child| child != "." end.sort.map do |child| full_child_path_on_server = File.join("/", rel_path, child) %{#{child}
} end.join("\n") ) end def respond_to_file(res, local_abs_path) res.body = File.read(local_abs_path) res.content_type = guess_content_type(local_abs_path) end def guess_content_type(path) extension = File.extname(path).sub(/^\./, '') WEBrick::HTTPUtils::DefaultMimeTypes.fetch(extension) do "application/octet-stream" end end def rebuild_if_needed # Webrick is multi-threaded; guard against concurrent builds @mutex.synchronize do if @rebuilder @rebuilder.rebuild_if_needed end end end end class Rebuilder def initialize(command, ignored_paths) @command = command @ignored_paths = ignored_paths @last_disk_state = nil end def rebuild_if_needed if disk_state != @last_disk_state stdout_and_stderr, success = rebuild if !success message = "Failed to build! Command output:\n\n" + stdout_and_stderr raise RebuildFailed.new(message) end # Get a new post-build disk state so we don't pick up changes made during # the build. @last_disk_state = disk_state [stdout_and_stderr, success] end end def rebuild puts "Running command: #{@command}" puts " begin build".rjust(80, "=") start_time = Time.now stdout_and_stderr, status = Open3.capture2e(@command) print stdout_and_stderr puts (" built in %.03fs" % (Time.now - start_time)).rjust(80, "=") [stdout_and_stderr, status.success?] end def disk_state start_time = Time.now paths = [] Find.find(".") do |path| if ignore_path?(path) Find.prune else paths << path end end paths.map do |path| [path, File.stat(path).mtime.to_s] end.sort.tap do puts (" scanned in %.03fs" % (Time.now - start_time)).rjust(80, "=") end end def ignore_path?(path) @ignored_paths.any? do |ignored_path| File.absolute_path(path) == File.absolute_path(ignored_path) end end class RebuildFailed < RuntimeError; end end end if __FILE__ == $PROGRAM_NAME ServeIt.main end