Created
October 22, 2020 10:27
-
-
Save ollieatkinson/38e8561ab95a6a1ad6702d2ee37072c9 to your computer and use it in GitHub Desktop.
sane `fsevents` in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
Author
ollieatkinson
commented
Nov 24, 2020
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment