#!/usr/bin/perl

#
# cnid_maint: A script to maintain the consistency of CNID databases.
#
# $Id: cnid_maint.in,v 1.15 2003/02/17 02:32:45 jmarcus Exp $
#

use strict;
use Getopt::Std;
use vars qw(
    $APPLE_VOLUMES_FILE
    $STOP_CMD
    $START_CMD
    $PS_CMD
    $GREP
    $DB_STAT
    $DB_RECOVER
    $DB_VERIFY
    $VERSION
    $START_NETATALK
    $LOCK_FILE
    $HOLDING_LOCK
);

## Edit ME
$STOP_CMD  = '/usr/local/etc/rc.d/netatalk.sh stop';
$START_CMD = '/usr/local/etc/rc.d/netatalk.sh start';

# This ps command needs to output the following fields in the following order:
# USER,PID,PPID,COMMAND
# Below is the example of a BSD ps.  A SYSV example is:
# /bin/ps -eflouid,pid,ppid,comm
$PS_CMD             = '/bin/ps -axouser,pid,ppid,command';
$DB_STAT            = '/usr/local/bin/db_stat';
$DB_RECOVER         = '/usr/local/bin/db_recover';
$DB_VERIFY          = '/usr/local/bin/db_verify';
$APPLE_VOLUMES_FILE = '/etc/netatalk/AppleVolumes.default';
## End edit section

$VERSION        = '1.0';
$GREP           = '/usr/bin/grep';
$START_NETATALK = 0;
$LOCK_FILE      = tmpdir() . '/cnid_maint.LOCK';
$HOLDING_LOCK   = 0;

sub LOCK_SH { 1 }
sub LOCK_EX { 2 }
sub LOCK_NB { 4 }
sub LOCK_UN { 8 }

my $opts        = {};
my $extra_safe  = 0;
my $do_verify   = 0;
my $remove_logs = 0;

getopts('hsvVl', $opts);

if ($opts->{'v'}) {
        version();
        exit(0);
}
if ($opts->{'h'}) {
        help();
        exit(0);
}
if ($opts->{'s'}) {
        $extra_safe = 1;
}
if ($opts->{'V'}) {
        $do_verify = 1;
}
if ($opts->{'l'}) {
        $remove_logs = 1;
}

if ($< != 0) {
        die "You must be root to run this script.\n";
}

print "Beginning run of CNID DB Maintanence script at "
    . scalar(localtime) . ".\n\n";

if (-f $LOCK_FILE) {
        error(1, "Lock file $LOCK_FILE exists.");
        end();
}

unless (open(LOCK, ">" . $LOCK_FILE)) {
        error(2, "Unable to create $LOCK_FILE: $!");
}
flock(LOCK, LOCK_EX);
$HOLDING_LOCK = 1;

# Check to see if the AppleVolumes.default file exists.  We will use this file
# to get a list of database environments to recover.  We will ignore users'
# home directories since that could be a monumental under taking.
if (!-f $APPLE_VOLUMES_FILE) {
        error(2, "Unable to locate $APPLE_VOLUMES_FILE");
}

# Use ps to get a list of running afpds.  We will track all afpd PIDs that are
# running as root.
unless (open(PS, $PS_CMD . " | $GREP afpd | $GREP -v grep |")) {
        error(2, "Unable to open a pipe to ps: $!");
}

my $children  = 0;
my $processes = 0;
while (<PS>) {
        chomp;
        $processes++;
        my ($user, $pid, $ppid, $command) = split (/\s+/);
        if (($user eq "root" && $ppid != 1) || ($user ne "root")) {
                $children++;
        }
}

close(PS);

if ($children) {

        # We have some children.  We cannot run recovery.
        error(1,
                "Clients are still connected.  Database recovery will not be run at this time."
        );
        end();
}

if ($processes) {

        # Shutdown the running afpds.
        $START_NETATALK = 1;
        error(0, "Shutting down afpd process...");
        error(2, "Failed to shutdown afpd")
            if system($STOP_CMD . ">/dev/null 2>&1");
}

# Now, we parse AppleVolumes.default to get a list of volumes to run recovery
# on.
unless (open(VOLS, $APPLE_VOLUMES_FILE)) {
        error(2, "Unable to open $APPLE_VOLUMES_FILE: $!");
}
flock(VOLS, LOCK_SH);

my @paths = ();
while (<VOLS>) {
        s/#.*//;
        s/^\s+//;
        s/\s+$//;
        next unless length;
        my ($path, @options) = split (/\s+/, $_);
        next if ($path =~ /^~/);
        my $option = "";
        foreach $option (@options) {

                # We need to check for the dbpath option on each volume.  If 
                # that option is present, we should use its path instead of 
                # the actual volume path.
                if ($option =~ /^dbpath:/) {
                        push @paths, $';
                } else {
                        push @paths, $path;
                }
        }
}

