// // FileSystemEvent.swift // FileKit // // The MIT License (MIT) // // Copyright (c) 2015-2017 Nikolai Vazquez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation #if os(OSX) /// Watches a given set of paths and runs a callback per event. public class FileSystemWatcher { // MARK: - Private Static Properties /// The event stream callback for when events occur. private static let _eventCallback: FSEventStreamCallback = { // swiftlint:disable all (stream: ConstFSEventStreamRef, contextInfo: UnsafeMutableRawPointer?, numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer, eventIds: UnsafePointer) in // swiftlint:enable all FileSystemWatcher.log("Callback Fired") let watcher: FileSystemWatcher = unsafeBitCast(contextInfo, to: FileSystemWatcher.self) defer { watcher.lastEventId = eventIds[numEvents - 1] } guard let paths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String] else { return } for index in 0.. Void /// The last event ID for the watcher. public private(set) var lastEventId: FSEventStreamEventId /// Whether or not the watcher has started yet. private var _started = false /// The event stream for the watcher. private var _stream: FileSystemEventStream? // MARK: - Initialization /// Creates a watcher for the given paths. /// /// - Parameter paths: The paths. /// - Parameter sinceWhen: The date to start at. /// - Parameter flags: The create flags. /// - Parameter latency: The latency. /// - Parameter queue: The queue to be run within. /// - Parameter callback: The callback to be called on changes. public init(paths: [Path], sinceWhen: FSEventStreamEventId = FileSystemEvent.nowEventId, flags: FileSystemEventStreamCreateFlags = [.UseCFTypes, .FileEvents], latency: CFTimeInterval = 0, queue: DispatchQueue? = nil, callback: @escaping (FileSystemEvent) -> Void ) { self.lastEventId = sinceWhen self.paths = paths self.flags = flags self.latency = latency self.queue = queue self._callback = callback } // MARK: - Deinitialization deinit { self.close() } // MARK: - Private Methods /// Processes the event by logging it and then running the callback. /// /// - Parameter event: The file system event to be logged. private func _processEvent(_ event: FileSystemEvent) { FileSystemWatcher.log("\t\(event.id) - \(event.flags) - \(event.path)") self._callback(event) } /// Prints the message when in debug mode. /// /// - Parameter message: The message to be logged. private static func log(_ message: String) { #if DEBUG print(message) #endif } // MARK: - Methods // Start watching by creating the stream /// Starts watching. public func watch() { guard _started == false else { return } var context = FSEventStreamContext( version: 0, info: nil, retain: nil, release: nil, copyDescription: nil ) // add self into context context.info = Unmanaged.passUnretained(self).toOpaque() guard let streamRef = FSEventStreamCreate( kCFAllocatorDefault, FileSystemWatcher._eventCallback, &context, paths.map {$0.rawValue} as CFArray, // since when lastEventId, // how long to wait after an event occurs before forwarding it latency, UInt32(flags.rawValue) ) else { return } _stream = FileSystemEventStream(rawValue: streamRef) _stream?.scheduleWithRunLoop(runLoop, runLoopMode: runLoopMode) if let q = queue { _stream?.setDispatchQueue(q) } _stream?.start() _started = true } // Stops, invalidates and releases the stream /// Closes the watcher. public func close() { guard _started == true else { return } _stream?.stop() _stream?.invalidate() _stream?.release() _stream = nil _started = false } /// Requests that the fseventsd daemon send any events it has already /// buffered (via the latency parameter). /// /// This occurs asynchronously; clients will not have received all the /// callbacks by the time this call returns to them. public func flushAsync() { _stream?.flushAsync() } /// Requests that the fseventsd daemon send any events it has already /// buffered (via the latency). Then runs the runloop in its private mode /// till all events that have occurred have been reported (via the client's /// callback). /// /// This occurs synchronously; clients will have received all the callbacks /// by the time this call returns to them. public func flushSync() { _stream?.flushSync() } } #endif