-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathserveit
executable file
·218 lines (186 loc) · 5.69 KB
/
serveit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#!/usr/bin/env ruby
require "find"
require "webrick"
require "open3"
require "optparse"
class ServeIt
VERSION = [0, 0, 3]
def self.main
serve_dir, port, ignored_paths, command = parse_opts
serve_dir = File.expand_path(serve_dir)
Server.new(serve_dir, port, ignored_paths, command).serve
end
def self.parse_opts
options = {:serve_dir => ".",
:ignored_paths => [],
:port => 8000}
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("-p", "--port PORT", Integer, "TCP port number to listen on") do |port|
options[:port] = port
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(:port), options.fetch(:ignored_paths), command]
end
class Server
def initialize(serve_dir, port, ignored_paths, command)
@mutex = Mutex.new
@serve_dir = serve_dir
@port = port
@command = command
@rebuilder = Rebuilder.new(@command, ignored_paths) if @command
end
def serve
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
elsif missing_symlink_target?(path)
next
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 missing_symlink_target?(path)
is_symlink = File.symlink?(path)
return false unless is_symlink
full_symlink_path = File.join(File.dirname(path), File.readlink(path))
return (not File.exist?(full_symlink_path))
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