I am trying to come up with a nice and easy way of detecting when there has not been any write activity in a folder I'd like to watch.
Basically, what I'd like to have is something like this:
#!/bin/sh
# colored red text (error)
function errorMsg () {
echo '\033[0;31m'"$1"'\033[0m'
}
# check for folder to monitor argument
if [ "$#" -ne 1 ]
then
errorMsg "add experiment (folder) to monitor for activity!"
exit
fi
# time out time, 3 minutes
TIMEOUT_TIME=180
LAST_CHECKED=0
function refreshTimer () {
# when called, check seconds since epoch
CURRENT_TIME=date +%s
if [ CURRENT_TIME - LAST_CHECKED > TIMEOUT_TIME ]
then
echo "file write activity halted!" | mail -s "trouble!" "user@provider.ext"
fi
LAST_CHECKED=date +%s
}
# set last checked to now.
LAST_CHECKED=date +%s
# start monitoring for file changes, and update timer when new files are being written.
fswatch -r ${1} | refreshTimer
but all sorts of bash magic is required I presume, since fswatch is a background task and piping its output creates a subshell. I would also be in need of some timer logic... I was thinking something like a setTimeout of which the time argument keeps being added to when there IS activity, but I don't know how to write it all in one script.
Bash, Python, Ruby, anything that can be installed using homebrew on OSX is fine but the simpler the better (so I understand what is happening).
Try the following - note that it requires bash
:
#!/usr/bin/env bash
# colored red text (error)
function errorMsg () {
printf '\033[0;31m%s\033[0m\n' "$*" >&2
}
# check for folder to monitor argument
if [[ $# -ne 1 ]]
then
errorMsg "add experiment (folder) to monitor for activity!"
exit 2
fi
# time-out: 3 minutes
TIMEOUT_TIME=180
# Read fswatch output as long as it is
# within the timeout value; every change reported
# resets the timer.
while IFS= read -t $TIMEOUT_TIME -d '' -r file; do
echo "changed: [$file]"
done < <(fswatch -r -0 "${1}")
# Getting here means that read timed out.
echo "file write activity halted!" | mail -s "trouble!" "user@provider.ext"
fswatch
indefinitely outputs lines to stdout, which must be read line by line to take timely action on new output.fswatch -0
terminates lines with NULs (zero bytes), which read
then reads on be one by setting the line delimiter (separator) to an empty string (-d ''
).< <(fswatch -r -0 "${1}")
provides input via stdin <
to the while
loop, where read
consumes the stdin input one NUL-terminated line at a time.
<(fswatch -r -0 "${1}")
is a process substitution that forms an "ad-hoc file" (technically, a FIFO or named file descriptor) from the output produced by fswatch -r -0 "${1}"
(which watches folder ${1}
's subtree (-r
) for file changes, and reports each terminated with NUL (-0
)). fswatch
command runs indefinitely, the "ad-hoc file" will continue to provide input, although typically only intermittently, depending on filesystem activity.read
command receives a new line within the timeout period (-t $TIMEOUT_TIME
), it terminates successfully (exit code 0), causing the body of the loop to be executed and then read
to be invoked again.
read
invocation starts over with the timeout period.read
terminates unsuccessfully - with a nonzero exit code indicating failure, which causes the while
loop to terminate.read
command times out.As for your original code:
Note: Some of the problems discussed could have been detected with the help of shellecheck.net
echo '\033[0;31m'"$1"'\033[0m'
printf
is the better choice when it comes to interpreting escape sequences, since echo
's behavior varies across shells and platforms; for instance, running your script with bash
will not interpret them (unless you also add the -e
option).function refreshTimer ()
function
syntax is nonstandard (not POSIX-compliant), so you shouldn't use it with a sh
shebang line (that's what chepner meant in his comment). On OSX, you can get away with it, because bash
acts as sh
and most bashisms are still available when run as sh
, but it wouldn't work on other systems. If you know you'll be running with bash
anyway, it's better to use a bash
shebang line.CURRENT_TIME=date +%s
CURRENT_TIME=$(date +%s)
(the older syntax with backticks - CURRENT_TIME=`date +%s`
- works too, but has disadvantages).[ CURRENT_TIME - LAST_CHECKED > TIMEOUT_TIME ]
>
in [ ... ]
and bash's [[ ... ]]
conditionals is lexical comparison and the variable names must be $
-prefixed; you'd have to use an arithmetic conditional with your syntax: (( CURRENT_TIME - LAST_CHECKED > TIMEOUT_TIME ))
fswatch -r ${1} | refreshTimer
refreshTimer
will not get called until the pipe fills up (the timing of which you won't be able to predict), because you make no attempt to read line by line.refreshTimer
won't be preserved, because refreshTimer
runs in a new subshell every time, due to use of a pipeline (|
). In bash
, this problem is frequently worked around by providing input via a process substitution (<(...)
), which you can see in action in my code above.