halcyon 0.3.7 → 0.3.18

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "lib")))
4
4
 
5
5
  include FileUtils
6
6
 
7
- require 'halcyon'
7
+ require 'lib/halcyon'
8
8
 
9
9
  project = {
10
10
  :name => "halcyon",
@@ -21,9 +21,16 @@ project = {
21
21
  --op rdoc
22
22
  --line-numbers
23
23
  --inline-source
24
- --title "Halcyon\ documentation"
25
- --exclude "^(_darcs|spec|pkg)/"
26
- ]
24
+ --title "Halcyon\ Documentation"
25
+ --exclude "^(_darcs|spec|pkg|.svn)/"
26
+ ],
27
+ :dependencies => {
28
+ 'json_pure' => '>=1.1.1',
29
+ 'rack' => '>=0.2.0',
30
+ 'merb' => '>=0.4.1'
31
+ },
32
+ :requirements => 'install the json gem to get faster JSON parsing',
33
+ :ruby_version_required => '>=1.8.6'
27
34
  }
28
35
 
29
36
  BASEDIR = File.expand_path(File.dirname(__FILE__))
@@ -43,10 +50,11 @@ spec = Gem::Specification.new do |s|
43
50
  s.executables = project[:bin_files]
44
51
  s.bindir = "bin"
45
52
  s.require_path = "lib"
46
- s.add_dependency('rack', '>=0.2.0')
47
- s.add_dependency('json', '>=1.1.1')
48
- s.add_dependency('merb', '>=0.4.1')
49
- s.required_ruby_version = '>= 1.8.6'
53
+ project[:dependencies].each{|dep|
54
+ s.add_dependency(dep[0], dep[1])
55
+ }
56
+ s.requirements << project[:requirements]
57
+ s.required_ruby_version = project[:ruby_version_required]
50
58
  s.files = (project[:rdoc_files] + %w[Rakefile] + Dir["{spec,lib}/**/*"]).uniq
51
59
  end
52
60
 
@@ -133,3 +141,8 @@ task :pushsite => [:rdoc] do
133
141
  sh "rsync -avz doc/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/doc/"
134
142
  sh "rsync -avz site/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/"
135
143
  end
144
+
145
+ desc "find . -name \"*.rb\" | xargs wc -l | grep total"
146
+ task :loc do
147
+ sh "find . -name \"*.rb\" | xargs wc -l | grep total"
148
+ end
data/bin/halcyon CHANGED
@@ -12,20 +12,14 @@
12
12
  # dependencies
13
13
  #++
14
14
 
15
- %w(optparse).each{|dep|require dep}
15
+ %w(rubygems halcyon/server optparse).each{|dep|require dep}
16
16
 
17
17
  #--
18
18
  # default options
19
19
  #++
20
20
 
21
21
  $debug = false
22
- options = {
23
- :environment => 'none',
24
- :port => 9267,
25
- :host => 'localhost',
26
- :server => 'mongrel',
27
- :log_file => '/tmp/halcyon.log'
28
- }
22
+ options = Halcyon::Server::DEFAULT_OPTIONS
29
23
 
30
24
  #--
31
25
  # parse options
@@ -35,15 +29,18 @@ opts = OptionParser.new("", 24, ' ') do |opts|
35
29
  opts.banner << "Halcyon, JSON Server Framework\n"
36
30
  opts.banner << "http://halcyon.rubyforge.org/\n"
37
31
  opts.banner << "\n"
38
- opts.banner << "Usage: halcyon [options] appname"
32
+ opts.banner << "Usage: halcyon [options] appname\n"
33
+ opts.banner << "\n"
34
+ opts.banner << "Put -c or --config first otherwise it will overwrite higher precedence options."
39
35
 
40
36
  opts.separator ""
41
37
  opts.separator "Options:"
42
38
 
43
- opts.on("-d", "--debug", "set debugging flags (set $debug to true)") { $debug = true }
39
+ opts.on("-d", "--debug", "set debugging flag (set $debug to true)") { $debug = true }
40
+ opts.on("-D", "--Debug", "enable verbose debugging (set $debug and $DEBUG to true)") { $debug = true; $DEBUG = true }
44
41
  opts.on("-w", "--warn", "turn warnings on for your script") { $-w = true }
45
42
 
46
- opts.on("-I", "--include PATH", "specify $LOAD_PATH (may be used more than once)") do |path|
43
+ opts.on("-I", "--include PATH", "specify $LOAD_PATH (multiples OK)") do |path|
47
44
  $:.unshift(*path.split(":"))
48
45
  end
49
46
 
@@ -51,8 +48,44 @@ opts = OptionParser.new("", 24, ' ') do |opts|
51
48
  require library
52
49
  end
53
50
 
