Merge pull request #17904 from Homebrew/lock_improvements

This commit is contained in:
Mike McQuaid 2024-07-30 18:02:02 +01:00 committed by GitHub
commit ca22e9ccfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 82 additions and 30 deletions

View File

@ -87,6 +87,7 @@ begin
if internal_cmd || Commands.external_ruby_v2_cmd_path(cmd) if internal_cmd || Commands.external_ruby_v2_cmd_path(cmd)
cmd = T.must(cmd) cmd = T.must(cmd)
cmd_class = Homebrew::AbstractCommand.command(cmd) cmd_class = Homebrew::AbstractCommand.command(cmd)
Homebrew.running_command = cmd
if cmd_class if cmd_class
command_instance = cmd_class.new command_instance = cmd_class.new
@ -97,6 +98,7 @@ begin
Homebrew.public_send Commands.method_name(cmd) Homebrew.public_send Commands.method_name(cmd)
end end
elsif (path = Commands.external_ruby_cmd_path(cmd)) elsif (path = Commands.external_ruby_cmd_path(cmd))
Homebrew.running_command = cmd
require?(path) require?(path)
exit Homebrew.failed? ? 1 : 0 exit Homebrew.failed? ? 1 : 0
elsif Commands.external_cmd_path(cmd) elsif Commands.external_cmd_path(cmd)

View File

@ -411,7 +411,7 @@ module Homebrew
(downloads - referenced_downloads).each do |download| (downloads - referenced_downloads).each do |download|
if self.class.incomplete?(download) if self.class.incomplete?(download)
begin begin
LockFile.new(download.basename).with_lock do DownloadLock.new(download).with_lock do
download.unlink download.unlink
end end
rescue OperationInProgressError rescue OperationInProgressError

View File

