Monitoring Files Using Dispatch Sources

      4 Comments on Monitoring Files Using Dispatch Sources

If you need to know when a file has been modified, renamed, deleted... there's an easy way to do it using dispatch sources. In this post, we'll create a simple library that will notify you when a file has been changed.If you need to know when a file has been modified, renamed, deleted… there’s an easy way to do it using dispatch sources. In this post, we’ll create a simple library that will notify you when a file has been changed. I know I say this every time, but I’ll try and keep this post short (under 1000 words) 🙂

Dispatch Sources

Think of dispatch sources as a feature built on top of GCD. In a nutshell, with dispatch sources, you have the ability to receive low-level system events. You can register to be notified when one of these system events occur.

GCD supports a handful of events you can register to receive:

  • Timer sources
  • Signal sources
  • Descriptor sources
  • Process sources
  • Mach port sources
  • Custom sources

In this article, we’ll focus on descriptor sources. Descriptor sources notify you of file and socket operations, so these are perfect for our purpose.

Show Me The Code

We’ll be using a special type of a dispatch source called a vnode and it allows us to monitor file descriptors (among other things). When we create the dispatch source we pass in the event flags that we want to register for (like file deletion, modification…). Let’s see this on an example:

guard fileExists() == true else {
	return
}

let descriptor = open(self.filePath, O_EVTONLY)
if descriptor == -1 {
	return
}

self.eventSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: self.fileSystemEvent, queue: self.dispatchQueue)

We have to check if the file exists because if we try and use a non-existing file, we’ll crash and burn. We open a descriptor to the file and request to receive notifications only (the O_EVTONLY flag). Then at the end, we create our dispatch source by calling ‘makeFileSystemObjectSource’. Apart from the file descriptor, this method accepts the event mask, here we’ll register to receive write events. We can also specify a dispatch queue as a parameter. This will be the queue the event handler and the cancel handler will get called.

Every time a file is written to, the dispatch source we created will call the event handler. We have to set that up next:

self.eventSource?.setEventHandler {
	[weak self] in
	self?.onFileEvent?()
}

In our event handler, we’re just calling a closure that we have set as our instance variable, but you can do a bit more here if you wanted. For example, you can fetch the number of bytes written to the file.

One important thing we have to do in our example is to close the file descriptor. The best place to do this is in the cancel handler:

self.eventSource?.setCancelHandler() {
	close(descriptor)
}

Dispatch source will continue to receive events in the event handler until you cancel it. Once you cancel it, that’s it, it’s gone. But before it’s gone, it will call the cancel handler, here you have to close your file descriptors (or sockets if you’re using them), basically, clean up after yourself.

Dispatch source will not start dispatching events automatically. It will allow you to set everything up first. This means you have to tell it when it’s OK to start dispatching:

self.eventSource?.resume()

Couldn’t be simpler 🙂 Now you should be seeing events coming in every time you change a file.

DAFileMonitor

I created a simple wrapper around dispatch sources that will allow you to easily monitor your file changes, you can find the library on my GitHub account.

The library has a closure that gets called from the event handler to notify the receiver that the file has been modified:

public var onFileEvent: (() -> ())? {
	willSet {
		self.eventSource?.cancel()
	}
	didSet {
		if (onFileEvent != nil) {
			self.startObservingFileChanges()
		}
	}
}

We’re doing two things when setting the closure: we’re cancelling the event source just before setting the closure and we’re creating the event source after setting the closure (if the closure is not nil).

And, of course, in the deinit method we have to cancel the dispatch source:

deinit {
	self.eventSource?.cancel()
}

Usage

This class is really simple to use. If you’re happy with the default parameters, all you need to do is provide the path to the file you’re monitoring and you’ll get a callback when the file changes:

self.fileMonitor = DAFileMonitor(withFilePath: filePath)
self.fileMonitor?.onFileEvent = {
	let dict = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? [String: Any] ?? [:]
	self.onDataAvailable?(dict)
}

There we go, our simple file monitor is finished 🙂

Conclusion

If you have to monitor your files for changes there’s an easy way to do it and with this little wrapper, it’s even easier. To be honest with you, I was planning on writing about something else this week, but I needed a library like this one. After a bit of digging through Apple documentation, I wrote my own. Hopefully, in one of the future posts, you’ll see how this piece of the puzzle fits into the picture (I won’t spoil the surprise for you 🙂 ). You can get all the code on my GitHub account.

As promised, under 1000 words 🙂

Have a nice day 🙂

Dejan.

More resources

4 thoughts on “Monitoring Files Using Dispatch Sources

  1. Anand

    Hi Dejan,
    Very informative article, thanks for sharing.I have use case where I want to get notified inside in iOS application about about the text changes done in a file present on the desktop. So is this possible using the above code snippet. I have already used this code for the same purpose but I am not get any notification inside my application. I am missing something here ?

    TIA

    Reply
    1. Dejan Agostini Post author

      Your usecase is something completely different from what this article covers. You’ll need to use a backend of some sort to sync up your desktop app and the iOS app.

      Reply
      1. Anand

        I don’t have a desktop application, I am just making changes to a text file present on the desktop and I want to monitor the changes in the iOS application.

        Reply
  2. Devy

    Hi Dejan,

    I am trying to make your code work for me.
    All I would like to do is a GUI app where I would like to update a Label whenever a file changes.
    So far, I coded this:

    import Cocoa

    class ViewController: NSViewController {

    @IBOutlet weak var labelMessage: NSTextField!

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    monitorFile(filePath: “/Users/Devy/Desktop/test.txt”)
    }

    override var representedObject: Any? {
    didSet {
    // Update the view, if already loaded.
    }
    }

    func monitorFile(filePath: String) {
    var fileMonitor: DAFileMonitor?

    fileMonitor = DAFileMonitor(withFilePath: filePath)

    fileMonitor?.onFileEvent = {
    print(“File modified”)
    self.labelMessage.stringValue = (“File modified”)
    }
    }
    }

    But it does not do anything other than creates the files if not yet existed.
    What do I do wrong?

    Many thanks for your help in advance!
    Devy

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.