Using FileSystemEvent to monitor a Dir ?

Trying to find a way to monitor a directory for file changes but ending up a bit lost. Any source code using FileSystemEvent to do this ?

Accepted Reply

FSEvents is a low-level API and it’s quite tricky to call from Swift. I had some code for this lying around, so I added a bunch of comments, tidied it up a bit, and pasted it in below.

ps FSEvents is for monitoring an entire directory hierarchy. If you just want to monitor the top level of a directory, there are cheaper options (using a Dispatch event source). Let me know if that’s the case and I’ll dig up a reference.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
class DirMonitor {

    init(dir: URL, queue: DispatchQueue) {
        self.dir = dir
        self.queue = queue
    }

    deinit {
        // The stream has a reference to us via its `info` pointer. If the
        // client releases their reference to us without calling `stop`, that
        // results in a dangling pointer. We detect this as a programming error.
        // There are other approaches to take here (for example, messing around
        // with weak, or forming a retain cycle that’s broken on `stop`), but
        // this approach:
        //
        // * Has clear rules
        // * Is easy to implement
        // * Generate a sensible debug message if the client gets things wrong
        precondition(self.stream == nil, "released a running monitor")
        // I added this log line as part of my testing of the deallocation path.
        NSLog("did deinit")
    }

    let dir: URL
    let queue: DispatchQueue

    private var stream: FSEventStreamRef? = nil

    func start() -> Bool {
        precondition(self.stream == nil, "started a running monitor")

        // Set up our context.
        //
        // `FSEventStreamCallback` is a C function, so we pass `self` to the
        // `info` pointer so that it get call our `handleUnsafeEvents(…)`
        // method.  This involves the standard `Unmanaged` dance:
        //
        // * Here we set `info` to an unretained pointer to `self`.
        // * Inside the function we extract that pointer as `obj` and then use
        //   that to call `handleUnsafeEvents(…)`.

        var context = FSEventStreamContext()
        context.info = Unmanaged.passUnretained(self).toOpaque()

        // Create the stream.
        //
        // In this example I wanted to show how to deal with raw string paths,
        // so I’m not taking advantage of `kFSEventStreamCreateFlagUseCFTypes`
        // or the even cooler `kFSEventStreamCreateFlagUseExtendedData`.

        guard let stream = FSEventStreamCreate(nil, { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in
                let obj = Unmanaged<DirMonitor>.fromOpaque(info!).takeUnretainedValue()
                obj.handleUnsafeEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags, eventIDs: eventIds)
            },
            &context,
            [self.dir.path as NSString] as NSArray,
            UInt64(kFSEventStreamEventIdSinceNow),
            1.0,
            FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
        ) else {
            return false
        }
        self.stream = stream

        // Now that we have a stream, schedule it on our target queue.

        FSEventStreamSetDispatchQueue(stream, queue)
        guard FSEventStreamStart(stream) else {
            FSEventStreamInvalidate(stream)
            self.stream = nil
            return false
        }
        return true
    }

    private func handleUnsafeEvents(numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer<FSEventStreamEventFlags>, eventIDs: UnsafePointer<FSEventStreamEventId>) {
        // This takes the low-level goo from the C callback, converts it to
        // something that makes sense for Swift, and then passes that to
        // `handle(events:…)`.
        //
        // Note that we don’t need to do any rebinding here because this data is
        // coming C as the right type.
        let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self)
        let pathsBuffer = UnsafeBufferPointer(start: pathsBase, count: numEvents)
        let flagsBuffer = UnsafeBufferPointer(start: eventFlags, count: numEvents)
        let eventIDsBuffer = UnsafeBufferPointer(start: eventIDs, count: numEvents)
        // As `zip(_:_:)` only handles two sequences, I map over the index.
        let events = (0..<numEvents).map { i -> (url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId) in
            let path = pathsBuffer[i]
            // We set `isDirectory` to true because we only generate directory
            // events (that is, we don’t pass
            // `kFSEventStreamCreateFlagFileEvents` to `FSEventStreamCreate`.
            // This is generally the best way to use FSEvents, but if you decide
            // to take advantage of `kFSEventStreamCreateFlagFileEvents` then
            // you’ll need to code to `isDirectory` correctly.
            let url: URL = URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil)
            return (url, flagsBuffer[i], eventIDsBuffer[i])
        }
        self.handle(events: events)
    }

    private func handle(events: [(url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId)]) {
        // In this example we just print the events with get, prefixed by a
        // count so that we can see the batching in action.
        NSLog("%d", events.count)
        for (url, flags, eventID) in events {
            NSLog("%16x %8x %@", eventID, flags, url.path)
        }
    }

    func stop() {
        guard let stream = self.stream else {
            return          // We accept redundant calls to `stop`.
        }
        FSEventStreamStop(stream)
        FSEventStreamInvalidate(stream)
        self.stream = nil
    }
}

Replies

FSEvents is a low-level API and it’s quite tricky to call from Swift. I had some code for this lying around, so I added a bunch of comments, tidied it up a bit, and pasted it in below.

ps FSEvents is for monitoring an entire directory hierarchy. If you just want to monitor the top level of a directory, there are cheaper options (using a Dispatch event source). Let me know if that’s the case and I’ll dig up a reference.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
class DirMonitor {

    init(dir: URL, queue: DispatchQueue) {
        self.dir = dir
        self.queue = queue
    }