@ -359,7 +359,7 @@ homebrew-vendor-install() {
CACHED_LOCATION="${HOMEBREW_CACHE}/${VENDOR_FILENAME}" CACHED_LOCATION="${HOMEBREW_CACHE}/${VENDOR_FILENAME}"
lock "vendor-install-${VENDOR_NAME}" lock "vendor-install ${VENDOR_NAME}"
fetch fetch
install install

View File

@ -395,7 +395,7 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy
def fetch(timeout: nil) def fetch(timeout: nil)
end_time = Time.now + timeout if timeout end_time = Time.now + timeout if timeout
download_lock = LockFile.new(temporary_path.basename) download_lock = DownloadLock.new(temporary_path)
download_lock.lock download_lock.lock
urls = [url, *mirrors] urls = [url, *mirrors]

View File

@ -295,6 +295,10 @@ module Homebrew
"or `$HOME/.homebrew/livecheck_watchlist.txt` otherwise.", "or `$HOME/.homebrew/livecheck_watchlist.txt` otherwise.",
default: "#{ENV.fetch("HOMEBREW_USER_CONFIG_HOME")}/livecheck_watchlist.txt", default: "#{ENV.fetch("HOMEBREW_USER_CONFIG_HOME")}/livecheck_watchlist.txt",
}, },
HOMEBREW_LOCK_CONTEXT: {
description: "If set, Homebrew will add this output as additional context for locking errors. " \
"This is useful when running `brew` in the background.",
},
HOMEBREW_LOGS: { HOMEBREW_LOGS: {
description: "Use this directory to store log files.", description: "Use this directory to store log files.",
default_text: "macOS: `$HOME/Library/Logs/Homebrew`, " \ default_text: "macOS: `$HOME/Library/Logs/Homebrew`, " \

View File

@ -359,10 +359,14 @@ end
# Raised when another Homebrew operation is already in progress. # Raised when another Homebrew operation is already in progress.
class OperationInProgressError < RuntimeError class OperationInProgressError < RuntimeError
def initialize(name) sig { params(locked_path: Pathname).void }
def initialize(locked_path)
full_command = Homebrew.running_command_with_args.presence || "brew"
lock_context = if (env_lock_context = Homebrew::EnvConfig.lock_context.presence)
"\n#{env_lock_context}"
end
message = <<~EOS message = <<~EOS
Operation already in progress for #{name} A `#{full_command}` process has already locked #{locked_path}.#{lock_context}
Another active Homebrew process is already using #{name}.
Please wait for it to finish or terminate it to continue. Please wait for it to finish or terminate it to continue.
EOS EOS

View File

@ -110,6 +110,16 @@ module Homebrew
def auto_update_command? def auto_update_command?
ENV.fetch("HOMEBREW_AUTO_UPDATE_COMMAND", false).present? ENV.fetch("HOMEBREW_AUTO_UPDATE_COMMAND", false).present?
end end
sig { params(cmd: T.nilable(String)).void }
def running_command=(cmd)
@running_command_with_args = "#{cmd} #{ARGV.join(" ")}"
end
sig { returns String }
def running_command_with_args
"brew #{@running_command_with_args}".strip
end
end end
end end

View File

@ -3,13 +3,15 @@
require "fcntl" require "fcntl"
# A lock file. # A lock file to prevent multiple Homebrew processes from modifying the same path.
class LockFile class LockFile
attr_reader :path attr_reader :path
def initialize(name) sig { params(type: Symbol, locked_path: Pathname).void }
@name = name.to_s def initialize(type, locked_path)
@path = HOMEBREW_LOCKS/"#{@name}.lock" @locked_path = locked_path
lock_name = locked_path.basename.to_s
@path = HOMEBREW_LOCKS/"#{lock_name}.#{type}.lock"
@lockfile = nil @lockfile = nil
end end
@ -18,7 +20,7 @@ class LockFile
create_lockfile create_lockfile
return if @lockfile.flock(File::LOCK_EX | File::LOCK_NB) return if @lockfile.flock(File::LOCK_EX | File::LOCK_NB)
raise OperationInProgressError, @name raise OperationInProgressError, @locked_path
end end
def unlock def unlock
@ -51,14 +53,24 @@ end
# A lock file for a formula. # A lock file for a formula.
class FormulaLock < LockFile class FormulaLock < LockFile
def initialize(name) sig { params(rack_name: String).void }
super("#{name}.formula") def initialize(rack_name)
super(:formula, HOMEBREW_CELLAR/rack_name)
end end
end end
# A lock file for a cask. # A lock file for a cask.
class CaskLock < LockFile class CaskLock < LockFile
def initialize(name) sig { params(cask_token: String).void }
super("#{name}.cask") def initialize(cask_token)
super(:cask, HOMEBREW_PREFIX/"Caskroom/#{cask_token}")
end
end
# A lock file for a download.
class DownloadLock < LockFile
sig { params(download_path: Pathname).void }
def initialize(download_path)
super(:download, download_path)
end end
end end

View File

@ -190,6 +190,9 @@ module Homebrew::EnvConfig
sig { returns(String) } sig { returns(String) }
def livecheck_watchlist; end def livecheck_watchlist; end
sig { returns(String) }
def lock_context; end
sig { returns(String) } sig { returns(String) }
def logs; end def logs; end

View File

@ -149,9 +149,9 @@ RSpec.describe "Exception" do
end end
describe OperationInProgressError do describe OperationInProgressError do
subject(:error) { described_class.new("foo") } subject(:error) { described_class.new(Pathname("foo")) }
it(:to_s) { expect(error.to_s).to match(/Operation already in progress for foo/) } it(:to_s) { expect(error.to_s).to match(/has already locked foo/) }
end end
describe FormulaInstallationAlreadyAttemptedError do describe FormulaInstallationAlreadyAttemptedError do

View File

@ -3,7 +3,7 @@
require "lock_file" require "lock_file"
RSpec.describe LockFile do RSpec.describe LockFile do
subject(:lock_file) { described_class.new("foo") } subject(:lock_file) { described_class.new(:lock, Pathname("foo")) }
describe "#lock" do describe "#lock" do
it "does not raise an error when already locked" do it "does not raise an error when already locked" do
@ -16,7 +16,7 @@ RSpec.describe LockFile do
lock_file.lock lock_file.lock
expect do expect do
described_class.new("foo").lock described_class.new(:lock, Pathname("foo")).lock
end.to raise_error(OperationInProgressError) end.to raise_error(OperationInProgressError)
end end
end end
@ -30,7 +30,7 @@ RSpec.describe LockFile do
lock_file.lock lock_file.lock
lock_file.unlock lock_file.unlock
expect { described_class.new("foo").lock }.not_to raise_error expect { described_class.new(:lock, Pathname("foo")).lock }.not_to raise_error
end end
end end
end end

View File

@ -1,18 +1,20 @@
# create a lock using `flock(2)`. A name is required as first argument. # Create a lock using `flock(2)`. A command name with arguments is required as
# the lock will be automatically unlocked when the shell process quits. # first argument. The lock will be automatically unlocked when the shell process
# Noted due to the fixed FD, a shell process can only create one lock. # quits. Note due to the fixed FD, a shell process can only create one lock.
# HOMEBREW_LIBRARY is by brew.sh # HOMEBREW_LIBRARY is by brew.sh
# HOMEBREW_PREFIX is set by extend/ENV/super.rb # HOMEBREW_PREFIX is set by extend/ENV/super.rb
# shellcheck disable=SC2154 # shellcheck disable=SC2154
lock() { lock() {
local name="$1" local command_name_and_args="$1"
# use bash to replace spaces with dashes
local lock_filename="${command_name_and_args// /-}"
local lock_dir="${HOMEBREW_PREFIX}/var/homebrew/locks" local lock_dir="${HOMEBREW_PREFIX}/var/homebrew/locks"
local lock_file="${lock_dir}/${name}" local lock_file="${lock_dir}/${lock_filename}"
[[ -d "${lock_dir}" ]] || mkdir -p "${lock_dir}" [[ -d "${lock_dir}" ]] || mkdir -p "${lock_dir}"
if [[ ! -w "${lock_dir}" ]] if [[ ! -w "${lock_dir}" ]]
then then
odie <<EOS odie <<EOS
Can't create ${name} lock in ${lock_dir}! Can't create \`brew ${command_name_and_args}\` lock in ${lock_dir}!
Fix permissions by running: Fix permissions by running:
sudo chown -R ${USER-\$(whoami)} ${HOMEBREW_PREFIX}/var/homebrew sudo chown -R ${USER-\$(whoami)} ${HOMEBREW_PREFIX}/var/homebrew
EOS EOS
@ -28,10 +30,17 @@ EOS
exec 200>&- exec 200>&-
# open the lock file to FD, so the shell process can hold the lock. # open the lock file to FD, so the shell process can hold the lock.
exec 200>"${lock_file}" exec 200>"${lock_file}"
if ! _create_lock 200 "${name}"
if ! _create_lock 200 "${command_name_and_args}"
then then
local lock_context
if [[ -n "${HOMEBREW_LOCK_CONTEXT}" ]]
then
lock_context="\n${HOMEBREW_LOCK_CONTEXT}"
fi
odie <<EOS odie <<EOS
Another active Homebrew ${name} process is already in progress. Another \`brew ${command_name_and_args}\` process is already running.${lock_context}
Please wait for it to finish or terminate it to continue. Please wait for it to finish or terminate it to continue.
EOS EOS
fi fi
@ -39,7 +48,7 @@ EOS
_create_lock() { _create_lock() {
local lock_file_descriptor="$1" local lock_file_descriptor="$1"
local name="$2" local command_name_and_args="$2"
local ruby="/usr/bin/ruby" local ruby="/usr/bin/ruby"
local python="/usr/bin/python" local python="/usr/bin/python"
[[ -x "${ruby}" ]] || ruby="$(type -P ruby)" [[ -x "${ruby}" ]] || ruby="$(type -P ruby)"
@ -57,6 +66,6 @@ _create_lock() {
then then
"${python}" -c "import fcntl; fcntl.flock(${lock_fd}, fcntl.LOCK_EX | fcntl.LOCK_NB)" "${python}" -c "import fcntl; fcntl.flock(${lock_fd}, fcntl.LOCK_EX | fcntl.LOCK_NB)"
else else
onoe "Cannot create ${name} lock due to missing/too old ruby/flock/python, please avoid running Homebrew in parallel." onoe "Cannot create \`brew ${command_name_and_args}\` lock due to missing/too old ruby/flock/python, please avoid running Homebrew in parallel."
fi fi
} }

View File

@ -3872,6 +3872,11 @@ command execution e.g. `$(cat file)`.
`$XDG_CONFIG_HOME` is set or `$HOME/.homebrew/livecheck_watchlist.txt` `$XDG_CONFIG_HOME` is set or `$HOME/.homebrew/livecheck_watchlist.txt`
otherwise. otherwise.
`HOMEBREW_LOCK_CONTEXT`
: If set, Homebrew will add this output as additional context for locking
errors. This is useful when running `brew` in the background.
`HOMEBREW_LOGS` `HOMEBREW_LOGS`
: Use this directory to store log files. : Use this directory to store log files.

View File

@ -2526,6 +2526,9 @@ Consult this file for the list of formulae to check by default when no formula a
\fIDefault:\fP \fB$XDG_CONFIG_HOME/homebrew/livecheck_watchlist\.txt\fP if \fB$XDG_CONFIG_HOME\fP is set or \fB$HOME/\.homebrew/livecheck_watchlist\.txt\fP otherwise\. \fIDefault:\fP \fB$XDG_CONFIG_HOME/homebrew/livecheck_watchlist\.txt\fP if \fB$XDG_CONFIG_HOME\fP is set or \fB$HOME/\.homebrew/livecheck_watchlist\.txt\fP otherwise\.
.RE .RE
.TP .TP
\fBHOMEBREW_LOCK_CONTEXT\fP
If set, Homebrew will add this output as additional context for locking errors\. This is useful when running \fBbrew\fP in the background\.
.TP
\fBHOMEBREW_LOGS\fP \fBHOMEBREW_LOGS\fP
Use this directory to store log files\. Use this directory to store log files\.
.RS .RS