I found the watchman
docs hard to follow / bereft of useful examples. Below is my attempt at a quickstart guide; see ryan-williams/watchman-helpers for some aliases and other scripts.
- 0. Install
watchman
- 1. Configure a "watch" on the current directory
- 2. Set up a "trigger": continuously run a command in response to changes
- 3. Debugging / Monitoring
See installation docs.
watchman watch .
watchman watch-list
# {
# "version": "2023.02.20.00",
# "roots": [
# "$PWD"
# ]
# }
This should just show the directory you told it to watch above (I've substituted $PWD
here, it will be a literal absolute path on your system).
watchman watch-del .
In this example, we'll rsync
the current directory to a remote host
# Set up params
HOST=ec2 # configure this SSH host in your ~/.ssh/config
DST_DIR="`basename`" # remote directory to mirror this one to; `basename` will target a directory with the same name on the remote host (under your $HOME directory on that host)
trigger_name=ec2-sync # arbitrary name, used for `trigger-del` later
trigger_patterns=('**/*' '**/.*') # trigger on all files in the current directory
trigger_cmd=(rsync -avzh ./ "$HOST:$DST_DIR") # rsync the current directory to the remote host/dir
# Create the "trigger". It will begin running immediately and continuously (in response to file changes in the current directory)
watchman -- trigger "$PWD" "$trigger_name" "${trigger_patterns[@]}" -- "${trigger_cmd[@]}"
The patterns ('**/*' '**/.*')
are the only way I've found to get all files recursively under the watched root (including "hidden" files and directories, that begin with a .
). However, when creating the trigger I've had the watchman logs print an error like:
trigger <dir>:<name> failed: posix_spawnp: Argument list too long
Apparently there is an inital run attempt that includes all eligible files as positional arguments. Maybe there's a better way, or workaround 🤷♂️. The "Simple Pattern Syntax" page doesn't seem to say, and single globs like (
'' '.')` (Bash array syntax, as above) did not trigger for changes in subfolders.
If your command gets more complex, you might want to factor it out to its own file, e.g.:
sync.sh
#!/usr/bin/env bash
# Configure this SSH host in your ~/.ssh/config
HOST=ec2
# Remote directory to mirror the current directory to. As written here, will target a directory
# with the same `basename` on the remote host (under your `$HOME` directory on that host)
DST_DIR="$(basename "$(dirname "${BASH_SOURCE[0]}")")"
# List some exclude paths here to pass to `rsync`
excludes=(.DS_Store __pycache__ .ipynb_checkpoints .pytest_cache '*.egg-info')
exclude_args=()
for exclude in "${excludes[@]}"; do
exclude_args+=(--exclude "$exclude")
done
rsync -avzh "$@" "${exclude_args[@]}" ./ "$HOST:$DST_DIR/"
Then make sure to mark it as executable, and pass an absolute path to that script to watchman -- trigger …
:
chmod 755 sync.sh
watchman -- trigger "$PWD" "$trigger_name" "${trigger_patterns[@]}" -- "$PWD/sync.sh"
watchman trigger-list .
# {
# "version": "2023.02.20.00",
# "triggers": [
# {
# "append_files": true,
# "name": "ec2-sync",
# "stdin": [ "name", "exists", "new", "size", "mode" ],
# "expression": [ "anyof", [ "match", "*", "wholename" ] ],
# "command": [ "rsync", "-avzh", "./", "$HOST:$DST_DIR" ]
# }
# ]
# }
watchman trigger-del . "$trigger_name"
watchman-wait -m0 .
-m0
causes this to run indefinitely (by default, it exits after one event, i.e.-m1
).
watches the current directory
logfile="$(ps aux | grep watchman | grep -o -- '--logfile=\S*' | awk -F= '{ print $2 }')"
- For me (on macOS, with
watchman
installed by Homebrew]()), this is/opt/homebrew/var/run/watchman/ryan-state/log
. - Thanks to this StackOverflow answer
- "Where are the logs" docs were not helpful
Continuously stream new output to the logfile:
tail -f "$logfile"