Properties of actors are implicitly isolated. To be able to read the events stream from any concurrent context, we need to declare it nonisolated. And as AsyncStream conforms to Sendable, it is safe to make both events and the private eventStream nonisolated.
122 lines
3.8 KiB
Swift
122 lines
3.8 KiB
Swift
import Dispatch
|
|
import CInotify
|
|
|
|
public actor Inotify {
|
|
private let fd: CInt
|
|
private var excludedItemNames: Set<String> = []
|
|
private var watches = InotifyWatchManager()
|
|
private var eventReader: any DispatchSourceRead
|
|
private nonisolated let eventStream: AsyncStream<RawInotifyEvent>
|
|
public nonisolated var events: AsyncCompactMapSequence<AsyncStream<RawInotifyEvent>, InotifyEvent> {
|
|
self.eventStream.compactMap(self.transform(_:))
|
|
}
|
|
|
|
public init() throws {
|
|
self.fd = inotify_init1(CInt(IN_NONBLOCK | IN_CLOEXEC))
|
|
guard self.fd >= 0 else {
|
|
throw InotifyError.initFailed(errno: cinotify_get_errno())
|
|
}
|
|
(self.eventReader, self.eventStream) = Self.createEventReader(forFileDescriptor: fd)
|
|
}
|
|
|
|
public func isExcluded(_ name: String) -> Bool {
|
|
self.excludedItemNames.contains(name)
|
|
}
|
|
|
|
public func exclude(name: String) {
|
|
self.excludedItemNames.insert(name)
|
|
}
|
|
|
|
public func exclude(names: String...) {
|
|
self.exclude(names: names)
|
|
}
|
|
|
|
public func exclude(names: [String]) {
|
|
for name in names {
|
|
self.excludedItemNames.insert(name)
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
public func addWatch(path: String, mask: InotifyEventMask) throws -> CInt {
|
|
let wd = inotify_add_watch(self.fd, path, mask.rawValue)
|
|
guard wd >= 0 else {
|
|
throw InotifyError.addWatchFailed(path: path, errno: cinotify_get_errno())
|
|
}
|
|
watches.add(path, withId: wd, mask: mask)
|
|
return wd
|
|
}
|
|
|
|
@discardableResult
|
|
public func addRecursiveWatch(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
|
let directoryPaths = try await DirectoryResolver.resolve(path, excluding: self.excludedItemNames)
|
|
var result: [CInt] = []
|
|
for path in directoryPaths {
|
|
let wd = try self.addWatch(path: path.string, mask: mask)
|
|
result.append(wd)
|
|
}
|
|
return result
|
|
}
|
|
|
|
@discardableResult
|
|
public func addWatchWithAutomaticSubtreeWatching(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
|
let wds = try await self.addRecursiveWatch(forDirectory: path, mask: mask)
|
|
watches.enableAutomaticSubtreeWatching(forIds: wds)
|
|
return wds
|
|
}
|
|
|
|
public func removeWatch(_ wd: CInt) throws {
|
|
guard inotify_rm_watch(self.fd, wd) == 0 else {
|
|
throw InotifyError.removeWatchFailed(watchDescriptor: wd, errno: cinotify_get_errno())
|
|
}
|
|
watches.remove(forId: wd)
|
|
}
|
|
|
|
deinit {
|
|
cinotify_deinit(self.fd)
|
|
}
|
|
|
|
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
|
|
guard let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
|
|
guard !self.excludedItemNames.contains(rawEvent.name) else { return nil }
|
|
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
|
|
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
|
|
return InotifyEvent.init(from: rawEvent, inDirectory: path)
|
|
}
|
|
|
|
private func addWatchInCaseOfAutomaticSubtreeWatching(_ event: InotifyEvent) async {
|
|
guard watches.isAutomaticSubtreeWatching(event.watchDescriptor),
|
|
event.mask.contains(.create),
|
|
event.mask.contains(.isDir) else {
|
|
return
|
|
}
|
|
|
|
guard let mask = self.watches.mask(forId: event.watchDescriptor) else { return }
|
|
let _ = try? await self.addWatchWithAutomaticSubtreeWatching(forDirectory: event.path.string, mask: mask)
|
|
}
|
|
|
|
private static func createEventReader(forFileDescriptor fd: CInt) -> (any DispatchSourceRead, AsyncStream<RawInotifyEvent>) {
|
|
let (stream, continuation) = AsyncStream<RawInotifyEvent>.makeStream(
|
|
of: RawInotifyEvent.self,
|
|
bufferingPolicy: .bufferingNewest(512)
|
|
)
|
|
|
|
let reader = DispatchSource.makeReadSource(
|
|
fileDescriptor: fd,
|
|
queue: DispatchQueue(label: "Inotify.read", qos: .utility)
|
|
)
|
|
|
|
reader.setEventHandler {
|
|
for rawEvent in InotifyEventParser.parse(fromFileDescriptor: fd) {
|
|
continuation.yield(rawEvent)
|
|
}
|
|
}
|
|
reader.setCancelHandler {
|
|
continuation.finish()
|
|
}
|
|
reader.activate()
|
|
|
|
return (reader, stream)
|
|
}
|
|
}
|