Reputation: 239810
I have a little script that watches files for changes using inotifywait
. When something changes, a batch of files are sent through a process (compiled, compressed, reorganised, etc) that takes about ten seconds to run.
Consider the following example:
touch oli-test
inotifywait -mq oli-test | while read EV; do sleep 5; echo "$EV"; done
If you run touch oli-test
in another terminal a few times, you'll see that each loop completes before it moves on. That scenario is very real to me. If I forget to save a file while it's already processing, or notice a mistake, the events stack up and I'm waiting minutes.
It strikes me that there are two techniques that would make this workflow objectively better. I'm not sure what is easiest or best, so I'm presenting both:
Interrupt previous run-throughs, and restart immediately. The scripted process is currently just an inline set of commands. I could break them out to Bash functions, I'm not wild about breaking them further out than that.
Debounce the list of things waiting to be processed so that if five events happen at once (or while it's already processing), it only runs once more.
(Or both... because I'm certain there are cases where both would be useful)
I am also open to approaches that are different from inotifywait
but they need to give me the same outcome and work on Ubuntu.
Upvotes: 16
Views: 3431
Reputation: 603
Use of bc is totally superfluous, it's a sub-optimal implementation. It can be done all in integers.
A version with pure bash (no external commands except inotifywait), if we really want millisecond precision. (We can do even nano second precision, but will limit maximum debounce period)
#!/bin/bash
getTime() {
# EPOCHREALTIME has the EPOCH time in nanoseconds
# (i.e: 1653121901,339206 )
# We extract the milliseconds and seconds up to 9
# digits precision.
# Using the value above, this is "121901339"
# (Those values fit neatly into a bash integer)
[[ $EPOCHREALTIME =~ ([^,].....),(...).*$ ]] && \
echo "${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
# (This way of pattern extraction avoids the use of sed on this cases.
# Totally recommended)
}
lastRunTime=$(getTime)
# We avoid the pipe, so the main program always
# runs on the same process.
while read path event file; do
currentTime=$(getTime)
delta=$(( $currentTime - $lastRunTime ))
if [[ "${delta}" -gt 1000 ]] ; then
echo "run"
lastRunTime=$(getTime)
fi
done < <(inotifywait -mr ./web -e create -e delete -e modify)
# This way the extra process is the generator command, which is generally what you
# want.
That said, if you are interested on the events you receive, this treats all events as if they were the same. You don't know which events happened during the debouncing period.
This can be solved by using an associative array to store all the different events between debounce periods.
#!/bin/bash
getTime() {
[[ $EPOCHREALTIME =~ ([^,].....),(...).*$ ]] && echo "${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
}
declare -A Changes
lastRunTime=0
while read path event file; do
currentTime=$(getTime)
delta=$(( $currentTime - $lastRunTime ))
Changes["$event $file"]=1
if [[ "${delta}" -gt 1000 ]] ; then
for change in "${!Changes[@]}"; do
echo "run $change";
done
Changes=()
lastRunTime=$currentTime
fi
done < <(inotifywait -mr ./web/ -e create -e delete -e modify)
Upvotes: 1
Reputation: 1973
Here is a compact solution:
inotifywait -q -m -e modify -e create -e close_write --format "%w%f" /etc/nginx/ |\
while read -r path; do
echo $path changed
echo "Skipping $(timeout 3 cat | wc -l) further changes"
service nginx reload
done
The first read
waits for a line of data, so this won't eat your CPU. The timeout 3 cat
reads any further change notifications that come in over the next 3 seconds. Only then does nginx
get reloaded.
Upvotes: 15
Reputation: 1
I write this solution, working only if bc (calculator) application is installed, because bash can't handle float numbers.
lastRunTime=$(date +'%H%M%S.%N')
inotifywait -mr ./web -e create -e delete -e modify | while read file event tm; do
currentTime=$(date +'%H%M%S.%N')
delta=$(bc <<< "$lastRunTime - $currentTime")
echo "$currentTime, $lastRunTime, $delta"
if (( $(echo "$delta < -1.0" | bc -l) )); then
echo "run"
lastRunTime=$(date +'%H%M%S.%N')
fi
done
Explain: here we set last run datetime as NOW, and next run allowed only if last run datetime < delta (in my case is -1.0).
Example:
Setting up watches. Beware: since -r was given, this may take a while!
Watches established.
120845.009293691, 120842.581019388, -2.428274303
run
120845.018643243, 120845.017585539, -.001057704
120845.026360234, 120845.017585539, -.008774695
Upvotes: 0
Reputation: 239810
To interrupt, you can shift things around so the processing runs in a background subshell and each new inotifywait
event nukes the background processes:
inotifywait -mq oli-test | while read EV; do
jobs -p | xargs kill -9
(
# do expensive things here
sleep 5 # a placeholder for compiling
echo "$EV"
) &
done
Upvotes: 7