54
- opts.on("-c", "--config PATH", "configuration stored in PATH") do |conf|
55
- options[:config_file] = conf
51
+ opts.on("-c", "--config PATH", "load configuration (YAML) from PATH") do |conf_file|
52
+ if File.exist?(conf_file)
53
+ require 'yaml'
54
+
55
+ # load the config file
56
+ begin
57
+ conf = YAML.load_file(conf_file)
58
+ rescue Errno::EACCES
59
+ abort("Can't access #{conf_file}, try 'sudo #{$0}'")
60
+ end
61
+
62
+ # store config file path so SIGHUP and SIGUSR2 will reload the config in case it changes
63
+ options[:config_file] = conf_file
64
+
65
+ # parse config
66
+ case conf
67
+ when String
68
+ # config file given was just the commandline options
69
+ ARGV.replace(conf.split)
70
+ opts.parse! ARGV
71
+ when Hash
72
+ conf.symbolize_keys!
73
+ options = options.merge(conf)
74
+ when Array
75
+ # TODO (MT) support multiple servers (or at least specifying which
76
+ # server's configuration to load)
77
+ warn "Your configuration file is setup for multiple servers. This is not a supported feature yet."
78
+ warn "However, we've pulled the first server entry as this server's configuration."
79
+ # an array of server configurations
80
+ # default to the first entry since multiple server configurations isn't
81
+ # precisely worked out yet.
82
+ options = options.merge(conf[0])
83
+ else
84
+ abort "Config file in an unsupported format. Config files must be YAML or the commandline flags"
85
+ end
86
+ else
87
+ abort "Config file failed to load. #{conf_file} was not found. Correct the path and try again."
88
+ end
56
89
  end
57
90
 
58
91
  opts.on("-s", "--server SERVER", "serve using SERVER (default: #{options[:server]})") do |serv|
@@ -71,6 +104,14 @@ opts = OptionParser.new("", 24, ' ') do |opts|
71
104
  options[:log_file] = log_file
72
105
  end
73
106
 
107
+ opts.on("-L", "--loglevel LEVEL", "log level (default: #{options[:log_level]})") do |log_file|
108
+ options[:log_level] = log_file
109
+ end
110
+
111
+ opts.on("-P", "--pidfile PATH", "save PID to PATH (default: #{options[:pid_file]})") do |log_file|
112
+ options[:pid_file] = log_file
113
+ end
114
+
74
115
  opts.on("-e", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: #{options[:environment]})") do |env|
75
116
  options[:environment] = env
76
117
  end
@@ -96,15 +137,6 @@ end
96
137
  abort "Halcyon needs an app to run. Try: halcyon -h" if ARGV.empty?
97
138
  options[:app] = ARGV.shift
98
139
 
99
- #--
100
- # load dependencies
101
- #++
102
-
103
- %w(rubygems rack).each{|dep|require dep}
104
-
105
- $:.unshift '/Users/mtodd/Sites/halcyon/trunk/lib/'
106
- %w(halcyon/server).each {|dep|require dep}
107
-
108
140
  #--
109
141
  # load app
110
142
  #++
@@ -113,15 +145,29 @@ if !File.exists?("#{options[:app]}.rb")
113
145
  abort "Halcyon did not find the app #{options[:app]}. Check your path and try again."
114
146
  end
115
147
 
116
- require options[:app]
117
- app = Object.const_get(File.basename(options[:app]).capitalize.gsub(/_([a-z])/){|m|m[1].chr.capitalize})
148
+ begin
149
+ require options[:app]
150
+ app = Object.const_get(File.basename(options[:app]).capitalize.gsub(/_([a-z])/){|m|m[1].chr.capitalize})
151
+ rescue NameError => e
152
+ abort "Unable to load #{File.basename(options[:app]).capitalize.gsub(/_([a-z])/){|m|m[1].chr.capitalize}}. Please ensure your server is so named."
153
+ end
118
154
 
119
155
  #--
120
156
  # prepare server
121
157
  #++
122
-
123
- require options[:server]
124
- server = Rack::Handler.const_get(options[:server].capitalize)
158
+ begin
159
+ server = Rack::Handler.const_get(options[:server].capitalize)
160
+ rescue NameError
161
+ servers = {
162
+ 'cgi' => 'CGI',
163
+ 'fastcgi' => 'FastCGI',
164
+ 'lsws' => 'LSWS',
165
+ 'mongrel' => 'Mongrel',
166
+ 'webrick' => 'WEBrick'
167
+ }
168
+ abort "Unsupported server (missing Rack Handler). Did you mean to specify #{options[:server]}?" unless servers.key? options[:server]
169
+ server = Rack::Handler.const_get(servers[options[:server]])
170
+ end
125
171
 
126
172
  #--
127
173
  # prepare app environment
@@ -188,7 +188,7 @@ module Halcyon
188
188
  req = Net::HTTP::Post.new(uri)
189
189
  req["Content-Type"] = CONTENT_TYPE
190
190
  req["User-Agent"] = USER_AGENT
191
- req.body = data.to_json
191
+ req.body = format_body(data)
192
192
  request(req)
193
193
  end
194
194
 
@@ -205,7 +205,7 @@ module Halcyon
205
205
  req = Net::HTTP::Put.new(uri)
206
206
  req["Content-Type"] = CONTENT_TYPE
207
207
  req["User-Agent"] = USER_AGENT
208
- req.body = data.to_json
208
+ req.body = format_body(data)
209
209
  request(req)
210
210
  end
211
211
 
@@ -215,19 +215,37 @@ module Halcyon
215
215
  # JSON, and return it to the caller. This is a private method because the
216
216
  # user/developer should be quite satisfied with the +get+, +post+, +put+,
217
217
  # and +delete+ methods.
218
+ #
219
+ # == Request Failures
220
+ #
221
+ # If the server responds with any kind of failure (anything with a status
222
+ # that isn't 200), Halcyon will in turn raise the respective exception
223
+ # (defined in Halcyon::Exceptions) which all inherit from
224
+ # +Halcyon::Client::Exceptions::Base+. It is up to the client to handle
225
+ # these exceptions specifically.
218
226
  def request(req)
219
227
  # prepare and send HTTP request
220
228
  res = Net::HTTP.start(@uri.host, @uri.port) {|http|http.request(req)}
229
+
230
+ # parse response
221
231
  body = JSON.parse(res.body)
222
232
  body.symbolize_keys! if body.respond_to? :symbolize_keys!
223
233
 
224
234
  # handle non-successes
225
235
  raise Halcyon::Client::Base::Exceptions.lookup(body[:status]).new unless res.kind_of? Net::HTTPSuccess
226
236
 
227
- # parse response
237
+ # return response
228
238
  body
229
239
  rescue Halcyon::Client::Base::Exceptions::Base => e
230
- puts "#{e.status}: #{e.error}"
240
+ # log exception if logger is in place
241
+ raise
242
+ end
243
+
244
+ # Formats the data of a POST or PUT request (the body) into an acceptable
245
+ # format according to Net::HTTP for sending through as a Hash.
246
+ def format_body(data)
247
+ data = {:body => data} unless data.is_a? Hash
248
+ data.map{|key,value|"&#{key}=#{value}&"}.join
231
249
  end
232
250
 
233
251
  end
@@ -16,7 +16,7 @@ module Halcyon
16
16
  # Base Halcyon Exception
17
17
  #++
18
18
 
19
- class Base < Exception #:nodoc:
19
+ class Base < StandardError #:nodoc:
20
20
  attr_accessor :status, :error
21
21
  def initialize(status, error)
22
22
  @status = status
@@ -31,7 +31,7 @@ module Halcyon
31
31
  Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error|
32
32
  status, body = http_error
33
33
  class_eval(
34
- "class #{body.gsub(/ /,'')} < Base\n"+
34
+ "class #{body.gsub(/( |\-)/,'')} < Base\n"+
35
35
  " def initialize(s=#{status}, e='#{body}')\n"+
36
36
  " super s, e\n"+
37
37
  " end\n"+
@@ -44,7 +44,7 @@ module Halcyon
44
44
  #++
45
45
 
46
46
  def self.lookup(status)
47
- self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/ /,'').to_sym)
47
+ self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/( |\-)/,''))
48
48
  end
