class Borges::Session < Borges::RequestHandler @@subclasses = [] class << self def inherited(klass) @@subclasses << klass end def all_subclasses return @@subclasses end end def action_url_for_continuation(cont) return action_url_for_key(@continuations.store(wrap_continuation(cont))) end def action_url_for_key(str) return "#{@application.url_for_request_handler(self, self.path)}/#{str}" end ## # Add +filter+ to the filters active for the session. #-- # Implemented this way so that filters are backtraced appropriately. def add_filter(filter) @filters.contents += [filter] end def add_to_path(str) @path.contents("#{base_path}/#{str}") end def application return @application end def self.application(app) ses = self.new ses.initialize_with(app) return ses end def base_path return @application.base_path end def basic_auth_do(auth_block, &block) filter(Borges::BasicAuthentication.new(&auth_block), &block) end def bookmark_for_expiry return callcc do |cc| @expiry_continuation = wrap_continuation(proc do |v| redirect_with_message_in('That page has expired.', 3) cc.call(v) end) end end ## # HACK This is totally bogus! def self.current_session #context = thisContext #until context.nil? do # return context.receiver if context.receiver.kind_of? self # context = context.sender #end #return nil return Thread.current[:session] end def self.default_preferences prefs = Borges::Preferences.new prefs[:show_toolbar] = Borges::BooleanPreference.new(true) prefs[:always_redirect] = Borges::BooleanPreference.new(true) prefs[:error_page] = Borges::ListPreference.new(Borges::WalkbackPage, [Borges::WalkbackPage]) # XXX Hack prefs[:session_expiry_seconds] = Borges::NumberPreference.new(600) return prefs end def enter_session(&block) rv = nil in_thread do rv = with_escape_continuation do val = with_error_handler do redirect block.call end end end return rv end def enter_session_with(req) return enter_session do start(req) end end def error_page_class return @application.preferences[:error_page] end def filter(filter, &block) add_filter(filter) block.call remove_filter(filter) end def handle_error(e) #e.reactivateHandler # XXX return error_page_class.exception(e).show end def start_handler_thread @session_mutex = Mutex.new @thread_mutex = Mutex.new.lock @thread = Thread.start do cur_thr = Thread.current THREAD_GROUP.add cur_thr cur_thr[:session] = self cur_thr[:in_handler_thread] = true cur_thr.abort_on_exception = true begin loop do @thread_mutex.synchronize do cur_thr[:return] = cur_thr[:block].call cur_thr[:response_done].unlock end @thread_mutex.lock end rescue Exception => err err_str = %^Exception in #{cur_thr.inspect}:\n#{err}\n#{err.backtrace.join("\n")}\n!!^ puts err_str #cur_thr[:return].error(err_str, cur_thr[:request]) #cur_thr[:return] = cur_thr[:response_done].unlock cur_thr.kill end end end def in_thread(&block) val = nil if Thread.current[:in_handler_thread] then #val = handle_request(req) val = block.call else start_handler_thread unless defined? @thread @session_mutex.synchronize do response_lock = Mutex.new.lock # unlocked by #perform_request #@thread[:request] = req @thread[:block] = block @thread[:response_done] = response_lock @thread_mutex.unlock response_lock.lock val = @thread[:return] end end return val end def handle_request(req) in_thread do handle_request_intern(req) end end def handle_request_intern(req) return response_for_request(req) end ## # TODO Refactor into initialize # # HACK Thread.current[:session] is a hack def initialize_with(application) critical = Thread.critical Thread.critical = true current_ses = Thread.current[:session] Thread.current[:session] = self @application = application @continuations = Borges::LRUCache.new @state = Borges::StateRegistry.new @expiry_continuation = proc do |r| start end @last_access = Time.now.to_i @filters = Borges::StateHolder.new([]) @path = Borges::StateHolder.new(base_path) Thread.current[:session] = current_ses Thread.critical = critical end def active? return (Time.now.to_i - @last_access) < application.preferences[:session_expiry_seconds] end def isolate(&block) txn = Borges::Transaction.new filter(txn, &block) txn.close bookmark_for_expiry end def once(&block) filter(Borges::Once.new, &block) end def page_expired @expiry_continuation.call(nil) end def path return @path.contents end def perform_request(req) @last_access = Time.now.to_i cont = @continuations.fetch(req.action_key) if cont.nil? then return unknown_request(req) else return cont.call(req) end end def redirect respond do |url| Borges::RedirectResponse.new(url) end end def redirect_to(url) return_response(Borges::RedirectResponse.new(url)) end def redirect_with_message_in(message, seconds) respond do |url| Borges::RefreshResponse.new(message, url, seconds) end end def register_for_backtracking(obj) @state.register(obj) end ## # Remove +filter+ from the filters active for the session. #-- # Implemented this way so that filters are backtraced appropriately. def remove_filter(filter) @filters.contents -= [filter] end def respond(&block) request = callcc do |cc| return_response(block.call(action_url_for_continuation(cc))) end @filters.contents.each do |filter| filter.handle_request_in(request, self) end return request end def response_for(&block) return with_escape_continuation do block.call end end def response_for_request(req) return response_for do perform_request(req) end end def return_response(res) @escape_continuation.call(res) end def start(req = nil) raise NotImplementedError.new("Subclass Responsibility") end def unknown_request(req) page_expired end def with_error_handler(&block) begin block.call rescue Exception => e handle_error(e) end end def with_escape_continuation(&block) return callcc do |cc| @escape_continuation = cc block.call end end def wrap_continuation(cont) snap = @state.snapshot return proc do |v| @state.restore_snapshot(snap) cont.call(v) end end end