Tools Watchman Lead image: Lead Image © leksele, 123RF.com
Lead Image © leksele, 123RF.com
 

Look for file changes and

On Guard

Watchman watches files and directories for changes and triggers actions when specific changes are noted. By Alessio Ciregia

Watchman is an open source tool developed by Facebook and released under the terms of Apache License 2.0. The Watchman website [1] states: "Watchman exists to watch files and record when they change. It can also trigger actions (such as rebuilding assets) when matching files change."

Written in C, Watchman is multiplatform: It works on Linux, Mac OS X, BSD, and Illumos/Solaris. Windows is not listed as a supported operating system. Although the main goal of Watchman might not be as I describe in this article, its job is to trigger actions when a file changes. This functionality perfectly fits various needs.

Watchman might seem complex at first glance, with its options and configuration directives (yes, it is), but I'll start with some simple examples; then, you can delve deeper into the program on your own.

Replicating Files

Working with distributed or replicated systems is a common task these days. The magic word nowadays – the cloud – is an unclear term, but if you want to simplify the concept, the cloud is what years ago was called a "cluster." If you want to replicate a directory on a cluster of servers, a lot of software, besides shared storage like NFS, is at your disposal. However, often the learning curve is steep, yet the goal is so simple.

The basic scenario is as follows: You have a directory on a server and you want to replicate its content on another server. The simple way to achieve this is with a copy command (maybe using the rsync tool) triggered by a job scheduled in a crontab. Obviously this solution is not optimal, because the scheduled job will be executed even if the directory has not changed, the time between the execution of a task and a following one could be too long or too short, or the copy starts while a file is in a transitional stage, leading to some sort of inconsistency at a given time. Also, why wait until night to perform a backup? Sometimes it might be useful to copy a file as soon as possible.

In such a case, a handy tool like Watchman, which watches the tree of a directory (or a number of directories) and triggers an action when a change is detected, is a good solution.

Triggering Rsync

Install Watchman as instructed in the "Installation" box and start it with the command:

watchman watch /opt/repos

The path /opt/repos is an example of a directory that you might want to replicate to another server. In Watchman it is called the "root."

At this point the /usr/local/var/run/watchman/<username>.state configuration file will look like Listing 1. The Watchman process is up and running, but right now, it is simply watching the directory and reporting changes in the logfiles. (You can also query the daemon for changed files from the command line).

Listing 1: Configuration File

01 {
02   "version": "3.0.0",
03   "watched": [
04     {
05       "path": "/opt/repos",
06       "triggers": []
07     }
08   ]
09 }

The next step is to define what action to trigger. The syntax is:

watchman -- trigger /opt/repos 'repos-sync' \
             -- /usr/local/sbin/sync.sh /opt/repos

Where 'repos-sync' is the name of the trigger and /usr/local/sbin/sync.sh is the script to invoke. Now the configuration file looks like Listing 2.

Listing 2: Configuration File with Trigger Action

01 {
02   "version": "3.0.0",
03   "watched": [
04     {
05       "path": "/opt/repos",
06       "triggers": [
07         {
08           "name": "repos-sync",
09           "command": [
10             "/usr/local/sbin/sync.sh"
11           ],
12           "append_files": true,
13           "stdin": [
14             "name",
15             "exists",
16             "new",
17             "size",
18             "mode"
19           ]
20         }
21       ]
22     }
23   ]
24 }

The script that performs the rsync operation, sync.sh, will look like Listing 3. Your script might be more complex (e.g., by adding some sort of logging or notification via email). Please note that the environment variable WATCHMAN_ROOT is set by Watchman itself and contains the "root" (i.e., the watched directory). Make sync.sh executable by entering:

chmod +x /usr/local/sbin/sync.sh

However, you also need to implement SSH key authentication to use rsync over SSH without the need for a password. If you don't know how to do this, you'll find several articles on the Internet (e.g., at the TeachMeJoomla site [3]) describing this procedure.

Listing 3: sync.sh

01 #!/bin/bash
02
03 twinserver="remoteserver.your.domain"
04
05 rsync -avp --delete -e "ssh -i ~/.ssh/id_rsa_rsync" \
   ${WATCHMAN_ROOT}/ ${twinserver}:${WATCHMAN_ROOT}/

Watching Multiple Directories

If you need to monitor more than one directory, you can reuse the same script, because the aforementioned WATCHMAN_ROOT variable contains the correct path. To do so, you need to run the commands as follows:

watchman watch /opt/repos2
watchman -- trigger /opt/repos2 'repos2-sync' \
         -- /usr/local/sbin/sync.sh

Now run the commands

