Swiftpack.co - Package - Ponyboy47/inotify

Inotify

Version Build Status Platforms Swift Version

A swifty wrapper around Linux's inotify API. Trying to make using inotify in Swift as easy as possible.

Annoyed with the lack of FileSystemEvent notifications in Swift on Linux that are easily accessible to Swift on iOS/macOS? Well now there's no need to fret! Using the Linux inotify API's, this library is bringing first class support for file notifications to Swift! Easily watch files and directories for a large set of events and perform callbacks immediately when those events are triggered.

Features:

  • Easily add and remove paths to watch for specific events
    • Includes an easy to use enum for all of inotify's supported filesystem events
  • Easily start and stop watching for events
    • Two available methods of waiting for events (select(2) and manual polling)
  • Supports custom DispatchQueues for both the monitoring and executing
  • Easily execute callbacks when an event is triggered
  • Everything is done asynchronously
  • InotifyEvent wraps the inotify_event struct to allow access to the optional name string (not normally available in the C to Swift interop)
  • Handy error handling using the errno to give more descriptive errors when something goes wrong

Installation (SPM):

Add this to your Package.swift:

.package(url: "https://github.com/Ponyboy47/inotify.git", from: "0.5.3")

NOTE: The current version of Inotify (0.5.3) uses Swift 4.2. For Swift 3, use version 0.3.x

Usage:

Creating and watching paths for events

import Inotify

do {
    let inotify = try Inotify()

    try inotify.watch(path: "/tmp", for: .allEvents) { event in
        let mask = FileSystemEvent(rawValue: event.mask)
        print("A(n) \(mask) event was triggered!")
        if let name = event.name {
            // This should only be present when the event was triggered on a
            // file in the watched directory, and not on the directory itself.
            print("The filename for the event is '\(name)'.")
        }
    }

    inotify.start()
} catch InotifyError.InitError {
    print("Error initializing the inotify object: \(error)")
} catch InotifyError.WatchError {
    print("Error adding watcher to the inotify object: \(error)")
}

Using a different polling implementation:

import Inotify

do {
    let inotify = try Inotify(eventWatcherType: ManualWaitEventWatcher.self)

    try inotify.watch(path: "/tmp", for: .allEvents) { event in
        let mask = FileSystemEvent(rawValue: event.mask)
        print("A(n) \(mask) event was triggered!")
        if let name = event.name {
            // This should only be present when the event was triggered on a
            // file in the watched directory, and not on the directory itself.
            print("The filename for the event is '\(name)'.")
        }
    }

    inotify.start()
} catch InotifyError.InitError {
    print("Error initializing the inotify object: \(error)")
} catch InotifyError.WatchError {
    print("Error adding watcher to the inotify object: \(error)")
}

or

import Inotify

do {
    let watcher = ManualWaitEventWatcher()
    let inotify = try Inotify(eventWatcher: watcher)

    try inotify.watch(path: "/tmp", for: .allEvents) { event in
        let mask = FileSystemEvent(rawValue: event.mask)
        print("A(n) \(mask) event was triggered!")
        if let name = event.name {
            // This should only be present when the event was triggered on a
            // file in the watched directory, and not on the directory itself.
            print("The filename for the event is '\(name)'.")
        }
    }

    inotify.start()
} catch InotifyError.InitError {
    print("Error initializing the inotify object: \(error)")
} catch InotifyError.WatchError {
    print("Error adding watcher to the inotify object: \(error)")
}

^^ This can also be used to override default variables for the select or manual wait watchers:

import Inotify

