FormulaInstaller: implement installation locks

FormulaInstaller now attempts to take a lock on a "foo.brewing" file for
the formula and all of its dependencies before attempting installation.

The lock is an advisory lock implemented using flock(), and as such it
only locks out other processes that attempt to take the lock. It also
means that it is never necessary to manually remove the lock file,
because the lock is not enforced by I/O.

The uninstall, link, and unlink commands all learn to respect this lock
as well, so that the installation cannot be corrupted by a concurrent
Homebrew process, and keg operations cannot occur simultaneously.
This commit is contained in:
Jack Nagel 2013-01-23 00:26:25 -06:00
parent ea4188ecda
commit 37a56fa513
7 changed files with 79 additions and 8 deletions

View File

@ -35,11 +35,13 @@ module Homebrew extend self
next
end
keg.lock do
print "Linking #{keg}... " do
puts "#{keg.link(mode)} symlinks created"
end
end
end
end
private

View File

@ -7,11 +7,13 @@ module Homebrew extend self
if not ARGV.force?
ARGV.kegs.each do |keg|
keg.lock do
puts "Uninstalling #{keg}..."
keg.unlink
keg.uninstall
rm_opt_link keg.fname
end
end
else
ARGV.named.each do |name|
name = Formula.canonical_name(name)

View File

@ -3,8 +3,10 @@ module Homebrew extend self
raise KegUnspecifiedError if ARGV.named.empty?
ARGV.kegs.each do |keg|
keg.lock do
print "Unlinking #{keg}... "
puts "#{keg.unlink} links removed"
end
end
end
end

View File

@ -45,6 +45,18 @@ class FormulaUnavailableError < RuntimeError
end
end
class OperationInProgressError < RuntimeError
def initialize name
message = <<-EOS.undent
Operation already in progress for #{name}
Another active Homebrew process is already using #{name}.
Please wait for it to finish or terminate it to continue.
EOS
super message
end
end
module Homebrew
class InstallationError < RuntimeError
attr :formula

View File

@ -228,6 +228,21 @@ class Formula
end
end
def lock
lockpath = HOMEBREW_CACHE_FORMULA/"#{@name}.brewing"
@lockfile = lockpath.open(File::RDWR | File::CREAT)
unless @lockfile.flock(File::LOCK_EX | File::LOCK_NB)
raise OperationInProgressError, @name
end
end
def unlock
unless @lockfile.nil?
@lockfile.flock(File::LOCK_UN)
@lockfile.close
end
end
def == b
name == b.name
end

View File

@ -22,6 +22,7 @@ class FormulaInstaller
@@attempted ||= Set.new
lock
check_install_sanity
end
@ -226,6 +227,8 @@ class FormulaInstaller
print "#{f.prefix}: #{f.prefix.abv}"
print ", built in #{pretty_duration build_time}" if build_time
puts
unlock if hold_locks?
end
def build_time
@ -463,6 +466,29 @@ class FormulaInstaller
check_jars
check_non_libraries
end
private
def hold_locks?
@hold_locks || false
end
def lock
if (@@locked ||= []).empty?
f.recursive_deps.each { |d| @@locked << d } unless ignore_deps
@@locked.unshift(f)
@@locked.each(&:lock)
@hold_locks = true
end
end
def unlock
if hold_locks?
@@locked.each(&:unlock)
@@locked.clear
@hold_locks = false
end
end
end

View File

@ -61,6 +61,18 @@ class Keg < Pathname
parent.basename.to_s
end
def lock
path = HOMEBREW_CACHE_FORMULA/"#{fname}.brewing"
file = path.open(File::RDWR | File::CREAT)
unless file.flock(File::LOCK_EX | File::LOCK_NB)
raise OperationInProgressError, fname
end
yield
ensure
file.flock(File::LOCK_UN)
file.close
end
def linked_keg_record
@linked_keg_record ||= HOMEBREW_REPOSITORY/"Library/LinkedKegs"/fname
end