Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-06-22

Code Sample: FSEvents and Swift

FSEvents allow an application to process file system events.

My recent App needs to listen for changes in a folder. If a file is removed from the folder, it also needs to be removed from the datamodel.

Unfortunately the interface for FSEvents is at CF level, thus quite low level C, and it needs the dreaded CFunctionPointer. Swift 1.x is not capable of creating a CFunctionPointer, thus a pure Swift implementation for FSEvent handling needs to wait for Swift 2 later this year.

However, my App cannot wait, so I had to work out how to do this in a mix of Swift and Objective-C. It is a dual use solution: Swift calls Objective-C to create the FSEventStreamRef and to process the events (in the callback). And the callback in Objective-C calls Swift to do the final processing.

My Swift code looks as follows:

    func registerForFileSystemEvents() {
        
        // TODO: In Swift 2 replace this code with pure Swift implementation
                
        // Build array with unique folder paths
        var strings = Array<String>()
        for file in files {
            if strings.filter({ $0 == file.stringByDeletingLastPathComponent }).count == 0 {
                strings.append(file.stringByDeletingLastPathComponent)
            }
        }
        if strings.isEmpty { return }
        
        
        // Register for FSEvents

        registerForFsEventStream(strings, self, "dataModelFsEventCallback")
    }
    
    @objc func dataModelFsEventCallback() {
        updateStatus()
        removeRemovedLines()
    }

Luckily, in my data model I do not need to know which event was returned. All events are handled in the same way, by updating the status of all files. Once the status has been updated I can remove those files which are no longer present.

The Swift callback that is called from Objective-C must be marked with @objc.

In a supporting Objective-C header file (included in the bridging header!) the following is defined:

#ifndef c_support_h
#define c_support_h

void registerForFsEventStream(NSArray *arr, id obj, SEL sel);

#endif

And in the corresponding implementation:

#import <Foundation/Foundation.h>
#import "c_support.h"

FSEventStreamRef fsEventStreamRef = NULL;
id callbackObject = NULL;
SEL callbackMethod;

void fsEventStreamCallback(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) {
    
    if (callbackObject != NULL) {
        
        // Supress the "leak" warning.
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Warc-performSelector-leaks"

        [callbackObject performSelector:callbackMethod];
        
        // Restore the old diagnostics
        #pragma clang diagnostic pop
    }
}

void registerForFsEventStream(NSArray *arr, id obj, SEL sel) {

    if (fsEventStreamRef) {
        FSEventStreamStop(fsEventStreamRef);
        FSEventStreamUnscheduleFromRunLoop(fsEventStreamRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
        FSEventStreamInvalidate(fsEventStreamRef);
        FSEventStreamRelease(fsEventStreamRef);
        fsEventStreamRef = NULL;
    }
    
    callbackObject = obj;
    callbackMethod = sel;
    
    fsEventStreamRef = FSEventStreamCreate(
                                           kCFAllocatorDefault,
                                           fsEventStreamCallback,
                                           NULL,
                                           (__bridge CFArrayRef)(arr),
                                           kFSEventStreamEventIdSinceNow,
                                           1, // delay 1 second between event and callback
                                           kFSEventStreamCreateFlagIgnoreSelf);
    
    FSEventStreamScheduleWithRunLoop(fsEventStreamRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    FSEventStreamStart(fsEventStreamRef);
}

I think the implementation speaks for itself. It can be held very simple because the FSEvents flags are pretty useless. For example when a file is removed, it is put in the trash, hence two events are received: first a "rename" event and then a ".DS_store" event. But for a real file renaming there are also two events: first the "rename" event with the old file name and then a second "rename" event for the new filename. Unfortunately without any means to associate the two. The effect is that it becomes impossible to differentiate between a "real" rename event and a remove event.

PS: I know about the sequential event id's of a "real" rename event. However I have not found any guarantee from Apple which states that these id's will always be sequential. It is therefore possible that they won't be sequential and hence unusable.

The only solution I see is to treat any FSEvent as a signal for rescanning a folder and treat any possible rename simply as a removed file. This is harder on the user but easier on the programmer: there is no need for parameters on the call-back to Swift ;-)

Another thing I noticed is that fsEventStreamCallback will not be called for folders that are under the protection of the sandbox. Hence you need to make sure that the user allows the folder to be scanned for FSEvents. (My App does this by asking the user to open the directory in a file-open dialogue)

2015-07-20: To persist file access right between session, see this post

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment