From 37a56fa5133e287c765f70edbfc7753c8e8e27b3 Mon Sep 17 00:00:00 2001 From: Jack Nagel Date: Wed, 23 Jan 2013 00:26:25 -0600 Subject: [PATCH] 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. --- Library/Homebrew/cmd/link.rb | 6 ++++-- Library/Homebrew/cmd/uninstall.rb | 10 ++++++---- Library/Homebrew/cmd/unlink.rb | 6 ++++-- Library/Homebrew/exceptions.rb | 12 ++++++++++++ Library/Homebrew/formula.rb | 15 +++++++++++++++ Library/Homebrew/formula_installer.rb | 26 ++++++++++++++++++++++++++ Library/Homebrew/keg.rb | 12 ++++++++++++ 7 files changed, 79 insertions(+), 8 deletions(-) diff --git a/Library/Homebrew/cmd/link.rb b/Library/Homebrew/cmd/link.rb index a85690ecb6..6371c41de6 100644 --- a/Library/Homebrew/cmd/link.rb +++ b/Library/Homebrew/cmd/link.rb @@ -35,8 +35,10 @@ module Homebrew extend self next end - print "Linking #{keg}... " do - puts "#{keg.link(mode)} symlinks created" + keg.lock do + print "Linking #{keg}... " do + puts "#{keg.link(mode)} symlinks created" + end end end end diff --git a/Library/Homebrew/cmd/uninstall.rb b/Library/Homebrew/cmd/uninstall.rb index d719be96a2..19ad6eeb5d 100644 --- a/Library/Homebrew/cmd/uninstall.rb +++ b/Library/Homebrew/cmd/uninstall.rb @@ -7,10 +7,12 @@ module Homebrew extend self if not ARGV.force? ARGV.kegs.each do |keg| - puts "Uninstalling #{keg}..." - keg.unlink - keg.uninstall - rm_opt_link keg.fname + keg.lock do + puts "Uninstalling #{keg}..." + keg.unlink + keg.uninstall + rm_opt_link keg.fname + end end else ARGV.named.each do |name| diff --git a/Library/Homebrew/cmd/unlink.rb b/Library/Homebrew/cmd/unlink.rb index 12b0377815..37ef8f1da2 100644 --- a/Library/Homebrew/cmd/unlink.rb +++ b/Library/Homebrew/cmd/unlink.rb @@ -3,8 +3,10 @@ module Homebrew extend self raise KegUnspecifiedError if ARGV.named.empty? ARGV.kegs.each do |keg| - print "Unlinking #{keg}... " - puts "#{keg.unlink} links removed" + keg.lock do + print "Unlinking #{keg}... " + puts "#{keg.unlink} links removed" + end end end end diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index ae2dd20c41..edd5cf1d1f 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -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 diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 2e072afff9..e2fde2ef92 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -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 diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 373440ba88..b24641e558 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -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 diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb index 3a4d012622..4949d24dc3 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -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