chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationError
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import Logging
|
||||
import Synchronization
|
||||
import SystemPackage
|
||||
|
||||
/// Watches a directory for changes and invokes a handler when the contents change.
|
||||
///
|
||||
/// `DirectoryWatcher` uses `DispatchSource` file system events to monitor a directory.
|
||||
/// If the target directory does not exist yet, it polls until the directory is created.
|
||||
/// the target is created, then transitions to watching the target directly.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```swift
|
||||
/// let watcher = DirectoryWatcher(directoryPath: myPath, log: logger)
|
||||
/// try watcher.startWatching { paths in
|
||||
/// print("Directory contents changed: \(paths)")
|
||||
/// }
|
||||
/// ```
|
||||
public actor DirectoryWatcher {
|
||||
public static let watchPeriod = Duration.seconds(1)
|
||||
|
||||
/// The path of the directory being watched.
|
||||
public let directoryPath: FilePath
|
||||
|
||||
private var task: Task<Void, any Error>?
|
||||
private let monitorQueue: DispatchQueue
|
||||
private let source: Mutex<DispatchSourceFileSystemObject?>
|
||||
|
||||
private let log: Logger?
|
||||
|
||||
/// Creates a new `DirectoryWatcher` for the given directory path.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - directoryPath: The path of the directory to watch.
|
||||
/// - log: An optional logger for diagnostic messages.
|
||||
public init(directoryPath: FilePath, log: Logger?) {
|
||||
self.directoryPath = directoryPath
|
||||
self.monitorQueue = DispatchQueue(label: "monitor:\(directoryPath.string)")
|
||||
self.log = log
|
||||
self.source = Mutex(nil)
|
||||
}
|
||||
|
||||
/// Starts watching the directory for changes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - handler: handler to run on directory state change.
|
||||
public func startWatching(handler: @Sendable @escaping ([FilePath]) throws -> Void) {
|
||||
self.task = Task {
|
||||
var exists: Bool
|
||||
var isDir: ObjCBool = false
|
||||
|
||||
while true {
|
||||
do {
|
||||
exists = FileManager.default.fileExists(atPath: self.directoryPath.string, isDirectory: &isDir)
|
||||
if exists && isDir.boolValue && self.source.withLock({ $0 }) == nil {
|
||||
try _startWatching(handler: handler)
|
||||
}
|
||||
} catch {
|
||||
log?.error("failed to start watching", metadata: ["error": "\(error)"])
|
||||
}
|
||||
|
||||
try await Task.sleep(for: Self.watchPeriod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func _startWatching(
|
||||
handler: @escaping ([FilePath]) throws -> Void
|
||||
) throws {
|
||||
let descriptor = open(directoryPath.string, O_EVTONLY)
|
||||
guard descriptor > 0 else {
|
||||
throw ContainerizationError(.internalError, message: "cannot open \(directoryPath.string), descriptor=\(descriptor)")
|
||||
}
|
||||
|
||||
do {
|
||||
let files = try FileManager.default.contentsOfDirectory(atPath: directoryPath.string)
|
||||
try handler(files.map { directoryPath.appending($0) })
|
||||
} catch {
|
||||
throw ContainerizationError(.internalError, message: "failed to run handler for \(directoryPath.string)")
|
||||
}
|
||||
|
||||
log?.info("starting directory watcher", metadata: ["path": "\(directoryPath.string)"])
|
||||
|
||||
let dispatchSource = DispatchSource.makeFileSystemObjectSource(
|
||||
fileDescriptor: descriptor,
|
||||
eventMask: [.delete, .write],
|
||||
queue: monitorQueue
|
||||
)
|
||||
|
||||
dispatchSource.setCancelHandler {
|
||||
close(descriptor)
|
||||
}
|
||||
|
||||
dispatchSource.setEventHandler { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
guard !dispatchSource.data.contains(.delete) else {
|
||||
dispatchSource.cancel()
|
||||
self.source.withLock { $0 = nil }
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let files = try FileManager.default.contentsOfDirectory(atPath: directoryPath.string)
|
||||
try handler(files.map { directoryPath.appending($0) })
|
||||
} catch {
|
||||
self.log?.error(
|
||||
"failed to run watch handler",
|
||||
metadata: ["error": "\(error)", "path": "\(directoryPath.string)"])
|
||||
}
|
||||
}
|
||||
|
||||
source.withLock { $0 = dispatchSource }
|
||||
dispatchSource.resume()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.task?.cancel()
|
||||
source.withLock { $0?.cancel() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if os(macOS)
|
||||
import Darwin
|
||||
|
||||
/// Utility for triggering local network privacy alert.
|
||||
/// The local networking privacy feature introduced in
|
||||
/// macOS 15 requires users to authorize an app before it can
|
||||
/// access peers on the local network. This security feature
|
||||
/// affects runtime helpers that publish ports on the loopback
|
||||
/// interface.
|
||||
///
|
||||
/// The approach used here is for the application to trigger
|
||||
/// the alert before clients attemot to communicate with it.
|
||||
/// This is a best effort method; there is no guarantee that
|
||||
/// the alert will display.
|
||||
///
|
||||
/// See https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy
|
||||
/// for additional details.
|
||||
package struct LocalNetworkPrivacy {
|
||||
/// Attempts to trigger the local network privacy alert.
|
||||
///
|
||||
/// This builds a list of link-local IPv6 addresses and then creates a connected
|
||||
/// UDP socket to each in turn. Connecting a UDP socket triggers the local
|
||||
/// network alert without actually sending any traffic.
|
||||
package static func triggerLocalNetworkPrivacyAlert() {
|
||||
let addresses = selectedLinkLocalIPv6Addresses()
|
||||
for address in addresses {
|
||||
let sock6 = socket(AF_INET6, SOCK_DGRAM, 0)
|
||||
guard sock6 >= 0 else { return }
|
||||
defer { close(sock6) }
|
||||
|
||||
withUnsafePointer(to: address) { sa6 in
|
||||
sa6.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
|
||||
_ = connect(sock6, sa, socklen_t(sa.pointee.sa_len)) >= 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func selectedLinkLocalIPv6Addresses() -> [sockaddr_in6] {
|
||||
// Find the link-local broadcast-capable IPv6 interfaces, and
|
||||
// for each, create two peer socket addresses for the interface
|
||||
// with the port set to the discard service (port 9).
|
||||
let r1 = (0..<8).map { _ in UInt8.random(in: 0...255) }
|
||||
let r2 = (0..<8).map { _ in UInt8.random(in: 0...255) }
|
||||
return Array(
|
||||
ipv6AddressesOfBroadcastCapableInterfaces()
|
||||
.filter { isIPv6AddressLinkLocal($0) }
|
||||
.map {
|
||||
var addr = $0
|
||||
addr.sin6_port = UInt16(9).bigEndian
|
||||
return addr
|
||||
}
|
||||
.map { [setIPv6LinkLocalAddressHostPart(of: $0, to: r1), setIPv6LinkLocalAddressHostPart(of: $0, to: r2)] }
|
||||
.joined())
|
||||
}
|
||||
|
||||
private static func setIPv6LinkLocalAddressHostPart(of address: sockaddr_in6, to hostPart: [UInt8]) -> sockaddr_in6 {
|
||||
// Set the host part (the bottom 64 bits) of the supplied
|
||||
// IPv6 socket address.
|
||||
precondition(hostPart.count == 8)
|
||||
var result = address
|
||||
withUnsafeMutableBytes(of: &result.sin6_addr) { buf in
|
||||
buf[8...].copyBytes(from: hostPart)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static func isIPv6AddressLinkLocal(_ address: sockaddr_in6) -> Bool {
|
||||
// Link-local address have the fe:c0/10 prefix.
|
||||
address.sin6_addr.__u6_addr.__u6_addr8.0 == 0xfe
|
||||
&& (address.sin6_addr.__u6_addr.__u6_addr8.1 & 0xc0) == 0x80
|
||||
}
|
||||
|
||||
private static func ipv6AddressesOfBroadcastCapableInterfaces() -> [sockaddr_in6] {
|
||||
// Iterate all interfaces and return the IPv6 addresses
|
||||
// for those that can broadcast.
|
||||
var addrList: UnsafeMutablePointer<ifaddrs>? = nil
|
||||
let err = getifaddrs(&addrList)
|
||||
guard err == 0, let start = addrList else { return [] }
|
||||
defer { freeifaddrs(start) }
|
||||
return sequence(first: start, next: { $0.pointee.ifa_next })
|
||||
.compactMap { i -> sockaddr_in6? in
|
||||
guard
|
||||
(i.pointee.ifa_flags & UInt32(bitPattern: IFF_BROADCAST)) != 0,
|
||||
let sa = i.pointee.ifa_addr,
|
||||
sa.pointee.sa_family == AF_INET6,
|
||||
sa.pointee.sa_len >= MemoryLayout<sockaddr_in6>.size
|
||||
else { return nil }
|
||||
return UnsafeRawPointer(sa).load(as: sockaddr_in6.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user