diff --git a/Sources/Inotify/Inotify.swift b/Sources/Inotify/Inotify.swift index 582ad05..3d5d856 100644 --- a/Sources/Inotify/Inotify.swift +++ b/Sources/Inotify/Inotify.swift @@ -5,7 +5,7 @@ public actor Inotify { private let fd: CInt private var excludedItemNames: Set = [] private var watches = InotifyWatchManager() - private var eventReader: any DispatchSourceRead + private nonisolated(unsafe) let eventReader: any DispatchSourceRead private nonisolated let eventStream: AsyncStream public nonisolated var events: AsyncCompactMapSequence, InotifyEvent> { self.eventStream.compactMap(self.transform(_:)) @@ -73,7 +73,11 @@ public actor Inotify { } deinit { - cinotify_deinit(self.fd) + // The file descriptor is closed by the reader's cancel handler once + // libdispatch has unregistered it. Closing it here would leave a + // registration behind that a later instance reusing the descriptor + // number could inherit, silently losing its events. + self.eventReader.cancel() } private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? { @@ -112,6 +116,7 @@ public actor Inotify { } } reader.setCancelHandler { + cinotify_deinit(fd) continuation.finish() } reader.activate() diff --git a/Tests/InotifyIntegrationTests/LifecycleTests.swift b/Tests/InotifyIntegrationTests/LifecycleTests.swift new file mode 100644 index 0000000..f3d1bb4 --- /dev/null +++ b/Tests/InotifyIntegrationTests/LifecycleTests.swift @@ -0,0 +1,24 @@ +import Foundation +import Testing +@testable import Inotify + +@Suite("Instance Lifecycle") +struct LifecycleTests { + @Test func aDeallocatedInstanceDoesNotStealEventsOfItsSuccessor() async throws { + try await withTempDir { dir in + let filename = "after-reuse.txt" + do { + let predecessor = try Inotify() + try await predecessor.addWatch(path: dir, mask: .create) + } + + let events = try await getEventsForTrigger( + in: dir, + mask: .create, + ) { try createFile(at: "\($0)/\(filename)") } + + let createEvent = events.first { $0.mask.contains(.create) && $0.path.lastComponent?.string == filename } + #expect(createEvent != nil, "Expected CREATE for '\(filename)', got: \(events)") + } + } +}