RadrootsAppleFileMaintenance.swift (4967B)
1 import Darwin 2 import Foundation 3 4 /// Coordinates cooperating users of one governed root. Every operation that can 5 /// create a reference, read, replace or write a collected file must hold a use 6 /// reservation until its underlying work has drained, including after cancellation. 7 /// This mechanism does not establish application reference absence or retention policy. 8 public struct RadrootsAppleFileMaintenance: Sendable { 9 private let root: URL 10 11 public init(root: URL) { 12 self.root = root 13 } 14 15 /// Contention returns nil. No waiting, background work or automatic retry. 16 public func reserveUse() throws -> RadrootsFileUseReservation? { 17 try RadrootsFileMaintenanceGate.open(root: root, exclusive: false).map(RadrootsFileUseReservation.init) 18 } 19 20 /// Acquire before inspecting references; retain through the last conditional unlink. 21 public func reserveMaintenance() throws -> RadrootsFileMaintenanceReservation? { 22 try RadrootsFileMaintenanceGate.open(root: root, exclusive: true).map(RadrootsFileMaintenanceReservation.init) 23 } 24 } 25 26 /// Release by dropping the last owner only after every underlying user has drained. 27 public final class RadrootsFileUseReservation: Sendable { 28 private let gate: RadrootsFileMaintenanceGate 29 fileprivate init(_ gate: RadrootsFileMaintenanceGate) { 30 self.gate = gate 31 } 32 33 public func validate() throws { 34 try gate.validate() 35 } 36 } 37 38 /// Child scans retain this exclusive reservation. There is no early unlock API. 39 public final class RadrootsFileMaintenanceReservation: Sendable { 40 let gate: RadrootsFileMaintenanceGate 41 fileprivate init(_ gate: RadrootsFileMaintenanceGate) { 42 self.gate = gate 43 } 44 45 /// Empty path selects the governed root. Other paths must be canonical relative 46 /// directories without symlinks. Streams are live and never persisted as cookies. 47 public func openDirectory(relativePath: String = "") throws -> RadrootsFileMaintenanceScan { 48 try RadrootsFileMaintenanceScan(reservation: self, relativePath: relativePath) 49 } 50 51 public func validate() throws { 52 try gate.validate() 53 } 54 } 55 56 /// Immutable descriptor ownership; descriptors close only at final deinitialization. 57 /// Methods neither mutate descriptor state nor release the advisory reservation. 58 final class RadrootsFileMaintenanceGate: @unchecked Sendable { 59 static let name = ".radroots_file_maintenance.lock" 60 let directory: RadrootsAtomicFile.Directory 61 private let descriptor: Int32 62 63 private init(directory: RadrootsAtomicFile.Directory, descriptor: Int32) { 64 self.directory = directory 65 self.descriptor = descriptor 66 } 67 68 deinit { 69 Darwin.close(descriptor) 70 Darwin.close(directory.descriptor) 71 } 72 73 static func open(root: URL, exclusive: Bool) throws -> RadrootsFileMaintenanceGate? { 74 guard root.isFileURL, root.path.hasPrefix("/"), !root.path.utf8.contains(0) else { 75 throw RadrootsAppleFileError.invalidRequest 76 } 77 let parts = root.path.split(separator: "/").map(String.init) 78 guard !parts.isEmpty, parts.allSatisfy({ $0 != "." && $0 != ".." }) else { 79 throw RadrootsAppleFileError.invalidRequest 80 } 81 let directory = try RadrootsAtomicFile.Directory.open(parts, create: true) 82 var owned = false 83 defer { 84 if !owned { 85 Darwin.close(directory.descriptor) 86 } 87 } 88 for attempt in 0 ..< 4 { 89 try directory.validate() 90 let descriptor = Darwin.openat(directory.descriptor, name, 91 O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK | (exclusive ? O_EXLOCK : O_SHLOCK), 0o600) 92 if descriptor >= 0 { 93 let gate = RadrootsFileMaintenanceGate(directory: directory, descriptor: descriptor) 94 owned = true 95 try gate.validate() 96 return gate 97 } 98 let error = errno 99 if error == EWOULDBLOCK { 100 return nil 101 } 102 guard error == ENOENT else { throw RadrootsAppleFileError.permanentFailure } 103 if attempt == 3 { 104 throw RadrootsAppleFileError.transientFailure 105 } 106 } 107 throw RadrootsAppleFileError.transientFailure 108 } 109 110 func validate() throws { 111 try directory.validate() 112 var held = stat() 113 var current = stat() 114 guard Darwin.fstat(descriptor, &held) == 0, 115 Darwin.fstatat(directory.descriptor, Self.name, ¤t, AT_SYMLINK_NOFOLLOW) == 0, 116 held.st_mode & S_IFMT == S_IFREG, current.st_mode & S_IFMT == S_IFREG, 117 held.st_nlink == 1, held.st_size == 0, held.st_uid == geteuid(), 118 held.st_dev == current.st_dev, held.st_ino == current.st_ino 119 else { 120 throw RadrootsAppleFileError.permanentFailure 121 } 122 } 123 }