49
49
 
50
50
  end
@@ -10,7 +10,13 @@ $:.unshift File.dirname(__FILE__)
10
10
  # dependencies
11
11
  #++
12
12
 
13
- %w(halcyon rubygems json).each {|dep|require dep}
13
+ %w(rubygems halcyon).each {|dep|require dep}
14
+ begin
15
+ require 'json/ext'
16
+ rescue LoadError => e
17
+ warn 'Using the Pure Ruby JSON... install the json gem to get faster JSON parsing.'
18
+ require 'json/pure'
19
+ end
14
20
 
15
21
  #--
16
22
  # module
@@ -26,7 +32,6 @@ module Halcyon
26
32
  # For documentation on using Halcyon, check out the Halcyon::Server::Base and
27
33
  # Halcyon::Client::Base classes which contain much more usage documentation.
28
34
  class Client
29
- VERSION.replace [0,2,12]
30
35
  def self.version
31
36
  VERSION.join('.')
32
37
  end
@@ -36,8 +41,9 @@ module Halcyon
36
41
  #++
37
42
 
38
43
  autoload :Base, 'halcyon/client/base'
39
- autoload :Exceptions, 'halcyon/client/exceptions'
40
44
  autoload :Router, 'halcyon/client/router'
41
45
 
42
46
  end
43
47
  end
48
+
49
+ %w(halcyon/client/exceptions).each {|dep|require dep}
@@ -10,10 +10,71 @@
10
10
  module Halcyon
11
11
  module Exceptions #:nodoc:
12
12
  HTTP_ERROR_CODES = {
13
- 403 => "Forbidden",
14
- 404 => "Not Found",
15
- 406 => "Not Acceptable",
16
- 415 => "Unsupported Media Type"
13
+ 400 => 'Bad Request',
14
+ 401 => 'Unauthorized',
15
+ 402 => 'Payment Required',
16
+ 403 => 'Forbidden',
17
+ 404 => 'Not Found',
18
+ 405 => 'Method Not Allowed',
19
+ 406 => 'Not Acceptable',
20
+ 407 => 'Proxy Authentication Required',
21
+ 408 => 'Request Time-out',
22
+ 409 => 'Conflict',
23
+ 410 => 'Gone',
24
+ 411 => 'Length Required',
25
+ 412 => 'Precondition Failed',
26
+ 413 => 'Request Entity Too Large',
27
+ 414 => 'Request-URI Too Large',
28
+ 415 => 'Unsupported Media Type',
29
+ 500 => 'Internal Server Error',
30
+ 501 => 'Not Implemented',
31
+ 502 => 'Bad Gateway',
32
+ 503 => 'Service Unavailable',
33
+ 504 => 'Gateway Time-out',
34
+ 505 => 'HTTP Version not supported'
17
35
  }
18
36
  end
19
37
  end