do {
    // either
    let watcher = ManualWaitEventWatcher(delay: 0.5)
    // or
    let timeout: timeval = timeval(tv_sec: 1, tv_usec: 0)
    let watcher = SelectEventWatcher(timeout: timeout)

    let inotify = try Inotify(eventWatcher: watcher)

    try inotify.watch(path: "/tmp", for: .allEvents) { event in
        let mask = FileSystemEvent(rawValue: event.mask)
        print("A(n) \(mask) event was triggered!")
        if let name = event.name {
            // This should only be present when the event was triggered on a
            // file in the watched directory, and not on the directory itself.
            print("The filename for the event is '\(name)'.")
        }
    }

    inotify.start()
} catch InotifyError.InitError {
    print("Error initializing the inotify object: \(error)")
} catch InotifyError.WatchError {
    print("Error adding watcher to the inotify object: \(error)")
}

Stop watching paths:

try inotify.unwatch(path: "/tmp")

try inotify.unwatchAll()

More examples to come later...

Creating custom watchers:

It is possible to write your own watcher that will block a thread until a file descriptor is ready for reading. By default, I've provided one using the C select API's and I plan on adding more later. (See the Todo for the others I plan on adding and feel free to help me out by making them yourself and submitting a pull request)

There are 2 Protocols to choose from when implementing a watcher:

  • InotifyEventWatcher
  • InotifyStoppableEventWatcher

The only difference, is that the Stoppable watcher can be force stopped while it is blocking a thread (ie: receive a signal to be interrupted and stop gracefully, like epoll)

A watcher just needs to monitor the inotify file descriptor and block a thread. Once the file descriptor is prepared to be read from, unblock the thread and the Inotify class object will handle the actual reading of the file descriptor and subsequent creating of the InotifyEvents and executing of the callbacks.

You can look at polling+Select.swift to see how I implemented the select-based watcher. It may be helpful to read the select man pages (or other documentation) in order to more fully understand what it does in the backend.

Which watcher is best?

This really depends on what you plan on doing with it and what kinds of capabilities you need for your project.

The manual poller is probably not ever going to be your first choice because it's horribly inneficient and I mostly just made it for completion sake and so in the simplest of instances you always have something that will work.
I implemented the select-based watcher first because I've used select for inotify monitoring before and was already familiar with how to use it.
I'm not really familiar with poll, epoll, or pselect since I've never used them.

These links though contain a great amount of information about the differences, shortcomings, and strengths of select, poll, and epoll and may be handy when deciding on which watcher you would like to use:

  • https://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/
  • https://gist.github.com/beyondwdq/1261042
  • https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll/
  • https://stackoverflow.com/questions/17355593/why-is-epoll-faster-than-select
  • http://amsekharkernel.blogspot.com/2013/05/what-is-epoll-epoll-vs-select-call-and.html

Known Issues:

Todo:

  • [x] Init with inotify_init1 for flags
  • [x] Useful errors with ErrNo
  • [x] Asynchronous monitoring
  • [ ] Synchronous monitoring
  • [ ] Better error propogation in the asynchronous monitors
  • [x] Update to Swift 4
  • [ ] Support various watcher implementations
    • [x] manual polling
    • [x] select
    • [ ] pselect // Maybe not
    • [ ] poll
    • [ ] epoll
  • [ ] Write tests for the watchers
    • [x] manual polling
    • [x] select
    • [ ] pselect
    • [ ] poll
    • [ ] epoll
  • [x] Make watchers more modular (so that others could easily write their own custom ones)
  • [x] Auto-stop the watcher if there are no more paths to watch (occurs when all paths were one-shot events and they've all been triggered already)
  • [ ] Handle inotify event cookie values
  • [ ] Automatically set up recursive watchers (Since by default inotify only monitors one directory deep)

Github

link
Stars: 1
Help us keep the lights on

Releases

0.5.1 - Sep 18, 2018

Updated to swift 4.2 and made a few minor changes/fixes

0.4.2 - Nov 30, 2017

Fixes for when an inotify_event struct mask contains special mask values such as the isDirectory mask.

0.4.1 - Nov 16, 2017

0.3.0 - Oct 31, 2017

Fixed a number of bugs, simplified some of the workflows, more robust coverage of inotify's capabilities, more/better test cases

0.2.0 - Oct 28, 2017

Modular event watchers and bug fixes