halcyon 0.3.7 → 0.3.18

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/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: