#!/bin/bash
-# Copyright (C) 2016 Ian Kelling
+# I, Ian Kelling, follow the GNU license recommendations at
+# https://www.gnu.org/licenses/license-recommendations.en.html. They
+# recommend that small programs, < 300 lines, be licensed under the
+# Apache License 2.0. This file contains or is part of one or more small
+# programs. If a small program grows beyond 300 lines, I plan to switch
+# its license to GPL.
+
+# Copyright 2024 Ian Kelling
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# limitations under the License.
+
set -eE -o pipefail
trap 'echo "$0:$LINENO:error: \"$BASH_COMMAND\" returned $?" >&2' ERR
errors=3
-cbase=/var/local/cron-errors
-case "$1" in
+tmp=(~)
+cbase="${tmp[0]}/sysd-mail-once-state"
+to=root
+dryrun=false
+print_all=false
+while [[ $1 == -* ]]; do
+ case "$1" in
-h|--help)
- cat <<EOF
-Usage: sysd-log-once [-ERRORS] SERVICE COMMAND [ARGS...]
-For systemd timers, email on repeated failure & success after failure.
+ cat <<EOF
+Usage: sysd-mail-once [-t TO_ADDRESS] [-ERRORS] SERVICE COMMAND [COMMAND_ARGS...]
+
+For use with systemd timers, to email (with exim) on repeated failure &
+success after failure.
In the service triggered by the timer, prepend this script to the ExecStart.
The email will contain the service's logs for the last ERRORS runs.
Stores error counts in $cbase
--ERRORS: ERRORS is the number of failurs to accumulate before mailing the error.
- Default is 3.
+-ERRORS ERRORS is the number of failurs to accumulate before mailing the error.
+ Default is 3.
+
+-a Email the logs of all errors intead of just the last one.
+-t TO_ADDRESS Address to email about errors
+-n Dry run. Execute command but only print out what we would do in response.
EOF
- exit 0
- ;;
+ exit 0
+ ;;
-[0-9]*)
- errors=${1#-}
- shift
- ;;
-esac
-service=$1
+ errors=${1#-}
+ ;;
+ -a)
+ print_all=true
+ ;;
+ -t)
+ to="$2"
+ shift
+ ;;
+ -n)
+ dryrun=true
+ ;;
+ *)
+ echo "error: unexpected arg: $1"
+ exit 1
+ ;;
+ esac
+ shift
+done
+
+if (( $# < 2 )); then
+ echo "error: expected at least 2 args after options. args:"
+ exit 1
+fi
+
+service="$1"
shift
-c=$cbase/$service
-glob="$c[0-9]*"
+# maybe run, depending on $dryrun
+m() {
+ if $dryrun; then
+ printf "%s\n" "$*"
+ else
+ "$@"
+ fi
+}
+# maybe run, with stdin
+mi() {
+ if $dryrun; then
+ printf "%s <<'EOF'\n" "$*"
+ cat
+ echo EOF
+ else
+ "$@"
+ fi
+}
+e() {
+ if $dryrun; then
+ printf "dryrun: %s\n" "$*"
+ fi
+}
+
+c=$cbase/$service # c for command file path base
+
+e "c=$c"
+
+glob="${c}[0-9]*"
arr=($glob); file="${arr[0]}"; [[ $glob != "$file" ]] || file=
-u=${USER:-root}
-[[ -d $cbase ]] || mkdir -p $cbase
+if [[ $file ]]; then
+ e "file=$file"
+fi
-if [[ ! $file ]]; then
- cursor=$(journalctl --show-cursor -qn0|sed 's/^\s*--\scursor:\s*//')
+if ! [[ -d $cbase ]]; then
+ mkdir -p $cbase
fi
-if "$@"; then
- if [[ $file ]]; then
- rm -f $file
- if [[ $file == $c$errors ]]; then
- echo | mail -s "$HOSTNAME: $service success" $u@localhost
+
+
+code=0
+"$@" || code=$?
+if (( code )); then
+ send_mail=false
+ cursor=$(journalctl --show-cursor -qn0|sed 's/^\s*--\scursor:\s*//')
+ if [[ $file ]]; then
+ i=${file#"$c"}
+ if (( i < errors )); then
+ new_file=$c$((i+1))
+ m mv $file $new_file
+ file=$new_file
+ if [[ $file == $c$errors ]]; then
+ send_mail=true
+ else
+ if $dryrun; then
+ printf "dryrun: appending to file $file: %s\n" "$cursor"
+ else
+ printf "%s\n" "$cursor" >>$file
fi
+ fi
fi
-else # $@ failed
- if [[ $file ]]; then
- i=${file#$c}
- if (( i < errors )); then
- new_file=$c$((i+1))
- mv $file $new_file
- file=$new_file
- if [[ $file == $c$errors ]]; then
- journalctl -u $service.service --after-cursor=$(<$file) | \
- mail -s "$HOSTNAME: $service failure" $u@localhost
- fi
- fi
+ else
+ file=${c}1
+ if $dryrun; then
+ printf "dryrun: creating $file, contents: %s\n" "$cursor"
+ else
+ printf "%s\n" "$cursor" >$file
+ fi
+ if (( errors == 1 )); then
+ send_mail=true
+ fi
+ fi
+ if $send_mail; then
+ if $print_all; then
+ cursor=$(head -n1 $file)
else
- file=${c}1
- printf "%s\n" "$cursor" >$file
+ cursor=$(tail -n1 $file)
+ fi
+ echo "sysd-mail-once: emailing on $errors errors. exit code: $code"
+ mi exim -odf -t <<EOF
+To: $to
+From: $(id -u -n)@$(hostname -f)
+Subject: $HOSTNAME: $service exit code: $code
+
+$(journalctl -u $service.service --after-cursor="$cursor")
+EOF
+ fi
+else
+ if [[ $file ]]; then
+ m rm -f $file
+ if [[ $file == $c$errors ]]; then
+ echo "sysd-mail-once: emailing success after >= $errors errors."
+ mi exim -odf -t <<EOF
+To: $to
+From: $(id -u -n)@$(hostname -f)
+Subject: $HOSTNAME: $service success
+
+EOF
fi
+ fi
fi