blob: 216e57df83b2e478fc9b84032ebdb52004ae2f9c [file] [log] [blame]
require 'fileutils'
require 'thread'
require 'securerandom'
require 'concurrent'
require 'whimsy/asf/config'
#
# Low-tech, file based session manager. Each session is stored as a separate
# file on disk, and expires after two days. Each request for a new session
# is guaranteed to return a session with a minimum of a day left before
# expiring.
#
# Concurrent::Map provides thread safe access to data; a mutex is used
# to additionally prevent concurrent updates.
#
# No direct use of timers, events, or threads are made allowing this
# service to be used in a variety of contexts (e.g. Sinatra and
# EventMachine).
#
class Session
if ENV['RACK_ENV'] == 'test'
AGENDA_WORK = File.expand_path('test/work/data')
else
AGENDA_WORK = ASF::Config.get(:agenda_work) || '/srv/agenda'
end
WORKDIR = File.expand_path('sessions', AGENDA_WORK)
DAY = 24*60*60 # seconds
@@sessions = Concurrent::Map.new
@@users = Concurrent::Map.new {|map,key| map[key]=[]}
@@semaphore = Mutex.new
# find the latest session for the given user, creating one if necessary.
def self.user(id)
session = @@users[id].max_by {|session| session[:mtime]}
session = nil if session and session[:mtime] < Time.now - DAY
# if not found, try refreshing data from disk and try again
if not session
Session.load
session = @@users[id].max_by {|session| session[:mtime]}
session = nil if session and session[:mtime] < Time.now - DAY
end
# if still not found, generate a new session
if not session
@@semaphore.synchronize do
secret = SecureRandom.hex(16)
file = File.join(WORKDIR, secret)
File.write(file, id)
session = {id: id, secret: secret, mtime: File.mtime(file)}
@@sessions[secret] = session
@@users[id] << session
end
end
# return the secret
session[:secret]
end
# retrieve session for a given secret
def self.[](secret)
session = @@sessions[secret]
# if not found, try refreshing data from disk and try again
if not session
Session.load
session = @@sessions[secret]
end
session
end
# load sessions from disk
def self.load(files=nil)
@@semaphore.synchronize do
# default files to all files in the workdir and @@sessions hash
files ||= Dir["#{WORKDIR}/*"] +
@@sessions.keys.map {|secret| File.join(WORKDIR, secret)}
files.uniq.each do |file|
next if file =~ /\.yml$/
secret = File.basename(file)
session = @@sessions[secret]
if File.exist?(file) and File.writable?(file)
if File.mtime(file) < Time.now - 2 * DAY
begin
File.delete file
rescue RuntimeError => error
STDERR.puts "Error deleting #{file}: #{error}"
end
else
# update class variables if the file changed
mtime = File.mtime(file)
next if session and session[:mtime] == mtime
session = {id: File.read(file), secret: secret, mtime: mtime}
@@sessions[secret] = session
@@users[session[:id]] << session
end
else
# remove session if the file no longer exists
@@users[session[:id]].delete(session) if session
@@sessions.delete(secret)
end
end
end
end
# ensure the working directory exists
FileUtils.mkdir_p WORKDIR
# load initial data from disk
self.load
end