Developing a FileSystemWatcher for OS X by using FSEvents with Swift 2.0

When Swift was announced at WWDC 2014, I was very disappointed to not be able to use some of powerful C APIs such as FSEvents, to write applications in pure Swift language.

A lot of C APIs needs a C function pointer to pass a callback function when you use the API. From Swift 1.0 to Swift 1.2, it was not possible to use C function pointer in pure Swift language.

It was possible to use FSEvents by wrapping it in an Objective-C class, and by calling this class in Swift by using the bridging features offered by the language, but it was not what I attempted to accomplish.

But things change and at WWDC 2015, Apple announced the possibility to use C function pointers with Swift 2.0.

Today I’m very pleased to give you a sample code which is a good starting point to create your own FileSystemWatcher using the FSEvents API written with Swift 2.0.

import Foundation

public class FileSystemWatcher {

    // MARK: - Initialization / Deinitialization
    
    public init(pathsToWatch: [String], sinceWhen: FSEventStreamEventId) {
        self.lastEventId = sinceWhen
        self.pathsToWatch = pathsToWatch
    }

    convenience public init(pathsToWatch: [String]) {
        self.init(pathsToWatch: pathsToWatch, sinceWhen: FSEventStreamEventId(kFSEventStreamEventIdSinceNow))
    }
    
    deinit {
        stop()
    }

    // MARK: - Private Properties
    
    private let eventCallback: FSEventStreamCallback = { (stream: ConstFSEventStreamRef, contextInfo: UnsafeMutablePointer<Void>, numEvents: Int, eventPaths: UnsafeMutablePointer<Void>, eventFlags: UnsafePointer<FSEventStreamEventFlags>, eventIds: UnsafePointer<FSEventStreamEventId>) in
        print("***** FSEventCallback Fired *****", appendNewline: true)
        
        let fileSystemWatcher: FileSystemWatcher = unsafeBitCast(contextInfo, FileSystemWatcher.self)
        let paths = unsafeBitCast(eventPaths, NSArray.self) as! [String]
        
        for index in 0..<numEvents {
            fileSystemWatcher.processEvent(eventIds[index], eventPath: paths[index], eventFlags: eventFlags[index])
        }
        
        fileSystemWatcher.lastEventId = eventIds[numEvents - 1]
    }
    private let pathsToWatch: [String]
    private var started = false
    private var streamRef: FSEventStreamRef!
    
    // MARK: - Private Methods
    
    private func processEvent(eventId: FSEventStreamEventId, eventPath: String, eventFlags: FSEventStreamEventFlags) {
        print("\t\(eventId) - \(eventFlags) - \(eventPath)", appendNewline: true)
    }
    
    // MARK: - Pubic Properties
    
    public private(set) var lastEventId: FSEventStreamEventId
    
    // MARK: - Pubic Methods
    
    public func start() {
        guard started == false else { return }
        
        var context = FSEventStreamContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
        context.info = UnsafeMutablePointer<Void>(unsafeAddressOf(self))
        let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents)
        streamRef = FSEventStreamCreate(kCFAllocatorDefault, eventCallback, &context, pathsToWatch, lastEventId, 0, flags)
        
        FSEventStreamScheduleWithRunLoop(streamRef, CFRunLoopGetMain(), kCFRunLoopDefaultMode)
        FSEventStreamStart(streamRef)
        
        started = true
    }
    
    public func stop() {
        guard started == true else { return }

        FSEventStreamStop(streamRef)
        FSEventStreamInvalidate(streamRef)
        FSEventStreamRelease(streamRef)
        streamRef = nil

        started = false
    }
    
}

I hope this will help you in your future applications and feel free to share this article if you found it helpful 😉

Advertisements

18 thoughts on “Developing a FileSystemWatcher for OS X by using FSEvents with Swift 2.0

  1. Thomas Kilian

    Nice work. When I run this I eventually get EXC_BAD_ACCESS in libswiftCore.dylib`_swift_release_dealloc: once I Alt-Tab from the app to the Finder. Any thought?

    Liked by 1 person

    Reply
  2. Thomas Kilian

    The culprit seems to be in
    let fileSystemWatcher = unsafeBitCast(contextInfo, FileSystemWatcher.self)
    When I comment this (and the depending part below) I don’t get the above error any more.

    Liked by 1 person

    Reply
    1. Stéphane Cordonnier (@s_cordonnier) Post author

      What’s your code to instantiate the FileSystemWatcher object and where your instance was declared ?

      Regarding the errror message, it seems that when the callback closure is called then when the code try to get the instance from the context, the object was deallocated.

      In my own application, my code which works looks like this :

      class MainController: NSViewController {
      let fileSystemWatcher = FileSystemWatcher(…)
      }

      The instance has the same liftetime as the main application view controller.

      Like

      Reply
      1. Thomas Kilian

        Huu. I don’t know what happened. I put this aside for the moment and worked on other stuff. Now when I started it once again I had no more issues. I guess you know what I mean 😉 I’ll dive into this more seriously the next days and will come back with good (or bad) details. Thanks for the fast response.

        Like

        Reply
  3. Pingback: Developing a FileSystemWatcher for OS X by using FSEvents with Swift 2.0 | Dinesh Ram Kali.

  4. Bipin Vayalu

    Hi Stephane, I am setting closer in start function and want to call it in eventCallback like fileSystemWatcher.eventHandler(eventPath:eventPath). But i am getting nil exception. Looks like object is changed in eventCallback. Any idea how can i do that.

    Like

    Reply
      1. Bipin Vayalu

        Thanks Stephane for quick response, Here is my code:
        Add some stuff to FileSystemWatcher.swift class

        // created eventHandler property
        public var eventHandler: ((eventPath: String)->())?

        // setting up eventHandler property
        func start(handler: (eventPath: String) -> ()) {
        self.eventHandler = handler
        // other code as it is
        }

        private let eventCallback: FSEventStreamCallback = { (stream: ConstFSEventStreamRef, contextInfo: UnsafeMutablePointer, numEvents: Int, eventPaths: UnsafeMutablePointer, eventFlags: UnsafePointer, eventIds: UnsafePointer) in
        print(“***** FSEventCallback Fired *****”)

        let fileSystemWatcher = unsafeBitCast(contextInfo, FileSystemWatcher.self)
        let paths = unsafeBitCast(eventPaths, NSArray.self) as! [String]

        for index in 0..<numEvents {
        fileSystemWatcher.processEvent(eventIds[index], eventPath: paths[index], eventFlags: eventFlags[index])
        // Calling eventHandler closure here – Getting error here
        fileSystemWatcher.eventHandler!(eventPath:paths[index])
        }
        fileSystemWatcher.lastEventId = eventIds[numEvents – 1]
        }

        I hope this will help you to figure out issue.

        Like

        Reply
      2. Stéphane Cordonnier (@s_cordonnier) Post author

        I found the problem and there was a bug in the code.

        I updated the article and the problem was corrected with these 2 lines :

        var context = FSEventStreamContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
        context.info = UnsafeMutablePointer(unsafeAddressOf(self))

        Now it should work as expected 😉

        Liked by 1 person

        Reply
  5. warlockosx

    Hello, all works fine, but I have a problem to convert eventFlag in

    private func processEvent(eventId: FSEventStreamEventId, eventPath: String, eventFlag: FSEventStreamEventFlags) {
    print(“\t\(eventId) – \(eventFlag)) – \(eventPath)”)
    }

    from Int value like 107776 to something like that as it described in documentation
    kFSEventStreamEventFlagItemCreated = 0x00000100

    Thanks!

    Like

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s