38
+
39
+ # Taken from Rack's definition:
40
+ # http://chneukirchen.org/darcs/darcsweb.cgi?r=rack;a=plainblob;f=/lib/rack/utils.rb
41
+ #
42
+ # HTTP_STATUS_CODES = {
43
+ # 100 => 'Continue',
44
+ # 101 => 'Switching Protocols',
45
+ # 200 => 'OK',
46
+ # 201 => 'Created',
47
+ # 202 => 'Accepted',
48
+ # 203 => 'Non-Authoritative Information',
49
+ # 204 => 'No Content',
50
+ # 205 => 'Reset Content',
51
+ # 206 => 'Partial Content',
52
+ # 300 => 'Multiple Choices',
53
+ # 301 => 'Moved Permanently',
54
+ # 302 => 'Moved Temporarily',
55
+ # 303 => 'See Other',
56
+ # 304 => 'Not Modified',
57
+ # 305 => 'Use Proxy',
58
+ # 400 => 'Bad Request',
59
+ # 401 => 'Unauthorized',
60
+ # 402 => 'Payment Required',
61
+ # 403 => 'Forbidden',
62
+ # 404 => 'Not Found',
63
+ # 405 => 'Method Not Allowed',
64
+ # 406 => 'Not Acceptable',
65
+ # 407 => 'Proxy Authentication Required',
66
+ # 408 => 'Request Time-out',
67
+ # 409 => 'Conflict',
68
+ # 410 => 'Gone',
69
+ # 411 => 'Length Required',
70
+ # 412 => 'Precondition Failed',
71
+ # 413 => 'Request Entity Too Large',
72
+ # 414 => 'Request-URI Too Large',
73
+ # 415 => 'Unsupported Media Type',
74
+ # 500 => 'Internal Server Error',
75
+ # 501 => 'Not Implemented',
76
+ # 502 => 'Bad Gateway',
77
+ # 503 => 'Service Unavailable',
78
+ # 504 => 'Gateway Time-out',
79
+ # 505 => 'HTTP Version not supported'
80
+ # }
@@ -16,12 +16,25 @@
16
16
  module Halcyon
17
17
  class Server
18
18
 
19
- DEFAULT_OPTIONS = {}
19
+ DEFAULT_OPTIONS = {
20
+ :environment => 'none',
21
+ :port => 9267,
22
+ :host => 'localhost',
23
+ :server => 'mongrel',
24
+ :pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid',
25
+ :log_file => '/var/log/halcyon.{app}.log',
26
+ :log_level => 'info',
27
+ :log_format => proc{|s,t,p,m|"#{s} [#{t.strftime("%Y-%m-%d %H:%M:%S")}] (#{$$}) #{p} :: #{m}\n"},
28
+ # handled internally
29
+ :acceptable_requests => [],
30
+ :acceptable_remotes => []
31
+ }
20
32
  ACCEPTABLE_REQUESTS = [
33
+ # ENV var to check, Regexp the value should match, the status code to return in case of failure, the message with the code
21
34
  ["HTTP_USER_AGENT", /JSON\/1\.1\.\d+ Compatible( \(en-US\) Halcyon\/(\d+\.\d+\.\d+) Client\/(\d+\.\d+\.\d+))?/, 406, 'Not Acceptable'],
22
35
  ["CONTENT_TYPE", /application\/json/, 415, 'Unsupported Media Type']
23
36
  ]
24
- ACCEPTABLE_REMOTES = ['localhost', '127.0.0.1']
37
+ ACCEPTABLE_REMOTES = ['localhost', '127.0.0.1', '0.0.0.0']
25
38
 
26
39
  # = Building Halcyon Server Apps
27
40
  #
@@ -86,7 +99,40 @@ module Halcyon
86
99
  # route actually matches, so it doesn't need any of the extra path to match
87
100
  # against.
88
101
  #
89
- # ==
102
+ # == The Filesystem
103
+ #
104
+ # It's important to note that the +halcyon+ commandline tool expects the to
105
+ # find your server inheriting +Halcyon::Server::Base+ with the same exact
106
+ # name as its filename, though with special rules.
107
+ #
108
+ # To clarify, when your server is stored in +app_server.rb+, it expects
109
+ # that your server's class name be +AppServer+ as it capitalizes each word
110
+ # and removes all underscores, etc.
111
+ #
112
+ # Keep this in mind when naming your class and your file.
113
+ #
114
+ # NOTE: This really isn't a necessary step if you write your own deployment
115
+ # script instead of using the +halcyon+ commandline tool (as it is simply
116
+ # a convenience tool). In such, feel free to name your server however you
117
+ # prefer and the file likewise.
118
+ #
119
+ # == Running Your Server On Your Own
120
+ #
121
+ # If you're wanting to run your server without the help of the +halcyon+
122
+ # commandline tool, you will simply need to initialize the server as you
123
+ # pass it to the Rack handler of choice along with any configuration
124
+ # options you desire.
125
+ #
126
+ # The following should be enough:
127
+ #
128
+ # Rack::Handler::Mongrel.run YourAppName.new(options), :Port => 9267
129
+ #
130
+ # Of course Halcyon already handles most of your dependencies for you, so
131
+ # don't worry about requiring Rack, et al. And again, the options are not
132
+ # mandatory as the default options are certainly acceptable.
133
+ #
134
+ # NOTE: If you want to provide debugging information, just set +$debug+ to
135
+ # +true+ and you should receive all the debugging information available.
90
136
  class Base
91
137
 
92
138
  #--
@@ -115,8 +161,52 @@ module Halcyon
115
161
  #
116
162
  # DO NOT try to call +to_json+ on the +body+ contents as this will cause
117
163
  # errors when trying to parse JSON.
