hara.io.watch watch for filesystem changes

Author: Chris Zheng  (z@caudate.me)
Date: 11 February 2018
Repository: https://github.com/zcaudate/hara
Version: 2.8.2

1    Introduction

1.1    Installation

Add to project.clj dependencies:

[zcaudate/hara.io.watch "2.8.2"]

All functions are in the hara.common.watch namespace, with hara.io.watch providing filewatch extensions:

(require '[hara.io.watch]
                '[hara.common.watch :as watch])

1.2    Motivation

hara.io.watch wraps the java.nio.file.WatchService api for an add-watch compatible interface for. There are many file watches implementations around:

As well as seperate filewatch implementations as part of larger application libraries.

The novelty of hara.io.watch lies in the concept that it extends hara.common.watch (which is an abstraction around clojures add-watch` semantics).

2    Walkthrough

2.1    Watching Atoms

There's a pattern for watching things that already exists in clojure:

(add-watch object :key (fn [object key previous next]))

However, add-watch is a generic concept that exists beyond atoms. It can be applied to all sorts of objects. Furthermore, watching something usually comes with a condition. We usually don't react on every change that comes to us in our lives. We only react when a certain condition comes about. For example, we can see the condition that is placed on this statement:

Watch the noodles on the stove and IF it starts boiling over, add some cold water to the pot

The hara.common.watch package provides for additional options to be specified when watching the object in question. Is the following example, :select :b is used to focus on :b and :diff true is a setting that configures the watcher so that it will only take action when :b has been changed:

(def subject  (atom {:a 1 :b 2}))
(def observer (atom nil))
(watch/add subject :clone
           (fn [_ _ p n] (reset! observer n))

           ;; Options
           {:select :b   ;; we will only look at :b
            :diff true   ;; we will only trigger if :b changes

(swap! subject assoc :a 0) ;; change in :a does not

@observer => nil           ;; affect watch

(swap! subject assoc :b 1) ;; change in :b does

@observer => 1

2.2    Watching Files

The same concept of watch is used for filesystems. So instead of an atom, a directory is specified using very similar semantics:

(def ^:dynamic *happy* (promise))

;; We add a watch
(watch/add (io/file ".") :save
           (fn [f k _ [cmd ^java.io.File file]]

             ;; One-shot strategy where we remove the
             ;; watch after a single event
             (watch/remove f k)
             (.delete file)
             (deliver *happy* [cmd (.getName file)]))

           ;; Options
           {:types #{:create :modify}
            :recursive false
            :filter  [".hara"]
            :exclude [".git" "target"]
            :mode :async})

;; We can look at the watches on the current directory
(watch/list (io/file "."))
=> (contains {:save fn?})

;; Create a file to see if the watch triggers
(spit "happy.hara" "hello")

;; It does!
=> (contains [anything "happy.hara"])

;; We see that the one-shot watch has worked
(watch/list (io/file "."))
=> {}

2.3    Watch Options

There are a couple of cenfigurable options for the filewatch:

  • :types determine which actions are responded to. The possible values are
    • :create, when a file is created
    • :modify, when a file is mobifies
    • :delete, when a file is deleted
    • or a combination of them
  • :recursive determines if subfolders are also going to be responded to
  • :filter will pick out only files that match this pattern.
  • :exclude wil leave out files that match this patter
  • :mode, can be either :sync or :async

2.4    Components

It was actually very easy to build hara.io.watch using the idea of something that is startable and stoppable. watcher, start-watcher and stop-watcher all follow the conventions and so it becomes easy to wrap the component model around the three methods:

(require '[hara.component :as component]
         '[hara.io.watch :refer :all])

(extend-protocol component/IComponent
  (component/-start [watcher]
    (println "Starting Watcher")
    (start-watcher watcher))

  (component/-stop [watcher]
    (println "Stopping Watcher")
    (stop-watcher watcher)))

(def w (component/start
        (watcher ["."] println
                 {:types #{:create :modify}
                  :recursive false
                  :filter  [".clj"]
                  :exclude [".git"]
                  :async false})))

(component/stop w)