close(VOLS);

my $path = "";
foreach $path (@paths) {
        my $dbpath = $path . "/.AppleDB";
        if (!-d $dbpath) {
                error(1, "Database environment $dbpath does not exist");
                next;
        }
        if ($extra_safe) {
                error(0,
                        "Checking database environment $dbpath for open connections..."
                );
                unless (open(STAT, $DB_STAT . " -h $dbpath -e |")) {
                        error(1, "Failed to open a pipe to $DB_STAT: $!");
                        next;
                }

                # Now, check each DB environment for any open connections 
		# (db_stat calls them as references).  If a volume has no 
		# references, we can do recovery on it.  Only check this option
		# if the user wants to play things extra safe.
                my $connections = 0;
                while (<STAT>) {
                        chomp;
                        s/\s//g;
                        if (/References\.$/) {
                                $connections = $`;
                                last;
                        }
                }

                close(STAT);

                # Print out two different skip messages.  This is just for 
		# anality.
                if ($connections == 1) {
                        error(1,
                                "Skipping $dbpath since it has one active connection"
                        );
                        next;
                }

                if ($connections > 0) {
                        error(1,
                                "Skipping $dbpath since it has $connections active connections"
                        );
                        next;
                }
        }

        # Run the db_recover command on the environment.
        error(0, "Running db_recover on $dbpath");
        if (system($DB_RECOVER . " -h $dbpath >/dev/null 2>&1")) {
                error(1, "Failed to run db_recover on $dbpath");
                next;
        }

        if ($do_verify) {
                error(0, "Verifying $dbpath/cnid.db");
                if (system($DB_VERIFY . " -q -h $dbpath cnid.db")) {
                        error(1, "Verification of $dbpath/cnid.db failed");
                        next;
                }

                error(0, "Verifying $dbpath/devino.db");
                if (system($DB_VERIFY . " -q -h $dbpath devino.db")) {
                        error(1, "Verification of $dbpath/devino.db failed");
                        next;
                }

                error(0, "Verifying $dbpath/didname.db");
                if (system($DB_VERIFY . " -q -h $dbpath didname.db")) {
                        error(1, "Verification of $dbpath/didname.db failed");
                        next;
                }
                if (-f "$dbpath/mangle.db") {
                        error(0, "Verifying $dbpath/mangle.db");
                        if (system($DB_VERIFY . " -q -h $dbpath mangle.db")) {
                                error(1,
                                        "Verification of $dbpath/mangle.db failed"
                                );
                                next;
                        }
                }
        }

        if ($remove_logs) {

                # Remove the log files if told to do so.
                unless (opendir(DIR, $dbpath)) {
                        error(1, "Failed to open $dbpath for browsing: $!");
                        next;
                }

                my $file = "";
                while (defined($file = readdir(DIR))) {
                        if ($file =~ /^log\.\d+$/) {
                                error(0, "Removing $dbpath/$file");
                                unless (unlink($dbpath . "/" . $file)) {
                                        error(1,
                                                "Failed to remove $dbpath/$file: $!"
                                        );
                                        next;
                                }
                        }
                }

                closedir(DIR);
        }

}

end();

sub tmpdir {
        my $tmpdir;

        foreach ($ENV{TMPDIR}, "/tmp") {
                next unless defined && -d && -w _;
                $tmpdir = $_;
                last;
        }
        $tmpdir = '' unless defined $tmpdir;
        return $tmpdir;
}

sub error {
        my ($code, $msg) = @_;

        my $err_types = {
                0 => "INFO",
                1 => "WARNING",
                2 => "ERROR",
        };

        print $err_types->{$code} . ": " . $msg . "\n";

        end() if ($code == 2);
}

sub end {
        if ($START_NETATALK) {
                error(0, "Restarting Netatalk");
                if (system($START_CMD . " >/dev/null 2>&1")) {
                        print "ERROR: Failed to restart Netatalk\n";
                }
        }
        if ($HOLDING_LOCK) {
                close(LOCK);
                unlink($LOCK_FILE);
        }
        print "\nRun of CNID DB Maintenance script ended at "
            . scalar(localtime) . ".\n";
        exit(0);
}

sub version {
        print "$0 version $VERSION\n";
}

sub help {
        print "usage: $0 [-hlsvV]\n";
        print "\t-h   view this message\n";
        print "\t-l   remove transaction logs after running recovery\n";
        print
            "\t-s   be extra safe in verifying there are no open DB connections\n";
        print "\t-v   print version and exit\n";
        print
            "\t-V   run a verification on all database files after recovery\n";
}