164
+ #
165
+ # == Request and Response
166
+ #
167
+ # If you need access to the Request and Response, the instance variables
168
+ # +@req+ and +@res+ will be sufficient for you.
169
+ #
170
+ # If you need specific documentation for these objects, check the
171
+ # corresponding docs in the Rack documentation.
172
+ #
173
+ # == Requests and POST Data
174
+ #
175
+ # Most of your requests will have all the data it needs inside of the
176
+ # +params+ you receive for your action, but for POST and PUT requests
177
+ # (you are being RESTful, right?) you will need to retrieve your data
178
+ # from the +POST+ property of the +@req+ request. Here's how:
179
+ #
180
+ # @req.POST['key'] => "value"
181
+ #
182
+ # As you can see, keys specifically are strings and values as well. What
183
+ # this means is that your POST data that you send to the server needs to
184
+ # be careful to provide a flat Hash (if anything other than a Hash is
185
+ # passed, it is packed up into a hash similar to +{:body=>data}+) or at
186
+ # least send a complicated structure as a JSON object so that transport
187
+ # is clean. Resurrecting the object is still on your end for POST data
188
+ # (though this could change). Here's how you would reconstruct your
189
+ # special hash:
190
+ #
191
+ # value = JSON.parse(@req.POST['key'])
192
+ #
193
+ # That will take care of reconstructing your Hash.
194
+ #
195
+ # And that is essentially all you need to worry about for retreiving your
196
+ # POST contents. Sending POST contents should be documented well enough
197
+ # in Halcyon::Client::Base.
198
+ #
199
+ # == Logging
200
+ #
201
+ # Logging can be done by logging to +@logger+ when inside the scope of
202
+ # application instance (inside of your instance methods and modules).
203
+ #
204
+ # The +@env+ instance variable has been modified to include a
205
+ # +halcyon.logger+ property including the given logger. Use this for
206
+ # logging if you need to step outside of the scope of the current
207
+ # application instance (just be sure to pass @env along with you).
118
208
  def call(env)
119
- @start_time = Time.now if $debug
209
+ @time_started = Time.now
120
210
 
121
211
  # collect env information, create request and response objects, prep for dispatch
122
212
  # puts env.inspect if $debug # request information (huge)
@@ -124,18 +214,32 @@ module Halcyon
124
214
  @res = Rack::Response.new
125
215
  @req = Rack::Request.new(env)
126
216
 
127
- ACCEPTABLE_REMOTES.replace([@env["REMOTE_ADDR"]]) if $debug
217
+ # add the logger to the @env instance variable for global access if for
218
+ # some reason the environment needs to be passed outside of the
219
+ # instance
220
+ @env['halcyon.logger'] = @logger
221
+
222
+ # set the acceptable remotes to include the remote IP if debugging is enabled
223
+ @config[:acceptable_remotes] << @env["REMOTE_ADDR"] if $debug
128
224
 
129
225
  # pre run hook
130
- before_run(Time.now - @start_time) if respond_to? :before_run
226
+ before_run(Time.now - @time_started) if respond_to? :before_run
227
+
228
+ # prepare route and provide it for callers
229
+ route = Router.route(@env)
230
+ @env['halcyon.route'] = route
131
231
 
132
232
  # dispatch
133
- @res.write(run(Router.route(env)).to_json)
233
+ @res.write(run(route).to_json)
134
234
 
135
235
  # post run hook
136
- after_run(Time.now - @start_time) if respond_to? :after_run
236
+ after_run(Time.now - @time_started) if respond_to? :after_run
137
237
 
138
- puts "Served #{env['REQUEST_URI']} in #{(Time.now - @start_time)}" if $debug
238
+ @time_finished = Time.now - @time_started
239
+
240
+ # logs access in the following format: [200] / => index (0.0029s;343.79req/s)
241
+ req_time, req_per_sec = ((@time_finished*1e4).round.to_f/1e4), (((1.0/@time_finished)*1e2).round.to_f/1e2)
242
+ @logger.info "[#{@res.status}] #{@env['REQUEST_URI']} => #{route[:module].to_s}#{((route[:module].nil?) ? "" : "::")}#{route[:action]} (#{req_time}s;#{req_per_sec}req/s)"
139
243
 
140
244
  # finish request
141
245
  @res.finish
@@ -206,10 +310,10 @@ module Halcyon
206
310
  # (or one of its inheriters) instead of handling them manually.
207
311
  def run(route)
208
312
  # make sure the request meets our expectations
209
- ACCEPTABLE_REQUESTS.each do |req|
313
+ @config[:acceptable_requests].each do |req|
210
314
  raise Exceptions::Base.new(req[2], req[3]) unless @env[req[0]] =~ req[1]
211
315
  end
212
- raise Exceptions::Forbidden.new unless ACCEPTABLE_REMOTES.member? @env["REMOTE_ADDR"]
316
+ raise Exceptions::Forbidden.new unless @config[:acceptable_remotes].member? @env["REMOTE_ADDR"]
213
317
 
214
318
  # pull params
215
319
  params = route.reject{|key, val| [:action, :module].include? key}
@@ -234,7 +338,7 @@ module Halcyon
234
338
 
235
339
  res
236
340
  rescue Exceptions::Base => e
237
- # puts @env.inspect if $debug
341
+ @logger.warn "#{uri} => #{e.error}"
238
342
  # handles all content error exceptions
239
343
  @res.status = e.status
240
344
  {:status => e.status, :body => e.error}
@@ -246,24 +350,209 @@ module Halcyon
246
350
 
247
351
  # Called when the Handler gets started and stores the configuration
248
352
  # options used to start the server.
