Commit | Line | Data |
---|---|---|
c818aced MW |
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 | rc=$( | |
12e09263 | 149 | { { { { set +e; $lbuf "$cmd" "$@" 3>&- 4>&- 5>&-; echo $? >&5; } | |
561dff8c MW |
150 | while IFS= read line; do echo "| $line"; done >&4; } 2>&1 | |
151 | while IFS= read line; do echo "* $line"; done >&4; } 4>&1 | | |
1e22187d | 152 | cat -u >&3; } 5>&1 </dev/null |
c818aced MW |
153 | ) |
154 | ||
155 | ## Write the log trailer. | |
156 | cat >&3 <<EOF | |
157 | ||
158 | Ended $cmd at $(date +"%Y-%m-%d %H:%M:%S %z") with status $rc | |
159 | EOF | |
160 | exec 3>&- | |
161 | ||
162 | ###-------------------------------------------------------------------------- | |
163 | ### Delete old log files if there are too many. | |
164 | ||
165 | ## Count up the logfiles. | |
166 | nlog=0 | |
167 | for i in "$logdir/$tag".*; do | |
168 | if [ ! -f "$i" ]; then continue; fi | |
169 | nlog=$(( nlog + 1 )) | |
170 | done | |
171 | ||
172 | ## If there are too many, go through and delete some early ones. | |
173 | if [ $nlog -gt $maxlog ]; then | |
174 | n=$(( nlog - maxlog )) | |
175 | for i in "$logdir/$tag".*; do | |
176 | if [ ! -f "$i" ]; then continue; fi | |
177 | rm -f "$i" | |
178 | n=$(( n - 1 )) | |
179 | if [ $n -eq 0 ]; then break; fi | |
180 | done | |
181 | fi | |
182 | ||
183 | ###-------------------------------------------------------------------------- | |
184 | ### Do something useful with the result. | |
185 | ||
186 | case $rc,${mail+t} in | |
187 | 0,*) | |
188 | ## Everything worked. Leave the results in the log file in case someone | |
189 | ## cares. | |
190 | ;; | |
191 | *,t) | |
192 | ## Failed, and we have an email address. Send mail and appear to | |
193 | ## succeed: we've done our job and reported the situation. The idea is | |
194 | ## to prevent something else (e.g., cron) from producing another report | |
195 | ## for the same problem, but without the useful content. | |
196 | mail -s "$tag: $cmd failed (status = $rc)" "$mail" <"$log" | |
197 | rc=0 | |
198 | ;; | |
199 | *) | |
200 | ## Failed, and no email address. Write the accumulated stuff. | |
201 | cat "$log" | |
202 | ;; | |
203 | esac | |
204 | ||
205 | ## Exit with an appropriate status. | |
206 | exit $rc | |
207 | ||
208 | ###----- That's all, folks -------------------------------------------------- |