Use SystemPath for DirectoryWatcher. (#1522)

- Closes #1520.
- Using URL for filesystem paths is bad practice. FilePath is safer and
more ergonomic.
- Same pattern as #1480 (HostDNSResolver) and #1518 (PacketFilter).
This commit is contained in:
Chris George
2026-05-11 15:06:32 -07:00
committed by GitHub
parent cec58654ac
commit 509fa047dc
3 changed files with 99 additions and 90 deletions
+10 -5
View File
@@ -34,22 +34,27 @@ actor LocalhostDNSHandler: DNSHandler {
public init(configPath: FilePath = HostDNSResolver.defaultConfigPath, ttl: UInt32 = 5, log: Logger) {
self.ttl = ttl
self.watcher = DirectoryWatcher(directoryURL: URL(filePath: configPath.string), log: log)
self.watcher = DirectoryWatcher(directoryPath: configPath, log: log)
self.dns = [DNSName: IPv4Address]()
}
public func monitorResolvers() async {
await self.watcher.startWatching { [weak self] fileURLs in
await self.watcher.startWatching { [weak self] filePaths in
var dns: [DNSName: IPv4Address] = [:]
let regex = try Regex(HostDNSResolver.localhostOptionsRegex)
for file in fileURLs.filter({ $0.lastPathComponent.starts(with: HostDNSResolver.containerizationPrefix) }) {
let content = try String(contentsOf: file, encoding: .utf8)
for file in filePaths.filter({
$0.lastComponent?.string.starts(with: HostDNSResolver.containerizationPrefix) == true
}) {
let content = try String(contentsOfFile: file.string, encoding: .utf8)
if let match = content.firstMatch(of: regex),
let ipv4 = (match[1].substring.flatMap { try? IPv4Address(String($0)) })
{
let name = String(file.lastPathComponent.dropFirst(HostDNSResolver.containerizationPrefix.count))
guard let lastName = file.lastComponent?.string else {
continue
}
let name = String(lastName.dropFirst(HostDNSResolver.containerizationPrefix.count))
guard let dnsName = try? DNSName(name) else {
continue
}
+23 -22
View File
@@ -19,6 +19,7 @@ import ContainerizationOS
import Foundation
import Logging
import Synchronization
import SystemPackage
/// Watches a directory for changes and invokes a handler when the contents change.
///
@@ -28,16 +29,16 @@ import Synchronization
///
/// Example usage:
/// ```swift
/// let watcher = DirectoryWatcher(directoryURL: myURL, log: logger)
/// try watcher.startWatching { urls in
/// print("Directory contents changed: \(urls)")
/// 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 URL of the directory being watched.
public let directoryURL: URL
/// The path of the directory being watched.
public let directoryPath: FilePath
private var task: Task<Void, any Error>?
private let monitorQueue: DispatchQueue
@@ -45,14 +46,14 @@ public actor DirectoryWatcher {
private let log: Logger?
/// Creates a new `DirectoryWatcher` for the given directory URL.
/// Creates a new `DirectoryWatcher` for the given directory path.
///
/// - Parameters:
/// - directoryURL: The URL of the directory to watch.
/// - directoryPath: The path of the directory to watch.
/// - log: An optional logger for diagnostic messages.
public init(directoryURL: URL, log: Logger?) {
self.directoryURL = directoryURL
self.monitorQueue = DispatchQueue(label: "monitor:\(directoryURL.path)")
public init(directoryPath: FilePath, log: Logger?) {
self.directoryPath = directoryPath
self.monitorQueue = DispatchQueue(label: "monitor:\(directoryPath.string)")
self.log = log
self.source = Mutex(nil)
}
@@ -61,14 +62,14 @@ public actor DirectoryWatcher {
///
/// - Parameters:
/// - handler: handler to run on directory state change.
public func startWatching(handler: @Sendable @escaping ([URL]) throws -> Void) {
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.directoryURL.path, isDirectory: &isDir)
exists = FileManager.default.fileExists(atPath: self.directoryPath.string, isDirectory: &isDir)
if exists && isDir.boolValue && self.source.withLock({ $0 }) == nil {
try _startWatching(handler: handler)
}
@@ -82,21 +83,21 @@ public actor DirectoryWatcher {
}
private func _startWatching(
handler: @escaping ([URL]) throws -> Void
handler: @escaping ([FilePath]) throws -> Void
) throws {
let descriptor = open(directoryURL.path, O_EVTONLY)
let descriptor = open(directoryPath.string, O_EVTONLY)
guard descriptor > 0 else {
throw ContainerizationError(.internalError, message: "cannot open \(directoryURL.path), descriptor=\(descriptor)")
throw ContainerizationError(.internalError, message: "cannot open \(directoryPath.string), descriptor=\(descriptor)")
}
do {
let files = try FileManager.default.contentsOfDirectory(atPath: directoryURL.path)
try handler(files.map { directoryURL.appending(path: $0) })
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 \(directoryURL.path)")
throw ContainerizationError(.internalError, message: "failed to run handler for \(directoryPath.string)")
}
log?.info("starting directory watcher", metadata: ["path": "\(directoryURL.path)"])
log?.info("starting directory watcher", metadata: ["path": "\(directoryPath.string)"])
let dispatchSource = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: descriptor,
@@ -118,12 +119,12 @@ public actor DirectoryWatcher {
}
do {
let files = try FileManager.default.contentsOfDirectory(atPath: directoryURL.path)
try handler(files.map { directoryURL.appending(path: $0) })
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": "\(directoryURL.path)"])
metadata: ["error": "\(error)", "path": "\(directoryPath.string)"])
}
}