353
+ #
354
+ # Feel free to define initialize for your app (which is only called once
355
+ # per server instance), just be sure to call +super+.
356
+ #
357
+ # == PID File
358
+ #
359
+ # A PID file is created when the server is first initialized with the
360
+ # current process ID. Where it is located depends on the default option,
361
+ # the config file, the commandline option, and the debug status,
362
+ # increasing in precedence in that order.
363
+ #
364
+ # By default, the PID file is placed in +/var/run/+ and is named
365
+ # +halcyon.{server}.{app}.{port}.pid+ where +{server}+ is replaced by the
366
+ # running server, +{app}+ is the app name (suffixed with +#debug+ if
367
+ # running in debug mode), and +{port}+ being the server port (if there
368
+ # are multiple servers running, this helps clarify).
369
+ #
370
+ # There is an option to numerically label your server via the +{n}+
371
+ # value, but this is deprecated and will be removed soon. Using the
372
+ # +{port}+ option makes much more sense and creates much more meaning.
249
373
  def initialize(options = {})
250
- # debug mode handling
251
- if $debug
252
- puts "Entering debugging mode..."
253
- @logger = Logger.new(STDOUT)
254
- ACCEPTABLE_REQUESTS.replace([
255
- ["HTTP_USER_AGENT", /.*/, 406, 'Not Acceptable'],
256
- ["HTTP_USER_AGENT", /.*/, 415, 'Unsupported Media Type'] # content type isn't set when navigating via browser
257
- ])
258
- end
259
-
260
374
  # save configuration options
261
375
  @config = DEFAULT_OPTIONS.merge(options)
262
376
 
377
+ # apply name options to log_file and pid_file configs
378
+ apply_log_and_pid_file_name_options
379
+
380
+ # debug mode handling
381
+ enable_debugging if $debug
382
+
263
383
  # setup logging
264
- @logger ||= Logger.new(@config[:log_file])
384
+ setup_logging unless $debug
385
+
386
+ # setup request filtering
387
+ setup_request_filters unless $debug
388
+
389
+ # create PID file
390
+ @pid = File.new(@config[:pid_file].gsub('{n}', server_cluster_number), "w", 0644)
391
+ @pid << "#{$$}\n"; @pid.close
392
+
393
+ # log existence and ready status
394
+ @logger.info "PID file created. PID is #{$$}."
395
+ @logger.info "Started. Awaiting connectivity. Listening on #{@config[:port]}..."
396
+
397
+ # trap signals to die (when killed by the user) gracefully
398
+ finalize = Proc.new do
399
+ @logger.info "Shutting down #{$$}."
400
+ @logger.close
401
+ File.delete(@pid.path)
402
+ exit
403
+ end
404
+ # http://en.wikipedia.org/wiki/Signal_%28computing%29
405
+ %w(INT KILL TERM QUIT HUP).each{|sig|trap(sig, finalize)}
265
406
 
