blob: 527f8caaaaf132821d2acd720369d73b736c5b3b [file] [log] [blame]
# Aggressively cache agendas.
#
# Most of the heavy lifting is done by ASF::Board::Agenda in the whimsy-asf
# gem. This class is mainly focused on caching the results.
#
class Agenda
@@mutex = Mutex.new
def self.[](file)
IPC[file]
end
def self.[]=(file, data)
IPC[file] = data
end
def self.update_cache(file, path, contents, quick)
parsed = ASF::Board::Agenda.parse(contents, quick)
Agenda[file] = {mtime: (quick ? -1 : File.mtime(path)), parsed: parsed}
IPC.post type: :agenda, file: file unless quick
end
def self.uptodate(file)
path = File.expand_path(file, FOUNDATION_BOARD).untaint
return false unless File.exist? path
return Agenda[file][:mtime] == File.mtime(path)
end
def self.parse(file, mode)
# for quick mode, anything will do
mode = :quick if ENV['RACK_ENV'] == 'test'
return Agenda[file][:parsed] if mode == :quick and Agenda[file][:mtime] != 0
file.untaint if file =~ /\Aboard_\w+_[\d_]+\.txt\Z/
path = File.expand_path(file, FOUNDATION_BOARD).untaint
return unless File.exist? path
if Agenda[file][:mtime] != File.mtime(path)
@@mutex.synchronize do
if Agenda[file][:mtime] != File.mtime(path)
self.update_cache(file, path, File.read(path), mode == :quick)
end
end
# do a full parse in the background if a quick parse was done
if Agenda[file][:mtime] == -1
Thread.new do
self.update(file, nil)
parse(file, :full)
end
end
end
Agenda[file][:parsed]
end
# update agenda file in SVN
def self.update(file, message, retries=20, &block)
commit_rc = 999
# Create a temporary work directory
dir = Dir.mktmpdir
#extract context from block
_, env = eval('[_, env]', block.binding)
auth = [[]]
if env.password
auth = [['--username', env.user, '--password', env.password]]
end
@@mutex.synchronize do
file.untaint if file =~ /\Aboard_\w+_[\d_]+\.txt\Z/
# capture current version of the file
path = File.join(FOUNDATION_BOARD, file)
baseline = File.read(path) if Agenda[file][:mtime] == File.mtime(path)
# check out empty directory
board = `svn info #{FOUNDATION_BOARD}`[/URL: (.*)/, 1]
_.system ['svn', 'checkout', auth, '--depth', 'empty', board, dir]
# update the file in question
path = File.join(dir, file)
_.system ['svn', 'update', auth, path]
# invoke block, passing it the current contents of the file
if block and message
input = IO.read(path)
output = yield input.dup
# if the output differs, update and commit the file in question
if output != input
IO.write(path, output)
commit_rc = _.system ['svn', 'commit', auth, path, '-m', message]
@@seen[path] = File.mtime(path)
else
commit_rc = 0
end
end
# update the file in question; update output if mtime changed
# (it may not: during testing, commits are prevented)
path = File.join(FOUNDATION_BOARD, file)
File.open(path, 'r') do |fh|
fh.flock(File::LOCK_EX)
_.system ['svn', 'cleanup', FOUNDATION_BOARD]
mtime = File.mtime(path) if output
_.system ['svn', 'update', auth, path]
output = IO.read(path) if mtime != File.mtime(path)
end
# reparse the file if the output changed
if output != baseline or mtime != File.mtime(path)
self.update_cache(file, path, output, ENV['RACK_ENV'] == 'test')
end
# return the result
_.method_missing(:_agenda, Agenda[file][:parsed])
end
ensure
FileUtils.rm_rf dir
unless commit_rc == 0
if retries > 0
sleep rand(41-retries*2)*0.1 if retries <= 20
update(file, message, retries-1, &block)
else
raise Exception.new("svn commit failed")
end
end
end
# listen for changes to agenda files
@@listener = Listen.to(FOUNDATION_BOARD) do |modified, added, removed|
modified.each do |path|
next if File.exist?(path) and @@seen[path] == File.mtime(path)
file = File.basename(path)
if file =~ /^board_agenda_[\d_]+.txt$/
self.update_cache(file, path, File.read(path), false)
end
end
end
# disable listening when running tests
@@listener = Struct.new(:start, :stop).new if ENV['RACK_ENV'] == 'test'
@@seen = {}
@@listener.start
end