Cancel the event reader before closing the descriptor
`deinit` closed the inotify descriptor while its dispatch source was still active. The kernel drops the epoll registration on close, but libdispatch keeps its own; an instance created afterwards that reuses the descriptor number could inherit that stale state and never receive events. Roughly one test run in three lost a single event this way. The reader is now cancelled in `deinit` and the descriptor closed in its cancel handler, as libdispatch requires.
This commit is contained in:
@@ -5,7 +5,7 @@ public actor Inotify {
|
||||
private let fd: CInt
|
||||
private var excludedItemNames: Set<String> = []
|
||||
private var watches = InotifyWatchManager()
|
||||
private var eventReader: any DispatchSourceRead
|
||||
private nonisolated(unsafe) let eventReader: any DispatchSourceRead
|
||||
private nonisolated let eventStream: AsyncStream<RawInotifyEvent>
|
||||
public nonisolated var events: AsyncCompactMapSequence<AsyncStream<RawInotifyEvent>, 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()
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user