watchman watch-list
watchman trigger-list <root>

to see the list of watched directories (without going to the .state file) and the list of triggers.

Excluding Files

When you define a trigger, you can specify files to exclude; that is, you can prevent the trigger from executing if a file name contains a particular pattern. Note that with rsync, such an exclusion will not be taken in account. The following command can be performed at run time, and the configuration will be updated accordingly:

watchman -- trigger /opt/repos 'repos-sync' \
             -X '*.css' \
             -I '*' -- /usr/local/sbin/sync.sh

The -X excludes a pattern and -I includes a pattern. For a full accounting of pattern syntax, please refer to the Watchman website [4].

When a new file or any change inside the watched directory is detected, Watchman waits for the filesystem (the directory) to be idle for a time before dispatching triggers. By default, this time is 20 milliseconds, and this option is named "settle". To increase the amount of time, you must set the "settle" option in the global configuration file – /etc/watchman.json by default. This file uses JSON syntax, as well:

{
   "settle": 60
}

The settle option is useful if you want to prevent the script from executing before a large file write has completed.

Real-Time Virus Scanner

Watching a real-time virus scanner, such as the free antivirus ClamAV [5] or the free anti-malware tool Linux Malware Detect (a.k.a., maldet) [6], would be another interesting use case for Watchman. The maldet tool uses the Linux inotify facility to perform real-time scanning; however, Solaris/Illumos do not have inotify, so you would be unable to use this functionality on such operating systems. In this case, a good solution would be to use Watchman.

For example, suppose you want to scan files for viruses with the clamscan utility. Whenever a file is added, changed, or deleted, the Watchman service will trigger the scanner.sh script (Listing 4), which invokes the virus scanner, passing in the file name to the $@ variable.

Listing 4: scanner.sh

01 #!/bin/bash
02
03 for file in $@
04 do
05   clamscan --stdout --no-summary --infected ${WATCHMAN_ROOT}/$file
06   EL=$?
07   if [ $EL -eq 1 ]
08   then
09     # Virus found
10     echo "Virus found: ${WATCHMAN_ROOT}/$file" | \
         mailx -s "Virus found" mail@example.com
11   elif [ $EL -eq 2 ]
12   then
13     # Clamscan error
14     echo "Scan error: ${WATCHMAN_ROOT}/$file" | \
         mailx -s "Scan error" mail@example.com
15   fi
16   # Else no virus found
17 done

However, when you delete a file, Watchman still kicks the trigger, and the name of the deleted file is passed to your script, even though you don't need to scan a non-existent file; therefore, you need to use extended trigger syntax (Listing 5). The key options are "exists" and "append_files": true. The append_files entry enables the arguments – that is, the list of new or changed files – to be passed to the script, and "exists" tells Watchman to trigger the action only if the file exists.

Now, if you execute

watchman watch /opt/share

Watchman passes a list of changed, but not deleted, files to the script, using the relative path to "root" (/opt/share). Of course, you could also use clamdscan, if the clamd service is up and running, or the aforementioned maldet tool on Linux.

Listing 5: Extended Trigger Syntax

01 watchman -j <<-EOT
02 ["trigger", "/opt/share", {
03   "name": "scanner",
04   "expression": ["allof", ["match", "*"], ["exists"]],
05   "command": ["/usr/local/sbin/scanner.sh"],
06   "append_files": true
07 }]
08 EOT

Shutdown and Reboot

The correct way to shut down Watchman is:

watchman shutdown-server

Remember that if you reboot your server, you need a way to start Watchman automatically at bootup. For SmartOS [7], you can import a sample SMF manifest from the watchman-pkgsrc GitHub site [8] (check the paths). For CentOS 7 with the systemd suite, you can create the file /etc/systemd/system/watchman.service containing something like Listing 6. Finally, you need to issue

systemctl enable watchman
systemctl start watchman

to enable Watchman on bootup and start the service.

Listing 6: watchman.service

01 [Unit]
02 Description=Watchman
03 After=network.target
04
05 [Service]
06 Type=simple
07 User=root
08 ExecStart=/usr/local/bin/watchman --log-level 1 watch-list -f
09 ExecStop=/usr/local/bin/watchman shutdown-server
10
11 [Install]
12 WantedBy=multi-user.target

Conclusion

Watchman is a flexible tool under active development. Even if its usage seems unfriendly, learning some basic functionality can help you accomplish complex tasks with your own scripts. Here, I looked at two use cases: triggering rsync and a real-time virus scanner. Instead of triggering an action, you could also simply query Watchman for changes that happen inside a directory during a given period. The uses for Watchman are numerous: No doubt a number of applications have occurred to you already.