Implement async event streaming
This commit is contained in:
@@ -1,14 +1,21 @@
|
||||
import Dispatch
|
||||
import CInotify
|
||||
|
||||
public actor Inotify {
|
||||
private let fd: Int32
|
||||
private var watches: [Int32: String] = [:]
|
||||
private var eventReader: any DispatchSourceRead
|
||||
private var eventStream: AsyncStream<RawInotifyEvent>
|
||||
public var events: AsyncCompactMapSequence<AsyncStream<RawInotifyEvent>, InotifyEvent> {
|
||||
self.eventStream.compactMap(self.transform(_:))
|
||||
}
|
||||
|
||||
public init() throws {
|
||||
self.fd = inotify_init1(Int32(IN_NONBLOCK | IN_CLOEXEC))
|
||||
guard self.fd >= 0 else {
|
||||
throw InotifyError.initFailed(errno: cinotify_get_errno())
|
||||
}
|
||||
(self.eventReader, self.eventStream) = Self.createEventReader(forFileDescriptor: fd)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -31,4 +38,33 @@ public actor Inotify {
|
||||
deinit {
|
||||
cinotify_deinit(self.fd)
|
||||
}
|
||||
|
||||
private func transform(_ rawEvent: RawInotifyEvent) -> InotifyEvent? {
|
||||
guard let path = self.watches[rawEvent.watchDescriptor] else { return nil }
|
||||
return InotifyEvent.init(from: rawEvent, inDirectory: path)
|
||||
}
|
||||
|
||||
private static func createEventReader(forFileDescriptor fd: Int32) -> (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)
|
||||
}
|
||||
}
|
||||
|
||||
26
Sources/Inotify/InotifyEvent.swift
Normal file
26
Sources/Inotify/InotifyEvent.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
import SystemPackage
|
||||
|
||||
public struct InotifyEvent: Sendable, Hashable, CustomStringConvertible {
|
||||
public let watchDescriptor: Int32
|
||||
public let mask: InotifyEventMask
|
||||
public let cookie: UInt32
|
||||
public let path: FilePath
|
||||
|
||||
public var description: String {
|
||||
var parts = ["InotifyEvent(wd: \(watchDescriptor), mask: \(mask), path: \"\(path)\""]
|
||||
if cookie != 0 { parts.append("cookie: \(cookie)") }
|
||||
return parts.joined(separator: ", ") + ")"
|
||||
}
|
||||
}
|
||||
|
||||
extension InotifyEvent {
|
||||
public init(from rawEvent: RawInotifyEvent, inDirectory path: String) {
|
||||
let dirPath = FilePath(path)
|
||||
self.init(
|
||||
watchDescriptor: rawEvent.watchDescriptor,
|
||||
mask: rawEvent.mask,
|
||||
cookie: rawEvent.cookie,
|
||||
path: dirPath.appending(rawEvent.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
57
Sources/Inotify/InotifyEventParser.swift
Normal file
57
Sources/Inotify/InotifyEventParser.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import CInotify
|
||||
|
||||
struct InotifyEventParser {
|
||||
static let readBufferSize = 4096
|
||||
|
||||
static func parse(fromFileDescriptor fd: Int32) -> [RawInotifyEvent] {
|
||||
let buffer = UnsafeMutableRawPointer.allocate(
|
||||
byteCount: Self.readBufferSize,
|
||||
alignment: MemoryLayout<inotify_event>.alignment
|
||||
)
|
||||
defer { buffer.deallocate() }
|
||||
|
||||
let bytesRead = read(fd, buffer, readBufferSize)
|
||||
guard bytesRead > 0 else { return [] }
|
||||
|
||||
return Self.parseEventBuffer(buffer, bytesRead: bytesRead)
|
||||
}
|
||||
|
||||
private static func parseEventBuffer(
|
||||
_ buffer: UnsafeMutableRawPointer,
|
||||
bytesRead: Int
|
||||
) -> [RawInotifyEvent] {
|
||||
var events: [RawInotifyEvent] = []
|
||||
var offset = 0
|
||||
|
||||
while offset < bytesRead {
|
||||
let eventPointer = buffer.advanced(by: offset)
|
||||
let rawEvent = eventPointer.assumingMemoryBound(to: inotify_event.self).pointee
|
||||
|
||||
events.append(RawInotifyEvent(
|
||||
watchDescriptor: rawEvent.wd,
|
||||
mask: InotifyEventMask(rawValue: rawEvent.mask),
|
||||
cookie: rawEvent.cookie,
|
||||
name: Self.extractName(from: eventPointer, nameLength: rawEvent.len)
|
||||
))
|
||||
|
||||
offset += Self.eventSize(nameLength: rawEvent.len)
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
private static func extractName(
|
||||
from eventPointer: UnsafeMutableRawPointer,
|
||||
nameLength: UInt32
|
||||
) -> String {
|
||||
guard nameLength > 0 else { return "" }
|
||||
let namePointer = eventPointer
|
||||
.advanced(by: MemoryLayout<inotify_event>.size)
|
||||
.assumingMemoryBound(to: CChar.self)
|
||||
return String(cString: namePointer)
|
||||
}
|
||||
|
||||
private static func eventSize(nameLength: UInt32) -> Int {
|
||||
MemoryLayout<inotify_event>.size + Int(nameLength)
|
||||
}
|
||||
}
|
||||
12
Sources/Inotify/RawInotifyEvent.swift
Normal file
12
Sources/Inotify/RawInotifyEvent.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
public struct RawInotifyEvent: Sendable, Hashable, CustomStringConvertible {
|
||||
public let watchDescriptor: Int32
|
||||
public let mask: InotifyEventMask
|
||||
public let cookie: UInt32
|
||||
public let name: String
|
||||
|
||||
public var description: String {
|
||||
var parts = ["RawInotifyEvent(wd: \(watchDescriptor), mask: \(mask), name: \"\(name)\""]
|
||||
if cookie != 0 { parts.append("cookie: \(cookie)") }
|
||||
return parts.joined(separator: ", ") + ")"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user