mirror of
https://github.com/jlfwong/speedscope.git
synced 2024-11-23 14:37:09 +03:00
228 lines
6.3 KiB
Plaintext
228 lines
6.3 KiB
Plaintext
|
#!/usr/bin/env ruby
|
||
|
|
||
|
# Copyright (c) 2015 Gary Bernhardt
|
||
|
#
|
||
|
# MIT License
|
||
|
#
|
||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||
|
# a copy of this software and associated documentation files (the
|
||
|
# "Software"), to deal in the Software without restriction, including
|
||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||
|
# the following conditions:
|
||
|
#
|
||
|
# The above copyright notice and this permission notice shall be
|
||
|
# included in all copies or substantial portions of the Software.
|
||
|
#
|
||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
|
||
|
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 = "<pre>" + message + "</pre>"
|
||
|
end
|
||
|
|
||
|
def respond_to_dir(res, rel_path, local_abs_path)
|
||
|
res.content_type = "text/html"
|
||
|
res.body = (
|
||
|
"<p><h3>Listing for /#{rel_path}</h3></p>\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)
|
||
|
%{<a href="#{full_child_path_on_server}">#{child}</a><br>}
|
||
|
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
|