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 let fd: CInt
|
||||||
private var excludedItemNames: Set<String> = []
|
private var excludedItemNames: Set<String> = []
|
||||||
private var watches = InotifyWatchManager()
|
private var watches = InotifyWatchManager()
|
||||||
private var eventReader: any DispatchSourceRead
|
private nonisolated(unsafe) let eventReader: any DispatchSourceRead
|
||||||
private nonisolated let eventStream: AsyncStream<RawInotifyEvent>
|
private nonisolated let eventStream: AsyncStream<RawInotifyEvent>
|
||||||
public nonisolated var events: AsyncCompactMapSequence<AsyncStream<RawInotifyEvent>, InotifyEvent> {
|
public nonisolated var events: AsyncCompactMapSequence<AsyncStream<RawInotifyEvent>, InotifyEvent> {
|
||||||
self.eventStream.compactMap(self.transform(_:))
|
self.eventStream.compactMap(self.transform(_:))
|
||||||
@@ -73,7 +73,11 @@ public actor Inotify {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
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? {
|
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
|
||||||
@@ -112,6 +116,7 @@ public actor Inotify {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.setCancelHandler {
|
reader.setCancelHandler {
|
||||||
|
cinotify_deinit(fd)
|
||||||
continuation.finish()
|
continuation.finish()
|
||||||
}
|
}
|
||||||
reader.activate()
|
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