#!/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
+ 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.
-Use to wrap systemd timers and to email on repeated failure & success after failure
-Stores error counts in $c\$error_count
+Stores error counts in $cbase
--ERRORS: ERRORS is the number of failurs to accumulate before mailing the error
+-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
-
-cursor=$(journalctl --show-cursor -qn0|sed 's/^\s*--\scursor:\s*//')
-if "$@"; then
- if [[ $file ]]; then
- rm -f $file
- if [[ $file == $c$errors ]]; then
- echo | mail -s "$HOSTNAME: $service success" $u@localhost
+if [[ $file ]]; then
+ e "file=$file"
+fi
+
+if ! [[ -d $cbase ]]; then
+ mkdir -p $cbase
+fi
+
+
+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
- 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=$cursor | \
- 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
- touch $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