    deinit {
        // The stream has a reference to us via its `info` pointer. If the
        // client releases their reference to us without calling `stop`, that
        // results in a dangling pointer. We detect this as a programming error.
        // There are other approaches to take here (for example, messing around
        // with weak, or forming a retain cycle that’s broken on `stop`), but
        // this approach:
        //
        // * Has clear rules
        // * Is easy to implement
        // * Generate a sensible debug message if the client gets things wrong
        precondition(self.stream == nil, "released a running monitor")
        // I added this log line as part of my testing of the deallocation path.
        NSLog("did deinit")
    }

    let dir: URL
    let queue: DispatchQueue

    private var stream: FSEventStreamRef? = nil

    func start() -> Bool {
        precondition(self.stream == nil, "started a running monitor")

        // Set up our context.
        //
        // `FSEventStreamCallback` is a C function, so we pass `self` to the
        // `info` pointer so that it get call our `handleUnsafeEvents(…)`
        // method.  This involves the standard `Unmanaged` dance:
        //
        // * Here we set `info` to an unretained pointer to `self`.
        // * Inside the function we extract that pointer as `obj` and then use
        //   that to call `handleUnsafeEvents(…)`.

        var context = FSEventStreamContext()
        context.info = Unmanaged.passUnretained(self).toOpaque()

        // Create the stream.
        //
        // In this example I wanted to show how to deal with raw string paths,
        // so I’m not taking advantage of `kFSEventStreamCreateFlagUseCFTypes`
        // or the even cooler `kFSEventStreamCreateFlagUseExtendedData`.

        guard let stream = FSEventStreamCreate(nil, { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in
                let obj = Unmanaged<DirMonitor>.fromOpaque(info!).takeUnretainedValue()
                obj.handleUnsafeEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags, eventIDs: eventIds)
            },
            &context,
            [self.dir.path as NSString] as NSArray,
            UInt64(kFSEventStreamEventIdSinceNow),
            1.0,
            FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
        ) else {
            return false
        }
        self.stream = stream

        // Now that we have a stream, schedule it on our target queue.

        FSEventStreamSetDispatchQueue(stream, queue)
        guard FSEventStreamStart(stream) else {
            FSEventStreamInvalidate(stream)
            self.stream = nil
            return false
        }
        return true
    }

    private func handleUnsafeEvents(numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer<FSEventStreamEventFlags>, eventIDs: UnsafePointer<FSEventStreamEventId>) {
        // This takes the low-level goo from the C callback, converts it to
        // something that makes sense for Swift, and then passes that to
        // `handle(events:…)`.
        //
        // Note that we don’t need to do any rebinding here because this data is
        // coming C as the right type.
        let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self)
        let pathsBuffer = UnsafeBufferPointer(start: pathsBase, count: numEvents)
        let flagsBuffer = UnsafeBufferPointer(start: eventFlags, count: numEvents)
        let eventIDsBuffer = UnsafeBufferPointer(start: eventIDs, count: numEvents)
        // As `zip(_:_:)` only handles two sequences, I map over the index.
        let events = (0..<numEvents).map { i -> (url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId) in
            let path = pathsBuffer[i]
            // We set `isDirectory` to true because we only generate directory
            // events (that is, we don’t pass
            // `kFSEventStreamCreateFlagFileEvents` to `FSEventStreamCreate`.
            // This is generally the best way to use FSEvents, but if you decide
            // to take advantage of `kFSEventStreamCreateFlagFileEvents` then
            // you’ll need to code to `isDirectory` correctly.
            let url: URL = URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil)
            return (url, flagsBuffer[i], eventIDsBuffer[i])
        }
        self.handle(events: events)
    }

    private func handle(events: [(url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId)]) {
        // In this example we just print the events with get, prefixed by a
        // count so that we can see the batching in action.
        NSLog("%d", events.count)
        for (url, flags, eventID) in events {
            NSLog("%16x %8x %@", eventID, flags, url.path)
        }
    }

    func stop() {
        guard let stream = self.stream else {
            return          // We accept redundant calls to `stop`.
        }
        FSEventStreamStop(stream)
        FSEventStreamInvalidate(stream)
        self.stream = nil
    }
}

Thanks looks good. I tried it and it deinitializes as soon as it starts ?

it deinitializes as soon as it starts ?

You prevent deinitialisation here the same way you prevent deinitialisation in any other context, that is, by holding on to a reference to the object. You can keep this reference in a property of some other object that remains in memory (like your app delegate, or something hung off your app delegate), in a global variable, and so on.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Just being inquisitive, is there any way to check all current queues / streams ?


I changed the FSEventStreamCreateFlags to "kFSEventStreamCreateFlagFileEvents" and had a look what it returns. Unfortunatly for rename / move / remove etc. is all one flag "67584" and can't see a way to know each individual action. What I had in mind is if a file were renamed to get what it's been renamed to ? Besides knowing if a file had been removed or one had been added. It's extremely vague.

is there any way to check all current queues / streams ?

I don’t understand this question.

It's extremely vague.

Yes. Lots of folks think that FSEvents will give them a log of every individual operation that ever occurred on the file system. This is not the case. Rather, FSEvents tell you that something has happened, and it’s up to you to decide how to react.

The best way to think about FSEvents is to consider its two most important system clients, namely Spotlight and Time Machine. Both of these have their own view of the file system (for Spotlight it’s the index, for Time Machine it’s the most recent backup), and they use FSEvents to optimise their updates of that view. So they only care that something changed, not the exact details of that change. This is the expected service guarantee provided by FSEvents. If you expect something beyond that then you’re likely to be disappointed.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"