heel 0.6.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|