| 1 | #! /bin/sh |
| 2 | ### |
| 3 | ### Run a program, but stash its output unless it fails |
| 4 | ### |
| 5 | ### (c) 2011 Mark Wooding |
| 6 | ### |
| 7 | |
| 8 | ###----- Licensing notice --------------------------------------------------- |
| 9 | ### |
| 10 | ### This program is free software; you can redistribute it and/or modify |
| 11 | ### it under the terms of the GNU General Public License as published by |
| 12 | ### the Free Software Foundation; either version 2 of the License, or |
| 13 | ### (at your option) any later version. |
| 14 | ### |
| 15 | ### This program is distributed in the hope that it will be useful, |
| 16 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 17 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 18 | ### GNU General Public License for more details. |
| 19 | ### |
| 20 | ### You should have received a copy of the GNU General Public License |
| 21 | ### along with this program; if not, write to the Free Software |
| 22 | ### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA |
| 23 | |
| 24 | set -e |
| 25 | |
| 26 | quis=${0##*/} |
| 27 | usage="usage: $quis [-d DIR] [-m EMAIL] [-n NLOG] TAG COMMAND [ARGS ...]" |
| 28 | ver="@VERSION@" |
| 29 | version () { echo "$quis, @PACKAGE@ version $ver"; } |
| 30 | |
| 31 | ###-------------------------------------------------------------------------- |
| 32 | ### Parse the command line. |
| 33 | |
| 34 | ## Initialize variables for storing command-line option values. |
| 35 | logdir="@logdir@" |
| 36 | maxlog=16 |
| 37 | unset mail |
| 38 | unset owner |
| 39 | unset mode |
| 40 | |
| 41 | ## Scan the options. |
| 42 | while getopts "hvd:m:n:p:u:" opt; do |
| 43 | case "$opt" in |
| 44 | h) |
| 45 | version |
| 46 | cat <<EOF |
| 47 | |
| 48 | $usage |
| 49 | |
| 50 | Run COMMAND with ARGS, logging output to DIR: if COMMAND succeeds, output |
| 51 | nothing; if it fails, also write its output to stdout or mail it to EMAIL. |
| 52 | |
| 53 | Options: |
| 54 | -h Show this help text and exit. |
| 55 | -v Show the program's version number and exit. |
| 56 | |
| 57 | -d DIR Write log files to DIR (default $logdir). |
| 58 | -m EMAIL Send email on failure to EMAIL. |
| 59 | -n MAXLOG Keep at most MAXLOG log files (default $maxlog). |
| 60 | -p MODE Set log permissions to MODE (default umask). |
| 61 | -u [OWNER][:GROUP] Set log file OWNER and GROUP (default system). |
| 62 | EOF |
| 63 | exit |
| 64 | ;; |
| 65 | v) |
| 66 | version |
| 67 | exit |
| 68 | ;; |
| 69 | |
| 70 | d) logdir=$OPTARG ;; |
| 71 | m) mail=$OPTARG ;; |
| 72 | n) maxlog=$OPTARG ;; |
| 73 | p) mode=$OPTARG ;; |
| 74 | u) owner=$OPTARG ;; |
| 75 | *) echo >&2 "$usage"; exit 1 ;; |
| 76 | esac |
| 77 | done |
| 78 | shift $(( OPTIND - 1 )) |
| 79 | |
| 80 | ## Check the arguments. |
| 81 | case $# in 0 | 1) echo >&2 "$usage"; exit 1 ;; esac |
| 82 | tag=$1 cmd=$2; shift 2 |
| 83 | |
| 84 | ###-------------------------------------------------------------------------- |
| 85 | ### Check out the environment. |
| 86 | |
| 87 | ## Force a command to line-buffer its output. How does one do this on BSD, |
| 88 | ## for example? |
| 89 | if stdbuf --version >/dev/null 2>&1; then |
| 90 | lbuf="stdbuf -oL --" |
| 91 | else |
| 92 | lbuf="" |
| 93 | fi |
| 94 | |
| 95 | ###-------------------------------------------------------------------------- |
| 96 | ### Set up the log file. |
| 97 | |
| 98 | ## Find a name for the log file. In unusual circumstances, we may have |
| 99 | ## deleted old logs from today, so just checking for an unused sequence |
| 100 | ## number is insufficient. Instead, check all of the logfiles for today, and |
| 101 | ## use a sequence number that's larger than any of them. |
| 102 | date=$(date +%Y-%m-%d) seq=1 |
| 103 | for i in "$logdir/$tag.$date#"*; do |
| 104 | tail=${i##*#} |
| 105 | case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac |
| 106 | if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi |
| 107 | done |
| 108 | log="$logdir/$tag.$date#$seq" |
| 109 | |
| 110 | ## Create the file. Make sure we create it with restrictive permissions |
| 111 | ## and then slacken them off if necessary. This means that we don't (for |
| 112 | ## example) end up giving the wrong group write permission to the file for a |
| 113 | ## little bit. |
| 114 | umask=$(umask) |
| 115 | case ${mode+t} in t) ;; *) mode=$(printf %o $(( 0666 & ~umask ))) ;; esac |
| 116 | umask 077; exec 3>"$log"; umask $umask |
| 117 | case ${owner+t} in t) chown "$owner" "$log" ;; esac |
| 118 | chmod $mode "$log" |
| 119 | |
| 120 | ###-------------------------------------------------------------------------- |
| 121 | ### Run the program. |
| 122 | |
| 123 | ## Write a log header. |
| 124 | cat >&3 <<EOF |
| 125 | Started $cmd at $(date +"%Y-%m-%d %H:%M:%S %z") |
| 126 | Lines beginning \`|' are stdout; lines beginning \`*' are stderr |
| 127 | |
| 128 | EOF |
| 129 | |
| 130 | ## Run the program, interleaving stdout and stderr in a vaguely useful way. |
| 131 | ## This involves what I can only describe as a `shell game' (sorry) with file |
| 132 | ## descriptors. |
| 133 | ## |
| 134 | ## In the middle, we have the actual command, hacked so as to line-buffer |
| 135 | ## stdout (so that we can better interleave stderr). We capture its stdout |
| 136 | ## and stderr into pipelines, one at a time, in which we pluck out lines one |
| 137 | ## by one and prefix them with distinctive characters, and then write them to |
| 138 | ## another pipe (fd 4) which is written via cat(1) to the log file. (This is |
| 139 | ## not a `useless use of cat': I rely on the write atomicity guarantee of |
| 140 | ## pipes in order to prevent intermingling of the stdout and stderr lines -- |
| 141 | ## of course, if they're too long to fit in the pipe buffer then we'll just |
| 142 | ## lose.) |
| 143 | ## |
| 144 | ## Finally, there's a problem because we only get the exit status of the last |
| 145 | ## stage of a pipeline, where we actually wanted the status of the first. So |
| 146 | ## we write that to another pipe (fd 5) and pick it out using command |
| 147 | ## substitution. |
| 148 | copy () { while IFS= read -r line; do printf "%s %s\n" "$1" "$line"; done; } |
| 149 | rc=$( |
| 150 | { { { { set +e; $lbuf "$cmd" "$@" 3>&- 4>&- 5>&-; echo $? >&5; } | |
| 151 | copy "|" >&4; } 2>&1 | |
| 152 | copy "*" >&4; } 4>&1 | |
| 153 | cat -u >&3; } 5>&1 </dev/null |
| 154 | ) |
| 155 | |
| 156 | ## Write the log trailer. |
| 157 | cat >&3 <<EOF |
| 158 | |
| 159 | Ended $cmd at $(date +"%Y-%m-%d %H:%M:%S %z") with status $rc |
| 160 | EOF |
| 161 | exec 3>&- |
| 162 | |
| 163 | ###-------------------------------------------------------------------------- |
| 164 | ### Delete old log files if there are too many. |
| 165 | |
| 166 | ## Find out the tails of the logfile names. We assume that we're responsible |
| 167 | ## for all of these, and therefore that they're nicely formed. |
| 168 | logs="" nlog=0 |
| 169 | for i in "$logdir/$tag".*; do |
| 170 | if [ ! -f "$i" ]; then continue; fi |
| 171 | nlog=$(( $nlog + 1 )) |
| 172 | logs="$logs ${i#$logdir/$tag.}" |
| 173 | done |
| 174 | |
| 175 | ## If there are too many, go through and delete some early ones. |
| 176 | if [ $nlog -gt $maxlog ]; then |
| 177 | n=$(( $nlog - $maxlog )) |
| 178 | for i in $logs; do echo $i; done | sort -t# -k1,1 -k2n | while read i; do |
| 179 | rm -f "$logdir/$tag.$i" |
| 180 | n=$(( $n - 1 )) |
| 181 | if [ $n -eq 0 ]; then break; fi |
| 182 | done |
| 183 | fi |
| 184 | |
| 185 | ###-------------------------------------------------------------------------- |
| 186 | ### Do something useful with the result. |
| 187 | |
| 188 | case $rc,${mail+t} in |
| 189 | 0,*) |
| 190 | ## Everything worked. Leave the results in the log file in case someone |
| 191 | ## cares. |
| 192 | ;; |
| 193 | *,t) |
| 194 | ## Failed, and we have an email address. Send mail and appear to |
| 195 | ## succeed: we've done our job and reported the situation. The idea is |
| 196 | ## to prevent something else (e.g., cron) from producing another report |
| 197 | ## for the same problem, but without the useful content. |
| 198 | mail -s "$tag: $cmd failed (status = $rc)" "$mail" <"$log" |
| 199 | rc=0 |
| 200 | ;; |
| 201 | *) |
| 202 | ## Failed, and no email address. Write the accumulated stuff. |
| 203 | cat "$log" |
| 204 | ;; |
| 205 | esac |
| 206 | |
| 207 | ## Exit with an appropriate status. |
| 208 | exit $rc |
| 209 | |
| 210 | ###----- That's all, folks -------------------------------------------------- |