Class: Halcyon::Application
- Inherits:
-
Object
- Object
- Halcyon::Application
- Includes:
- Hooks, Exceptions
- Defined in:
- lib/halcyon/application.rb,
lib/halcyon/application/hooks.rb,
lib/halcyon/application/router.rb
Overview
The core of Halcyon on the server side is the Halcyon::Application class which handles dispatching requests and responding with appropriate messages to the client (which can be specified).
Manages shutting down and starting up hooks, routing, dispatching, etc. Also restricts the requests to acceptable clients, defaulting to all.
Defined Under Namespace
Modules: Hooks Classes: Router
Constant Summary
Constants included from Exceptions
Class Attribute Summary collapse
-
.booted ⇒ Object
Used to keep track of whether the boot process has been run yet.
Class Method Summary collapse
-
.boot(&block) ⇒ Object
Runs through the bootup process.
-
.route ⇒ Object
Defines routes for the application.
Instance Method Summary collapse
-
#acceptable_request!(env) ⇒ Object
Filters unacceptable requests depending on the configuration of the
:allow_from
option. -
#call(env) ⇒ Object
Sets up the request and response objects for use in the controllers and dispatches requests.
-
#dispatch(env) ⇒ Object
Dispatches the controller and action according the routed request.
-
#filter_params_for_log(request, env) ⇒ Object
Assemble params for logging.
-
#initialize ⇒ Application
constructor
Initializes the app: * runs startup hooks * registers shutdown hooks.
Methods included from Hooks
Constructor Details
#initialize ⇒ Application
Initializes the app:
-
runs startup hooks
-
registers shutdown hooks
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/halcyon/application.rb', line 22 def initialize self.logger.info "Starting up..." Halcyon.hooks[:startup].each {|hook| hook.call(Halcyon.config) } # clean after ourselves and get prepared to start serving things self.logger.debug "Starting GC." GC.start self.logger.info "Started. PID is #{$$}" at_exit do self.logger.info "Shutting down #{$$}." Halcyon.hooks[:shutdown].each {|hook| hook.call(Halcyon.config) } self.logger.info "Done." end end |
Class Attribute Details
.booted ⇒ Object
Used to keep track of whether the boot process has been run yet.
272 273 274 |
# File 'lib/halcyon/application.rb', line 272 def booted @booted end |
Class Method Details
.boot(&block) ⇒ Object
Runs through the bootup process. This involves:
-
establishing configuration directives
-
loading required libraries
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/halcyon/application.rb', line 278 def boot(&block) Halcyon.config ||= Halcyon::Config.new # Set application name Halcyon.app = Halcyon.config[:app] || Halcyon.root.split('/').last.camel_case # Load configuration files (when available) Dir.glob(%w(config app).map{|conf|Halcyon.paths[:config]/conf+'.{yml,yaml}'}).each do |config_file| Halcyon.config.load_from(config_file) if File.exist?(config_file) end # Run configuration files (when available) # These are unique in that they are Ruby files that we require so we # can get rid of YAML config files and use Ruby configuration files. Dir.glob(%w(init config environment).map{|file| Halcyon.paths[:config]/file+'.rb' }).each do |config_file| require config_file if File.exist?(config_file) end # Yield to the block to handle boot configuration (and other tasks). Halcyon.config.use(&block) if block_given? # Setup logger if Halcyon.config[:logger] Halcyon.config[:logging] = (Halcyon.config[:logging] || Halcyon::Config.defaults[:logging]).merge({ :type => Halcyon.config[:logger].class.to_s, :logger => Halcyon.config[:logger] }) end Halcyon::Logging.set(Halcyon.config[:logging][:type]) Halcyon.logger = Halcyon::Logger.setup(Halcyon.config[:logging]) # Run initializers Dir.glob(%w(requires hooks routes *).map{|init|Halcyon.paths[:init]/init+'.rb'}).uniq.each do |initializer| self.logger.debug "Init: #{File.basename(initializer).chomp('.rb').camel_case}" if require initializer.chomp('.rb') end # Setup autoloads for Controllers found in Halcyon.root/'app' (by default) Dir.glob([Halcyon.paths[:controller]/'application.rb', Halcyon.paths[:controller]/'*.rb']).uniq.each do |controller| self.logger.debug "Load: #{File.basename(controller).chomp('.rb').camel_case} Controller" if require controller.chomp('.rb') end # Setup autoloads for Models found in Halcyon.root/'app'/'models' (by default) Dir.glob(Halcyon.paths[:model]/'*.rb').each do |model| self.logger.debug "Load: #{File.basename(model).chomp('.rb').camel_case} Model" if require model.chomp('.rb') end # Set to loaded so additional calls to boot are ignored (unless # forcefully loaded by ignoring this value). self.booted = true end |
.route ⇒ Object
Defines routes for the application.
Refer to Halcyon::Application::Router for documentation and resources.
259 260 261 262 263 264 265 |
# File 'lib/halcyon/application.rb', line 259 def route if block_given? Router.prepare do |router| Router.default_to yield(router) || {:controller => 'application', :action => 'not_found'} end end end |
Instance Method Details
#acceptable_request!(env) ⇒ Object
Filters unacceptable requests depending on the configuration of the :allow_from
option.
This method is not directly called by the user, instead being called in the #call method.
Acceptable values include:
<tt>:all</tt>:: allow every request to go through
<tt>:halcyon_clients</tt>:: only allow Halcyon clients
<tt>:local</tt>:: do not allow for requests from an outside host
Raises Forbidden
227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/halcyon/application.rb', line 227 def acceptable_request!(env) case Halcyon.config[:allow_from].to_sym when :all # allow every request to go through when :halcyon_clients # only allow Halcyon clients raise Forbidden.new unless env['USER_AGENT'] =~ /JSON\/1\.1\.\d+ Compatible \(en-US\) Halcyon::Client\(\d+\.\d+\.\d+\)/ when :local # do not allow for requests from an outside host raise Forbidden.new unless ['localhost', '127.0.0.1', '0.0.0.0'].member? env["REMOTE_ADDR"] else logger.warn "Unrecognized allow_from configuration value (#{Halcyon.config[:allow_from].to_s}); use all, halcyon_clients, or local. Allowing all requests." end end |
#call(env) ⇒ Object
Sets up the request and response objects for use in the controllers and dispatches requests. Renders response data as JSON for response.
+env+ the request environment details
The internal router (which inherits from the Merb router) is sent the request to pass back the route for the dispatcher to call. This route is stored in env['halcyon.route']
(as per the Rack spec).
Configs
<tt>Halcyon.config[:allow_from]</tt> #=> (default) <tt>:all</tt>
:all => does not restrict requests from any User-Agent
:local => restricts requests to only local requests (from
localhost, 0.0.0.0, 127.0.0.1)
:halcyon_clients => restricts to only Halcyon clients (identified by
User-Agent)
Exceptions
If a request raises an exception that inherits from
<tt>Halcyon::Exceptions::Base</tt> (<tt>NotFound</tt>, etc), then the
response is sent with this information.
If a request raises any other kind of <tt>Exception</tt>, it is logged
as an error and a <tt>500 Internal Server Error</tt> is returned.
Returns [Fixnum:status, => String:value, [String:body].to_json]
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/halcyon/application.rb', line 65 def call(env) timing = {:started => Time.now} request = Rack::Request.new(env) response = Rack::Response.new response['Content-Type'] = "application/json" response['User-Agent'] = "JSON/#{JSON::VERSION} Compatible (en-US) Halcyon::Application/#{Halcyon.version}" begin acceptable_request!(env) env['halcyon.route'] = Router.route(request) result = dispatch(env) rescue Exceptions::Base => e result = {:status => e.status, :body => e.body} self.logger.info e. rescue Exception => e result = {:status => 500, :body => 'Internal Server Error'} self.logger.error "#{e.}\n\t" << e.backtrace.join("\n\t") end # This handles various sorts of response formats. # # The primary format is <tt>{:status => 200, :body => ...}</tt> which # also supports a <tt>:headers</tt> entity (also a Hash). # # If this format is not followed, we assume they've just returned data so # we package it up like normal and respond (for the user). # # The one exception to the previous rule is if the data is in the # standard response structure [200, {}, 'OK'] or some such, we construct # the reply accordingly. # response.status = 200 # we assume 200, but when specified get updated appropriately result = case result when Hash if result[:status] and result[:body] # {:status => 200, :body => 'OK'} # no coercion necessary result else # {*} {:status => 200, :body => result} end when Array if result[0].is_a?(Integer) and result[1].is_a?(Hash) and result[2] # [200, {}, 'OK'] format followed {:status => result[0], :headers => result[1], :body => result[2]} else # [*] {:status => 200, :body => result} end else # * {:status => 200, :body => result} end # set response data headers = result.delete(:headers) || {} response.status = result[:status] headers.each {|(header,val)| response[header] = val } response.write result.to_json timing[:finished] = Time.now # the rescue is necessary if for some reason the timing is faster than # system timing usec. this actually happens on Windows timing[:total] = ((((timing[:finished] - timing[:started])*1e4).round.to_f/1e4) rescue 0.01) timing[:per_sec] = (((1.0/(timing[:total]))*1e2).round.to_f/1e2) self.logger.info "[#{response.status}] #{URI.parse(env['REQUEST_URI'] || env['PATH_INFO']).path} (#{timing[:total]}s;#{timing[:per_sec]}req/s)" # self.logger << "Session ID: #{self.session.id}\n" # TODO: Implement session self.logger << "Params: #{filter_params_for_log(request, env).inspect}\n\n" response.finish end |
#dispatch(env) ⇒ Object
Dispatches the controller and action according the routed request.
+env+ the request environment details, including "halcyon.route"
If no :controller
is specified, the default Application
controller is dispatched to.
Once the controller is selected and instantiated, the action is called, defaulting to :default
if no action is provided.
If the action called is not defined, a 404 Not Found
exception will be raised. This will be sent to the client as such, or handled by the Rack application container, such as the Rack Cascade middleware to failover to another application (such as Merb or Rails).
Refer to Halcyon::Application::Router for more details on defining routes and for where to get further documentation.
Returns (String|Array|Hash):body
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/halcyon/application.rb', line 160 def dispatch(env) route = env['halcyon.route'] # make sure that the right controller/action is called based on the route controller = case route[:controller] when NilClass # default to the Application controller ::Application.new(env) when String # pulled from URL, so camelize (from extlib) and symbolize first begin Object.full_const_get(route[:controller].to_const_string).new(env) rescue NameError => e raise NotFound.new end end # Establish the selected action, defaulting to +default+. action = (route[:action] || 'default').to_sym # Respond correctly that a non-existent action was specified if the # method does not exist. raise NotFound.new unless controller.methods.include?(action.to_s) # if no errors have occured up to this point, the route should be fully # valid and all exceptions raised should be treated as # <tt>500 Internal Server Error</tt>s, which is handled by <tt>call</tt>. controller._dispatch(action) end |
#filter_params_for_log(request, env) ⇒ Object
Assemble params for logging.
This method exists to be overridden or method-chained to filter out params from being logged for applications with sensitive data like passwords.
Returns Hash:params_to_log
249 250 251 |
# File 'lib/halcyon/application.rb', line 249 def filter_params_for_log(request, env) request.params.merge(env['halcyon.route']) end |