266
- puts "Started. Awaiting input. Listening on #{@config[:port]}..." if $debug
407
+ # listen for USR1 signals and toggle debugging accordingly
408
+ trap("USR1") do
409
+ if $debug
410
+ disable_debugging
411
+ else
412
+ enable_debugging
413
+ end
414
+ end
415
+ end
416
+
417
+ # Retreives the server cluster sequence number for the PID file.
418
+ #
419
+ # This is deprecated and will be removed soon, probably for the 0.4.0
420
+ # release. Use of the +{port}+ value is much more appropriate and
421
+ # meaningful.
422
+ def server_cluster_number
423
+ # if there are no +{n}+ references in the PID file name, then simply
424
+ # return 0 as the cluster number. (This is the preferred behavior and
425
+ # this test allows the method to fail fast. +{n}+ is deprecated and
426
+ # will be removed before 0.4.0 is released.)
427
+ return 0.to_s if @config[:pid_file]['{n}'].nil?
428
+
429
+ # warn users that they're using a deprecated convention.
430
+ warn "Your PID file name contains '{n}' (#{@config[:pid_file]}). This is deprecatd and will be removed by the 0.4.0 release. Use '{port}' instead."
431
+
432
+ # counts the number of PID files already existing.
433
+ server_count = Dir[@config[:pid_file].gsub('{n}','*')].length
434
+ # since the counting starts at 0, if the file with the count exists,
435
+ # then one of the lesser number servers isn't running, so check each
436
+ # PID file until the one not running is found.
437
+ # if no files exist, then 0 will be the count, which won't exist, so
438
+ # it will be the default number.
439
+ while File.exist?(@config[:pid_file].gsub('{n}',server_count.to_s))
440
+ server_count -= 1
441
+ end
442
+ # return that number.
443
+ server_count.to_s
444
+ end
445
+
446
+ # If the server receives a SIGUSR1 signal it will toggle debugging. This
447
+ # method is used to setup logging and the request handling methods for
448
+ # debugging.
449
+ def enable_debugging
450
+ $debug = true
451
+ # setup logger to STDOUT and log entering debugging mode
452
+ @logger = Logger.new(STDOUT)
453
+ @logger.progname = "#{self.class}#debug"
454
+ @logger.level = Logger::DEBUG
455
+ @logger.formatter = @config[:log_format]
456
+ @logger.info "Entering debugging mode..."
457
+
458
+ # set the PID file name to /tmp/ unless PID file already exists
459
+ @config[:pid_file] = '/tmp/halcyon.{server}.{app}.{port}.pid' unless @pid.is_a? File
460
+ apply_log_and_pid_file_name_options # reapply for {server}, {app}, and {port} to be set
461
+
462
+ # modify acceptable request's profiles
463
+ @config[:acceptable_requests] = [
464
+ ["HTTP_USER_AGENT", /.*/, 406, 'Not Acceptable'],
465
+ ["HTTP_USER_AGENT", /.*/, 415, 'Unsupported Media Type'] # content type isn't set when navigating via browser
466
+ ]
467
+ @logger.debug "ACCEPTABLE_REQUESTS modified to accept all User Agents (browsers)"
468
+ rescue Errno::EACCES
469
+ abort "Can't access #{@config[:pid_file]}, try 'sudo #{$0}'"
470
+ end
471
+
472
+ # Disables all of the affects of debugging mode and returns logging and
473
+ # request filtering back to normal.
474
+ #
475
+ # Refer to +enable_debugging+ for more information.
476
+ def disable_debugging
477
+ # disable logging and log leaving debugging mode
478
+ $debug = false
479
+ @logger.info "Leaving debugging mode."
480
+
481
+ # setup normal logging
482
+ setup_logging
483
+
484
+ # reenable request filtering
485
+ setup_request_filters
486
+ end
487
+
488
+ # Sets up logging based on the configuration options in +@config+, which
489
+ # is set (in order of lowest to highest precedence) in the default
490
+ # options, in the configuration file provided, on the commandline, and
491
+ # debug mode options.
492
+ #
493
+ # == Levels
494
+ #
495
+ # The accepted level values are as follows:
496
+ #
497
+ # debug
498
+ # info
499
+ # warn
500
+ # error
501
+ # fatal
502
+ # unknown
503
+ #
504
+ # These are the exact way you can refer to the logger level you'd like to
505
+ # log at from all points of option specification (listed above in order
506
+ # of ascending precedence).
507
+ #
508
+ # If a bogus value is entered, a warning will be issued and the value
509
+ # will be defaulted to 'debug'. (So don't mess up.)
510
+ def setup_logging
511
+ # get the logging level based on the name supplied
512
+ level = {
513
+ 'debug' => Logger::DEBUG,
514
+ 'info' => Logger::INFO,
515
+ 'warn' => Logger::WARN,
516
+ 'error' => Logger::ERROR,
517
+ 'fatal' => Logger::FATAL,
518
+ 'unknown' => Logger::UNKNOWN # wtf?
519
+ }[@config[:log_level]]
520
+ if level.nil?
521
+ warn "Logging level specified not acceptable. Defaulting to 'debug'. Check the documentation for the acceptable values."
522
+ @config[:log_level] = 'debug'
523
+ level = Logger::DEBUG
524
+ end
525
+
526
+ # setup the logger
527
+ @logger = Logger.new(@config[:log_file])
528
+ @logger.progname = self.class
529
+ @logger.level = level
530
+ @logger.formatter = @config[:log_format]
531
+ rescue Errno::EACCES
532
+ abort "Can't access #{@config[:log_file]}, try 'sudo #{$0}'"
533
+ end
534
+
535
+ # Sets up request filters based on User-Agent, Content-Type, and Remote
536
+ # IP/address values.
537
+ #
538
+ # Extracted from +initialize+ to reduce repetition.
539
+ def setup_request_filters
540
+ @config[:acceptable_requests] = ACCEPTABLE_REQUESTS
541
+ @config[:acceptable_remotes] = ACCEPTABLE_REMOTES
542
+ end
543
+
544
+ # Searches through the PID file name and the Log file name stored in the
545
+ # +@config+ variable for +{server}+, +{app}+, and +{port}+ values and
546
+ # sets them accordingly.
547
+ def apply_log_and_pid_file_name_options
548
+ # DEFAULT :pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid',
549
+ @config[:pid_file].gsub!('{server}', @config[:server])
550
+ @config[:pid_file].gsub!('{port}', @config[:port].to_s)
551
+ @config[:pid_file].gsub!('{app}', File.basename(@config[:app]))
552
+ # DEFAULT :log_file => '/var/log/halcyon.{app}.log',
553
+ @config[:log_file].gsub!('{server}', @config[:server])
554
+ @config[:log_file].gsub!('{port}', @config[:port].to_s)
555
+ @config[:log_file].gsub!('{app}', File.basename(@config[:app]))
267
556
  end
268
557
 
269
558
  # = Routing
@@ -378,7 +667,9 @@ module Halcyon
378
667
 
379
668
  # Returns the URI requested
380
669
  def uri
381
- @env['REQUEST_URI']
670
+ # special parsing is done to remove the protocol, host, and port that
671
+ # some Handlers leave in there. (Fixes inconsistencies.)
672
+ URI.parse(@env['REQUEST_URI']).path
382
673
  end
383
674
 
384
675
  # Returns the Request Method as a lowercase symbol
@@ -16,7 +16,7 @@ module Halcyon
16
16
  # Base Halcyon Exception
17
17
  #++
18
18
 
19
- class Base < Exception #:nodoc:
19
+ class Base < StandardError #:nodoc:
20
20
  attr_accessor :status, :error
21
21
  def initialize(status, error)
22
22
  @status = status
@@ -31,7 +31,7 @@ module Halcyon
31
31
  Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error|
32
32
  status, body = http_error
33
33
  class_eval(
34
- "class #{body.gsub(/ /,'')} < Base\n"+
34
+ "class #{body.gsub(/( |\-)/,'')} < Base\n"+
35
35
  " def initialize(s=#{status}, e='#{body}')\n"+
36
36
  " super s, e\n"+
37
37
  " end\n"+
@@ -44,7 +44,7 @@ module Halcyon
44
44
  #++
45
45
 
46
46
  def self.lookup(status)
47
- self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/ /,'').to_sym)
47
+ self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/( |\-)/,''))
48
48
  end
49
49
 
50
50
  end
@@ -75,7 +75,8 @@ module Halcyon
75
75
  # params list defined in the +to+ routing definition, opting for the
76
76
  # default route if no match is made.
77
77
  def self.route(env)
78
- uri = env['REQUEST_URI']
78
+ # pull out the path requested (WEBrick keeps the host and port and protocol in REQUEST_URI)
79
+ uri = URI.parse(env['REQUEST_URI']).path
79
80
 
80
81
  # prepare request
81
82
  path = (uri ? uri.split('?').first : '').sub(/\/+/, '/')
@@ -88,6 +89,7 @@ module Halcyon
88
89
  # make sure a route is returned even if no match is found
89
90
  if route[0].nil?
90
91
  #return default route
92
+ env['halcyon.logger'].debug "No route found. Using default."
91
93
  @@default_route
92
94
  else
93
95
  # params (including action and module if set) for the matching route
@@ -10,7 +10,13 @@ $:.unshift File.dirname(__FILE__)
10
10
  # dependencies
11
11
  #++
12
12
 
13
- %w(halcyon rubygems rack json).each {|dep|require dep}
13
+ %w(rubygems halcyon rack).each {|dep|require dep}
14
+ begin
15
+ require 'json/ext'
16
+ rescue LoadError => e
17
+ warn 'Using the Pure Ruby JSON... install the json gem to get faster JSON parsing.'
18
+ require 'json/pure'
19
+ end
14
20
 
15
21
  #--
16
22
  # module
@@ -35,7 +41,6 @@ module Halcyon
35
41
  # For documentation on using Halcyon, check out the Halcyon::Server::Base and
36
42
  # Halcyon::Client::Base classes which contain much more usage documentation.
37
43
  class Server
38
- VERSION.replace [0,3,7]
39
44
  def self.version
40
45
  VERSION.join('.')
41
46
  end
@@ -45,11 +50,13 @@ module Halcyon
45
50
  #++
46
51
 
47
52
  autoload :Base, 'halcyon/server/base'
48
- autoload :Exceptions, 'halcyon/server/exceptions'
49
53
  autoload :Router, 'halcyon/server/router'
50
54
 
51
55
  end
52
56
 
53
57
  end
54
58
 
55
- %w(server/exceptions).each {|dep|require dep}
59
+ # Loads the Exceptions class first which sets up all the dynamically generated
60
+ # exceptions used by the system. Must occur before Base is loaded since Base
61
+ # depends on it.
62
+ %w(halcyon/server/exceptions).each {|dep|require dep}
data/lib/halcyon.rb CHANGED
@@ -21,7 +21,7 @@ end
21
21
  #++
22
22
 
23
23
  module Halcyon
24
- VERSION = [0,3,7]
24
+ VERSION = [0,3,18]
25
25
  def self.version
26
26
  VERSION.join('.')
27
27
  end
@@ -42,13 +42,7 @@ module Halcyon
42
42
  def introduction
43
43
  abort "READ THE DAMNED RDOCS FOO"
44
44
  end
45
-
46
- #--
47
- # module dependencies
48
- #++
49
45
 
50
- autoload :Exceptions, 'halcyon/exceptions'
51
- autoload :Server, 'halcyon/server'
52
- autoload :Client, 'halcyon/client'
53
-
54
46
  end
47
+
48
+ %w(halcyon/exceptions).each {|dep|require dep}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: halcyon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.7
4
+ version: 0.3.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Todd
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2007-12-16 00:00:00 -05:00
12
+ date: 2007-12-31 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -22,22 +22,22 @@ dependencies:
22
22
  version: 0.2.0
23
23
  version:
24
24
  - !ruby/object:Gem::Dependency
25
- name: json
25
+ name: merb
26
26
  version_requirement:
27
27
  version_requirements: !ruby/object:Gem::Requirement
28
28
  requirements:
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: 1.1.1
31
+ version: 0.4.1
32
32
  version:
33
33
  - !ruby/object:Gem::Dependency
34
- name: merb
34
+ name: json_pure
35
35
  version_requirement:
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 0.4.1
40
+ version: 1.1.1
41
41
  version:
42
42
  description: A JSON App Server Framework
43
43
  email: chiology@gmail.com
@@ -76,9 +76,9 @@ rdoc_options:
76
76
  - --line-numbers
77
77
  - --inline-source
78
78
  - --title
79
- - "\"Halcyon documentation\""
79
+ - "\"Halcyon Documentation\""
80
80
  - --exclude
81
- - "\"^(_darcs|spec|pkg)/\""
81
+ - "\"^(_darcs|spec|pkg|.svn)/\""
82
82
  require_paths:
83
83
  - lib
84
84
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -93,8 +93,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  - !ruby/object:Gem::Version
94
94
  version: "0"
95
95
  version:
96
- requirements: []
97
-
96
+ requirements:
97
+ - install the json gem to get faster JSON parsing
98
98
  rubyforge_project:
99
99
  rubygems_version: 0.9.5
100
100
  signing_key: