Skip to content

Instantly share code, notes, and snippets.

@ollieatkinson
Created October 22, 2020 10:27
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ollieatkinson/38e8561ab95a6a1ad6702d2ee37072c9 to your computer and use it in GitHub Desktop.
Save ollieatkinson/38e8561ab95a6a1ad6702d2ee37072c9 to your computer and use it in GitHub Desktop.
sane `fsevents` in Swift
import Foundation
import Combine
public struct Event: Identifiable {
public let id: FSEventStreamEventId
public let path: String
public let flags: Flags
}
public class FileSystemEventStream {
public let events$ = PassthroughSubject<Event, Never>()
/// An array of strings each specifying a path to a directory,
/// signifying the root of a filesystem hierarchy to be watched
/// for modifications.
public let paths: [String]
/// The number of seconds the service should wait after hearing about an
/// event from the kernel before passing it along to the client via its
/// callback. Specifying a larger value may result in more effective
/// temporal coalescing, resulting in fewer callbacks and greater overall
/// efficiency.
public let latency: TimeInterval
public init(_ paths: [String], latency: TimeInterval = 0) {
self.paths = paths
self.latency = latency
start()
}
deinit { stop() }
public private(set) var sinceWhen: FSEventStreamEventId = FSEventStreamEventId(kFSEventStreamEventIdSinceNow)
private var reference: FSEventStreamRef!
@discardableResult
public func start(on runLoop: RunLoop = .main, mode: RunLoop.Mode = .default) -> Bool {
guard reference == nil else { return false }
var context = FSEventStreamContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
context.info = Unmanaged.passUnretained(self).toOpaque()
let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents)
reference = FSEventStreamCreate(kCFAllocatorDefault, { stream, context, count, paths, flags, ids in
guard let paths = unsafeBitCast(paths, to: NSArray.self) as? [String] else { return }
let stream = unsafeBitCast(context, to: FileSystemEventStream.self)
for index in 0..<count {
stream.process(id: ids[index], path: paths[index], flags: flags[index])
}
}, &context, paths as CFArray, sinceWhen, latency, flags)
FSEventStreamSetDispatchQueue(reference, .main)
return FSEventStreamStart(reference)
}
public func stop() {
guard let reference = reference else { return }
FSEventStreamStop(reference)
FSEventStreamInvalidate(reference)
FSEventStreamRelease(reference)
self.reference = nil
}
func process(id: FSEventStreamEventId, path: String, flags: FSEventStreamEventFlags) {
events$.send(Event(id: id, path: path, flags: flags))
sinceWhen = id
}
}
extension Event: Codable { }
extension Event.Flags: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.init(rawValue: try container.decode(UInt32.self))
}
public func encode(to encoder: Encoder) throws {
try rawValue.encode(to: encoder)
}
}
extension Event {
public init(id: FSEventStreamEventId, path: String, flags: FSEventStreamEventFlags) {
self.id = id
self.path = path
self.flags = .init(rawValue: flags)
}
}
extension Event {
public struct Flags: OptionSet {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let none: Flags = 0x00000000;
static let subdirectoryChanged: Flags = 0x00000001;
static let userDropped: Flags = 0x00000002;
static let kernelDropped: Flags = 0x00000004;
static let eventIdsWrapped: Flags = 0x00000008;
static let historyDone: Flags = 0x00000010;
static let rootChanged: Flags = 0x00000020;
static let mount: Flags = 0x00000040;
static let unmount: Flags = 0x00000080;
static let created: Flags = 0x00000100;
static let removed: Flags = 0x00000200;
static let indexNodeMetadata: Flags = 0x00000400;
static let renamed: Flags = 0x00000800;
static let modified: Flags = 0x00001000;
static let finder: Flags = 0x00002000;
static let changeOwner: Flags = 0x00004000;
static let extendedFileAttributes: Flags = 0x00008000;
static let isFile: Flags = 0x00010000;
static let isDir: Flags = 0x00020000;
static let isSymlink: Flags = 0x00040000;
static let isHardlink: Flags = 0x00100000;
static let isLastHardlink: Flags = 0x00200000;
static let ownEvent: Flags = 0x00080000;
static let cloned: Flags = 0x00400000;
}
}
extension Event.Flags: ExpressibleByIntegerLiteral {
public init(integerLiteral value: UInt32) {
rawValue = value
}
}
extension Data {
public func string(encoding: String.Encoding = .utf8) -> String? {
String(data: self, encoding: encoding)
}
}
@ollieatkinson
Copy link
Author

let watch = FileSystemEventStream(["/Users/oliver/"])

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
watch.events$.sink { event in
    try? FileHandle.standardOutput.write(encoder.encode(event))
}.store(in: &bag)

@ollieatkinson
Copy link
Author

let watch = FileSystemEventStream(["/Users/oliver/"])
watch.events$.sink { event in
    // ...
}.store(in: &bag)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment