| 1 | #!/usr/bin/perl -w |
| 2 | ##------------------------------------------------------------------------## |
| 3 | ## File: |
| 4 | ## $Id: logcmd,v 1.1 2002/10/01 22:49:46 ehood Exp $ |
| 5 | ## Author: |
| 6 | ## Earl Hood earl@earlhood.com |
| 7 | ## Description: |
| 8 | ## Program to log the output of a program. |
| 9 | ## POD at __END__. |
| 10 | ##------------------------------------------------------------------------## |
| 11 | ## Copyright (C) 2002 Earl Hood, earl@earlhood.com |
| 12 | ## |
| 13 | ## This program is free software; you can redistribute it and/or modify |
| 14 | ## it under the terms of the GNU General Public License as published by |
| 15 | ## the Free Software Foundation; either version 2 of the License, or |
| 16 | ## (at your option) any later version. |
| 17 | ## |
| 18 | ## This program is distributed in the hope that it will be useful, |
| 19 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 20 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 21 | ## GNU General Public License for more details. |
| 22 | ## |
| 23 | ## You should have received a copy of the GNU General Public License |
| 24 | ## along with this program; if not, write to the Free Software |
| 25 | ## Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA |
| 26 | ## 02111-1307, USA |
| 27 | ##------------------------------------------------------------------------## |
| 28 | |
| 29 | package Devtools::locmd; |
| 30 | |
| 31 | use strict; |
| 32 | use Getopt::Long; |
| 33 | use POSIX; |
| 34 | |
| 35 | my $our_name = 'logcmd'; |
| 36 | my @cmd = ( ); |
| 37 | my $cmd_name = ""; |
| 38 | my $time_fmt = '%Y-%m-%d %H:%M:%S'; |
| 39 | |
| 40 | my $first_time = 1; |
| 41 | my $log_file = '-'; |
| 42 | my $log_fh; |
| 43 | my $child_pid; |
| 44 | |
| 45 | MAIN: { |
| 46 | my @mailto = ( ); |
| 47 | my ($help, $man); |
| 48 | |
| 49 | GetOptions( |
| 50 | 'log=s' => \$log_file, |
| 51 | 'mailto=s@' => \@mailto, # not implemented, yet |
| 52 | 'time-fmt=s' => \$time_fmt, |
| 53 | |
| 54 | 'help' => \$help, |
| 55 | 'man' => \$man, |
| 56 | ) || die qq/ERROR: Use -help for usage information\n/; |
| 57 | usage(1, 0) if ($help); |
| 58 | usage(2, 0) if ($man); |
| 59 | |
| 60 | if (!@ARGV) { |
| 61 | die qq/ERROR: No command specified\n/; |
| 62 | } |
| 63 | ($cmd_name = $ARGV[0]) =~ s/.*\///; |
| 64 | @cmd = @ARGV; |
| 65 | |
| 66 | $child_pid = open(CMD, "-|"); |
| 67 | if ($child_pid) { # parent |
| 68 | { |
| 69 | local @SIG{'PIPE','TERM','INT'} = (sub { |
| 70 | log_line($our_name, $$, "Caught signal: SIG$_[0]\n"); |
| 71 | }) x 3; |
| 72 | |
| 73 | while (<CMD>) { |
| 74 | log_line($cmd_name, $child_pid, $_); |
| 75 | } |
| 76 | } |
| 77 | if (!close(CMD)) { |
| 78 | log_line($our_name, $$, qq/Non-zero exit for [$child_pid]: $?\n/); |
| 79 | } |
| 80 | log_close(); |
| 81 | |
| 82 | } else { # child |
| 83 | open(STDERR, '>&STDOUT'); |
| 84 | exec(@cmd) || die qq/ERROR: Cannot exec "@cmd": $!\n/; |
| 85 | } |
| 86 | |
| 87 | } # End: MAIN |
| 88 | |
| 89 | ##=========================================================================## |
| 90 | |
| 91 | sub format_time { |
| 92 | POSIX::strftime($time_fmt, localtime); |
| 93 | } |
| 94 | |
| 95 | sub log_line { |
| 96 | my $label = shift; |
| 97 | my $pid = shift; |
| 98 | my $line = shift; |
| 99 | my $have_newline = $line =~ /\n\Z/; |
| 100 | my $eol = ($have_newline) ? "" : "\n"; |
| 101 | if ($first_time) { |
| 102 | $first_time = 0; |
| 103 | log_open($log_file); |
| 104 | print $log_fh join('', '[',format_time(),']',' [',$$,'] ',$our_name,': ', |
| 105 | "Command: [$child_pid] @cmd\n"); |
| 106 | } |
| 107 | print $log_fh join('', '[',format_time(),']',' [',$pid,'] ',$label,': ', |
| 108 | $line,$eol); |
| 109 | } |
| 110 | |
| 111 | sub log_open { |
| 112 | my $file = shift; |
| 113 | if (!defined($file) || $file eq '-') { |
| 114 | $log_fh = \*STDOUT; |
| 115 | } else { |
| 116 | open(LOG, ">>$file") || |
| 117 | die qq/ERROR: Unable to open "$file" for appending: $!\n/; |
| 118 | $log_fh = \*LOG; |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | sub log_close { |
| 123 | if (!$first_time) { |
| 124 | if ($log_fh != \*STDOUT) { |
| 125 | close($log_fh) || |
| 126 | warn qq/Warning: Problem closing log file: $!\n/; |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | sub usage { |
| 132 | require Pod::Usage; |
| 133 | my $verbose = shift || 0; |
| 134 | my $exit_code = shift; |
| 135 | |
| 136 | if ($verbose == 0) { |
| 137 | Pod::Usage::pod2usage(-verbose => $verbose); |
| 138 | } else { |
| 139 | my $pager = $ENV{'PAGER'} || 'more'; |
| 140 | local(*PAGER); |
| 141 | my $fh = (-t STDOUT && open(PAGER, "|$pager")) ? \*PAGER : \*STDOUT; |
| 142 | Pod::Usage::pod2usage(-verbose => $verbose, |
| 143 | -output => $fh); |
| 144 | close(PAGER) if ($fh == \*PAGER); |
| 145 | } |
| 146 | defined($exit_code) && exit($exit_code); |
| 147 | } |
| 148 | |
| 149 | ##=========================================================================## |
| 150 | __END__ |
| 151 | |
| 152 | =head1 NAME |
| 153 | |
| 154 | logcmd - Log the output of a program. |
| 155 | |
| 156 | =head1 SYNOPSIS |
| 157 | |
| 158 | logcmd [options] -- <command> [command-args ...] |
| 159 | |
| 160 | =head1 DESCRIPTION |
| 161 | |
| 162 | B<logcmd> logs the output of a program. This program is useful |
| 163 | for logging program output for programs that do not have a built-in |
| 164 | logging facility. |
| 165 | |
| 166 | Typical usage is to use B<logcmd> in crontabs to log any command |
| 167 | output to a file instead of mail automatically being sent to the |
| 168 | crontab owner. |
| 169 | |
| 170 | When invoking B<logcmd>, you B<must> use "C<-->" to separate the |
| 171 | B<logcmd> and its options from the command you are invoking. For |
| 172 | example, |
| 173 | |
| 174 | logcmd -log out.log -- some-cmd arg1 arg2 arg3 |
| 175 | |
| 176 | This tells B<logcmd> to run "C<some-cmd arg1 arg2 arg3>" and have |
| 177 | any output goto C<out.log>. |
| 178 | |
| 179 | Each line logged will be preceded by a timestamp, the process ID, and |
| 180 | the name of the command. Also, before the first log message for a command |
| 181 | is printed, B<logcmd> will print out a line giving the full command-line |
| 182 | of the command invoked. Example: |
| 183 | |
| 184 | [2002-07-30 14:52:13] [25392] logcmd: Command: [25393] mhonarc -quiet -out out /var/archiver/mail |
| 185 | [2002-07-30 14:52:13] [25393] mhonarc: |
| 186 | [2002-07-30 14:52:13] [25393] mhonarc: Warning: Unrecognized character set: utf-8 |
| 187 | [2002-07-30 14:52:13] [25393] mhonarc: Message-Id: <1022714308.2028.1.camel@xxx.xxx.xxx> |
| 188 | [2002-07-30 14:52:13] [25393] mhonarc: Message Number: 00367 |
| 189 | [2002-07-30 14:52:13] [25393] mhonarc: |
| 190 | [2002-07-30 14:52:13] [25393] mhonarc: Warning: Unrecognized character set: utf-8 |
| 191 | [2002-07-30 14:52:13] [25393] mhonarc: Message-Id: <1022716087.2028.3.camel@xxx.xxx.xxx> |
| 192 | [2002-07-30 14:52:13] [25393] mhonarc: Message Number: 00368 |
| 193 | [2002-07-30 14:52:14] [25393] mhonarc: |
| 194 | [2002-07-30 14:52:14] [25393] mhonarc: Warning: Unrecognized character set: utf-8 |
| 195 | [2002-07-30 14:52:14] [25393] mhonarc: Message-Id: <1024365245.9394.0.camel@xxx.xxx.xxx> |
| 196 | [2002-07-30 14:52:14] [25393] mhonarc: Message Number: 00398 |
| 197 | |
| 198 | |
| 199 | =head1 OPTIONS |
| 200 | |
| 201 | =over |
| 202 | |
| 203 | =item C<-help> |
| 204 | |
| 205 | Display SYNOPSIS and OPTIONS sections. |
| 206 | |
| 207 | =item C<-log> I<pathname> |
| 208 | |
| 209 | Command output will be logged to I<pathname>. If this option is |
| 210 | not specified, then standard out is used. |
| 211 | |
| 212 | =item C<-man> |
| 213 | |
| 214 | Display manpage. |
| 215 | |
| 216 | =item C<-time-fmt> I<fmt> |
| 217 | |
| 218 | Date/time format to use when printing time before each logged line. |
| 219 | The format string should be in C<strftime>(3) format. If this |
| 220 | option is not specified, then "C<%Y-%m-%d %H:%M:%S>" is used. |
| 221 | |
| 222 | =item C<--> |
| 223 | |
| 224 | Terminate option processing for B<logcmd>. Any arguments after |
| 225 | C<--> comprise the command that B<logcmd> should execute. |
| 226 | |
| 227 | =back |
| 228 | |
| 229 | =head1 EXAMPLES |
| 230 | |
| 231 | The following is an example crontab entries of a user that has |
| 232 | any output of commands goto a personal log file: |
| 233 | |
| 234 | 0 2 * * * logcmd -log $HOME/log/cron.log -- $HOME/bin/clean_account |
| 235 | 0 3 * * * logcmd -log $HOME/log/cron.log -- $HOME/bin/pack_mail |
| 236 | |
| 237 | =head1 LIMITATIONS |
| 238 | |
| 239 | =over |
| 240 | |
| 241 | =item * |
| 242 | |
| 243 | B<logcmd> performs no file locking on the log file specified. To help |
| 244 | minimize that a single output line to the log does not get broken up |
| 245 | due to multiple writers, the entire line is dumped in a single call |
| 246 | to Perl's C<print> operator. |
| 247 | |
| 248 | =back |
| 249 | |
| 250 | =head1 DEPENDENCIES |
| 251 | |
| 252 | C<Getopt::Long>, C<POSIX>. |
| 253 | |
| 254 | =head1 AUTHOR |
| 255 | |
| 256 | Earl Hood, earl@earlhood.com |
| 257 | |
| 258 | This program comes with ABSOLUTELY NO WARRANTY and may be copied only |
| 259 | under the terms of the GNU General Public License. |
| 260 | |
| 261 | =cut |
| 262 | |