heel 0.6.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/{CHANGES → HISTORY} +3 -3
- data/LICENSE +1 -1
- data/README +28 -26
- data/bin/heel +5 -1
- data/data/error.rhtml +1 -1
- data/data/listing.rhtml +2 -2
- data/gemspec.rb +42 -0
- data/lib/heel.rb +18 -13
- data/lib/heel/configuration.rb +64 -0
- data/lib/heel/directory_indexer.rb +114 -0
- data/lib/heel/error_response.rb +43 -0
- data/lib/heel/logger.rb +42 -0
- data/lib/heel/mime_map.rb +87 -0
- data/lib/heel/rackapp.rb +142 -0
- data/lib/heel/request.rb +66 -0
- data/lib/heel/server.rb +253 -219
- data/lib/heel/version.rb +21 -16
- data/spec/configuration_spec.rb +20 -0
- data/spec/directory_indexer_spec.rb +34 -0
- data/spec/rackapp_spec.rb +43 -0
- data/spec/server_spec.rb +107 -108
- data/tasks/announce.rake +42 -0
- data/tasks/config.rb +103 -0
- data/tasks/distribution.rake +52 -0
- data/tasks/documentation.rake +35 -0
- data/tasks/rspec.rb +33 -0
- data/tasks/rubyforge.rb +52 -0
- data/tasks/utils.rb +85 -0
- metadata +52 -44
- data/lib/heel/dir_handler.rb +0 -310
- data/lib/heel/error_handler.rb +0 -30
- data/lib/heel/gemspec.rb +0 -56
- data/lib/heel/specification.rb +0 -128
- data/spec/dir_handler_spec.rb +0 -128
- data/spec/error_handler_spec.rb +0 -29
data/lib/heel/logger.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2007, 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. Licensed under the BSD license. See LICENSE for details
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'heel'
|
7
|
+
require 'rack'
|
8
|
+
|
9
|
+
module Heel
|
10
|
+
|
11
|
+
# wrapper around the rack common logger to open up the file and flush the logs
|
12
|
+
# this is invoked with a 'use' command in the Builder so a new instance of
|
13
|
+
# 'Logger' in created with each request, so we do all the heavy lifting in the
|
14
|
+
# meta class.
|
15
|
+
#
|
16
|
+
class Logger < ::Rack::CommonLogger
|
17
|
+
class << self
|
18
|
+
def log
|
19
|
+
# the log can get closed if daemonized, the at_exit will close it.
|
20
|
+
if @log.closed? then
|
21
|
+
@log = File.open(@log_file, "a")
|
22
|
+
end
|
23
|
+
@log
|
24
|
+
end
|
25
|
+
|
26
|
+
def log_file=(lf)
|
27
|
+
@log_file = lf
|
28
|
+
@log = File.open(@log_file, "a")
|
29
|
+
at_exit { @log.close unless @log.closed? }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(app)
|
34
|
+
super(app)
|
35
|
+
end
|
36
|
+
|
37
|
+
def <<(str)
|
38
|
+
Heel::Logger.log.write( str )
|
39
|
+
Heel::Logger.log.flush
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2007, 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. Licensed under the BSD license. See LICENSE for details
|
4
|
+
#++
|
5
|
+
#
|
6
|
+
require 'mime/types'
|
7
|
+
|
8
|
+
module Heel
|
9
|
+
|
10
|
+
# MimeMap is a Heel specific mime mapping utility. It is based upon
|
11
|
+
# MIME::Type and adds some additional mime types. It can also say what the
|
12
|
+
# icon name for a particular mime type is.
|
13
|
+
#
|
14
|
+
class MimeMap
|
15
|
+
class << self
|
16
|
+
def icons_by_mime_type
|
17
|
+
@icons_by_mime_type ||= {
|
18
|
+
"text/plain" => "page_white_text.png",
|
19
|
+
"image" => "picture.png",
|
20
|
+
"pdf" => "page_white_acrobat.png",
|
21
|
+
"xml" => "page_white_code.png",
|
22
|
+
"compress" => "compress.png",
|
23
|
+
"gzip" => "compress.png",
|
24
|
+
"zip" => "compress.png",
|
25
|
+
"application/xhtml+xml" => "xhtml.png",
|
26
|
+
"application/word" => "page_word.png",
|
27
|
+
"application/excel" => "page_excel.png",
|
28
|
+
"application/powerpoint" => "page_white_powerpoint.png",
|
29
|
+
"text/html" => "html.png",
|
30
|
+
"application" => "application.png",
|
31
|
+
"text" => "page_white_text.png",
|
32
|
+
:directory => "folder.png",
|
33
|
+
:default => "page_white.png",
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
# if any other mime types are needed, add them directly via the
|
38
|
+
# mime-types calls.
|
39
|
+
def additional_mime_types
|
40
|
+
@additional_mime_types ||= [
|
41
|
+
# [ content-type , [ array, of, filename, extentions] ]
|
42
|
+
["images/svg+xml", ["svg"]],
|
43
|
+
["video/x-flv", ["flv"]],
|
44
|
+
["application/x-shockwave-flash", ["swf"]],
|
45
|
+
["text/plain", ["rb", "rhtml"]],
|
46
|
+
]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
MimeMap.additional_mime_types.each do |mt|
|
52
|
+
if MIME::Types[mt.first].size == 0 then
|
53
|
+
type = MIME::Type.from_array(mt)
|
54
|
+
MIME::Types.add(type)
|
55
|
+
else
|
56
|
+
type = MIME::Types[mt.first].first
|
57
|
+
mt[1].each do |ext|
|
58
|
+
type.extensions << ext unless type.extensions.include?(ext)
|
59
|
+
end
|
60
|
+
# have to reindex if new extensions added
|
61
|
+
MIME::Types.index_extensions(type)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def default_mime_type
|
67
|
+
@default_mime_type ||= MIME::Types["application/octet-stream"].first
|
68
|
+
end
|
69
|
+
|
70
|
+
# returns the mime type of the file at a given pathname
|
71
|
+
#
|
72
|
+
def mime_type_of(f)
|
73
|
+
MIME::Types.of(f).first || default_mime_type
|
74
|
+
end
|
75
|
+
|
76
|
+
# return the icon name for a particular mime type
|
77
|
+
#
|
78
|
+
def icon_for(mime_type)
|
79
|
+
icon = nil
|
80
|
+
[:content_type, :sub_type, :media_type].each do |t|
|
81
|
+
icon = MimeMap.icons_by_mime_type[mime_type.send(t)]
|
82
|
+
return icon if icon
|
83
|
+
end
|
84
|
+
icon = MimeMap.icons_by_mime_type[:default]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/heel/rackapp.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2007, 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. Licensed under the BSD license. See LICENSE for details
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'heel'
|
7
|
+
require 'rack'
|
8
|
+
require 'rack/utils'
|
9
|
+
require 'coderay'
|
10
|
+
require 'coderay/helpers/file_type'
|
11
|
+
|
12
|
+
module Heel
|
13
|
+
class RackApp
|
14
|
+
|
15
|
+
attr_reader :document_root
|
16
|
+
attr_reader :directory_index_html
|
17
|
+
attr_reader :icon_url
|
18
|
+
attr_reader :highlighting
|
19
|
+
attr_reader :ignore_globs
|
20
|
+
|
21
|
+
|
22
|
+
def initialize(options = {})
|
23
|
+
@ignore_globs = options[:ignore_globs] ||= %w( *~ .htaccess . )
|
24
|
+
@document_root = options[:document_root] ||= Dir.pwd
|
25
|
+
@directory_listing_allowed = options[:directory_listing_allowed] ||= true
|
26
|
+
@directory_index_html = options[:directory_index_html] ||= "index.html"
|
27
|
+
@using_icons = options[:using_icons] ||= true
|
28
|
+
@icon_url = options[:icon_url] ||= "/heel_icons"
|
29
|
+
@highlighting = options[:highlighting] ||= false
|
30
|
+
@options = options
|
31
|
+
end
|
32
|
+
|
33
|
+
def directory_listing_allowed?
|
34
|
+
@directory_listing_allowed
|
35
|
+
end
|
36
|
+
|
37
|
+
def highlighting?
|
38
|
+
@highlighting
|
39
|
+
end
|
40
|
+
|
41
|
+
def mime_map
|
42
|
+
@mime_map ||= Heel::MimeMap.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def directory_index_template_file
|
46
|
+
@directory_index_template_file ||= Heel::Configuration.data_path("listing.rhtml")
|
47
|
+
end
|
48
|
+
|
49
|
+
def directory_indexer
|
50
|
+
indexer_ignore = ( ignore_globs + [ document_root] ).flatten
|
51
|
+
@directory_indexer ||= DirectoryIndexer.new( directory_index_template_file, @options )
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def should_ignore?(fname)
|
56
|
+
ignore_globs.each do |glob|
|
57
|
+
return true if ::File.fnmatch(glob,fname)
|
58
|
+
end
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
# formulate a directory index response
|
63
|
+
#
|
64
|
+
def directory_index_response(req)
|
65
|
+
response = ::Rack::Response.new
|
66
|
+
dir_index = File.join(req.request_path, directory_index_html)
|
67
|
+
if File.file?(dir_index) and File.readable?(dir_index) then
|
68
|
+
response['Content-Type'] = mime_map.mime_type_of(dir_index).to_s
|
69
|
+
response['Content-Length'] = File.size(dir_index).to_s
|
70
|
+
response.body = File.open(dir_index)
|
71
|
+
elsif directory_listing_allowed? then
|
72
|
+
body = directory_indexer.index_page_for(req)
|
73
|
+
response['Content-Type'] = 'text/html'
|
74
|
+
response['Content-Length'] = body.length.to_s
|
75
|
+
response.body << body
|
76
|
+
else
|
77
|
+
return ::Heel::ErrorResponse.new(req.path_info,"Directory index is forbidden", 403).finish
|
78
|
+
end
|
79
|
+
return response.finish
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# formulate a file content response. Possibly a coderay highlighted file if
|
84
|
+
# it is a type that code ray can deal with and the file is not already an
|
85
|
+
# html file.
|
86
|
+
#
|
87
|
+
def file_response(req)
|
88
|
+
code_ray_type = ::FileType[req.request_path, true]
|
89
|
+
response = ::Rack::Response.new
|
90
|
+
response['Last-Modified'] = req.stat.mtime.rfc822
|
91
|
+
|
92
|
+
if highlighting? and req.highlighting? and code_ray_type and (code_ray_type != :html) then
|
93
|
+
body = <<-EOM
|
94
|
+
<html>
|
95
|
+
<head>
|
96
|
+
<title>#{req.path_info}</title>
|
97
|
+
<!-- CodeRay syntax highlighting CSS -->
|
98
|
+
<link rel="stylesheet" href="/heel_css/coderay-cycnus.css" type="text/css" />
|
99
|
+
</head>
|
100
|
+
<body>
|
101
|
+
<div class="CodeRay">
|
102
|
+
<pre>
|
103
|
+
#{CodeRay.scan_file(req.request_path,:auto).html({:line_numbers => :inline})}
|
104
|
+
</pre>
|
105
|
+
</div>
|
106
|
+
</body>
|
107
|
+
</html>
|
108
|
+
EOM
|
109
|
+
response['Content-Type'] = 'text/html'
|
110
|
+
response['Content-Length'] = body.length.to_s
|
111
|
+
response.body << body
|
112
|
+
else
|
113
|
+
file_type = mime_map.mime_type_of(req.request_path)
|
114
|
+
response['Content-Type'] = file_type.to_s
|
115
|
+
response['Content-Length'] = req.stat.size.to_s
|
116
|
+
response.body = File.open(req.request_path)
|
117
|
+
end
|
118
|
+
return response.finish
|
119
|
+
end
|
120
|
+
|
121
|
+
# interface to rack, env is a hash
|
122
|
+
#
|
123
|
+
# returns [ status, headers, body ]
|
124
|
+
#
|
125
|
+
def call(env)
|
126
|
+
req = Heel::Request.new(env, document_root)
|
127
|
+
if req.get? then
|
128
|
+
if req.forbidden? or should_ignore?(req.request_path) then
|
129
|
+
return ErrorResponse.new(req.path_info,"You do not have permissionto view #{req.path_info}",403).finish
|
130
|
+
end
|
131
|
+
return ErrorResponse.new(req.path_info, "File not found: #{req.path_info}",403).finish unless req.found?
|
132
|
+
return directory_index_response(req) if req.for_directory?
|
133
|
+
return file_response(req) if req.for_file?
|
134
|
+
else
|
135
|
+
return ErrorResponse.new(req.path_info,
|
136
|
+
"Method #{req.request_method} Not Allowed. Only GET is honored.",
|
137
|
+
405,
|
138
|
+
{ "Allow" => "GET" }).finish
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/heel/request.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2007, 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. Licensed under the BSD license. See LICENSE for details
|
4
|
+
#++
|
5
|
+
#
|
6
|
+
require 'rack'
|
7
|
+
module Heel
|
8
|
+
# nothing more than a rack request with some additional methods and overriding
|
9
|
+
# where the erros get written
|
10
|
+
class Request < ::Rack::Request
|
11
|
+
|
12
|
+
attr_reader :root_dir
|
13
|
+
|
14
|
+
# Initialize the request with the environment and the root directory of the
|
15
|
+
# request
|
16
|
+
def initialize(env, root_dir)
|
17
|
+
super(env)
|
18
|
+
@root_dir = root_dir
|
19
|
+
end
|
20
|
+
|
21
|
+
# a stat of the file mentioned in the request path
|
22
|
+
#
|
23
|
+
def stat
|
24
|
+
@stat ||= ::File.stat(request_path)
|
25
|
+
end
|
26
|
+
|
27
|
+
# normalize the request path to the full file path of the request from the
|
28
|
+
# +root_dir+
|
29
|
+
#
|
30
|
+
def request_path
|
31
|
+
@request_path ||= ::File.expand_path(::File.join(root_dir, ::Rack::Utils.unescape(path_info)))
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
def base_uri
|
36
|
+
@base_uri ||= ::Rack::Utils.unescape(path_info)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# a request must be for something that below the root directory
|
41
|
+
#
|
42
|
+
def forbidden?
|
43
|
+
request_path.index(root_dir) != 0
|
44
|
+
end
|
45
|
+
|
46
|
+
# a request is only good for something that actually exists and is readable
|
47
|
+
#
|
48
|
+
def found?
|
49
|
+
File.exist?(request_path) and (stat.directory? or stat.file?) and stat.readable?
|
50
|
+
end
|
51
|
+
|
52
|
+
def for_directory?
|
53
|
+
stat.directory?
|
54
|
+
end
|
55
|
+
|
56
|
+
def for_file?
|
57
|
+
stat.file?
|
58
|
+
end
|
59
|
+
|
60
|
+
# was the highlighting parameter true or false?
|
61
|
+
#
|
62
|
+
def highlighting?
|
63
|
+
return !(%w[ off false ].include? self.GET['highlighting'].to_s.downcase)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/heel/server.rb
CHANGED
@@ -1,261 +1,295 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2007, 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. Licensed under the BSD license. See LICENSE for details
|
4
|
+
#++
|
5
|
+
|
1
6
|
require 'heel'
|
7
|
+
require 'thin'
|
2
8
|
require 'ostruct'
|
3
9
|
require 'launchy'
|
4
|
-
require 'tmpdir'
|
5
10
|
require 'fileutils'
|
11
|
+
require 'heel/rackapp'
|
6
12
|
|
7
13
|
module Heel
|
8
|
-
|
14
|
+
class Server
|
9
15
|
|
10
|
-
|
11
|
-
|
16
|
+
attr_accessor :options
|
17
|
+
attr_accessor :parsed_options
|
12
18
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
attr_reader :stdout
|
20
|
+
attr_reader :stderr
|
21
|
+
attr_reader :stdin
|
22
|
+
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# thank you Jamis - from Capistrano
|
26
|
+
def home_directory # :nodoc:
|
27
|
+
ENV["HOME"] ||
|
28
|
+
(ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") ||
|
23
29
|
"/"
|
24
|
-
|
25
|
-
|
26
|
-
def kill_existing_proc
|
27
|
-
Heel::Server.new.kill_existing_proc
|
28
|
-
end
|
29
|
-
end
|
30
|
+
end
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
def kill_existing_proc
|
33
|
+
Heel::Server.new.kill_existing_proc
|
34
|
+
end
|
35
|
+
end
|
33
36
|
|
34
|
-
|
35
|
-
|
36
|
-
@options = default_options
|
37
|
-
@parsed_options = ::OpenStruct.new
|
38
|
-
@parser = option_parser
|
39
|
-
@error_message = nil
|
37
|
+
def initialize(argv = [])
|
38
|
+
argv ||= []
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
set_io
|
41
|
+
|
42
|
+
@options = default_options
|
43
|
+
@parsed_options = ::OpenStruct.new
|
44
|
+
@parser = option_parser
|
45
|
+
@error_message = nil
|
46
|
+
|
47
|
+
begin
|
48
|
+
@parser.parse!(argv)
|
49
|
+
rescue ::OptionParser::ParseError => pe
|
50
|
+
msg = ["#{@parser.program_name}: #{pe}",
|
45
51
|
"Try `#{@parser.program_name} --help` for more information"]
|
46
|
-
|
47
|
-
|
48
|
-
|
52
|
+
@error_message = msg.join("\n")
|
53
|
+
end
|
54
|
+
end
|
49
55
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
56
|
+
def default_options
|
57
|
+
if @default_options.nil? then
|
58
|
+
@default_options = ::OpenStruct.new
|
59
|
+
@default_options.show_version = false
|
60
|
+
@default_options.show_help = false
|
61
|
+
@default_options.address = "0.0.0.0"
|
62
|
+
@default_options.port = 4331
|
63
|
+
@default_options.document_root = Dir.pwd
|
64
|
+
@default_options.daemonize = false
|
65
|
+
@default_options.highlighting = true
|
66
|
+
@default_options.kill = false
|
67
|
+
@default_options.launch_browser = true
|
68
|
+
end
|
69
|
+
return @default_options
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_directory
|
73
|
+
ENV["HEEL_DEFAULT_DIRECTORY"] || File.join(::Heel::Server.home_directory,".heel")
|
74
|
+
end
|
75
|
+
|
76
|
+
def pid_file
|
77
|
+
File.join(default_directory,"heel.pid")
|
78
|
+
end
|
79
|
+
|
80
|
+
def log_file
|
81
|
+
File.join(default_directory,"heel.log")
|
82
|
+
end
|
83
|
+
|
84
|
+
def option_parser
|
85
|
+
OptionParser.new do |op|
|
86
|
+
op.separator ""
|
87
|
+
|
88
|
+
op.on("-a", "--address ADDRESS", "Address to bind to",
|
89
|
+
" (default: #{default_options.address})") do |add|
|
90
|
+
@parsed_options.address = add
|
91
|
+
end
|
92
|
+
|
93
|
+
op.on("-d", "--daemonize", "Run daemonized in the background") do
|
94
|
+
raise ::OptionParser::ParseError, "Daemonizing is not supported on windows" if Thin.win?
|
95
|
+
@parsed_options.daemonize = true
|
72
96
|
end
|
73
|
-
|
74
|
-
|
75
|
-
|
97
|
+
|
98
|
+
op.on("-h", "--help", "Display this text") do
|
99
|
+
@parsed_options.show_help = true
|
76
100
|
end
|
77
101
|
|
78
|
-
|
79
|
-
|
80
|
-
|
102
|
+
op.on("-k", "--kill", "Kill an existing daemonized heel process") do
|
103
|
+
@parsed_options.kill = true
|
104
|
+
end
|
81
105
|
|
82
|
-
|
83
|
-
" (default: #{default_options.address})") do |add|
|
84
|
-
@parsed_options.address = add
|
85
|
-
end
|
86
|
-
|
87
|
-
op.on("-d", "--daemonize", "Run daemonized in the background") do
|
88
|
-
@parsed_options.daemonize = true
|
89
|
-
end
|
90
|
-
|
91
|
-
op.on("-h", "--help", "Display this text") do
|
92
|
-
@parsed_options.show_help = true
|
93
|
-
end
|
94
|
-
|
95
|
-
op.on("-k", "--kill", "Kill an existing daemonized heel process") do
|
96
|
-
@parsed_options.kill = true
|
97
|
-
end
|
98
|
-
|
99
|
-
op.on("--[no-]highlighting", "Turn on or off syntax highlighting",
|
106
|
+
op.on("--[no-]highlighting", "Turn on or off syntax highlighting",
|
100
107
|
" (default: on)") do |highlighting|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
108
|
+
@parsed_options.highlighting = highlighting
|
109
|
+
end
|
110
|
+
|
111
|
+
op.on("--[no-]launch-browser", "Turn on or off automatic browser launch",
|
105
112
|
" (default: on)") do |l|
|
106
|
-
|
107
|
-
|
113
|
+
@parsed_options.launch_browser = l
|
114
|
+
end
|
108
115
|
|
109
|
-
|
116
|
+
op.on("-p", "--port PORT", Integer, "Port to bind to",
|
110
117
|
" (default: #{default_options.port})") do |port|
|
111
|
-
|
112
|
-
|
118
|
+
@parsed_options.port = port
|
119
|
+
end
|
113
120
|
|
114
|
-
|
121
|
+
op.on("-r","--root ROOT",
|
115
122
|
"Set the document root"," (default: #{default_options.document_root})") do |document_root|
|
116
|
-
|
117
|
-
|
118
|
-
|
123
|
+
@parsed_options.document_root = File.expand_path(document_root)
|
124
|
+
raise ::OptionParser::ParseError, "#{@parsed_options.document_root} is not a valid directory" if not File.directory?(@parsed_options.document_root)
|
125
|
+
end
|
119
126
|
|
120
|
-
|
121
|
-
|
122
|
-
end
|
123
|
-
end
|
127
|
+
op.on("-v", "--version", "Show version") do
|
128
|
+
@parsed_options.show_version = true
|
124
129
|
end
|
130
|
+
end
|
131
|
+
end
|
125
132
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
133
|
+
def merge_options
|
134
|
+
options = default_options.marshal_dump
|
135
|
+
@parsed_options.marshal_dump.each_pair do |key,value|
|
136
|
+
options[key] = value
|
137
|
+
end
|
138
|
+
|
139
|
+
@options = OpenStruct.new(options)
|
140
|
+
end
|
141
|
+
|
142
|
+
# set the IO objects in a single method call. This is really only for testing
|
143
|
+
# instrumentation
|
144
|
+
def set_io(stdin = $stdin, stdout = $stdout ,setderr = $stderr)
|
145
|
+
@stdin = stdin
|
146
|
+
@stdout = stdout
|
147
|
+
@stderr = stderr
|
148
|
+
end
|
131
149
|
|
132
|
-
|
150
|
+
# if Version or Help options are set, then output the appropriate information instead of
|
151
|
+
# running the server.
|
152
|
+
def error_version_help_kill
|
153
|
+
if @parsed_options.show_version then
|
154
|
+
@stdout.puts "#{@parser.program_name}: version #{Heel::VERSION}"
|
155
|
+
exit 0
|
156
|
+
elsif @parsed_options.show_help then
|
157
|
+
@stdout.puts @parser.to_s
|
158
|
+
exit 0
|
159
|
+
elsif @error_message then
|
160
|
+
@stdout.puts @error_message
|
161
|
+
exit 1
|
162
|
+
elsif @parsed_options.kill then
|
163
|
+
kill_existing_proc
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# kill an already running background heel process
|
168
|
+
def kill_existing_proc
|
169
|
+
if File.exists?(pid_file) then
|
170
|
+
begin
|
171
|
+
pid = open(pid_file).read.to_i
|
172
|
+
@stdout.puts "Sending TERM to process #{pid}"
|
173
|
+
Process.kill("TERM", pid)
|
174
|
+
rescue Errno::ESRCH
|
175
|
+
@stdout.puts "Unable to kill process with pid #{pid}. Process does not exist. Removing stale pid file."
|
176
|
+
File.unlink(pid_file)
|
177
|
+
rescue Errno::EPERM
|
178
|
+
@stdout.puts "Unable to kill process with pid #{pid}. No permissions to kill process."
|
133
179
|
end
|
180
|
+
else
|
181
|
+
@stdout.puts "No pid file exists, no process to kill"
|
182
|
+
end
|
183
|
+
@stdout.puts "Done."
|
184
|
+
exit 0
|
185
|
+
end
|
134
186
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
187
|
+
# setup the directory that heel will use as the location to run from, where its logs will
|
188
|
+
# be stored and its PID file if backgrounded.
|
189
|
+
def setup_heel_dir
|
190
|
+
if not File.exists?(default_directory) then
|
191
|
+
FileUtils.mkdir_p(default_directory)
|
192
|
+
@stdout.puts "Created #{default_directory}"
|
193
|
+
@stdout.puts "heel's PID (#{pid_file}) and log file (#{log_file}) are stored here"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# make sure that if we are daemonizing the process is not running
|
198
|
+
def ensure_not_running
|
199
|
+
if options.daemonize and File.exist?(pid_file) then
|
200
|
+
@stdout.puts "ERROR: PID File #{pid_file} already exists. Heel may already be running."
|
201
|
+
@stdout.puts "ERROR: Check the Log file #{log_file}"
|
202
|
+
@stdout.puts "ERROR: Heel will not start until the .pid file is cleared (`heel --kill' to clean it up)."
|
203
|
+
exit 1
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def launch_browser
|
208
|
+
Thread.new do
|
209
|
+
print "Launching your browser"
|
210
|
+
if options.daemonize then
|
211
|
+
puts " at http://#{options.address}:#{options.port}/"
|
212
|
+
else
|
213
|
+
puts "..."
|
141
214
|
end
|
215
|
+
::Launchy.open("http://#{options.address}:#{options.port}/")
|
216
|
+
end
|
217
|
+
end
|
142
218
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
219
|
+
def thin_server
|
220
|
+
server = Thin::Server.new(options.address, options.port)
|
221
|
+
|
222
|
+
# overload the name of the process so it shows up as heel not thin
|
223
|
+
def server.name
|
224
|
+
"heel (v#{Heel::VERSION})"
|
225
|
+
end
|
226
|
+
|
227
|
+
server.pid_file = pid_file
|
228
|
+
server.log_file = log_file
|
229
|
+
|
230
|
+
app = Heel::RackApp.new({ :document_root => options.document_root,
|
231
|
+
:highlighting => options.highlighting})
|
232
|
+
|
233
|
+
Heel::Logger.log_file = log_file
|
234
|
+
server.app = Rack::Builder.new {
|
235
|
+
use Heel::Logger
|
236
|
+
map "/" do
|
237
|
+
run app
|
158
238
|
end
|
159
|
-
|
160
|
-
|
161
|
-
def kill_existing_proc
|
162
|
-
if File.exists?(pid_file) then
|
163
|
-
begin
|
164
|
-
pid = open(pid_file).read.to_i
|
165
|
-
@stdout.puts "Sending TERM to process #{pid}"
|
166
|
-
Process.kill("TERM", pid)
|
167
|
-
rescue Errno::ESRCH
|
168
|
-
@stdout.puts "Unable to kill process with pid #{pid}. Process does not exist. Removing stale pid file."
|
169
|
-
File.unlink(pid_file)
|
170
|
-
rescue Errno::EPERM
|
171
|
-
@stdout.puts "Unable to kill process with pid #{pid}. No permissions to kill process."
|
172
|
-
end
|
173
|
-
else
|
174
|
-
@stdout.puts "No pid file exists, no process to kill"
|
175
|
-
end
|
176
|
-
@stdout.puts "Done."
|
177
|
-
exit 0
|
239
|
+
map "/heel_css" do
|
240
|
+
run Rack::File.new(Heel::Configuration.data_path( "css" ))
|
178
241
|
end
|
179
|
-
|
180
|
-
|
181
|
-
# be stored and its PID file if backgrounded.
|
182
|
-
def setup_heel_dir
|
183
|
-
if not File.exists?(default_directory) then
|
184
|
-
FileUtils.mkdir_p(default_directory)
|
185
|
-
@stdout.puts "Created #{default_directory}"
|
186
|
-
@stdout.puts "heel's PID (#{pid_file}) and log file (#{log_file}) are stored here"
|
187
|
-
end
|
242
|
+
map "/heel_icons" do
|
243
|
+
run Rack::File.new(Heel::Configuration.data_path("famfamfam", "icons"))
|
188
244
|
end
|
189
245
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
stats = ::Mongrel::StatisticsFilter.new(:sample_rate => 1)
|
208
|
-
config = ::Mongrel::Configurator.new(:host => options.address, :port => options.port, :pid_file => c_pid_file) do
|
209
|
-
if c_background_me then
|
210
|
-
if File.exists?(c_pid_file) then
|
211
|
-
log "ERROR: PID File #{c_pid_file} already exists. Heel may already be running."
|
212
|
-
log "ERROR: Check the Log file #{c_log_file}"
|
213
|
-
log "ERROR: Heel will not start until the .pid file is cleared (`heel --kill' to clean it up)."
|
214
|
-
exit 1
|
215
|
-
end
|
216
|
-
daemonize({:cwd => c_default_dir, :log_file => c_log_file})
|
217
|
-
end
|
218
|
-
|
219
|
-
begin
|
220
|
-
|
221
|
-
listener do
|
222
|
-
uri "/", :handler => stats
|
223
|
-
uri "/", :handler => Heel::DirHandler.new({:document_root => c_document_root,
|
224
|
-
:highlighting => c_highlighting })
|
225
|
-
uri "/", :handler => Heel::ErrorHandler.new
|
226
|
-
uri "/heel_css", :handler => Heel::DirHandler.new({:document_root =>
|
227
|
-
File.join(APP_DATA_DIR, "css")})
|
228
|
-
uri "/heel_icons", :handler => Heel::DirHandler.new({ :document_root =>
|
229
|
-
File.join(APP_DATA_DIR, "famfamfam", "icons")})
|
230
|
-
uri "/heel_status", :handler => ::Mongrel::StatusHandler.new(:stats_filter => stats)
|
231
|
-
end
|
232
|
-
rescue Errno::EADDRINUSE
|
233
|
-
log "ERROR: Address (#{c_address}:#{c_port}) is already in use, please check running processes or run `heel --kill'"
|
234
|
-
exit 1
|
235
|
-
end
|
236
|
-
setup_signals
|
237
|
-
end
|
238
|
-
|
239
|
-
config.run
|
240
|
-
config.log "heel running at http://#{options.address}:#{options.port} with document root #{options.document_root}"
|
241
|
-
|
242
|
-
if c_background_me then
|
243
|
-
config.write_pid_file
|
246
|
+
}
|
247
|
+
|
248
|
+
server.app = Thin::Stats::Adapter.new(server.app, "/heel_stats")
|
249
|
+
|
250
|
+
return server
|
251
|
+
end
|
252
|
+
|
253
|
+
def start_thin_server
|
254
|
+
server = thin_server
|
255
|
+
|
256
|
+
server_thread = Thread.new do
|
257
|
+
begin
|
258
|
+
if options.daemonize then
|
259
|
+
if cpid = fork then
|
260
|
+
# wait for the top child of the server double fork to exit
|
261
|
+
Process.waitpid(cpid)
|
244
262
|
else
|
245
|
-
|
263
|
+
server.daemonize
|
264
|
+
server.start
|
246
265
|
end
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
::Launchy.open("http://#{options.address}:#{options.port}/")
|
266
|
+
else
|
267
|
+
begin
|
268
|
+
server.start
|
269
|
+
rescue RuntimeError
|
270
|
+
$stderr.puts "ERROR: Unable to start server. Heel may already be running. Please check running processes or run `heel --kill'"
|
271
|
+
exit 1
|
254
272
|
end
|
255
|
-
|
256
|
-
config.join
|
257
|
-
|
273
|
+
end
|
258
274
|
end
|
259
|
-
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
# run the heel server with the current options.
|
280
|
+
def run
|
281
|
+
|
282
|
+
error_version_help_kill
|
283
|
+
merge_options
|
284
|
+
setup_heel_dir
|
285
|
+
ensure_not_running
|
286
|
+
|
287
|
+
server_thread = start_thin_server
|
288
|
+
|
289
|
+
if options.launch_browser then
|
290
|
+
launch_browser.join
|
291
|
+
end
|
292
|
+
server_thread.join
|
260
293
|
end
|
294
|
+
end
|
261
295
|
end
|