From 9da1deec6d5a14659fc6baad63712644e3fd0bcf Mon Sep 17 00:00:00 2001
From: John Doty
Date: Sat, 28 Oct 2017 07:57:10 -0700
Subject: [PATCH] Serveit script
---
bin/serveit | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 203 insertions(+)
create mode 100755 bin/serveit
diff --git a/bin/serveit b/bin/serveit
new file mode 100755
index 0000000..77f4f37
--- /dev/null
+++ b/bin/serveit
@@ -0,0 +1,203 @@
+#!/usr/bin/env ruby
+
+require "find"
+require "webrick"
+require "open3"
+require "optparse"
+
+class ServeIt
+ VERSION = [0, 0, 1]
+ 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