Module: Agent::Lock::Freeze
- Defined in:
- lib/agent/lock/freeze.rb
Overview
Optional teeth, for the scope you want nobody to touch at all.
An advisory lock works because every agent checks it. That covers the
agents that check. chflags uchg on macOS makes the file unwritable by
anything, checking or not, which is the right answer for a handful of
files that must not move while something else runs.
It is opt-in for three reasons. It is macOS only, since Linux's chattr +i needs root. It denies the holder too, so it fits a freeze rather than
a file you are editing. And a session that dies with files frozen leaves
a tree where git checkout and rm -rf fail with "Operation not
permitted", which is why every frozen path is written into the lock: the
thaw does not depend on the process that froze them still being alive.
Defined Under Namespace
Classes: TooBroad
Constant Summary collapse
- LIMIT =
A freeze walks and flags every matched file, so a scope covering a whole checkout is a mistake rather than an instruction.
500- BATCH =
How many paths one
chflagsinvocation is given. 200
Class Method Summary collapse
-
.apply(paths, tree:, force: false) ⇒ Array<String>
What was frozen.
-
.chflags(flag, paths, tree) ⇒ Array<String>
Only the files
chflagsactually accepted come back. -
.clear(paths, tree:) ⇒ Array<String>
Best effort on purpose: a path that has since been deleted or already thawed is not a reason to leave the rest frozen.
-
.existing(batch, tree) ⇒ Array<String>
Absolute paths that are still there to flag.
-
.flag_batch(flag, absolute) ⇒ Array<String>
The ones the command accepted.
-
.matches(scope, tree) ⇒ Array<String>
The files it would freeze, relative to the root.
-
.recursive(pattern) ⇒ String
A trailing
**means "everything under here" to this gem, and to anybody typing it. -
.run(flag, paths) ⇒ Boolean
Whether the command reported success.
- .supported? ⇒ Boolean
Class Method Details
.apply(paths, tree:, force: false) ⇒ Array<String>
Returns what was frozen.
58 59 60 61 62 63 64 |
# File 'lib/agent/lock/freeze.rb', line 58 def apply(paths, tree:, force: false) raise TooBroad, "--enforce needs macOS; this is #{RUBY_PLATFORM}" unless supported? return [] if paths.empty? raise TooBroad, "#{paths.size} files is wider than a freeze should be" if paths.size > LIMIT && !force chflags("uchg", paths, tree) end |
.chflags(flag, paths, tree) ⇒ Array<String>
Only the files chflags actually accepted come back.
A batch that fails is retried one file at a time, because the usual reason is a single path somebody else owns, and reporting the whole batch as frozen would leave the lock claiming protection it does not have. Reporting the whole batch as failed would be just as wrong.
84 85 86 87 88 89 90 |
# File 'lib/agent/lock/freeze.rb', line 84 def chflags(flag, paths, tree) return [] unless supported? paths.each_slice(BATCH) .flat_map { |batch| flag_batch(flag, existing(batch, tree)) } .map { |path| path.delete_prefix("#{tree.root}/") } end |
.clear(paths, tree:) ⇒ Array<String>
Best effort on purpose: a path that has since been deleted or already thawed is not a reason to leave the rest frozen.
70 71 72 73 74 |
# File 'lib/agent/lock/freeze.rb', line 70 def clear(paths, tree:) return [] if paths.nil? || paths.empty? chflags("nouchg", paths, tree) end |
.existing(batch, tree) ⇒ Array<String>
Returns absolute paths that are still there to flag.
93 94 95 |
# File 'lib/agent/lock/freeze.rb', line 93 def existing(batch, tree) batch.map { |rel| File.join(tree.root, rel) }.select { |path| File.exist?(path) } end |
.flag_batch(flag, absolute) ⇒ Array<String>
Returns the ones the command accepted.
98 99 100 101 102 103 |
# File 'lib/agent/lock/freeze.rb', line 98 def flag_batch(flag, absolute) return [] if absolute.empty? return absolute if run(flag, absolute) absolute.select { |path| run(flag, [path]) } end |
.matches(scope, tree) ⇒ Array<String>
Returns the files it would freeze, relative to the root.
38 39 40 41 42 43 |
# File 'lib/agent/lock/freeze.rb', line 38 def matches(scope, tree) Dir.glob(recursive(scope.pattern), base: tree.root, flags: File::FNM_EXTGLOB) .select { |rel| File.file?(File.join(tree.root, rel)) } .reject { |rel| rel.start_with?(".git/") } .sort end |
.recursive(pattern) ⇒ String
A trailing ** means "everything under here" to this gem, and to
anybody typing it. It does not mean that to Dir.glob, where a bare
** at the end matches one level, exactly like *. Only **/ walks
down. So a scope that covers a subtree is spelled out before globbing.
52 |
# File 'lib/agent/lock/freeze.rb', line 52 def recursive(pattern) = pattern.end_with?("**") ? "#{pattern}/*" : pattern |
.run(flag, paths) ⇒ Boolean
Returns whether the command reported success.
106 |
# File 'lib/agent/lock/freeze.rb', line 106 def run(flag, paths) = system("chflags", flag, *paths, out: File::NULL, err: File::NULL) || false |
.supported? ⇒ Boolean
33 |
# File 'lib/agent/lock/freeze.rb', line 33 def supported? = RUBY_PLATFORM.include?("darwin") |