#!/usr/bin/perl

# Log rotator
# Copyright (C) 1995 Red Hat Software

$recordfile = "/var/run/rotatelog.stat";
$tempscript = "/tmp/rotatelog.temp";

# Set to 1 to get debug messages and to prevent
# original logs from ever being deleted.
$debug = 0;

# 10 minute flexibility -- with this we make sure
# a log will get rotated if the script was run at
# 12:30am one day, then 12:29 the next.
$statfudge = 600;

# config file format
# logfile  when         what

# when :=    daily
#            weekly
#            days=X
#            size=X

# what := [rotate[=X]] [logdir=dir] [compress] [mail=who@where.com] [shell]
# 
# logdir   - save logs to this director rather then keep in place
# rotate   - save forever
# rotate=X - save X old logs
# rotate=0 - save no logs
# compress - compress saved logs
# mail=X   - mail log to X
# shell    - subsequent lines, up to "endshell", are executed after rotate.
#            the saved copy of the log file is passed as an argument.

sub debug {
    printf(STDERR @_) if ($debug);
}

sub read_config {
    my ( $inshell, $log, @tokens, $what );

    while(<>) {
	if ($inshell) {
	    /^endshell/ && do {$inshell = 0; next};
	    $shell{$log} .= $_;
	    next;
	}

	/^\s+#/ && next;

	chop;
	@tokens = split;
	if (@tokens) {
	    $log = shift(@tokens);
	    push(@logfiles, $log);

	    foreach $_ (@tokens) {
		/^daily$/ && ($days{$log} = 1, next);
		/^weekly$/ && ($days{$log} = 7, next);
		/^days=([0-9]+)$/ && ($days{$log} = $1, next);
		/^size=([0-9]+)$/ && ($size{$log} = $1, next);
		/^rotate$/ && ($rotate{$log} = -1, next);
		/^rotate=([0-9]+)$/ && ($rotate{$log} = $1, next);
		/^compress$/ && ($compress{$log} = 1, next);
		/^mail=(.+)$/ && ($mail{$log} = $1, next);
		/^shell$/ && ($shell{$log} = "", $inshell = 1, next);
		/^logdir=(\/.+)$/ && ($rotatedir{$log} = $1, next);
		die("Bad config entry: $_");
	    }
	}
    }
    if ($inshell) {
	die("Unterminated shell directive ($log)");
    }
}

sub read_stat {
    my ( $log, $stat );

    if (-f $recordfile) {
	open(F, $recordfile) || die "Failed opening record file $recordfile";
	while (<F>) {
	    chop;
	    ($log, $stat) = split;
	    $stat{$log} = $stat;
	}
	close(F);
    }
}

sub write_stat {
    open (F, ">$recordfile") || die "Failed writing record file $recordfile";
    foreach (keys %stat) {
	printf(F "%s %d\n", $_, $stat{$_});
    }
    close(F);
}

sub process_log {
    my ( $log ) = @_;
    my ( $interval, $period, $ret, $to, $rotate, $dir, $base );

    $ret = 0;

    &debug("Processing $log\n");

    my ( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
	$atime, $mtime, $ctime, $blksize, $blocks ) = stat($log);
    if (! defined($dev)) {
	print(STDERR "Failed to stat $log\n");
	return(1);
    }

    # First see if it is time to handle to log
    $rotate = 0;
    if ($size{$log} && ($size > $size{$log})) {
	&debug("$size > $size{$log}\n");
	$rotate = 1;
    }
    if (! defined($stat{$log})) {
	# We don't have a record of this one yet
	$stat{$log} = $nstat;
    }
    $interval = $nstat - $stat{$log};
    &debug("Last rotated $interval seconds ago\n");
    $period = $days{$log} * 60 * 60 * 24;
    &debug("Period == $days{$log} day == $period seconds\n");
    if (($interval + $statfudge) >= $period) {
	$rotate = 1;
    }
    return(0) if (! $rotate);

    # Now we actually process the log
    if (defined($rotate{$log}) || $mail{$log}) {
	# record that we rotated the log
	$stat{$log} = $nstat;

	# We are handling this log ...
	# There is a chance we could miss a log entry or two here,
	# I think, but it is pretty damn unlikely.
	link($log, "$log.rotate");
	if (! $debug) {
	    unlink($log);
	    open(F, ">$log");
	    close(F);
	} else { 
	    &debug("Would truncate $log\n");
	}

	if ($rotate{$log}) {
	    ($dir, $base) = $log =~ m|(.+)/(.+)|;
	    if ($rotatedir{$log}) {
		$dir = $rotatedir{$log};
	    }
	    &rotate_log($dir, $base, "$log.rotate",
			$rotate{$log}, $compress{$log});
	}

	if ($mail{$log}) {
	    &debug("Mailing to $mail{$log}\n");
	    system("mail -s \"ROTATELOG: $log\" $mail{$log} < $log.rotate");
	}
    }

    # Now run a shell sript, if any
    if ($shell{$log}) {
	&debug("Running shell script\n");
	# Run a shell script
	if (! open(F, ">$tempscript")) {
	    print(STDERR "Could not write shell script to process ($log)");
	    $ret = 1;
	} else {
	    print(F $shell{$log});
	    close(F);
	    system("/bin/sh $tempscript $log.rotate");
	    if ($?) {
		print(STDERR "Script returned non-zero exit code ($log)");
		unlink($tempscript);
		return(1);
	    }
	}
    }
	
    unlink($tempscript);
    unlink("$log.rotate");
    return($ret);
}

sub rotate_log {
    my ( $dir, $base, $file, $count, $compress ) = @_;
    my ( $max, $n, @files, @text, $t );

    &debug("Rotating: $dir $base $file count=$count\n");

    # A count of zero means save no logs
    return if (! $count);

    $compress = ".gz" if ($compress);

    chdir($dir);
    opendir(D, ".");
    @files = readdir(D);
    closedir(D);
    @files = grep(m|$base\.[0-9]+$compress|, @files);

    $max = 0;
    foreach (@files) {
	($n) = m|$base\.([0-9]+)$compress|;
	$max = $n if ($n > $max);
    }
    &debug("Rotate: max = $max\n");

    # We need to rotate up
    while ($max >= 0) {
	$n = $max + 1;
	$max = sprintf("%03d", $max);
	$n = sprintf("%03d", $n);
	&debug("Rotate: mv $base.$max$compress $base.$n$compress\n");
	link("$base.$max$compress", "$base.$n$compress");
	unlink("$base.$max$compress");
	$max--;
    }

    # Unlink the last one
    # A count of -1 means save all logs
    if ($count != -1) {
	$max = sprintf("%03d", $count);
	&debug("Rotate: unlinking $base.$max$compress\n");
	unlink("$base.$max$compress");
    }

    open(F, $file);
    @text = <F>;
    close(F);
    open(F, ">$base.000");
    while ($t = shift(@text)) { print(F $t) };
    close(F);

    if ($compress) {
	system("gzip $base.000");
    }
}

$ret = 0;
$nstat = time;
&read_config();
&read_stat();
foreach (@logfiles) {
    $ret |= &process_log($_);
}
&write_stat();
exit($ret);
