A camera phone that stops recording does not tell you. The folder just stops growing. So the job is a small check that runs every 15 minutes and asks one question: has camera 1 written a clip lately? If not, send an alert.
This is a companion to set -o pipefail Failed My Clip Script on Some Nights and Not Others, which showed why a pipeline ending in head or grep -q exits 141 under pipefail once there is more output than a pipe holds. A reader, c-wind, added the case that post skipped: what to do when the early exit is not something find can express, and how a real error can hide behind the 141. They also asked a direct question. This post builds the 15-minute check and answers it.
Disclosure: I make an Android camera app. Nothing below uses its code or describes how it names or stores files. The folder is fake, the log is fake, and every result comes from the scripts shown, run on this machine:
GNU bash, version 5.1.16(1)-release (aarch64-unknown-linux-gnu)
find (GNU findutils) 4.8.0
CPUs: 2
The fake folder
Same generator as the first post: empty files, one minute apart, all from camera 1, all dated October 2025, so every clip is stale.
#!/bin/bash
# mk.sh N : make N empty clip files, one minute apart, oldest first.
n=$1
rm -rf clips && mkdir clips
for i in $(seq 1 "$n"); do
f=$(printf 'clips/cam1_%05d.mp4' "$i")
: > "$f"
touch -d "@$((1761000000 + i * 60))" "$f"
done
The answer to the question first
The question was: on the 3000-clip grep -q runs, was PIPESTATUS 141 0, the same as the sort | head trace? Here it is, 100 runs per folder size:
#!/bin/bash
set -o pipefail
for n in 10 3000; do
./mk.sh "$n"
declare -A seen=()
for k in $(seq 1 100); do
find clips -name '*.mp4' | grep -q 'cam1_'
s="${PIPESTATUS[*]}"
seen["$s"]=$(( ${seen["$s"]:-0} + 1 ))
done
for s in "${!seen[@]}"; do echo "clips=$n PIPESTATUS=$s ${seen[$s]}/100"; done
unset seen
done
clips=10 PIPESTATUS=0 0 100/100
clips=3000 PIPESTATUS=141 0 100/100
Yes. grep -q matched and exited 0; find, still listing, took SIGPIPE. On 10 clips find finished before grep left, so both are 0.
Version one: the newest clip, by ls -t
The obvious first draft asks for the newest clip and checks its age. Before any age check, the name lookup itself:
#!/bin/bash
set -euo pipefail
# Name of the newest camera 1 clip, by modification time.
newest=$(ls -t clips/cam1_*.mp4 | head -n 1)
echo "newest: $newest"
newest_ls clips=50 failed=0/100
newest_ls clips=3000 failed=95/100
On 3000 clips, a single run printed nothing and exited 141. It is the same failure as the first post with ls as the writer: 95 of 100 runs died before the check ever looked at a time. If the scheduler treats a non-zero exit as "alert", that is 95 false alarms; if it ignores non-zero exits, that is 95 checks that never happened.
Version two: let find ask the whole question
The question is not "what is the newest clip". It is "is there at least one camera 1 clip modified in the last 15 minutes". find can say that directly with -mmin -15, and stop at the first hit with -quit. No pipe, nothing left writing into a closed one:
#!/bin/bash
set -euo pipefail
dir="${1:-clips}"
# Any camera 1 clip modified in the last 15 minutes? find stops at the first one.
recent=$(find "$dir" -name 'cam1_*.mp4' -mmin -15 -print -quit)
if [ -n "$recent" ]; then echo "OK: $recent"; else echo "ALERT: no cam1 clip in 15 min"; fi
Three cases: the stale 3000-clip folder, the same folder after touching one clip so it is fresh, and a folder that does not exist.
--- stale folder (all clips from 2025) ---
ALERT: no cam1 clip in 15 min
exit 0
--- after touching newest now ---
OK: clips/cam1_03000.mp4
exit 0
--- missing dir ---
find: ‘no_such_dir’: No such file or directory
exit 1
Right answer on the stale folder, right answer on the fresh one, and a missing folder is an error (exit 1), not a quiet "OK" and not a quiet "ALERT". That last one matters: "the folder is gone" and "the camera stopped" need different fixes, and the exit status is the only thing that tells them apart.
For a clip folder, this is the whole check. Most of the time, the fix is to stop asking a pipeline a question the first command can answer itself.
When find can't express it
c-wind's point was that -quit only works when the test is a find predicate. Sometimes it isn't. Say the recorder writes a text log, and the real signal is a line like cam1 segment closed in the last 5000 lines. That is a content test, so it needs grep:
tail -n 5000 rec.log | grep -q ' cam1 segment closed'
The fake log is 20,000 lines, alternating camera 1 and camera 2, about 1.3 MB. The 5000 lines tail sends are more than a pipe holds, so grep -q leaves early and tail takes SIGPIPE.
Their approach: keep pipefail, and branch on each side's status yourself.
#!/bin/bash
set -uo pipefail
# Did camera 1 close a segment in the last 5000 log lines? Branch on each side's status.
tail -n 5000 rec.log | grep -q ' cam1 segment closed'
st=("${PIPESTATUS[@]}")
case "${st[0]} ${st[1]}" in
"0 0"|"141 0") echo "MATCH (tail=${st[0]} grep=${st[1]})" ;;
"0 1") echo "NOMATCH (tail=${st[0]} grep=${st[1]})" ;;
*) echo "ERROR (tail=${st[0]} grep=${st[1]})"; exit 2 ;;
esac
set -e is off here on purpose: the script reads the statuses instead of dying on them. PIPESTATUS has to be copied on the very next line, because the next command overwrites it.
100 runs on the log, then a log with every cam1 changed to cam3, then no log at all:
MATCH (tail=141 grep=0) 100/100
--- log with no cam1 ---
NOMATCH (tail=0 grep=1)
--- log missing ---
tail: cannot open 'rec.log' for reading: No such file or directory
ERROR (tail=1 grep=1)
exit 2
All three come out right. A match is 141 0 every time on this log, so a case that only accepted 0 0 would have called every one of those 100 runs an error.
The error that 141 hides
The same thread had the catch: "a real find error (EACCES, directory gone) after grep has already left still gets hidden, because pipefail reports the 141 and drops the 1." Here is that case. The 3000-clip folder gets a subfolder that find cannot read:
mkdir clips/locked && touch clips/locked/cam2_x.mp4 && chmod 000 clips/locked
Then the same question two ways. grep -q leaves at the first match; grep -c counts, so it reads to the end:
#!/bin/bash
set -uo pipefail
find clips -name '*.mp4' | grep -q 'cam1_'
echo "PIPESTATUS=${PIPESTATUS[*]}"
#!/bin/bash
set -uo pipefail
# grep -c reads to the end, so find finishes and its own status survives.
n=$(find clips -name '*.mp4' | grep -c 'cam1_')
st=$?
echo "count=$n pipeline status=$st"
With the subfolder locked, then unlocked:
PIPESTATUS=141 0
find: ‘clips/locked’: Permission denied
count=3000 pipeline status=1
--- unlocked ---
PIPESTATUS=141 0
count=3000 pipeline status=0
With grep -q, the locked and unlocked runs are identical: 141 0, nothing on stderr. In this run find was killed before it reported the folder at all, so there was no error message to lose; the status could not have shown it either. With grep -c, find gets to the locked folder, prints Permission denied, exits 1, and pipefail carries that 1 out.
So the PIPESTATUS branch is correct about what grep saw, and blind to anything the writer would have hit after the reader left. Whether that matters depends on the check:
-
"Is there at least one?" A match is a match. A permission error further down does not change the answer, so
grep -qplus the branch is fine. -
"Did we look everywhere?" Then a reader that leaves early can never give you that, because the writer never finishes. Use a reader that drains (
grep -c,grep ... > /dev/null,awkwith anEND), and accept that it reads the whole stream.
For a camera heartbeat, "at least one fresh clip" is the question, so early exit is fine. For a nightly job that counts clips before deleting old ones, it is "did we look everywhere", and a hidden Permission denied means it counted less than the folder holds.
The check I'd schedule
#!/bin/bash
set -euo pipefail
dir="${1:-clips}"
recent=$(find "$dir" -name 'cam1_*.mp4' -mmin -15 -print -quit)
if [ -n "$recent" ]; then
exit 0
else
echo "camera 1: no new clip in 15 minutes" >&2
exit 3
fi
Exit 0 means fresh clip. Exit 3 means the camera stopped. Exit 1 (from find under set -e) means the folder itself is broken. Three different problems, three different numbers, and no pipe for a 141 to come out of.
Checklist
- Ask
findthe whole question when you can:-mmin,-newer,-name, then-print -quit. - When you need a content test, copy
PIPESTATUSon the next line and treat141 0as a match, not an error. - Know which question you're asking. "At least one" tolerates a reader that leaves early. "Did we look everywhere" does not.
- Give "camera stopped" and "folder broken" different exit codes, so the alert says which one it is.
Things I did not test
-
Clock jumps.
-mminuses the system clock. A phone or server whose clock jumps will make clips look older or newer than they are. -
Clips still being written. A clip that is open and growing has a fresh mtime; one written to a temp name and renamed at the end may not match
cam1_*.mp4until it closes. Check how your recorder writes before picking the window. - Network shares. Over SMB or NFS, mtime and caching may differ. I only tested a local folder.
-
Other shells and
findbuilds. bash 5.1 and GNU findutils 4.8 only.-printf,-mminand-quitare GNU; BSDfinddiffers. -
Order.
findlists in directory order, not sorted. Here the locked subfolder came 655th of 3002 entries, about 13 KB of names in, andgrep -qhad already left. In another folder it could come first, and the error would show.
Sources. The quote is c-wind's comment on the first post. PIPESTATUS and pipefail are documented in the Bash manual; -mmin and -quit in the GNU find manual. Related: find -mtime +7 keeps eight days and rm *.mp4 said Argument list too long. Outputs are pasted unedited.
If you want an old phone recording clips in the background so there's a folder like this to watch, the app I make is Background Camera RemoteStream on Google Play. Nothing above depends on it.
A heartbeat check should fail for one reason at a time. Let find answer the question, and keep the pipe out of it.
Top comments (0)